From c9b4e999ee2eac4b95ee4a7560fb10cad98ebb09 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 11 Aug 2026 13:42:08 +0200 Subject: allow for custom name & description in MbTiles --- hittekaart/src/storage.rs | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs index 6cb3629..8bc8f6e 100644 --- a/hittekaart/src/storage.rs +++ b/hittekaart/src/storage.rs @@ -227,6 +227,8 @@ impl Storage for OsmAnd { } } +static MBTILES_DEFAULT_NAME: &str = "Heatmap"; + /// MBTiles storage (SQLite backed). /// /// This stores tiles into a SQLite database with the following tables: @@ -246,14 +248,21 @@ impl Storage for OsmAnd { /// meaning that `tile_row = 2^zoom - 1 - y`. /// /// The metadata table will contain two rows, one with `name = "name"` and one with `name = -/// "format"`. You can set a custom name/title of the map with the following SQL statement: +/// "format"`. You can set a custom title and description of the map using the [MbTiles::with_name] +/// and [MbTiles::with_description] methods. +/// +/// Alternatively, you can set a custom name/title of the map afterwards with the following SQL +/// statements: /// /// ```sql -/// UPDATE metadata SET value = "My cool heatmap!" WHERE name = "name"; +/// UPDATE metadata SET value = 'My cool heatmap!' WHERE name = 'name'; +/// INSERT INTO metadata(name, value) VALUES ('description', 'This is a collection of my trips!'); /// ``` #[derive(Debug)] pub struct MbTiles { connection: Connection, + name: String, + description: Option, } impl MbTiles { @@ -267,7 +276,25 @@ impl MbTiles { return Err(Error::OutputAlreadyExists(path.to_path_buf())); } let connection = Connection::open(path)?; - Ok(MbTiles { connection }) + Ok(MbTiles { + connection, + name: MBTILES_DEFAULT_NAME.into(), + description: None, + }) + } + + pub fn with_name(self, name: String) -> Self { + MbTiles { + name, + .. self + } + } + + pub fn with_description(self, description: Option) -> Self { + MbTiles { + description, + .. self + } } } @@ -315,12 +342,18 @@ impl Storage for MbTiles { fn finish(&mut self) -> Result<()> { self.connection.execute( "INSERT INTO metadata (name, value) VALUES (?, ?);", - params!["name", "Heatmap"], + params!["name", &self.name], )?; self.connection.execute( "INSERT INTO metadata (name, value) VALUES (?, ?);", params!["format", "png"], )?; + if let Some(desc) = &self.description { + self.connection.execute( + "INSERT INTO metadata (name, value) VALUES (?, ?);", + params!["description", desc], + )?; + } self.connection.execute("COMMIT;", ())?; Ok(()) } -- cgit v1.2.3 From 064d5baba348fdb7a9bdd8d3d6ab411ed599c42c Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 11 Aug 2026 13:48:30 +0200 Subject: fix wrong link --- hittekaart/src/storage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs index 8bc8f6e..d52a900 100644 --- a/hittekaart/src/storage.rs +++ b/hittekaart/src/storage.rs @@ -2,8 +2,8 @@ //! //! The main trait to use here is [`Storage`], which provides the necessary interface to store //! tiles. Usually you want to have a `dyn Storage`, and then instantiate it with a concrete -//! implementation (either [`Folder`] or [`Sqlite`]), depending on the command line flags or -//! similar. +//! implementation (either [`Folder`], [`OsmAnd`] or [`MbTiles`]), depending on the command line +//! flags or similar. use rusqlite::{params, Connection}; use std::{ fs, -- cgit v1.2.3 From b1d4e3151667f6e1d7942435374aac8ab6b6469d Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 11 Aug 2026 15:19:02 +0200 Subject: add name/description to Python MbTiles struct --- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 207 bytes hittekaart-py/hittekaart_py/hittekaart_py.pyi | 2 +- hittekaart-py/src/lib.rs | 18 +++++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc diff --git a/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc b/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..11caa7e Binary files /dev/null and b/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc differ diff --git a/hittekaart-py/hittekaart_py/hittekaart_py.pyi b/hittekaart-py/hittekaart_py/hittekaart_py.pyi index b3d110c..00ed13c 100644 --- a/hittekaart-py/hittekaart_py/hittekaart_py.pyi +++ b/hittekaart-py/hittekaart_py/hittekaart_py.pyi @@ -17,7 +17,7 @@ class Storage: def OsmAnd(path: bytes) -> "Storage": ... @staticmethod - def MbTiles(path: bytes) -> "Storage": ... + def MbTiles(path: bytes, name: str | None, description: str | None) -> "Storage": ... class HeatmapRenderer: diff --git a/hittekaart-py/src/lib.rs b/hittekaart-py/src/lib.rs index b93d9d6..3a9b3ed 100644 --- a/hittekaart-py/src/lib.rs +++ b/hittekaart-py/src/lib.rs @@ -88,7 +88,7 @@ impl Track { enum StorageType { Folder(PathBuf), OsmAnd(PathBuf), - MbTiles(PathBuf), + MbTiles(PathBuf, Option, Option), } /// Represents a storage target. @@ -125,17 +125,17 @@ impl Storage { } #[staticmethod] - #[pyo3(name = "MbTiles")] - fn mbtiles(path: &[u8]) -> Self { + #[pyo3(name = "MbTiles", signature = (path, name = None, description = None))] + fn mbtiles(path: &[u8], name: Option, description: Option) -> Self { let path = OsStr::from_bytes(path); - Storage(StorageType::MbTiles(path.into())) + Storage(StorageType::MbTiles(path.into(), name, description)) } fn __repr__(&self) -> String { match self.0 { StorageType::Folder(ref path) => format!("", path.display()), StorageType::OsmAnd(ref path) => format!("", path.display()), - StorageType::MbTiles(ref path) => format!("", path.display()), + StorageType::MbTiles(ref path, ..) => format!("", path.display()), } } } @@ -152,9 +152,13 @@ impl Storage { .map_err(|e| err_to_py(&e))?; Ok(Box::new(storage)) } - StorageType::MbTiles(ref path) => { - let storage = hittekaart::storage::MbTiles::open(path.clone()) + StorageType::MbTiles(ref path, ref name, ref desc) => { + let mut storage = hittekaart::storage::MbTiles::open(path.clone()) .map_err(|e| err_to_py(&e))?; + if let Some(n) = name { + storage = storage.with_name(n.clone()); + } + storage = storage.with_description(desc.clone()); Ok(Box::new(storage)) } } -- cgit v1.2.3 From 58a69ea8688b4d1ec7c70caffaf30a608bccd4bc Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 11 Aug 2026 16:07:39 +0200 Subject: add integration tests for the Rust side of things --- hittekaart/tests/generate.rs | 97 +++++++++++++++++++++++++++++++++ test-assets/Synthetic_BRouter_1.gpx.gz | Bin 0 -> 697 bytes 2 files changed, 97 insertions(+) create mode 100644 hittekaart/tests/generate.rs create mode 100644 test-assets/Synthetic_BRouter_1.gpx.gz diff --git a/hittekaart/tests/generate.rs b/hittekaart/tests/generate.rs new file mode 100644 index 0000000..3c80a11 --- /dev/null +++ b/hittekaart/tests/generate.rs @@ -0,0 +1,97 @@ +use std::{ + collections::hash_map::RandomState, + env, + error::Error, + fs, + hash::{BuildHasher, Hasher}, + path::PathBuf, +}; + +use hittekaart::{ + gpx, + renderer::{self, heatmap}, + storage::{self, Storage}, +}; + +const ZOOM: u32 = 10; + +fn prepare() -> heatmap::HeatCounter { + let track = gpx::extract_from_file("../test-assets/Synthetic_BRouter_1.gpx.gz", gpx::Compression::Gzip).unwrap(); + renderer::prepare( + &heatmap::Renderer, + ZOOM, + &[track], + || Ok(()), + ).unwrap() +} + +#[test] +fn test_osmand() { + let heat = prepare(); + let mut store = storage::OsmAnd::open(":memory:").unwrap(); + store.prepare().unwrap(); + store.prepare_zoom(ZOOM).unwrap(); + renderer::colorize( + &heatmap::Renderer, + heat, + |tile| { + store.store(ZOOM, tile.x, tile.y, &tile.data) + }, + ).unwrap(); + store.finish().unwrap(); +} + +#[test] +fn test_mbtiles() { + let heat = prepare(); + let mut store = storage::MbTiles::open(":memory:").unwrap() + .with_name("new name".into()) + .with_description(Some("new description".into())); + store.prepare().unwrap(); + store.prepare_zoom(ZOOM).unwrap(); + renderer::colorize( + &heatmap::Renderer, + heat, + |tile| { + store.store(ZOOM, tile.x, tile.y, &tile.data) + }, + ).unwrap(); + store.finish().unwrap(); +} + +// Zero dependency random number generator +fn rand() -> u64 { + RandomState::new().build_hasher().finish() +} + +fn run_folder(base: PathBuf) -> Result<(), Box> { + let heat = prepare(); + let mut store = storage::Folder::new(base); + store.prepare()?; + store.prepare_zoom(ZOOM)?; + renderer::colorize( + &heatmap::Renderer, + heat, + |tile| { + store.store(ZOOM, tile.x, tile.y, &tile.data) + }, + )?; + store.finish()?; + Ok(()) +} + +#[test] +fn test_folder() { + let mut tmp = env::temp_dir(); + tmp.push(&format!("hitte_test_{}", rand())); + + fs::create_dir(&tmp).unwrap(); + + let result = run_folder(tmp.clone()); + + fs::remove_dir_all(&tmp).unwrap(); + + if let Err(e) = result { + panic!("{}", e); + } +} diff --git a/test-assets/Synthetic_BRouter_1.gpx.gz b/test-assets/Synthetic_BRouter_1.gpx.gz new file mode 100644 index 0000000..e380fc7 Binary files /dev/null and b/test-assets/Synthetic_BRouter_1.gpx.gz differ -- cgit v1.2.3 From bfdb5b970f313e7cb64cd7e2b34394fea5358d6b Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 17 Aug 2026 20:17:47 +0200 Subject: add Python tests mostly to check that the API and the calling works --- .woodpecker/tests.yaml | 4 ++- hittekaart-py/tests/test_api.py | 71 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 hittekaart-py/tests/test_api.py diff --git a/.woodpecker/tests.yaml b/.woodpecker/tests.yaml index 4ca95a0..697a524 100644 --- a/.woodpecker/tests.yaml +++ b/.woodpecker/tests.yaml @@ -5,8 +5,10 @@ steps: - name: test image: rust commands: - - apt update && apt install -y python3-dev + - apt update && apt install -y python3-dev python3-pip + - pip3 install ./hittekaart-py pytest - cargo test + - cd hittekaart-py && pytest - name: compile-benches image: rust commands: diff --git a/hittekaart-py/tests/test_api.py b/hittekaart-py/tests/test_api.py new file mode 100644 index 0000000..1ea51c2 --- /dev/null +++ b/hittekaart-py/tests/test_api.py @@ -0,0 +1,71 @@ +from hittekaart_py import ( + Track, HeatmapRenderer, MarktileRenderer, TilehuntRenderer, Settings, Storage, generate +) + + +def tracks(): + return [ + Track.from_file(b"../test-assets/Synthetic_BRouter_1.gpx.gz", "gzip"), + ] + + +def storage(): + return Storage.MbTiles(b":memory:", "heat", "mappy") + + +def test_track_from_file(): + track = Track.from_file(b"../test-assets/Synthetic_BRouter_1.gpx.gz", "gzip") + assert track is not None + + +def test_track_from_coordinates(): + track = Track.from_coordinates([(1.0, 2.0), (3.0, 4.0)]) + assert track is not None + + +def test_storage_folder(): + storage = Storage.Folder(b"/tmp") + assert storage is not None + + +def test_storage_osmand(): + storage = Storage.OsmAnd(b":memory:") + assert storage is not None + + +def test_storage_mbtiles(): + storage = Storage.MbTiles(b":memory:") + assert storage is not None + + storage = Storage.MbTiles(b":memory:", "Heatmap") + assert storage is not None + + storage = Storage.MbTiles(b":memory:", "Heatmap", "This is my heatmap") + assert storage is not None + + +def test_generate_heatmap(): + generate( + Settings(), + tracks(), + HeatmapRenderer(), + storage(), + ) + + +def test_generate_marktile(): + generate( + Settings(), + tracks(), + MarktileRenderer(), + storage(), + ) + + +def test_generate_tilehunt(): + generate( + Settings(), + tracks(), + TilehuntRenderer(10), + storage(), + ) -- cgit v1.2.3 From 182f3366fbbe9d00d3a25c630199d91dd307e771 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 17 Aug 2026 20:19:27 +0200 Subject: force install python packages :) --- .woodpecker/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/tests.yaml b/.woodpecker/tests.yaml index 697a524..f8ce807 100644 --- a/.woodpecker/tests.yaml +++ b/.woodpecker/tests.yaml @@ -6,7 +6,7 @@ steps: image: rust commands: - apt update && apt install -y python3-dev python3-pip - - pip3 install ./hittekaart-py pytest + - pip3 install --break-system-packages ./hittekaart-py pytest - cargo test - cd hittekaart-py && pytest - name: compile-benches -- cgit v1.2.3 From ce81087037956f2a33f550585f32d0070039ade0 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 17 Aug 2026 20:25:38 +0200 Subject: add docstrings for with_name/with_description --- hittekaart/src/storage.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs index d52a900..5c2d453 100644 --- a/hittekaart/src/storage.rs +++ b/hittekaart/src/storage.rs @@ -283,6 +283,9 @@ impl MbTiles { }) } + /// Set the name of this MBTiles tile store. + /// + /// The default name is `"Heatmap"`. pub fn with_name(self, name: String) -> Self { MbTiles { name, @@ -290,6 +293,10 @@ impl MbTiles { } } + /// Set (or unset) the description of this MBTiles tile store. + /// + /// By default, no description will be written. If a description has been set, it can be + /// removed by using `with_description(None)`. pub fn with_description(self, description: Option) -> Self { MbTiles { description, -- cgit v1.2.3 From 99db2d1e499c3ca0b113e50ddd46f46f52c9e28b Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 17 Aug 2026 20:28:10 +0200 Subject: also add getters for name/description --- hittekaart/src/storage.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs index 5c2d453..af743a9 100644 --- a/hittekaart/src/storage.rs +++ b/hittekaart/src/storage.rs @@ -293,6 +293,11 @@ impl MbTiles { } } + /// Returns the current name of the tile store. + pub fn name(&self) -> &str { + &self.name + } + /// Set (or unset) the description of this MBTiles tile store. /// /// By default, no description will be written. If a description has been set, it can be @@ -303,6 +308,11 @@ impl MbTiles { .. self } } + + /// Returns the current description of the tile store. + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } } impl Storage for MbTiles { -- cgit v1.2.3