From 8247f7d7e1fe30debf738435b3cdccdd15c24e2f Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 9 Jul 2025 19:48:43 +0200 Subject: first working Python interface --- hittekaart-py/src/lib.rs | 155 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 hittekaart-py/src/lib.rs (limited to 'hittekaart-py/src/lib.rs') 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, +} + +#[pymethods] +impl Track { + #[staticmethod] + fn from_file(path: &[u8], compression: Option<&str>) -> PyResult { + 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, +} + +#[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::()?.inner); + } + + if let Ok(r) = renderer.downcast::() { + do_generate(tracks, &r.borrow().inner, &mut storage.borrow_mut()) + } else { + Err(PyTypeError::new_err("Expected a HeatmapRenderer")) + } +} + +fn do_generate( + tracks: Vec>, + 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::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(generate, m)?)?; + m.add("HitteError", py.get_type::())?; + Ok(()) +} -- cgit v1.2.3 From d839b9b7950c949eaddb967495f533e99a8dcafb Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 28 Jul 2025 20:35:11 +0200 Subject: implement sqlite output in hittekaart-py We cannot use a Box anymore, because the Sqlite connection is not thread-safe. Therefore, we use a normal enum and open the connection late. --- hittekaart-py/src/lib.rs | 55 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 17 deletions(-) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index 9f4bd8d..4b67777 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -7,6 +7,7 @@ use std::error::Error; use std::ffi::OsStr; use std::fmt::Write as _; use std::os::unix::ffi::OsStrExt as _; +use std::path::PathBuf; create_exception!(hittekaart_py, HitteError, pyo3::exceptions::PyException); @@ -61,20 +62,45 @@ impl Track { } } -#[pyclass] -struct Storage { - inner: Box, +#[derive(Debug, Clone, PartialEq, Eq)] +enum StorageType { + Folder(PathBuf), + Sqlite(PathBuf), } +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq)] +struct Storage(StorageType); + #[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), + Storage(StorageType::Folder(path.into())) + } + + #[staticmethod] + #[pyo3(name = "Sqlite")] + fn sqlite(path: &[u8]) -> Self { + let path = OsStr::from_bytes(path); + Storage(StorageType::Sqlite(path.into())) + } +} + +impl Storage { + fn open(&self) -> PyResult> { + match self.0 { + StorageType::Folder(ref path) => { + let storage = hittekaart::storage::Folder::new(path.clone()); + Ok(Box::new(storage)) + } + StorageType::Sqlite(ref path) => { + let storage = hittekaart::storage::Sqlite::connect(path.clone()) + .map_err(|e| err_to_py(&e))?; + Ok(Box::new(storage)) + } } } } @@ -108,7 +134,7 @@ fn generate( } if let Ok(r) = renderer.downcast::() { - do_generate(tracks, &r.borrow().inner, &mut storage.borrow_mut()) + do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) } else { Err(PyTypeError::new_err("Expected a HeatmapRenderer")) } @@ -117,28 +143,23 @@ fn generate( fn do_generate( tracks: Vec>, renderer: &R, - storage: &mut Storage, + storage: &mut dyn hittekaart::storage::Storage, ) -> PyResult<()> { - storage.inner.prepare().map_err(|e| err_to_py(&e))?; + storage.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))?; + storage.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)?; + storage.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))?; + storage.finish().map_err(|e| err_to_py(&e))?; Ok(()) } -- cgit v1.2.3 From c0c95e4bc5f50929499f0070ab79801ea9f52218 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 28 Jul 2025 22:05:36 +0200 Subject: expose function to set thread count --- hittekaart-py/src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index 4b67777..2ad0e56 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -164,6 +164,14 @@ fn do_generate( Ok(()) } +#[pyfunction] +fn set_threads(threads: usize) -> PyResult<()> { + rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build_global() + .map_err(|e| err_to_py(&e)) +} + /// A Python module implemented in Rust. #[pymodule] fn hittekaart_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -171,6 +179,7 @@ fn hittekaart_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(generate, m)?)?; + m.add_function(wrap_pyfunction!(set_threads, m)?)?; m.add("HitteError", py.get_type::())?; Ok(()) } -- cgit v1.2.3 From 8fa97cc60ebe04bba99c2f16465772808f533dc8 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 28 Jul 2025 22:34:22 +0200 Subject: add some docstrings They are not really visible but useful documentation anyway. --- hittekaart-py/src/lib.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index 2ad0e56..782f9e7 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -1,3 +1,6 @@ +//! Python bindings for hittekaart. +//! +//! This module provides simple-to-use bindings for hittekaart generation from Python scripts. use hittekaart::gpx::{self, Compression, Coordinates}; use hittekaart::renderer::{self, Renderer}; use pyo3::create_exception; @@ -11,6 +14,11 @@ use std::path::PathBuf; create_exception!(hittekaart_py, HitteError, pyo3::exceptions::PyException); +/// Converts an error to a Python error. +/// +/// This basically maps everything to [`HitteError`] and provides a stringified error explanation. +/// +/// This recursively uses `::source()` to walk the chain. fn err_to_py(mut error: &dyn Error) -> PyErr { let mut text = error.to_string(); loop { @@ -23,6 +31,7 @@ fn err_to_py(mut error: &dyn Error) -> PyErr { HitteError::new_err(text) } +/// Python representation of a track. #[pyclass] #[derive(Debug, Clone)] struct Track { @@ -31,6 +40,9 @@ struct Track { #[pymethods] impl Track { + /// Load a track from a file. + /// + /// Compression - if given - should be one of the strings "gzip" or "brotli". #[staticmethod] fn from_file(path: &[u8], compression: Option<&str>) -> PyResult { let compression = match compression { @@ -44,6 +56,7 @@ impl Track { Ok(Track { inner: track }) } + /// Load a track from the given coordinates. #[staticmethod] fn from_coordinates(coordinates: Vec<(f64, f64)>) -> Track { Track { @@ -53,7 +66,9 @@ impl Track { .collect(), } } +} +impl Track { fn coordinates(&self) -> Vec<(f64, f64)> { self.inner .iter() @@ -68,12 +83,14 @@ enum StorageType { Sqlite(PathBuf), } +/// Python representation of a storage target. #[pyclass] #[derive(Debug, Clone, PartialEq, Eq)] struct Storage(StorageType); #[pymethods] impl Storage { + /// Output to the given folder. #[staticmethod] #[pyo3(name = "Folder")] fn folder(path: &[u8]) -> Self { @@ -81,6 +98,7 @@ impl Storage { Storage(StorageType::Folder(path.into())) } + /// Output to the given sqlite file. #[staticmethod] #[pyo3(name = "Sqlite")] fn sqlite(path: &[u8]) -> Self { @@ -105,6 +123,7 @@ impl Storage { } } +/// Python representation of a heatmap renderer. #[pyclass] struct HeatmapRenderer { inner: renderer::heatmap::Renderer, @@ -120,6 +139,11 @@ impl HeatmapRenderer { } } +/// Generate the heatmap. +/// +/// * `items` is an iterable of [`Track`]s +/// * `renderer` should be a renderer (like [`HeatmapRenderer`]) +/// * `storage` is the [`Storage`] output #[pyfunction] fn generate( items: &Bound<'_, PyAny>, -- cgit v1.2.3 From 8025f682fdd49b4e52b4356952ec1f64890d6c4c Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 29 Jul 2025 21:46:30 +0200 Subject: more docs They are actually visible as Python docstrings, so they are useful! --- hittekaart-py/src/lib.rs | 68 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 18 deletions(-) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index 782f9e7..8cd51d9 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -31,7 +31,13 @@ fn err_to_py(mut error: &dyn Error) -> PyErr { HitteError::new_err(text) } -/// Python representation of a track. +/// Represents a track. +/// +/// This is what you want to load in order to render the heatmaps. +/// +/// Tracks can be loaded from GPX files (see Track.from_file()) or from in-memory coordinates +/// (Track.from_coordinates()). Otherwise, tracks should be treated as opaque objects, whose only +/// purpose is to be passed to generate(). #[pyclass] #[derive(Debug, Clone)] struct Track { @@ -42,7 +48,10 @@ struct Track { impl Track { /// Load a track from a file. /// - /// Compression - if given - should be one of the strings "gzip" or "brotli". + /// The path should be given as a bytes object. + /// + /// The compression parameter - if given - should be one of the strings "gzip" or "brotli". It + /// can be set to None to use no compression. #[staticmethod] fn from_file(path: &[u8], compression: Option<&str>) -> PyResult { let compression = match compression { @@ -57,6 +66,9 @@ impl Track { } /// Load a track from the given coordinates. + /// + /// The coordinates should be a list of (longitude, latitude) tuples, where longitude and + /// latitude are represented as floats. #[staticmethod] fn from_coordinates(coordinates: Vec<(f64, f64)>) -> Track { Track { @@ -68,22 +80,18 @@ impl Track { } } -impl Track { - fn coordinates(&self) -> Vec<(f64, f64)> { - self.inner - .iter() - .map(|c| (c.longitude, c.latitude)) - .collect() - } -} - #[derive(Debug, Clone, PartialEq, Eq)] enum StorageType { Folder(PathBuf), Sqlite(PathBuf), } -/// Python representation of a storage target. +/// Represents a storage target. +/// +/// Hittekaart can store tiles either in folders (easy-to-use, but wasteful), or as a SQLite +/// database (more space-efficient). +/// +/// See hittekaart's README for more detailed information. #[pyclass] #[derive(Debug, Clone, PartialEq, Eq)] struct Storage(StorageType); @@ -91,6 +99,9 @@ struct Storage(StorageType); #[pymethods] impl Storage { /// Output to the given folder. + /// + /// This will create files path/{z}/{x}/{y}.png, where {z} is the zoom level, and {x} and {y} + /// are the tile coordinates. #[staticmethod] #[pyo3(name = "Folder")] fn folder(path: &[u8]) -> Self { @@ -98,7 +109,11 @@ impl Storage { Storage(StorageType::Folder(path.into())) } - /// Output to the given sqlite file. + /// Output to the given SQLite file. + /// + /// This will create a single table 'tiles' with the columns 'zoom', 'x', 'y' and 'data'. + /// + /// Note that you cannot "append" to an existing database, it must be a non-existing file. #[staticmethod] #[pyo3(name = "Sqlite")] fn sqlite(path: &[u8]) -> Self { @@ -123,7 +138,9 @@ impl Storage { } } -/// Python representation of a heatmap renderer. +/// A renderer that produces a heatmap. +/// +/// The constructor takes no parameters: HeatmapRenderer() #[pyclass] struct HeatmapRenderer { inner: renderer::heatmap::Renderer, @@ -141,9 +158,9 @@ impl HeatmapRenderer { /// Generate the heatmap. /// -/// * `items` is an iterable of [`Track`]s -/// * `renderer` should be a renderer (like [`HeatmapRenderer`]) -/// * `storage` is the [`Storage`] output +/// * items is an iterable of Track +/// * renderer should be one of the renderers (such as HeatmapRenderer) +/// * storage is the Storage output #[pyfunction] fn generate( items: &Bound<'_, PyAny>, @@ -188,6 +205,12 @@ fn do_generate( Ok(()) } +/// Set the number of threads that hittekaart will use. +/// +/// Note that this is a global function, it will affect all subsequent calls. +/// +/// Note further that you may only call this function once, at startup. Calls after the thread pool +/// has been initialized (e.g. via a generate() or set_threads() call) will raise an exception. #[pyfunction] fn set_threads(threads: usize) -> PyResult<()> { rayon::ThreadPoolBuilder::new() @@ -196,7 +219,16 @@ fn set_threads(threads: usize) -> PyResult<()> { .map_err(|e| err_to_py(&e)) } -/// A Python module implemented in Rust. +/// Python bindings for the hittekaart heatmap tile generator. +/// +/// hittekaart renders GPS tracks (usually from GPX files) to heatmap tiles for consumption with +/// OpenStreetMap data. +/// +/// You can find more documentation about hittekaart in its README: +/// +/// +/// You can find rendered docs as part of fietsboek's documentation: +/// #[pymodule] fn hittekaart_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; -- cgit v1.2.3 From a4784d5b783484e2009acf466f6af09f80d0b3b0 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 29 Jul 2025 22:31:53 +0200 Subject: implement other renderers in python module --- hittekaart-py/src/lib.rs | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index 8cd51d9..fe97aa7 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -156,6 +156,44 @@ impl HeatmapRenderer { } } +/// A renderer that only marks visited tiles. +/// +/// The constructor takes no parameters: MarktileRenderer() +#[pyclass] +struct MarktileRenderer { + inner: renderer::marktile::Renderer, +} + +#[pymethods] +impl MarktileRenderer { + #[new] + fn new() -> MarktileRenderer { + MarktileRenderer { + inner: renderer::marktile::Renderer, + } + } +} + +/// A renderer that renders a "tile hunt". +/// +/// A tile hunt marks visited tiles on a fixed zoom level. +/// +/// The constructor takes the tile hunt level as parameter: TilehuntRenderer(zoom) +#[pyclass] +struct TilehuntRenderer { + inner: renderer::tilehunt::Renderer, +} + +#[pymethods] +impl TilehuntRenderer { + #[new] + fn new(zoom: u32) -> TilehuntRenderer { + TilehuntRenderer { + inner: renderer::tilehunt::Renderer::new(zoom), + } + } +} + /// Generate the heatmap. /// /// * items is an iterable of Track @@ -176,8 +214,12 @@ fn generate( if let Ok(r) = renderer.downcast::() { do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) + } else if let Ok(r) = renderer.downcast::() { + do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) + } else if let Ok(r) = renderer.downcast::() { + do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) } else { - Err(PyTypeError::new_err("Expected a HeatmapRenderer")) + Err(PyTypeError::new_err("Expected a HeatmapRenderer, MarktileRenderer or TilehuntRenderer")) } } @@ -233,6 +275,8 @@ fn set_threads(threads: usize) -> PyResult<()> { fn hittekaart_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(generate, m)?)?; m.add_function(wrap_pyfunction!(set_threads, m)?)?; -- cgit v1.2.3 From 64bd6e7abd420ba5b8e35589d88642a1931a44f6 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 2 Aug 2025 15:19:16 +0200 Subject: custom min_zoom/max_zoom/threads This replaces the set_threads function, and works even better because we don't rely on a global thread pool! --- hittekaart-py/src/lib.rs | 126 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 36 deletions(-) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index fe97aa7..2f99793 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -123,7 +123,7 @@ impl Storage { } impl Storage { - fn open(&self) -> PyResult> { + fn open(&self) -> PyResult> { match self.0 { StorageType::Folder(ref path) => { let storage = hittekaart::storage::Folder::new(path.clone()); @@ -194,13 +194,72 @@ impl TilehuntRenderer { } } +/// Tile generation settings. +/// +/// This contains everything that is overarching to the renderers and output modules, such as zoom +/// levels and thread count. +#[pyclass] +struct Settings { + #[pyo3(get, set)] + /// Smallest zoom level that will be generated. + min_zoom: u32, + + #[pyo3(get, set)] + /// Largest zoom level that will be generated. + max_zoom: u32, + + #[pyo3(get, set)] + /// How many threads to use for generation. + /// + /// A count of 0 will automatically use as many threads as you have CPU cores. + threads: u32, +} + +#[pymethods] +impl Settings { + #[new] + #[pyo3(signature = (min_zoom = 1, max_zoom = 19, threads = 0))] + fn new(min_zoom: u32, max_zoom: u32, threads: u32) -> Settings { + Settings { + min_zoom, + max_zoom, + threads, + } + } + + fn __repr__(&self) -> String { + format!( + "Settings(min_zoom={}, max_zoom={}, threads={})", + self.min_zoom, self.max_zoom, self.threads + ) + } +} + +macro_rules! dispatch_generate { + ($settings:expr, $tracks:expr, $renderer:expr, $storage:expr => <$type:ty>, $(<$types:ty>,)*) => { + if let Ok(r) = $renderer.downcast::<$type>() { + do_generate($settings, $tracks, &r.borrow().inner, $storage) + } else { + dispatch_generate!($settings, $tracks, $renderer, $storage => $(<$types>,)*) + } + }; + + ($settings:expr, $tracks:expr, $renderer:expr, $storage:expr =>) => { + Err(PyTypeError::new_err( + "Expected a HeatmapRenderer, MarktileRenderer or TilehuntRenderer", + )) + }; +} + /// Generate the heatmap. /// +/// * settings are the rendering Settings /// * items is an iterable of Track /// * renderer should be one of the renderers (such as HeatmapRenderer) /// * storage is the Storage output #[pyfunction] fn generate( + settings: &Bound<'_, Settings>, items: &Bound<'_, PyAny>, renderer: &Bound<'_, PyAny>, storage: &Bound<'_, Storage>, @@ -212,53 +271,48 @@ fn generate( tracks.push(item.extract::()?.inner); } - if let Ok(r) = renderer.downcast::() { - do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) - } else if let Ok(r) = renderer.downcast::() { - do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) - } else if let Ok(r) = renderer.downcast::() { - do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?) - } else { - Err(PyTypeError::new_err("Expected a HeatmapRenderer, MarktileRenderer or TilehuntRenderer")) + let settings = &*settings.borrow(); + + // We cannot easily do dynamic dispatch here, because Renderer::Prepared exists. Maybe this can + // change in the future, but for now we have to stick to this: + dispatch_generate! { + settings, tracks, renderer, &mut *storage.borrow().open()? => + , + , + , } } fn do_generate( + settings: &Settings, tracks: Vec>, renderer: &R, - storage: &mut dyn hittekaart::storage::Storage, + storage: &mut (dyn hittekaart::storage::Storage + Send), ) -> PyResult<()> { storage.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))?; + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(settings.threads.try_into().unwrap()) + .build() + .map_err(|e| err_to_py(&e))?; - storage.prepare_zoom(zoom).map_err(|e| err_to_py(&e))?; + pool.install(|| { + for zoom in settings.min_zoom..=settings.max_zoom { + let counter = + renderer::prepare(renderer, zoom, &tracks, || Ok(())).map_err(|e| err_to_py(&e))?; - renderer::colorize(renderer, counter, |rendered_tile| { - storage.store(zoom, rendered_tile.x, rendered_tile.y, &rendered_tile.data)?; - Ok(()) - }) - .map_err(|e| err_to_py(&e))?; - } - storage.finish().map_err(|e| err_to_py(&e))?; + storage.prepare_zoom(zoom).map_err(|e| err_to_py(&e))?; - Ok(()) -} + renderer::colorize(renderer, counter, |rendered_tile| { + storage.store(zoom, rendered_tile.x, rendered_tile.y, &rendered_tile.data)?; + Ok(()) + }) + .map_err(|e| err_to_py(&e))?; + } + storage.finish().map_err(|e| err_to_py(&e))?; -/// Set the number of threads that hittekaart will use. -/// -/// Note that this is a global function, it will affect all subsequent calls. -/// -/// Note further that you may only call this function once, at startup. Calls after the thread pool -/// has been initialized (e.g. via a generate() or set_threads() call) will raise an exception. -#[pyfunction] -fn set_threads(threads: usize) -> PyResult<()> { - rayon::ThreadPoolBuilder::new() - .num_threads(threads) - .build_global() - .map_err(|e| err_to_py(&e)) + Ok(()) + }) } /// Python bindings for the hittekaart heatmap tile generator. @@ -278,8 +332,8 @@ fn hittekaart_py(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(generate, m)?)?; - m.add_function(wrap_pyfunction!(set_threads, m)?)?; m.add("HitteError", py.get_type::())?; Ok(()) } -- cgit v1.2.3 From 6da6f0b5ce4cfb41aaf79699e289be678007e2ad Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 2 Aug 2025 15:29:40 +0200 Subject: string repr for Track and Storage --- hittekaart-py/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'hittekaart-py/src/lib.rs') diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index 2f99793..f14515a 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -78,6 +78,10 @@ impl Track { .collect(), } } + + fn __repr__(&self) -> String { + format!("", self.inner.len()) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -120,6 +124,13 @@ impl Storage { let path = OsStr::from_bytes(path); Storage(StorageType::Sqlite(path.into())) } + + fn __repr__(&self) -> String { + match self.0 { + StorageType::Folder(ref path) => format!("", path.display()), + StorageType::Sqlite(ref path) => format!("", path.display()), + } + } } impl Storage { -- cgit v1.2.3