diff options
-rw-r--r-- | Cargo.lock | 98 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | hittekaart-py/Cargo.toml | 13 | ||||
-rw-r--r-- | hittekaart-py/pyproject.toml | 15 | ||||
-rw-r--r-- | hittekaart-py/src/lib.rs | 155 | ||||
-rw-r--r-- | hittekaart/src/gpx.rs | 4 |
6 files changed, 284 insertions, 3 deletions
@@ -632,6 +632,14 @@ dependencies = [ ] [[package]] +name = "hittekaart-py" +version = "0.1.0" +dependencies = [ + "hittekaart", + "pyo3", +] + +[[package]] name = "image" version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -687,6 +695,12 @@ dependencies = [ ] [[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -788,6 +802,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1086,6 +1109,69 @@ dependencies = [ ] [[package]] +name = "pyo3" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] name = "qoi" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1395,6 +1481,12 @@ dependencies = [ ] [[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + +[[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1510,6 +1602,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1,5 +1,5 @@ [workspace] resolver = "3" members = [ - "hittekaart", "hittekaart-cli", + "hittekaart", "hittekaart-cli", "hittekaart-py", ] diff --git a/hittekaart-py/Cargo.toml b/hittekaart-py/Cargo.toml new file mode 100644 index 0000000..9a44118 --- /dev/null +++ b/hittekaart-py/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "hittekaart-py" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "hittekaart_py" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = "0.24.0" +hittekaart = { path = "../hittekaart" } diff --git a/hittekaart-py/pyproject.toml b/hittekaart-py/pyproject.toml new file mode 100644 index 0000000..865cc21 --- /dev/null +++ b/hittekaart-py/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.8,<2.0"] +build-backend = "maturin" + +[project] +name = "hittekaart-py" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs new file mode 100644 index 0000000..9f4bd8d --- /dev/null +++ b/hittekaart-py/src/lib.rs @@ -0,0 +1,155 @@ +use hittekaart::gpx::{self, Compression, Coordinates}; +use hittekaart::renderer::{self, Renderer}; +use pyo3::create_exception; +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use std::error::Error; +use std::ffi::OsStr; +use std::fmt::Write as _; +use std::os::unix::ffi::OsStrExt as _; + +create_exception!(hittekaart_py, HitteError, pyo3::exceptions::PyException); + +fn err_to_py(mut error: &dyn Error) -> PyErr { + let mut text = error.to_string(); + loop { + match error.source() { + None => break, + Some(e) => error = e, + } + write!(&mut text, "\ncaused by: {error}").unwrap(); + } + HitteError::new_err(text) +} + +#[pyclass] +#[derive(Debug, Clone)] +struct Track { + inner: Vec<Coordinates>, +} + +#[pymethods] +impl Track { + #[staticmethod] + fn from_file(path: &[u8], compression: Option<&str>) -> PyResult<Track> { + let compression = match compression { + None | Some("") => Compression::None, + Some("gzip") => Compression::Gzip, + Some("brotli") => Compression::Brotli, + Some(x) => return Err(HitteError::new_err(format!("invalid compression: {x}"))), + }; + let track = gpx::extract_from_file(OsStr::from_bytes(path), compression) + .map_err(|e| err_to_py(&e))?; + Ok(Track { inner: track }) + } + + #[staticmethod] + fn from_coordinates(coordinates: Vec<(f64, f64)>) -> Track { + Track { + inner: coordinates + .iter() + .map(|(lon, lat)| Coordinates::new(*lon, *lat)) + .collect(), + } + } + + fn coordinates(&self) -> Vec<(f64, f64)> { + self.inner + .iter() + .map(|c| (c.longitude, c.latitude)) + .collect() + } +} + +#[pyclass] +struct Storage { + inner: Box<dyn hittekaart::storage::Storage + Send + Sync>, +} + +#[pymethods] +impl Storage { + #[staticmethod] + #[pyo3(name = "Folder")] + fn folder(path: &[u8]) -> Self { + let path = OsStr::from_bytes(path); + let storage = hittekaart::storage::Folder::new(path.into()); + Storage { + inner: Box::new(storage), + } + } +} + +#[pyclass] +struct HeatmapRenderer { + inner: renderer::heatmap::Renderer, +} + +#[pymethods] +impl HeatmapRenderer { + #[new] + fn new() -> HeatmapRenderer { + HeatmapRenderer { + inner: renderer::heatmap::Renderer, + } + } +} + +#[pyfunction] +fn generate( + items: &Bound<'_, PyAny>, + renderer: &Bound<'_, PyAny>, + storage: &Bound<'_, Storage>, +) -> PyResult<()> { + let mut tracks = vec![]; + + for item in items.try_iter()? { + let item = item?; + tracks.push(item.extract::<Track>()?.inner); + } + + if let Ok(r) = renderer.downcast::<HeatmapRenderer>() { + do_generate(tracks, &r.borrow().inner, &mut storage.borrow_mut()) + } else { + Err(PyTypeError::new_err("Expected a HeatmapRenderer")) + } +} + +fn do_generate<R: Renderer>( + tracks: Vec<Vec<Coordinates>>, + renderer: &R, + storage: &mut Storage, +) -> PyResult<()> { + storage.inner.prepare().map_err(|e| err_to_py(&e))?; + + for zoom in 0..=19 { + let counter = + renderer::prepare(renderer, zoom, &tracks, || Ok(())).map_err(|e| err_to_py(&e))?; + + storage + .inner + .prepare_zoom(zoom) + .map_err(|e| err_to_py(&e))?; + + renderer::colorize(renderer, counter, |rendered_tile| { + storage + .inner + .store(zoom, rendered_tile.x, rendered_tile.y, &rendered_tile.data)?; + Ok(()) + }) + .map_err(|e| err_to_py(&e))?; + } + storage.inner.finish().map_err(|e| err_to_py(&e))?; + + Ok(()) +} + +/// A Python module implemented in Rust. +#[pymodule] +fn hittekaart_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::<Track>()?; + m.add_class::<HeatmapRenderer>()?; + m.add_class::<Storage>()?; + m.add_function(wrap_pyfunction!(generate, m)?)?; + m.add("HitteError", py.get_type::<HitteError>())?; + Ok(()) +} diff --git a/hittekaart/src/gpx.rs b/hittekaart/src/gpx.rs index 98019c3..68bd6fb 100644 --- a/hittekaart/src/gpx.rs +++ b/hittekaart/src/gpx.rs @@ -22,8 +22,8 @@ use super::error::{Error, Result}; /// World coordinates. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Coordinates { - longitude: f64, - latitude: f64, + pub longitude: f64, + pub latitude: f64, } impl Coordinates { |