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 {  | 
