mod files;
mod tileset;
use std::path::{Path, PathBuf};
use anyhow::{Result as AnyResult, bail};
use bytes::Bytes;
use clap::{Args, ValueEnum};
use indicatif::ProgressState;
use martin_tile_utils::{Encoding, decode_brotli, decode_gzip, decode_zlib, decode_zstd};
use mbtiles::{MbtType, NormalizedSchema};
use mlt_core::encoder::{EncodedUnknown, Encoder, EncoderConfig};
use mlt_core::mvt::mvt_to_tile_layers;
use mlt_core::{Decoder, Layer, Parser};
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "state.per_sec() is always non-negative and well below 2^63 tiles/sec"
)]
fn whole_rate_per_sec(state: &ProgressState, w: &mut dyn std::fmt::Write) {
let _ = w.write_fmt(format_args!("{}/s", state.per_sec() as u64));
}
#[derive(Clone, Default, ValueEnum)]
enum MbtFormat {
#[default]
Flat,
#[value(name = "flat-with-hash")]
FlatWithHash,
Normalized,
}
impl From<MbtFormat> for MbtType {
fn from(f: MbtFormat) -> Self {
match f {
MbtFormat::Flat => Self::Flat,
MbtFormat::FlatWithHash => Self::FlatWithHash,
MbtFormat::Normalized => Self::Normalized {
hash_view: true,
schema: NormalizedSchema::DedupId,
},
}
}
}
#[derive(Clone, Default, ValueEnum)]
enum SortMode {
#[default]
Auto,
None,
Morton,
Hilbert,
Id,
}
#[derive(Args)]
pub struct ConvertArgs {
input: PathBuf,
output: PathBuf,
#[clap(short, long, default_value = "false")]
tessellate: bool,
#[clap(long, default_value = "auto")]
sort: SortMode,
#[clap(long)]
mbtiles_format: Option<MbtFormat>,
#[clap(long, default_value = "false")]
no_shared_dict: bool,
}
pub fn convert(args: &ConvertArgs) -> AnyResult<()> {
let cfg = EncoderConfig {
tessellate: args.tessellate,
try_spatial_morton_sort: matches!(args.sort, SortMode::Auto | SortMode::Morton),
try_spatial_hilbert_sort: matches!(args.sort, SortMode::Auto | SortMode::Hilbert),
try_id_sort: matches!(args.sort, SortMode::Auto | SortMode::Id),
allow_shared_dict: !args.no_shared_dict,
..Default::default()
};
if is_mbtiles_extension(&args.input) {
if !is_mbtiles_extension(&args.output) {
bail!(
"Output must be an .mbtiles file when input is an .mbtiles file, got: {}",
args.output.display()
);
}
if args.output.exists() {
bail!(
"Output {} already exists; refusing to append. \
Delete it first or choose a different path.",
args.output.display()
);
}
return tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?
.block_on(tileset::convert_mbtiles(
&args.input,
&args.output,
args.mbtiles_format.clone(),
cfg,
));
}
files::convert_files(&args.input, &args.output, cfg)
}
fn is_mbtiles_extension(path: &Path) -> bool {
matches!(
path.extension().and_then(std::ffi::OsStr::to_str),
Some("mbtiles")
)
}
fn convert_mlt_buffer(buffer: &[u8], cfg: EncoderConfig) -> AnyResult<Vec<u8>> {
let layers = Parser::default().parse_layers(buffer)?;
let mut dec = Decoder::default();
let mut out: Vec<u8> = Vec::new();
for layer in layers {
match layer {
Layer::Tag01(l) => {
let tile = l.into_tile(&mut dec)?;
out.extend_from_slice(&tile.encode(cfg)?);
}
Layer::Unknown(u) => {
out.extend(EncodedUnknown::from(u).write_to(Encoder::default())?.data);
}
_ => {}
}
}
Ok(out)
}
fn convert_mvt_buffer(buffer: Vec<u8>, cfg: EncoderConfig) -> AnyResult<Vec<u8>> {
let mut out: Vec<u8> = Vec::new();
for tile in mvt_to_tile_layers(buffer)? {
out.extend_from_slice(&tile.encode(cfg)?);
}
Ok(out)
}
fn encode_one(data: Vec<u8>, encoding: Encoding, cfg: EncoderConfig) -> AnyResult<Bytes> {
let mvt = match encoding {
Encoding::Gzip => decode_gzip(&data)?,
Encoding::Zlib => decode_zlib(&data)?,
Encoding::Brotli => decode_brotli(&data)?,
Encoding::Zstd => decode_zstd(&data)?,
Encoding::Uncompressed | Encoding::Internal => data,
};
convert_mvt_buffer(mvt, cfg).map(Bytes::from_owner)
}