diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2026-08-17 20:29:02 +0200 |
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2026-08-17 20:29:02 +0200 |
| commit | 0eece020318bc9a0223dd4d3bac6b29949146699 (patch) | |
| tree | ae9c15922ebaf08005661ff310af898ac1347295 | |
| parent | 9a576a8d3a8cb1d143be0ade0310f71b490cacb9 (diff) | |
| parent | 99db2d1e499c3ca0b113e50ddd46f46f52c9e28b (diff) | |
| download | hittekaart-0eece020318bc9a0223dd4d3bac6b29949146699.tar.gz hittekaart-0eece020318bc9a0223dd4d3bac6b29949146699.tar.bz2 hittekaart-0eece020318bc9a0223dd4d3bac6b29949146699.zip | |
Merge branch 'mbtiles-desc'
| -rw-r--r-- | .woodpecker/tests.yaml | 4 | ||||
| -rw-r--r-- | hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc | bin | 0 -> 207 bytes | |||
| -rw-r--r-- | hittekaart-py/hittekaart_py/hittekaart_py.pyi | 2 | ||||
| -rw-r--r-- | hittekaart-py/src/lib.rs | 18 | ||||
| -rw-r--r-- | hittekaart-py/tests/test_api.py | 71 | ||||
| -rw-r--r-- | hittekaart/src/storage.rs | 62 | ||||
| -rw-r--r-- | hittekaart/tests/generate.rs | 97 | ||||
| -rw-r--r-- | test-assets/Synthetic_BRouter_1.gpx.gz | bin | 0 -> 697 bytes |
8 files changed, 239 insertions, 15 deletions
diff --git a/.woodpecker/tests.yaml b/.woodpecker/tests.yaml index 4ca95a0..f8ce807 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 --break-system-packages ./hittekaart-py pytest - cargo test + - cd hittekaart-py && pytest - name: compile-benches image: rust commands: diff --git a/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc b/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..11caa7e --- /dev/null +++ b/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc 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<String>, Option<String>), } /// 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<String>, description: Option<String>) -> 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!("<Storage.Folder path='{}'>", path.display()), StorageType::OsmAnd(ref path) => format!("<Storage.OsmAnd path='{}'>", path.display()), - StorageType::MbTiles(ref path) => format!("<Storage.MbTiles path='{}'>", path.display()), + StorageType::MbTiles(ref path, ..) => format!("<Storage.MbTiles path='{}'>", 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)) } } 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(), + ) diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs index 6cb3629..af743a9 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, @@ -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<String>, } impl MbTiles { @@ -267,7 +276,42 @@ 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, + }) + } + + /// Set the name of this MBTiles tile store. + /// + /// The default name is `"Heatmap"`. + pub fn with_name(self, name: String) -> Self { + MbTiles { + name, + .. self + } + } + + /// 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 + /// removed by using `with_description(None)`. + pub fn with_description(self, description: Option<String>) -> Self { + MbTiles { + description, + .. self + } + } + + /// Returns the current description of the tile store. + pub fn description(&self) -> Option<&str> { + self.description.as_deref() } } @@ -315,12 +359,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(()) } 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<dyn Error>> { + 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 Binary files differnew file mode 100644 index 0000000..e380fc7 --- /dev/null +++ b/test-assets/Synthetic_BRouter_1.gpx.gz |
