aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.woodpecker/tests.yaml4
-rw-r--r--hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pycbin0 -> 207 bytes
-rw-r--r--hittekaart-py/hittekaart_py/hittekaart_py.pyi2
-rw-r--r--hittekaart-py/src/lib.rs18
-rw-r--r--hittekaart-py/tests/test_api.py71
-rw-r--r--hittekaart/src/storage.rs62
-rw-r--r--hittekaart/tests/generate.rs97
-rw-r--r--test-assets/Synthetic_BRouter_1.gpx.gzbin0 -> 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
new file mode 100644
index 0000000..11caa7e
--- /dev/null
+++ b/hittekaart-py/hittekaart_py/__pycache__/__init__.cpython-314.pyc
Binary files 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<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
new file mode 100644
index 0000000..e380fc7
--- /dev/null
+++ b/test-assets/Synthetic_BRouter_1.gpx.gz
Binary files differ