diff options
| -rw-r--r-- | README.adoc | 27 | ||||
| -rw-r--r-- | hittekaart-cli/src/main.rs | 36 | ||||
| -rw-r--r-- | hittekaart/src/storage.rs | 110 |
3 files changed, 128 insertions, 45 deletions
diff --git a/README.adoc b/README.adoc index bb779b5..fa58a24 100644 --- a/README.adoc +++ b/README.adoc @@ -8,7 +8,7 @@ hittekaart - A GPX track heatmap generator. == SYNOPSIS ---- -hittekaart [--output=...] [--min-zoom=...] [--max-zoom=...] [--threads=...] [--sqlite] FILES... +hittekaart [--output=...] [--min-zoom=...] [--max-zoom=...] [--threads=...] [--format=...] FILES... ---- == INSTALLATION @@ -42,8 +42,8 @@ the `--output/-o` option. In order to overcome storage overhead when saving many small files (see the tip and table further below), `hittekaart` can instead output a SQLite database -with the heatmap tile data. To do so, use the `--sqlite` command line option, -and control where the SQLite file should be placed with `--output`/`-o`. +with the heatmap tile data. To do so, use the `--format=sqlite` command line +option, and control where the SQLite file should be placed with `--output`/`-o`. While this does not allow you to immediately serve the tiles with a HTTP server, it does cut down on the wasted space on non-optimal file systems. @@ -61,6 +61,20 @@ CREATE TABLE tiles ( ); ---- +=== OSMAND OUTPUT + +You can generate a file that can be read directly by +https://osmand.net/[OsmAnd]. To do so, pass the `--format=osm-and` command line +option, and control where the file should be placed with `--output`/`-o`. + +To use the map in the app, make sure to: + +* Give it a `.sqlitedb` extension, and +* place it under `Android/data/net.osmand.plus/files/tiles/`. + +For a reference of the format, see +https://osmand.net/docs/technical/osmand-file-formats/osmand-sqlite/. + === INPUT FILES `hittekaart` expects GPX track files with the `.gpx` extension. It will parse @@ -204,10 +218,9 @@ The following options are supported: when generating single files, and `tiles.sqlite` when storing the tiles in a SQLite database. -`--sqlite`:: - Output a single SQLite file with all tiles instead of saving each tile as a - separate PNG file. In this case, `-o` can be used to set the location of - the SQLite database. The schema is described above. +`--format FORMAT`:: + Sets the output format (folder, sqlite, osm-and). See the sections above on + OUTPUT FORMAT, SQLITE OUTPUT and OSMAND OUTPUT for more information. `-m MODE`, `--mode=MODE`:: Sets the overlay generation mode (heatmap, marktile, tilehunter). See diff --git a/hittekaart-cli/src/main.rs b/hittekaart-cli/src/main.rs index 11133af..880ea61 100644 --- a/hittekaart-cli/src/main.rs +++ b/hittekaart-cli/src/main.rs @@ -7,8 +7,8 @@ use color_eyre::{ }; use hittekaart::{ gpx::{self, Compression}, - renderer::{self, heatmap, marktile, tilehunt, Renderer}, - storage::{Folder, Sqlite, Storage}, + renderer::{self, Renderer, heatmap, marktile, tilehunt}, + storage::{Folder, Sqlite, Storage, TableFormat}, }; use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use is_terminal::IsTerminal; @@ -25,6 +25,14 @@ enum Mode { Tilehunter, } +/// Output format. +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum Format { + Folder, + Sqlite, + OsmAnd, +} + #[derive(Parser, Debug, Clone)] #[command(author, version, about)] struct Args { @@ -49,10 +57,9 @@ struct Args { #[arg(long, short)] output: Option<PathBuf>, - /// Store the tiles in a SQLite database. If given, `--output` will determine the SQLite - /// filename. + /// Output format. #[arg(long)] - sqlite: bool, + format: Format, /// Generation mode. #[arg(value_enum, long, short, default_value_t = Mode::Heatmap)] @@ -115,12 +122,19 @@ fn run<R: Renderer>(renderer: R, args: Args) -> Result<()> { let tracks = tracks.into_iter().collect::<Result<Vec<_>>>()?; bar.finish(); - let mut storage: Box<dyn Storage + Send> = if args.sqlite { - let output = args.output.unwrap_or_else(|| "tiles.sqlite".into()); - Box::new(Sqlite::connect(output)?) - } else { - let output = args.output.unwrap_or_else(|| "tiles".into()); - Box::new(Folder::new(output)) + let mut storage: Box<dyn Storage + Send> = match args.format { + Format::Folder => { + let output = args.output.unwrap_or_else(|| "tiles".into()); + Box::new(Folder::new(output)) + } + Format::Sqlite => { + let output = args.output.unwrap_or_else(|| "tiles.sqlite".into()); + Box::new(Sqlite::connect(output, TableFormat::Simple)?) + } + Format::OsmAnd => { + let output = args.output.unwrap_or_else(|| "tiles.sqlitedb".into()); + Box::new(Sqlite::connect(output, TableFormat::OsmAnd)?) + } }; storage.prepare()?; diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs index c21627f..1f4931f 100644 --- a/hittekaart/src/storage.rs +++ b/hittekaart/src/storage.rs @@ -97,22 +97,88 @@ impl Storage for Folder { } } +/// Describes the SQL table format that hittekaart should use. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum TableFormat { + /// A simple format, consisting of a single table: + /// + /// ```sql + /// CREATE TABLE tiles ( + /// zoom INTEGER, + /// x INTEGER, + /// y INTEGER, + /// data BLOB, + /// PRIMARY KEY (zoom, x, y) + /// ); + /// ``` + Simple, + /// Output a SQL file that conforms to the OsmAnd specification and can be loaded as an overlay + /// map. + /// + /// See <https://osmand.net/docs/technical/osmand-file-formats/osmand-sqlite/> for the + /// technical reference. + /// + /// To use the file, give it an `.sqlitedb` extension and place it under + /// `Android/data/net.osmand.plus/files/tiles/` + OsmAnd, +} + +impl TableFormat { + fn initializers(self) -> &'static [&'static str] { + match self { + TableFormat::Simple => &[ + "CREATE TABLE tiles ( + zoom INTEGER, + x INTEGER, + y INTEGER, + data BLOB, + PRIMARY KEY (zoom, x, y) + );" + ], + TableFormat::OsmAnd => &[ + "CREATE TABLE info ( + url TEXT, + randoms TEXT, + referer TEXT, + rule TEXT, + useragent TEXT, + minzoom INTEGER, + maxzoom INTEGER, + ellipsoid INTEGER, + inverted_y INTEGER, + timecolumn TEXT, + expireminutes INTEGER, + tilenumbering TEXT, + tilesize INTEGER + );", + "INSERT INTO info (inverted_y, tilenumbering, maxzoom) VALUES (0, \"\", 17);", + "CREATE TABLE tiles ( + x INTEGER, + y INTEGER, + z INTEGER, + image BLOB, + time INTEGER, + PRIMARY KEY (x, y, z) + );", + ], + } + } + + fn insert(self) -> &'static str { + match self { + TableFormat::Simple => "INSERT INTO tiles (zoom, x, y, data) VALUES (?, ?, ?, ?)", + TableFormat::OsmAnd => "INSERT INTO tiles (z, x, y, image) VALUES (?, ?, ?, ?)", + } + } +} + /// SQLite based storage. /// -/// This stores tiles in a SQLite database. The database will have a single table: -/// -/// ```sql -/// CREATE TABLE tiles ( -/// zoom INTEGER, -/// x INTEGER, -/// y INTEGER, -/// data BLOB, -/// PRIMARY KEY (zoom, x, y) -/// ); -/// ``` +/// This stores tiles in a SQLite database. See [`TableFormat`] to see the table layout. #[derive(Debug)] pub struct Sqlite { connection: Connection, + format: TableFormat, } impl Sqlite { @@ -120,28 +186,21 @@ impl Sqlite { /// /// The database will be saved at the given location. Note that the database must not yet /// exist. - pub fn connect<P: AsRef<Path>>(file: P) -> Result<Self> { + pub fn connect<P: AsRef<Path>>(file: P, format: TableFormat) -> Result<Self> { let path = file.as_ref(); if fs::metadata(path).is_ok() { return Err(Error::OutputAlreadyExists(path.to_path_buf())); } let connection = Connection::open(path)?; - Ok(Sqlite { connection }) + Ok(Sqlite { connection, format }) } } impl Storage for Sqlite { fn prepare(&mut self) -> Result<()> { - self.connection.execute( - "CREATE TABLE tiles ( - zoom INTEGER, - x INTEGER, - y INTEGER, - data BLOB, - PRIMARY KEY (zoom, x, y) - );", - (), - )?; + for stmt in self.format.initializers() { + self.connection.execute(stmt, ())?; + } self.connection.execute("BEGIN;", ())?; Ok(()) } @@ -151,10 +210,7 @@ impl Storage for Sqlite { } fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()> { - self.connection.execute( - "INSERT INTO tiles (zoom, x, y, data) VALUES (?, ?, ?, ?)", - params![zoom, x, y, data], - )?; + self.connection.execute(self.format.insert(), params![zoom, x, y, data])?; Ok(()) } |
