Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic copying functionality #712

Merged
merged 7 commits into from
Jun 17, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions martin-mbtiles/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};

use anyhow::Result;
use clap::{Parser, Subcommand};
use martin_mbtiles::Mbtiles;
use martin_mbtiles::{Mbtiles, TileCopier, TileCopierOptions};
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{Connection, SqliteConnection};

Expand Down Expand Up @@ -39,13 +39,26 @@ enum Commands {
// /// MBTiles file to modify
// file: PathBuf,
// },
// /// Copy tiles from one mbtiles file to another.
// Copy {
// /// MBTiles file to read from
// src_file: PathBuf,
// /// MBTiles file to write to
// dst_file: PathBuf,
// },
/// Copy tiles from one mbtiles file to another.
#[command(name = "copy")]
Copy {
/// MBTiles file to read from
src_file: PathBuf,
/// MBTiles file to write to
dst_file: PathBuf,
/// Display detailed copying information
#[arg(short, long)]
verbose: bool,
upsicleclown marked this conversation as resolved.
Show resolved Hide resolved
/// Minimum zoom level to copy
#[arg(long)]
min_zoom: Option<u8>,
/// Maximum zoom level to copy
#[arg(long)]
max_zoom: Option<u8>,
/// List of zoom levels to copy; if provided, min-zoom and max-zoom will be ignored
#[arg(long, value_delimiter(','))]
zoom_levels: Vec<u8>,
},
}

#[tokio::main]
Expand All @@ -56,6 +69,27 @@ async fn main() -> Result<()> {
Commands::MetaGetValue { file, key } => {
meta_get_value(file.as_path(), &key).await?;
}
Commands::Copy {
src_file,
dst_file,
verbose,
min_zoom,
max_zoom,
zoom_levels,
} => {
let mut tile_copier_options =
upsicleclown marked this conversation as resolved.
Show resolved Hide resolved
TileCopierOptions::new().verbose(verbose).zooms(zoom_levels);
if let Some(v) = min_zoom {
tile_copier_options = tile_copier_options.min_zoom(v);
};
if let Some(v) = max_zoom {
tile_copier_options = tile_copier_options.max_zoom(v);
};

let tile_copier = TileCopier::new(src_file, dst_file, tile_copier_options)?;

tile_copier.run().await?;
}
}

Ok(())
Expand Down
10 changes: 8 additions & 2 deletions martin-mbtiles/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ pub enum MbtError {
#[error("Inconsistent tile formats detected: {0} vs {1}")]
InconsistentMetadata(TileInfo, TileInfo),

#[error("Invalid data storage format for MBTile file {0}")]
InvalidDataStorageFormat(String),
#[error("Invalid data format for MBTile file {0}")]
InvalidDataFormat(String),

#[error(r#"Filename "{0}" passed to SQLite must be valid UTF-8"#)]
InvalidFilenameType(PathBuf),

#[error("No tiles found")]
NoTilesFound,

#[error("The destination file {0} is non-empty")]
NonEmptyTargetFile(PathBuf),
}

pub type MbtResult<T> = Result<T, MbtError>;
2 changes: 2 additions & 0 deletions martin-mbtiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ mod errors;
mod mbtiles;
mod mbtiles_pool;
mod mbtiles_queries;
mod tile_copier;

pub use errors::MbtError;
pub use mbtiles::{Mbtiles, Metadata};
pub use mbtiles_pool::MbtilesPool;
pub use tile_copier::{TileCopier, TileCopierOptions};
4 changes: 2 additions & 2 deletions martin-mbtiles/src/mbtiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ impl Mbtiles {
} else if is_tile_tables_type(&mut *conn).await? {
Ok(Type::TileTables)
} else {
Err(MbtError::InvalidDataStorageFormat(self.filepath.clone()))
Err(MbtError::InvalidDataFormat(self.filepath.clone()))
}
}
}
Expand Down Expand Up @@ -392,6 +392,6 @@ mod tests {

let (mut conn, mbt) = open(":memory:").await;
let res = mbt.detect_type(&mut conn).await;
assert!(matches!(res, Err(MbtError::InvalidDataStorageFormat(_))));
assert!(matches!(res, Err(MbtError::InvalidDataFormat(_))));
}
}
202 changes: 202 additions & 0 deletions martin-mbtiles/src/tile_copier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
extern crate core;

use crate::errors::MbtResult;
use crate::mbtiles::Type;
use crate::{MbtError, Mbtiles};
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{query, Connection, Row, SqliteConnection};
use std::collections::HashSet;
use std::path::PathBuf;

#[derive(Clone, Default, Debug)]
pub struct TileCopierOptions {
zooms: HashSet<u8>,
min_zoom: Option<u8>,
max_zoom: Option<u8>,
//self.bbox = bbox
verbose: bool,
}

#[derive(Clone, Debug)]
pub struct TileCopier {
src_mbtiles: Mbtiles,
dst_filepath: PathBuf,
options: TileCopierOptions,
}

