use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
use strum_macros::AsRefStr;
use utiles_core::{
BBox, LngLat, TileStringFormatter, VERSION, ZoomSet, geobbox_merge,
parsing::parse_bbox_ext, zoom,
};
use crate::cli::commands::dev::DevArgs;
use crate::cli::commands::serve::ServeArgs;
use crate::cli::commands::shapes::ShapesArgs;
use crate::cli::commands::{analyze_main, header_main, vacuum_main};
use crate::copy::CopyConfig;
use crate::errors::UtilesResult;
use crate::hash_types::HashType;
use crate::mbt::{MbtType, TilesFilter};
use crate::sqlite::InsertStrategy;
fn about() -> String {
let is_debug: bool = cfg!(debug_assertions);
if is_debug {
format!("utiles cli (rust) ~ v{VERSION} ~ DEBUG")
} else {
format!("utiles cli (rust) ~ v{VERSION}")
}
}
#[derive(Debug, Parser)]
pub struct AboutArgs {
#[arg(required = false, long, short, action = clap::ArgAction::SetTrue,)]
pub json: bool,
}
#[derive(Debug, Parser)]
#[command(name = "ut", about = about(), version = VERSION, author, max_term_width = 120)]
pub struct Cli {
#[arg(long, global = true, default_value = "false", action = clap::ArgAction::SetTrue)]
pub debug: bool,
#[arg(long, global = true, default_value = "false", action = clap::ArgAction::SetTrue)]
pub trace: bool,
#[arg(long, global = true, default_value = "false", action = clap::ArgAction::SetTrue)]
pub log_json: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Parser)]
pub struct TileInputStreamArgs {
#[arg(required = false)]
pub input: Option<String>,
}
fn tile_fmt_string_long_help() -> String {
r#"Format string for tiles (default: `{json_arr}`)
Example:
> utiles tiles 1 * --fmt "http://thingy.com/{z}/{x}/{y}.png"
http://thingy.com/1/0/0.png
http://thingy.com/1/0/1.png
http://thingy.com/1/1/0.png
http://thingy.com/1/1/1.png
> utiles tiles 1 * --fmt "SELECT * FROM tiles WHERE zoom_level = {z} AND tile_column = {x} AND tile_row = {-y};"
SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 0 AND tile_row = 1;
SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 0 AND tile_row = 0;
SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 1 AND tile_row = 1;
SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 1 AND tile_row = 0;
fmt-tokens:
`{json_arr}`/`{json}` -> [x, y, z]
`{json_obj}`/`{obj}` -> {x: x, y: y, z: z}
`{quadkey}`/`{qk}` -> quadkey string
`{pmtileid}`/`{pmid}` -> pmtile-id
`{x}` -> x tile coord
`{y}` -> y tile coord
`{z}` -> z/zoom level
`{-y}`/`{yup}` -> y tile coord flipped/tms
`{zxy}` -> z/x/y
`{bbox}` -> [w, s, e, n] bbox lnglat (wgs84)
`{projwin}` -> ulx,uly,lrx,lry projwin 4 gdal (wgs84)
`{bbox_web}` -> [w, s, e, n] bbox web-mercator (epsg:3857)
`{projwin_web}` -> ulx,uly,lrx,lry projwin 4 gdal (epsg:3857)
"#
.to_string()
}
#[derive(Debug, Parser)]
pub struct TileFmtOptions {
#[arg(required = false, long, action = clap::ArgAction::SetTrue)]
pub seq: bool,
#[arg(required = false, long, action = clap::ArgAction::SetTrue)]
pub obj: bool,
#[arg(
required = false,
long,
alias = "fmt",
short = 'F',
conflicts_with = "obj",
long_help = tile_fmt_string_long_help()
)]
pub fmt: Option<String>,
}
impl TileFmtOptions {
#[must_use]
pub fn formatter(&self) -> TileStringFormatter {
if let Some(fmt) = &self.fmt {
TileStringFormatter::new(fmt)
} else if self.obj {
TileStringFormatter::new("{json_obj}")
} else {
TileStringFormatter::default()
}
}
}
impl From<&TileFmtOptions> for TileStringFormatter {
fn from(opts: &TileFmtOptions) -> Self {
opts.formatter()
}
}
#[derive(Debug, Parser)]
pub struct TilesArgs {
#[arg(required = true, value_parser = clap::value_parser ! (u8).range(0..=30))]
pub zoom: u8,
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
}
#[derive(Debug, Parser)]
pub struct TileFmtArgs {
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
}
#[derive(Debug, Parser)]
pub struct EdgesArgs {
#[arg(required = false, long, action = clap::ArgAction::SetTrue,)]
pub wrapx: bool,
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
}
#[derive(Debug, Parser)]
pub struct BurnArgs {
#[arg(required = true, value_parser = clap::value_parser ! (u8).range(0..=30))]
pub zoom: u8,
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
}
#[derive(Debug, Parser)]
pub struct MergeArgs {
#[arg(long, short = 'Z', default_value = "0")]
pub minzoom: u8,
#[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
pub sort: bool,
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
}
#[derive(Debug, Parser)]
pub struct FmtStrArgs {
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
}
#[derive(Debug, Parser)]
pub struct ParentChildrenArgs {
#[command(flatten)]
pub inargs: TileInputStreamArgs,
#[command(flatten)]
pub fmtopts: TileFmtOptions,
#[arg(required = false, long, default_value = "1")]
pub depth: u8,
}
#[derive(Debug, Parser)]
pub struct SqliteDbCommonArgs {
#[arg(required = true)]
pub filepath: String,
#[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
pub min: bool,
}
#[derive(Debug, Parser)]
pub struct TilesFilterArgs {
#[arg(required = false, long, value_parser = parse_bbox_ext, allow_hyphen_values = true)]
pub bbox: Option<Vec<BBox>>,
#[command(flatten)]
pub zoom: Option<ZoomArgGroup>,
}
impl TilesFilterArgs {
#[must_use]
pub fn zooms(&self) -> Option<Vec<u8>> {
match &self.zoom {
Some(zoom) => zoom.zooms(),
None => None,
}
}
#[must_use]
pub fn bboxes(&self) -> Option<Vec<BBox>> {
self.bbox.clone()
}
#[must_use]
pub fn tiles_filter_maybe(&self) -> Option<TilesFilter> {
if self.bbox.is_none() && self.zoom.is_none() {
None
} else {
Some(TilesFilter::new(self.bboxes(), self.zooms()))
}
}
}
#[derive(Debug, Parser, Clone, clap::ValueEnum)]
pub enum DbtypeOption {
Flat,
Hash,
Norm,
}
impl From<&DbtypeOption> for MbtType {
fn from(opt: &DbtypeOption) -> Self {
match opt {
DbtypeOption::Flat => Self::Flat,
DbtypeOption::Hash => Self::Hash,
DbtypeOption::Norm => Self::Norm,
}
}
}
#[derive(Debug, Parser, Clone)]
pub struct TouchArgs {
#[arg(required = true)]
pub filepath: String,
#[arg(required = false, long)]
pub page_size: Option<i64>,
#[arg(
required = false, long = "dbtype", aliases = ["db-type", "mbtype", "mbt-type"], default_value = "flat"
)]
pub dbtype: Option<DbtypeOption>,
}
impl TouchArgs {
#[must_use]
pub fn mbtype(&self) -> MbtType {
self.dbtype.as_ref().map_or(MbtType::Flat, |opt| opt.into())
}
}
#[derive(Debug, Subcommand)]
pub enum SqliteCommands {
Analyze(AnalyzeArgs),
Header(SqliteHeaderArgs),
Vacuum(VacuumArgs),
}
impl SqliteCommands {
pub async fn run(&self) -> UtilesResult<()> {
match self {
Self::Analyze(args) => analyze_main(args).await,
Self::Header(args) => header_main(args).await,
Self::Vacuum(args) => vacuum_main(args).await,
}
}
}
#[derive(Debug, Parser)]
pub struct AnalyzeArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false, long)]
pub analysis_limit: Option<usize>,
}
#[derive(Debug, Parser)]
pub struct SqliteHeaderArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
}
#[derive(Debug, Parser)]
pub struct VacuumArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false)]
pub into: Option<String>,
#[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
pub analyze: bool,
#[arg(required = false, long)]
pub page_size: Option<i64>,
}
#[derive(Debug, Parser)]
pub struct MetadataArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false, long, action = clap::ArgAction::SetTrue)]
pub obj: bool,
#[arg(required = false, long, action = clap::ArgAction::SetTrue, default_value = "false")]
pub raw: bool,
}
#[derive(Debug, Parser)]
pub struct MetadataSetArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = true, value_name = "KEY/FSPATH")]
pub key: String,
#[arg(required = false)]
pub value: Option<String>,
#[arg(
required = false, long, aliases = ["dry-run"], short = 'n', action = clap::ArgAction::SetTrue
)]
pub dryrun: bool,
}
#[derive(Debug, Parser)]
pub struct TilejsonArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
pub tilestats: bool,
}
#[derive(Debug, Parser)]
pub struct LintArgs {
#[arg(required = true, num_args(1..))]
pub(crate) fspaths: Vec<String>,
#[arg(
required = false, long, action = clap::ArgAction::SetTrue,
default_value = "false", hide = true
)]
pub(crate) fix: bool,
}
#[derive(Debug, Parser)]
pub struct InfoArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false, long, action = clap::ArgAction::SetTrue)]
pub(crate) full: bool,
#[arg(required = false, long, short, visible_aliases = ["stats"], action = clap::ArgAction::SetTrue)]
pub(crate) statistics: bool,
}
#[derive(Debug, Parser)]
pub struct AggHashArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[command(flatten)]
pub filter_args: TilesFilterArgs,
#[arg(required = false, long)]
pub hash: Option<HashType>,
}
#[derive(Debug, Parser)]
pub struct UpdateArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false, long, short = 'n', action = clap::ArgAction::SetTrue)]
pub(crate) dryrun: bool,
}
#[derive(Debug, Parser)]
pub struct ZxyifyArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = false, long, action = clap::ArgAction::SetTrue)]
pub(crate) rm: bool,
}
#[derive(Debug, Parser)]
#[command(
name = "enumerate",
about = "enumerate db tiles like `tippecanoe-enumerate`"
)]
pub struct EnumerateArgs {
#[arg(required = true)]
pub(crate) fspaths: Vec<String>,
#[command(flatten)]
pub filter_args: TilesFilterArgs,
#[arg(required = false, long, short = 't', action = clap::ArgAction::SetTrue)]
pub(crate) tippecanoe: bool,
}
#[derive(Debug, Parser)]
pub struct OptimizeArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = true)]
pub dst: String,
}
#[derive(Debug, Parser)]
pub struct WebpifyArgs {
#[command(flatten)]
pub common: SqliteDbCommonArgs,
#[arg(required = true)]
pub dst: String,
#[arg(required = false, long, short)]
pub jobs: Option<u8>,
#[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
pub(crate) quiet: bool,
}
#[derive(Debug, Parser)]
pub struct CommandsArgs {
#[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
pub full: bool,
#[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
pub(crate) table: bool,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
#[command(name = "about", visible_alias = "aboot")]
About(AboutArgs),
#[command(name = "commands", visible_alias = "cmds")]
Commands(CommandsArgs),
#[command(subcommand, visible_alias = "db")]
Sqlite(SqliteCommands),
#[command(name = "tilejson", visible_alias = "tj", alias = "trader-joes")]
Tilejson(TilejsonArgs),
#[command(name = "touch")]
Touch(TouchArgs),
#[command(name = "copy", visible_alias = "cp")]
Copy(CopyArgs),
#[command(name = "lint")]
Lint(LintArgs),
#[command(name = "agg-hash")]
AggHash(AggHashArgs),
#[command(name = "metadata", visible_aliases = ["meta", "md"])]
Metadata(MetadataArgs),
#[command(name = "metadata-set", visible_aliases = ["meta-set", "mds"])]
MetadataSet(MetadataSetArgs),
#[command(name = "update", visible_aliases = ["up"])]
Update(UpdateArgs),
#[command(name = "enumerate", visible_aliases = ["enum"])]
Enumerate(EnumerateArgs),
#[command(name = "rimraf", visible_alias = "rmrf", hide = true)]
Rimraf(RimrafArgs),
#[command(name = "info")]
Info(InfoArgs),
#[command(name = "vacuum", visible_alias = "vac")]
Vacuum(VacuumArgs),
#[command(name = "dbcontains")]
Contains {
#[arg(required = true)]
filepath: String,
#[arg(required = true)]
lnglat: LngLat,
},
#[command(name = "zxyify", aliases = ["xyzify"])]
Zxyify(ZxyifyArgs),
#[command(
name = "fmtstr",
aliases = & ["fmt", "xt"],
verbatim_doc_comment,
)]
Fmt(TileFmtArgs),
#[command(
name = "bounding-tile",
verbatim_doc_comment,
about = "Echo bounding tile at zoom for bbox / geojson"
)]
BoundingTile(TileFmtArgs),
#[command(name = "quadkey", verbatim_doc_comment, visible_alias = "qk")]
Quadkey(TileFmtArgs),
#[command(name = "pmtileid", verbatim_doc_comment, visible_alias = "pmid")]
Pmtileid(TileFmtArgs),
#[command(
name = "tiles",
verbatim_doc_comment,
about = "Echo tiles at zoom intersecting geojson bbox / feature / collection"
)]
Tiles(TilesArgs),
#[command(name = "neighbors")]
Neighbors(TileFmtArgs),
#[command(name = "children", verbatim_doc_comment)]
Children(ParentChildrenArgs),
#[command(name = "parent")]
Parent(ParentChildrenArgs),
#[command(name = "shapes")]
Shapes(ShapesArgs),
#[command(name = "burn", visible_alias = "cover")]
Burn(BurnArgs),
#[command(name = "merge")]
Merge(MergeArgs),
#[command(name = "edges")]
Edges(EdgesArgs),
#[command(
name = "webpify",
about = "Convert raster mbtiles to webp format",
hide = true
)]
Webpify(WebpifyArgs),
#[command(
name = "optimize",
about = "Optimize tiles-db",
aliases = ["opt"],
hide = true
)]
Optimize(OptimizeArgs),
#[command(name = "serve", hide = true)]
Serve(ServeArgs),
#[command(name = "dev", hide = true)]
Dev(DevArgs),
#[command(name = "addo", hide = true)]
Addo,
#[command(name = "translate", hide = true)]
Translate,
}
#[derive(Debug, Parser, Clone)]
#[command(name = "rimraf", about = "rm-rf dirpath")]
pub struct RimrafArgs {
#[arg(required = true)]
pub dirpath: String,
#[arg(required = false, long, action = clap::ArgAction::SetTrue)]
pub(crate) size: bool,
#[arg(required = false, short = 'n', long, action = clap::ArgAction::SetTrue)]
pub(crate) dryrun: bool,
#[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
pub(crate) verbose: bool,
}
#[derive(Args, Debug)]
#[group(required = false, multiple = false, id = "minmaxzoom")]
pub struct MinMaxZoom {
#[arg(long)]
minzoom: Option<u8>,
#[arg(long)]
maxzoom: Option<u8>,
}
#[derive(Debug, Parser)]
pub struct ZoomArgGroup {
#[arg(short, long, required = false, value_delimiter = ',', value_parser = zoom::parse_zooms)]
pub zoom: Option<Vec<Vec<u8>>>,
#[arg(long, conflicts_with = "zoom", aliases = ["min-zoom", "min-z"])]
pub minzoom: Option<u8>,
#[arg(long, conflicts_with = "zoom", aliases = ["max-zoom", "max-z"])]
pub maxzoom: Option<u8>,
}
impl ZoomArgGroup {
#[must_use]
pub fn zooms(&self) -> Option<Vec<u8>> {
match &self.zoom {
Some(zooms) => Some(zooms.iter().flatten().copied().collect()),
None => match (self.minzoom, self.maxzoom) {
(Some(minzoom), Some(maxzoom)) => Some((minzoom..=maxzoom).collect()),
(Some(minzoom), None) => Some((minzoom..=31).collect()),
(None, Some(maxzoom)) => Some((0..=maxzoom).collect()),
(None, None) => None,
},
}
}
}
#[derive(
Debug, Copy, Parser, Clone, clap::ValueEnum, strum::EnumString, AsRefStr, Default,
)]
#[strum(serialize_all = "kebab-case")]
pub enum ConflictStrategy {
#[default]
Undefined,
Ignore,
Replace,
Abort,
Fail,
}
impl From<ConflictStrategy> for InsertStrategy {
fn from(cs: ConflictStrategy) -> Self {
match cs {
ConflictStrategy::Undefined => Self::None,
ConflictStrategy::Ignore => Self::Ignore,
ConflictStrategy::Replace => Self::Replace,
ConflictStrategy::Abort => Self::Abort,
ConflictStrategy::Fail => Self::Fail,
}
}
}
#[derive(Debug, Parser)]
#[command(name = "copy", about = "Copy tiles from src -> dst")]
pub struct CopyArgs {
#[arg(required = true)]
pub src: String,
#[arg(required = true)]
pub dst: String,
#[arg(required = false, long, short = 'n', action = clap::ArgAction::SetTrue)]
pub dryrun: bool,
#[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
pub force: bool,
#[command(flatten)]
pub zoom: Option<ZoomArgGroup>,
#[arg(required = false, long, value_parser = parse_bbox_ext, allow_hyphen_values = true)]
pub bbox: Option<BBox>,
#[arg(required = false, long, short, default_value = "undefined")]
pub conflict: ConflictStrategy,
#[arg(
required = false,
long = "dst-type",
aliases = ["dbtype", "dsttype", "mbtype", "mbt-type", "mbtiles-type"]
)]
pub dst_type: Option<DbtypeOption>,
#[arg(required = false, long)]
pub hash: Option<HashType>,
#[arg(required = false, long, short)]
pub jobs: Option<u8>,
#[arg(required = false, long, hide = true, action = clap::ArgAction::SetTrue)]
pub stream: bool,
}
impl CopyArgs {
#[must_use]
pub fn zooms(&self) -> Option<Vec<u8>> {
match &self.zoom {
Some(zoom) => zoom.zooms(),
None => None,
}
}
#[must_use]
pub fn zoom_set(&self) -> Option<ZoomSet> {
self.zooms().map(|zooms| ZoomSet::from_zooms(&zooms))
}
#[must_use]
pub fn bboxes(&self) -> Option<Vec<BBox>> {
self.bbox.as_ref().map(|bbox| vec![*bbox])
}
#[must_use]
pub fn bounds(&self) -> Option<String> {
if let Some(bboxes) = self.bboxes() {
let new_bbox = geobbox_merge(&bboxes);
Some(new_bbox.mbt_bounds())
} else {
None
}
}
}
impl From<&CopyArgs> for CopyConfig {
fn from(args: &CopyArgs) -> Self {
let dbtype = args.dst_type.as_ref().map(|dbtype| dbtype.into());
Self {
src: PathBuf::from(&args.src),
dst: PathBuf::from(&args.dst),
zset: args.zoom_set(),
zooms: args.zooms(),
verbose: true,
bboxes: args.bboxes(),
bounds_string: args.bounds(),
force: false,
dryrun: false,
jobs: args.jobs,
istrat: InsertStrategy::from(args.conflict),
hash: args.hash,
dst_type: dbtype,
stream: args.stream,
}
}
}