aboutsummaryrefslogtreecommitdiff
path: root/hittekaart/src/storage.rs
diff options
context:
space:
mode:
Diffstat (limited to 'hittekaart/src/storage.rs')
-rw-r--r--hittekaart/src/storage.rs165
1 files changed, 165 insertions, 0 deletions
diff --git a/hittekaart/src/storage.rs b/hittekaart/src/storage.rs
new file mode 100644
index 0000000..c21627f
--- /dev/null
+++ b/hittekaart/src/storage.rs
@@ -0,0 +1,165 @@
+//! Abstractions over different storage backends.
+//!
+//! 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.
+use rusqlite::{params, Connection};
+use std::{
+ fs,
+ io::ErrorKind,
+ path::{Path, PathBuf},
+};
+
+use super::error::{Error, Result};
+
+/// The trait that provides the interface for storing tiles.
+pub trait Storage {
+ /// Prepare the storage.
+ ///
+ /// This can be used to e.g. ensure the directory exists, or to create the database.
+ fn prepare(&mut self) -> Result<()>;
+ /// Prepare for a given zoom level.
+ ///
+ /// This function is called once per zoom, and can be used e.g. to set up the inner folder for
+ /// the level. This can avoid unnecesary syscalls if this setup would be done in
+ /// [`Storage::store`] instead.
+ fn prepare_zoom(&mut self, zoom: u32) -> Result<()>;
+ /// Store the given data for the tile.
+ fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()>;
+ /// Finish the storing operation.
+ ///
+ /// This can flush any buffers, commit database changes, and so on.
+ fn finish(&mut self) -> Result<()>;
+}
+
+/// Folder-based storage.
+///
+/// This stores the tiles according to the [slippy map
+/// tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames).
+#[derive(Debug)]
+pub struct Folder {
+ base_dir: PathBuf,
+}
+
+impl Folder {
+ /// Create a new folder based storage.
+ ///
+ /// The given directory is the "root" directory, so a tile would be saved as
+ /// `base_dir/{zoom}/{x}/{y}.png`.
+ pub fn new(base_dir: PathBuf) -> Self {
+ Folder { base_dir }
+ }
+}
+
+impl Storage for Folder {
+ fn prepare(&mut self) -> Result<()> {
+ let path = &self.base_dir;
+ let metadata = fs::metadata(path);
+ match metadata {
+ Err(e) if e.kind() == ErrorKind::NotFound => {
+ fs::create_dir(path)
+ .map_err(|e| Error::Io("creating the output directory", e))?
+ }
+ Err(e) => Err(Error::Io("checking the output direectory", e))?,
+ Ok(m) if m.is_dir() => (),
+ Ok(_) => Err(Error::InvalidOutputDirectory)?,
+ }
+ Ok(())
+ }
+
+ fn prepare_zoom(&mut self, zoom: u32) -> Result<()> {
+ let target = [&self.base_dir, &zoom.to_string().into()]
+ .iter()
+ .collect::<PathBuf>();
+ fs::create_dir(target)
+ .map_err(|e| Error::Io("creating the zoom directory", e))?;
+ Ok(())
+ }
+
+ fn store(&mut self, zoom: u32, x: u64, y: u64, data: &[u8]) -> Result<()> {
+ let folder = self.base_dir.join(zoom.to_string()).join(x.to_string());
+ let metadata = folder.metadata();
+ match metadata {
+ Err(_) => fs::create_dir(&folder)
+ .map_err(|e| Error::Io("creating the output directory", e))?,
+ Ok(m) if !m.is_dir() => Err(Error::InvalidOutputDirectory)?,
+ _ => {}
+ }
+ let file = folder.join(format!("{y}.png"));
+ fs::write(file, data)
+ .map_err(|e| Error::Io("writing the output file", e))?;
+ Ok(())
+ }
+
+ fn finish(&mut self) -> Result<()> {
+ Ok(())
+ }
+}
+
+/// 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)
+/// );
+/// ```
+#[derive(Debug)]
+pub struct Sqlite {
+ connection: Connection,
+}
+
+impl Sqlite {
+ /// Create a new SQLite backed tile store.
+ ///
+ /// 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> {
+ 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 })
+ }
+}
+
+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)
+ );",
+ (),
+ )?;
+ self.connection.execute("BEGIN;", ())?;
+ Ok(())
+ }
+
+ fn prepare_zoom(&mut self, _zoom: u32) -> Result<()> {
+ Ok(())
+ }
+
+ 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],
+ )?;
+ Ok(())
+ }
+
+ fn finish(&mut self) -> Result<()> {
+ self.connection.execute("COMMIT;", ())?;
+ Ok(())
+ }
+}