impl TileCopierOptions {
pub fn new() -> Self {
Self {
zooms: HashSet::new(),
min_zoom: None,
max_zoom: None,
verbose: false,
}
}

pub fn zooms(mut self, zooms: Vec<u8>) -> Self {
for zoom in zooms {
self.zooms.insert(zoom);
}
self
}

pub fn min_zoom(mut self, min_zoom: u8) -> Self {
self.min_zoom = Some(min_zoom);
self
}

pub fn max_zoom(mut self, max_zoom: u8) -> Self {
self.max_zoom = Some(max_zoom);
self
}

pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
}
impl TileCopier {
upsicleclown marked this conversation as resolved.
Show resolved Hide resolved
pub fn new(
src_filepath: PathBuf,
dst_filepath: PathBuf,
options: TileCopierOptions,
) -> MbtResult<Self> {
Ok(TileCopier {
src_mbtiles: Mbtiles::new(src_filepath)?,
dst_filepath,
options,
})
}

pub async fn run(self) -> MbtResult<()> {
let opt = SqliteConnectOptions::new()
.read_only(true)
.filename(PathBuf::from(&self.src_mbtiles.filepath()));
let mut conn = SqliteConnection::connect_with(&opt).await?;
let storage_type = self.src_mbtiles.detect_type(&mut conn).await?;

let opt = SqliteConnectOptions::new()
.create_if_missing(true)
.filename(&self.dst_filepath);
let mut conn = SqliteConnection::connect_with(&opt).await?;

if query("SELECT 1 FROM sqlite_schema LIMIT 1")
.fetch_optional(&mut conn)
.await?
.is_some()
{
return Err(MbtError::NonEmptyTargetFile(self.dst_filepath.clone()));
}

query("PRAGMA page_size = 512").execute(&mut conn).await?;
query("VACUUM").execute(&mut conn).await?;

query("ATTACH DATABASE ? AS sourceDb")
.bind(self.src_mbtiles.filepath())
.execute(&mut conn)
.await?;

let schema = query("SELECT sql FROM sourceDb.sqlite_schema WHERE tbl_name IN ('metadata', 'tiles', 'map', 'images')")
.fetch_all(&mut conn)
.await?;

for row in &schema {
let row: String = row.get(0);
query(row.as_str()).execute(&mut conn).await?;
}

query("INSERT INTO metadata SELECT * FROM sourceDb.metadata")
.execute(&mut conn)
.await?;

match storage_type {
Type::TileTables => self.copy_standard_compliant_tiles(&mut conn).await,
Type::DeDuplicated => self.copy_deduplicated_tiles(&mut conn).await,
}
}

async fn copy_standard_compliant_tiles(&self, conn: &mut SqliteConnection) -> MbtResult<()> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are all standard compliant - need a better name that matches the ones in the Type enum. BTW, the Type enum seems kinda weird tbh, maybe MbtType instead for clarity, and consistency with the errors?

// TODO: Handle options
// - bbox
// - verbose
// - zoom

self.run_query_with_options(
conn,
String::from("INSERT INTO tiles SELECT * FROM sourceDb.tiles"),
)
.await?;

Ok(())
}

async fn copy_deduplicated_tiles(&self, conn: &mut SqliteConnection) -> MbtResult<()> {
query("INSERT INTO map SELECT * FROM sourceDb.map")
.execute(&mut *conn)
.await?;

self.run_query_with_options(
conn,
String::from(
upsicleclown marked this conversation as resolved.
Show resolved Hide resolved
"INSERT INTO images
SELECT images.tile_data, images.tile_id
FROM sourceDb.images
JOIN sourceDb.map
ON images.tile_id = map.tile_id",
),
)
.await?;

Ok(())
}

async fn run_query_with_options(
&self,
conn: &mut SqliteConnection,
mut sql: String,
) -> MbtResult<()> {
let mut params: Vec<String> = vec![];

if !&self.options.zooms.is_empty() {
sql.push_str(
upsicleclown marked this conversation as resolved.
Show resolved Hide resolved
format!(
" WHERE zoom_level IN ({})",
vec!["?"; self.options.zooms.len()].join(",")
)
.as_str(),
);
for zoom_level in &self.options.zooms {
upsicleclown marked this conversation as resolved.
Show resolved Hide resolved
params.push(zoom_level.to_string());
}
} else if let Some(min_zoom) = &self.options.min_zoom {
if let Some(max_zoom) = &self.options.max_zoom {
sql.push_str(" WHERE zoom_level BETWEEN ? AND ?");

params.push(min_zoom.to_string());
params.push(max_zoom.to_string());
} else {
sql.push_str(" WHERE zoom_level >= ? ");

params.push(min_zoom.to_string());
}
} else if let Some(max_zoom) = &self.options.max_zoom {
sql.push_str(" WHERE zoom_level <= ? ");

params.push(max_zoom.to_string());
}

let mut query = query(sql.as_str());

for param in params {
query = query.bind(param);
}

query.execute(conn).await?;

Ok(())
}
}

// TODO: tests
// TODO: documentation