aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-08-02 15:19:16 +0200
committerDaniel Schadt <kingdread@gmx.de>2025-08-02 15:19:16 +0200
commit64bd6e7abd420ba5b8e35589d88642a1931a44f6 (patch)
treeedfcc1a2ed9fa22c68b5592a453d76d97f6ef96c
parenta4784d5b783484e2009acf466f6af09f80d0b3b0 (diff)
downloadhittekaart-64bd6e7abd420ba5b8e35589d88642a1931a44f6.tar.gz
hittekaart-64bd6e7abd420ba5b8e35589d88642a1931a44f6.tar.bz2
hittekaart-64bd6e7abd420ba5b8e35589d88642a1931a44f6.zip
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!
-rw-r--r--README.adoc5
-rw-r--r--hittekaart-py/src/lib.rs126
2 files changed, 93 insertions, 38 deletions
diff --git a/README.adoc b/README.adoc
index 5479979..65256fd 100644
--- a/README.adoc
+++ b/README.adoc
@@ -273,15 +273,16 @@ program may look like:
[source,python]
----
from hittekaart_py import (
- Track, HeatmapRenderer, Storage, generate
+ Track, HeatmapRenderer, Settings, Storage, generate
)
+settings = Settings(threads=3)
tracks = [
Track.from_file(b"Documents/track.gpx", None),
Track.from_coordinates([(45.0, 47.0)]),
]
storage = Storage.Sqlite(b"/tmp/tiles.sqlite")
-generate(tracks, HeatmapRenderer(), storage)
+generate(settings, tracks, HeatmapRenderer(), storage)
----
== BUGS
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<Box<dyn hittekaart::storage::Storage>> {
+ fn open(&self) -> PyResult<Box<dyn hittekaart::storage::Storage + Send>> {
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::<Track>()?.inner);
}
- if let Ok(r) = renderer.downcast::<HeatmapRenderer>() {
- do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?)
- } else if let Ok(r) = renderer.downcast::<MarktileRenderer>() {
- do_generate(tracks, &r.borrow().inner, &mut *storage.borrow().open()?)
- } else if let Ok(r) = renderer.downcast::<TilehuntRenderer>() {
- 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()? =>
+ <HeatmapRenderer>,
+ <MarktileRenderer>,
+ <TilehuntRenderer>,
}
}
fn do_generate<R: Renderer>(
+ settings: &Settings,
tracks: Vec<Vec<Coordinates>>,
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::<MarktileRenderer>()?;
m.add_class::<TilehuntRenderer>()?;
m.add_class::<Storage>()?;
+ m.add_class::<Settings>()?;
m.add_function(wrap_pyfunction!(generate, m)?)?;
- m.add_function(wrap_pyfunction!(set_threads, m)?)?;
m.add("HitteError", py.get_type::<HitteError>())?;
Ok(())
}