utiles/cli/
args.rs

1use clap::{Args, Parser, Subcommand};
2use std::path::PathBuf;
3use strum_macros::AsRefStr;
4
5use utiles_core::{
6    BBox, LngLat, TileStringFormatter, VERSION, ZoomSet, geobbox_merge,
7    parsing::parse_bbox_ext, zoom,
8};
9
10use crate::cli::commands::dev::DevArgs;
11use crate::cli::commands::serve::ServeArgs;
12use crate::cli::commands::shapes::ShapesArgs;
13use crate::cli::commands::{analyze_main, header_main, vacuum_main};
14use crate::copy::CopyConfig;
15use crate::errors::UtilesResult;
16use crate::hash_types::HashType;
17use crate::mbt::{MbtType, TilesFilter};
18use crate::sqlite::InsertStrategy;
19
20// ██╗   ██╗████████╗██╗██╗     ███████╗███████╗
21// ██║   ██║╚══██╔══╝██║██║     ██╔════╝██╔════╝
22// ██║   ██║   ██║   ██║██║     █████╗  ███████╗
23// ██║   ██║   ██║   ██║██║     ██╔══╝  ╚════██║
24// ╚██████╔╝   ██║   ██║███████╗███████╗███████║
25//  ╚═════╝    ╚═╝   ╚═╝╚══════╝╚══════╝╚══════╝
26
27/// utiles cli (rust) ~ v{VERSION}
28fn about() -> String {
29    let is_debug: bool = cfg!(debug_assertions);
30    if is_debug {
31        format!("utiles cli (rust) ~ v{VERSION} ~ DEBUG")
32    } else {
33        format!("utiles cli (rust) ~ v{VERSION}")
34    }
35}
36
37#[derive(Debug, Parser)]
38pub struct AboutArgs {
39    /// output `json`
40    #[arg(required = false, long, short, action = clap::ArgAction::SetTrue,)]
41    pub json: bool,
42}
43
44#[derive(Debug, Parser)]
45#[command(name = "ut", about = about(), version = VERSION, author, max_term_width = 120)]
46pub struct Cli {
47    /// debug mode (print/log more)
48    #[arg(long, global = true, default_value = "false", action = clap::ArgAction::SetTrue)]
49    pub debug: bool,
50
51    /// trace mode (print/log EVEN more)
52    #[arg(long, global = true, default_value = "false", action = clap::ArgAction::SetTrue)]
53    pub trace: bool,
54
55    /// format log as NDJSON
56    #[arg(long, global = true, default_value = "false", action = clap::ArgAction::SetTrue)]
57    pub log_json: bool,
58
59    /// CLI subcommands
60    #[command(subcommand)]
61    pub command: Commands,
62}
63
64#[derive(Debug, Parser)]
65pub struct TileInputStreamArgs {
66    #[arg(required = false)]
67    pub input: Option<String>,
68}
69
70fn tile_fmt_string_long_help() -> String {
71    r#"Format string for tiles (default: `{json_arr}`)
72
73Example:
74    > utiles tiles 1 * --fmt "http://thingy.com/{z}/{x}/{y}.png"
75    http://thingy.com/1/0/0.png
76    http://thingy.com/1/0/1.png
77    http://thingy.com/1/1/0.png
78    http://thingy.com/1/1/1.png
79    > utiles tiles 1 * --fmt "SELECT * FROM tiles WHERE zoom_level = {z} AND tile_column = {x} AND tile_row = {-y};"
80    SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 0 AND tile_row = 1;
81    SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 0 AND tile_row = 0;
82    SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 1 AND tile_row = 1;
83    SELECT * FROM tiles WHERE zoom_level = 1 AND tile_column = 1 AND tile_row = 0;
84
85fmt-tokens:
86    `{json_arr}`/`{json}`  -> [x, y, z]
87    `{json_obj}`/`{obj}`   -> {x: x, y: y, z: z}
88    `{quadkey}`/`{qk}`     -> quadkey string
89    `{pmtileid}`/`{pmid}`  -> pmtile-id
90    `{x}`                  -> x tile coord
91    `{y}`                  -> y tile coord
92    `{z}`                  -> z/zoom level
93    `{-y}`/`{yup}`         -> y tile coord flipped/tms
94    `{zxy}`                -> z/x/y
95    `{bbox}`               -> [w, s, e, n] bbox lnglat (wgs84)
96    `{projwin}`            -> ulx,uly,lrx,lry projwin 4 gdal (wgs84)
97    `{bbox_web}`           -> [w, s, e, n] bbox web-mercator (epsg:3857)
98    `{projwin_web}`        -> ulx,uly,lrx,lry projwin 4 gdal (epsg:3857)
99    "#
100        .to_string()
101}
102#[derive(Debug, Parser)]
103pub struct TileFmtOptions {
104    /// Write tiles as RS-delimited JSON sequence
105    #[arg(required = false, long, action = clap::ArgAction::SetTrue)]
106    pub seq: bool,
107
108    /// Format tiles as json objects (equiv to `-F/--fmt "{json_obj}"`)
109    #[arg(required = false, long, action = clap::ArgAction::SetTrue)]
110    pub obj: bool,
111
112    /// Format string for tiles (default: `{json_arr}`); see --help for more
113    #[arg(
114        required = false,
115        long,
116        alias = "fmt",
117        short = 'F',
118        conflicts_with = "obj",
119        long_help = tile_fmt_string_long_help()
120    )]
121    pub fmt: Option<String>,
122}
123
124impl TileFmtOptions {
125    #[must_use]
126    pub fn formatter(&self) -> TileStringFormatter {
127        if let Some(fmt) = &self.fmt {
128            TileStringFormatter::new(fmt)
129        } else if self.obj {
130            TileStringFormatter::new("{json_obj}")
131        } else {
132            TileStringFormatter::default()
133        }
134    }
135}
136impl From<&TileFmtOptions> for TileStringFormatter {
137    fn from(opts: &TileFmtOptions) -> Self {
138        opts.formatter()
139    }
140}
141
142#[derive(Debug, Parser)]
143pub struct TilesArgs {
144    /// Zoom level (0-30)
145    #[arg(required = true, value_parser = clap::value_parser ! (u8).range(0..=30))]
146    pub zoom: u8,
147
148    #[command(flatten)]
149    pub inargs: TileInputStreamArgs,
150
151    #[command(flatten)]
152    pub fmtopts: TileFmtOptions,
153}
154
155#[derive(Debug, Parser)]
156pub struct TileFmtArgs {
157    #[command(flatten)]
158    pub inargs: TileInputStreamArgs,
159
160    #[command(flatten)]
161    pub fmtopts: TileFmtOptions,
162}
163#[derive(Debug, Parser)]
164pub struct EdgesArgs {
165    /// Wrap x/longitude across antimeridian (default: false)
166    #[arg(required = false, long, action = clap::ArgAction::SetTrue,)]
167    pub wrapx: bool,
168
169    #[command(flatten)]
170    pub inargs: TileInputStreamArgs,
171
172    #[command(flatten)]
173    pub fmtopts: TileFmtOptions,
174}
175
176#[derive(Debug, Parser)]
177pub struct BurnArgs {
178    /// Zoom level (0-30)
179    #[arg(required = true, value_parser = clap::value_parser ! (u8).range(0..=30))]
180    pub zoom: u8,
181
182    #[command(flatten)]
183    pub inargs: TileInputStreamArgs,
184
185    #[command(flatten)]
186    pub fmtopts: TileFmtOptions,
187}
188
189#[derive(Debug, Parser)]
190pub struct MergeArgs {
191    /// min zoom level (0-30) to merge to
192    #[arg(long, short = 'Z', default_value = "0")]
193    pub minzoom: u8,
194
195    #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
196    pub sort: bool,
197
198    #[command(flatten)]
199    pub inargs: TileInputStreamArgs,
200
201    #[command(flatten)]
202    pub fmtopts: TileFmtOptions,
203}
204
205#[derive(Debug, Parser)]
206pub struct FmtStrArgs {
207    #[command(flatten)]
208    pub inargs: TileInputStreamArgs,
209
210    #[command(flatten)]
211    pub fmtopts: TileFmtOptions,
212}
213
214#[derive(Debug, Parser)]
215pub struct ParentChildrenArgs {
216    #[command(flatten)]
217    pub inargs: TileInputStreamArgs,
218
219    #[command(flatten)]
220    pub fmtopts: TileFmtOptions,
221
222    #[arg(required = false, long, default_value = "1")]
223    pub depth: u8,
224}
225
226#[derive(Debug, Parser)]
227pub struct SqliteDbCommonArgs {
228    /// sqlite filepath
229    #[arg(required = true)]
230    pub filepath: String,
231
232    /// compact/minified json (default: false)
233    #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
234    pub min: bool,
235}
236
237#[derive(Debug, Parser)]
238pub struct TilesFilterArgs {
239    /// bbox(es) (west, south, east, north)
240    #[arg(required = false, long, value_parser = parse_bbox_ext, allow_hyphen_values = true)]
241    pub bbox: Option<Vec<BBox>>,
242
243    #[command(flatten)]
244    pub zoom: Option<ZoomArgGroup>,
245}
246
247impl TilesFilterArgs {
248    #[must_use]
249    pub fn zooms(&self) -> Option<Vec<u8>> {
250        match &self.zoom {
251            Some(zoom) => zoom.zooms(),
252            None => None,
253        }
254    }
255
256    #[must_use]
257    pub fn bboxes(&self) -> Option<Vec<BBox>> {
258        self.bbox.clone()
259    }
260
261    #[must_use]
262    pub fn tiles_filter_maybe(&self) -> Option<TilesFilter> {
263        if self.bbox.is_none() && self.zoom.is_none() {
264            None
265        } else {
266            Some(TilesFilter::new(self.bboxes(), self.zooms()))
267        }
268    }
269}
270
271#[derive(Debug, Parser, Clone, clap::ValueEnum)]
272pub enum DbtypeOption {
273    Flat,
274    Hash,
275    Norm,
276}
277
278impl From<&DbtypeOption> for MbtType {
279    fn from(opt: &DbtypeOption) -> Self {
280        match opt {
281            DbtypeOption::Flat => Self::Flat,
282            DbtypeOption::Hash => Self::Hash,
283            DbtypeOption::Norm => Self::Norm,
284        }
285    }
286}
287
288#[derive(Debug, Parser, Clone)]
289pub struct TouchArgs {
290    /// mbtiles filepath
291    #[arg(required = true)]
292    pub filepath: String,
293
294    /// page size
295    #[arg(required = false, long)]
296    pub page_size: Option<i64>,
297
298    /// db-type (default: flat)
299    #[arg(
300        required = false, long = "dbtype", aliases = ["db-type", "mbtype", "mbt-type"], default_value = "flat"
301    )]
302    pub dbtype: Option<DbtypeOption>,
303}
304
305impl TouchArgs {
306    #[must_use]
307    pub fn mbtype(&self) -> MbtType {
308        self.dbtype.as_ref().map_or(MbtType::Flat, |opt| opt.into())
309    }
310}
311
312#[derive(Debug, Subcommand)]
313/// sqlite utils/cmds
314pub enum SqliteCommands {
315    Analyze(AnalyzeArgs),
316    Header(SqliteHeaderArgs),
317    Vacuum(VacuumArgs),
318}
319
320impl SqliteCommands {
321    pub async fn run(&self) -> UtilesResult<()> {
322        match self {
323            Self::Analyze(args) => analyze_main(args).await,
324            Self::Header(args) => header_main(args).await,
325            Self::Vacuum(args) => vacuum_main(args).await,
326        }
327    }
328}
329
330// #[derive(Debug, Parser)]
331// pub struct SqliteSchemaArgs {
332//     #[command(flatten)]
333//     pub common: SqliteDbCommonArgs,
334// }
335
336#[derive(Debug, Parser)]
337/// Analyze sqlite db
338pub struct AnalyzeArgs {
339    #[command(flatten)]
340    pub common: SqliteDbCommonArgs,
341
342    #[arg(required = false, long)]
343    pub analysis_limit: Option<usize>,
344}
345
346#[derive(Debug, Parser)]
347/// Dump sqlite db header
348pub struct SqliteHeaderArgs {
349    #[command(flatten)]
350    pub common: SqliteDbCommonArgs,
351}
352
353#[derive(Debug, Parser)]
354/// vacuum sqlite db inplace/into
355pub struct VacuumArgs {
356    #[command(flatten)]
357    pub common: SqliteDbCommonArgs,
358
359    /// fspath to vacuum db into
360    #[arg(required = false)]
361    pub into: Option<String>,
362
363    /// Analyze db after vacuum
364    #[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
365    pub analyze: bool,
366
367    /// page size to set
368    #[arg(required = false, long)]
369    pub page_size: Option<i64>,
370}
371
372#[derive(Debug, Parser)]
373pub struct MetadataArgs {
374    #[command(flatten)]
375    pub common: SqliteDbCommonArgs,
376
377    /// Output as json object not array
378    #[arg(required = false, long, action = clap::ArgAction::SetTrue)]
379    pub obj: bool,
380
381    /// Output as json string for values (default: false)
382    #[arg(required = false, long, action = clap::ArgAction::SetTrue, default_value = "false")]
383    pub raw: bool,
384}
385
386#[derive(Debug, Parser)]
387pub struct MetadataSetArgs {
388    #[command(flatten)]
389    pub common: SqliteDbCommonArgs,
390
391    /// key or json-fspath
392    #[arg(required = true, value_name = "KEY/FSPATH")]
393    pub key: String,
394
395    /// value
396    #[arg(required = false)]
397    pub value: Option<String>,
398
399    /// dryrun (don't actually set)
400    #[arg(
401        required = false, long, aliases = ["dry-run"], short = 'n', action = clap::ArgAction::SetTrue
402    )]
403    pub dryrun: bool,
404}
405
406#[derive(Debug, Parser)]
407pub struct TilejsonArgs {
408    #[command(flatten)]
409    pub common: SqliteDbCommonArgs,
410
411    /// include tilestats
412    #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
413    pub tilestats: bool,
414}
415
416#[derive(Debug, Parser)]
417pub struct LintArgs {
418    /// filepath(s) or dirpath(s)
419    #[arg(required = true, num_args(1..))]
420    pub(crate) fspaths: Vec<String>,
421
422    /// fix lint errors (NOT IMPLEMENTED)
423    #[arg(
424        required = false, long, action = clap::ArgAction::SetTrue,
425        default_value = "false", hide = true
426    )]
427    pub(crate) fix: bool,
428}
429
430#[derive(Debug, Parser)]
431pub struct InfoArgs {
432    #[command(flatten)]
433    pub common: SqliteDbCommonArgs,
434
435    #[arg(required = false, long, action = clap::ArgAction::SetTrue)]
436    pub(crate) full: bool,
437
438    #[arg(required = false, long, short, visible_aliases = ["stats"], action = clap::ArgAction::SetTrue)]
439    pub(crate) statistics: bool,
440}
441
442#[derive(Debug, Parser)]
443pub struct AggHashArgs {
444    #[command(flatten)]
445    pub common: SqliteDbCommonArgs,
446
447    #[command(flatten)]
448    pub filter_args: TilesFilterArgs,
449    // /// bbox(es) (west, south, east, north)
450    // #[arg(required = false, long, value_parser = parse_bbox_ext, allow_hyphen_values = true)]
451    // pub bbox: Option<Vec<BBox>>,
452    /// hash to use for blob-id if copying to normal/hash db type
453    #[arg(required = false, long)]
454    pub hash: Option<HashType>,
455}
456
457#[derive(Debug, Parser)]
458pub struct UpdateArgs {
459    #[command(flatten)]
460    pub common: SqliteDbCommonArgs,
461
462    /// dryrun (don't actually update)
463    #[arg(required = false, long, short = 'n', action = clap::ArgAction::SetTrue)]
464    pub(crate) dryrun: bool,
465}
466#[derive(Debug, Parser)]
467pub struct ZxyifyArgs {
468    #[command(flatten)]
469    pub common: SqliteDbCommonArgs,
470
471    /// un-zxyify a db
472    #[arg(required = false, long, action = clap::ArgAction::SetTrue)]
473    pub(crate) rm: bool,
474}
475#[derive(Debug, Parser)]
476#[command(
477    name = "enumerate",
478    about = "enumerate db tiles like `tippecanoe-enumerate`"
479)]
480pub struct EnumerateArgs {
481    #[arg(required = true)]
482    pub(crate) fspaths: Vec<String>,
483
484    #[command(flatten)]
485    pub filter_args: TilesFilterArgs,
486
487    /// tippecanoe-enumerate like output '{relpath} {x} {y} {z}'
488    #[arg(required = false, long, short = 't', action = clap::ArgAction::SetTrue)]
489    pub(crate) tippecanoe: bool,
490}
491
492#[derive(Debug, Parser)]
493/// Optimize tiles-db
494pub struct OptimizeArgs {
495    #[command(flatten)]
496    pub common: SqliteDbCommonArgs,
497
498    /// destination dataset fspath (mbtiles, dirpath)
499    #[arg(required = true)]
500    pub dst: String,
501}
502
503// #[derive(Debug, Parser)]
504// pub struct OxipngArgs {
505//     #[command(flatten)]
506//     pub common: SqliteDbCommonArgs,
507//
508//     /// destination dataset fspath (mbtiles, dirpath)
509//     #[arg(required = true)]
510//     pub dst: String,
511//
512//     /// optimize level
513//     #[arg(required = false, long, short, default_value = "2")]
514//     pub(crate) opt: u8,
515//
516//     /// n-jobs ~ 0=ncpus (default: max(4, ncpus))
517//     #[arg(required = false, long, short)]
518//     pub jobs: Option<u8>,
519//
520//     /// quiet
521//     #[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
522//     pub(crate) quiet: bool,
523// }
524
525#[derive(Debug, Parser)]
526pub struct WebpifyArgs {
527    #[command(flatten)]
528    pub common: SqliteDbCommonArgs,
529
530    /// destination dataset fspath (mbtiles, dirpath)
531    #[arg(required = true)]
532    pub dst: String,
533
534    /// n-jobs ~ 0=ncpus (default: max(4, ncpus))
535    #[arg(required = false, long, short)]
536    pub jobs: Option<u8>,
537
538    /// quiet
539    #[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
540    pub(crate) quiet: bool,
541}
542
543#[derive(Debug, Parser)]
544pub struct CommandsArgs {
545    #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
546    pub full: bool,
547
548    #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
549    pub(crate) table: bool,
550    // /// compact/minified json (default: false)
551    // #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
552    // pub min: bool,
553}
554
555#[derive(Debug, Subcommand)]
556pub enum Commands {
557    // Alias `aboot` for possible Canadian users as they will not understand
558    // `about` -- similarly I often alias `math` to `maths` for british
559    // colleagues who would otherwise have no idea what I'm talking about
560    /// Echo info about utiles
561    #[command(name = "about", visible_alias = "aboot")]
562    About(AboutArgs),
563
564    /// list all commands
565    #[command(name = "commands", visible_alias = "cmds")]
566    Commands(CommandsArgs),
567
568    #[command(subcommand, visible_alias = "db")]
569    Sqlite(SqliteCommands),
570
571    /// Echo the `tile.json` for mbtiles file
572    #[command(name = "tilejson", visible_alias = "tj", alias = "trader-joes")]
573    Tilejson(TilejsonArgs),
574
575    /// Create new mbtiles db w/ schema
576    #[command(name = "touch")]
577    Touch(TouchArgs),
578
579    /// Copy tiles from src -> dst
580    #[command(name = "copy", visible_alias = "cp")]
581    Copy(CopyArgs),
582
583    /// Lint mbtiles file(s) (wip)
584    #[command(name = "lint")]
585    Lint(LintArgs),
586
587    /// Aggregate tile hashes for tiles-db
588    #[command(name = "agg-hash")]
589    AggHash(AggHashArgs),
590
591    /// Echo metadata (table) as json arr/obj
592    #[command(name = "metadata", visible_aliases = ["meta", "md"])]
593    Metadata(MetadataArgs),
594
595    /// Set metadata key/value or from `json` file if key is fspath
596    #[command(name = "metadata-set", visible_aliases = ["meta-set", "mds"])]
597    MetadataSet(MetadataSetArgs),
598
599    /// Update mbtiles db
600    #[command(name = "update", visible_aliases = ["up"])]
601    Update(UpdateArgs),
602
603    /// Enumerate tiles db
604    #[command(name = "enumerate", visible_aliases = ["enum"])]
605    Enumerate(EnumerateArgs),
606
607    /// rm-rf dirpath
608    #[command(name = "rimraf", visible_alias = "rmrf", hide = true)]
609    Rimraf(RimrafArgs),
610
611    /// Echo mbtiles info/stats
612    #[command(name = "info")]
613    Info(InfoArgs),
614
615    #[command(name = "vacuum", visible_alias = "vac")]
616    Vacuum(VacuumArgs),
617
618    /// Determine if mbtiles contains a latlong
619    #[command(name = "dbcontains")]
620    Contains {
621        /// mbtiles filepath
622        #[arg(required = true)]
623        filepath: String,
624
625        /// lat/long
626        #[arg(required = true)]
627        lnglat: LngLat,
628    },
629
630    /// zxyify/unzxyify tiles-db
631    ///
632    /// Adds/removes `z/x/y` table/view for querying tiles not inverted
633    #[command(name = "zxyify", aliases = ["xyzify"])]
634    Zxyify(ZxyifyArgs),
635
636    /*
637    ========================================================================
638    TILE CLI UTILS - MERCANTILE LIKE CLI
639    ========================================================================
640    */
641    /// Format json-tiles `[x, y, z]` tiles w/ format-string
642    ///
643    /// fmt-tokens:
644    ///     `{json_arr}`/`{json}`  -> [x, y, z]
645    ///     `{json_obj}`/`{obj}`   -> {x: x, y: y, z: z}
646    ///     `{quadkey}`/`{qk}`     -> quadkey string
647    ///     `{pmtileid}`/`{pmid}`  -> pmtile-id
648    ///     `{x}`                  -> x tile coord
649    ///     `{y}`                  -> y tile coord
650    ///     `{z}`                  -> z/zoom level
651    ///     `{-y}`/`{yup}`         -> y tile coord flipped/tms
652    ///     `{zxy}`                -> z/x/y
653    ///
654    ///
655    /// Example:
656    ///     ```
657    ///     \> echo "[486, 332, 10]" | utiles fmtstr
658    ///     [486, 332, 10]
659    ///     \> echo "[486, 332, 10]" | utiles fmtstr --fmt "{x},{y},{z}"
660    ///     486,332,10
661    ///     \> echo "[486, 332, 10]" | utiles fmt --fmt "SELECT * FROM tiles WHERE zoom_level = {z} AND tile_column = {x} AND tile_row = {y};"
662    ///     SELECT * FROM tiles WHERE zoom_level = 10 AND tile_column = 486 AND tile_row = 332;
663    ///     ```
664    ///
665    #[command(
666        name = "fmtstr",
667        aliases = & ["fmt", "xt"],
668        verbatim_doc_comment,
669    )]
670    Fmt(TileFmtArgs),
671
672    /// Echo the Web Mercator tile at ZOOM level bounding `GeoJSON` [west, south,
673    /// east, north] bounding boxes, features, or collections read from stdin.
674    ///
675    /// Input may be a compact newline-delimited sequences of JSON or a
676    /// pretty-printed ASCII RS-delimited sequence of JSON (like
677    /// <https://tools.ietf.org/html/rfc8142> and
678    /// <https://tools.ietf.org/html/rfc7159>).
679    ///
680    /// Examples:
681    ///
682    ///   \> echo "[-105.05, 39.95, -105, 40]" | utiles bounding-tile
683    ///   [426, 775, 11]
684    #[command(
685        name = "bounding-tile",
686        verbatim_doc_comment,
687        about = "Echo bounding tile at zoom for bbox / geojson"
688    )]
689    BoundingTile(TileFmtArgs),
690
691    /// Converts tiles to/from quadkey/[x, y, z]
692    ///
693    /// Input may be a compact newline-delimited sequences of JSON or a
694    /// pretty-printed ASCII RS-delimited sequence of JSON (like
695    /// <https://tools.ietf.org/html/rfc8142> and
696    /// <https://tools.ietf.org/html/rfc7159>).
697    ///
698    /// Examples:
699    ///
700    ///   \> echo "[486, 332, 10]" | utiles quadkey
701    ///   0313102310
702    ///   \> echo "0313102310" | utiles quadkey
703    ///   [486, 332, 10]
704    ///   \> utiles quadkey 0313102310
705    ///   [486, 332, 10]
706    #[command(name = "quadkey", verbatim_doc_comment, visible_alias = "qk")]
707    Quadkey(TileFmtArgs),
708
709    /// Converts tile(s) to/from pmtile-id/[x, y, z]
710    ///
711    /// Input may be a compact newline-delimited sequences of JSON or a
712    /// pretty-printed ASCII RS-delimited sequence of JSON (like
713    /// <https://tools.ietf.org/html/rfc8142> and
714    /// <https://tools.ietf.org/html/rfc7159>).
715    ///
716    /// Examples:
717    ///
718    ///   \> echo "[486, 332, 10]" | utiles pmtileid
719    ///   506307
720    ///   \> echo "506307" | utiles pmtileid
721    ///   [486, 332, 10]
722    ///   \> utiles pmtileid 506307
723    ///   [486, 332, 10]
724    #[command(name = "pmtileid", verbatim_doc_comment, visible_alias = "pmid")]
725    Pmtileid(TileFmtArgs),
726
727    /// Echos web-mercator tiles at zoom level intersecting given geojson-bbox [west, south,
728    /// east, north], geojson-features, or geojson-collections read from stdin.
729    ///
730    /// Output format is a JSON `[x, y, z]` array by default; use --obj to output a
731    /// JSON object `{x: x, y: y, z: z}`.
732    ///
733    /// bbox shorthands (case-insensitive):
734    ///     "*"  | "world"     => [-180, -85.0511, 180, 85.0511]
735    ///     "n"  | "north"     => [-180, 0, 180, 85.0511]
736    ///     "s"  | "south"     => [-180, -85.0511, 180, 0]
737    ///     "e"  | "east"      => [0, -85.0511, 180, 85.0511]
738    ///     "w"  | "west"      => [-180, -85.0511, 0, 85.0511]
739    ///     "ne" | "northeast" => [0, 0, 180, 85.0511]
740    ///     "se" | "southeast" => [0, -85.0511, 180, 0]
741    ///     "nw" | "northwest" => [-180, 0, 0, 85.0511]
742    ///     "sw" | "southwest" => [-180, -85.0511, 0, 0]
743    ///
744    /// Input may be a compact newline-delimited sequences of JSON or a
745    /// pretty-printed ASCII RS-delimited sequence of JSON (like
746    /// <https://tools.ietf.org/html/rfc8142> and
747    /// <https://tools.ietf.org/html/rfc7159>).
748    ///
749    /// Example:
750    ///
751    ///   \\> echo "[-105.05, 39.95, -105, 40]" | utiles tiles 12
752    ///   [852, 1550, 12]
753    ///   [852, 1551, 12]
754    ///   [853, 1550, 12]
755    ///   [853, 1551, 12]
756    ///   \> utiles tiles 12 "[-105.05, 39.95, -105, 40]"
757    ///   [852, 1550, 12]
758    ///   [852, 1551, 12]
759    ///   [853, 1550, 12]
760    ///   [853, 1551, 12]
761    #[command(
762        name = "tiles",
763        verbatim_doc_comment,
764        about = "Echo tiles at zoom intersecting geojson bbox / feature / collection"
765    )]
766    Tiles(TilesArgs),
767
768    /// Echo the neighbor tiles for input tiles
769    ///
770    /// Input may be a compact newline-delimited sequences of JSON or a
771    /// pretty-printed ASCII RS-delimited sequence of JSON (like
772    /// <https://tools.ietf.org/html/rfc8142> and
773    /// <https://tools.ietf.org/html/rfc7159>).
774    #[command(name = "neighbors")]
775    Neighbors(TileFmtArgs),
776
777    /// Echo children tiles of input tiles
778    ///
779    /// Input may be a compact newline-delimited sequences of JSON or a
780    /// pretty-printed ASCII RS-delimited sequence of JSON (like
781    /// <https://tools.ietf.org/html/rfc8142> and
782    /// <https://tools.ietf.org/html/rfc7159>).
783    ///
784    /// Example:
785    ///
786    ///   \> echo "[486, 332, 10]" | utiles children
787    ///   [972, 664, 11]
788    #[command(name = "children", verbatim_doc_comment)]
789    Children(ParentChildrenArgs),
790
791    /// Echo parent tile of input tiles
792    #[command(name = "parent")]
793    Parent(ParentChildrenArgs),
794
795    /// Echo tiles as `GeoJSON` feature collections/sequences
796    ///
797    /// Input may be a compact newline-delimited sequences of JSON or a
798    /// pretty-printed ASCII RS-delimited sequence of JSON (like
799    /// <https://tools.ietf.org/html/rfc8142> and
800    /// <https://tools.ietf.org/html/rfc7159>).
801    ///
802    /// Example:
803    ///
804    ///   \> echo "[486, 332, 10]" | utiles shapes --precision 4 --bbox
805    ///   [-9.1406, 53.1204, -8.7891, 53.3309]
806    #[command(name = "shapes")]
807    Shapes(ShapesArgs),
808
809    /// Burn tiles from `GeoJSON` stream at zoom level (tile coverage)
810    #[command(name = "burn", visible_alias = "cover")]
811    Burn(BurnArgs),
812
813    /// Merge tiles from stream removing parent tiles if children are present
814    #[command(name = "merge")]
815    Merge(MergeArgs),
816
817    /// Echo edge tiles from stream of xyz tiles
818    #[command(name = "edges")]
819    Edges(EdgesArgs),
820
821    /// Convert raster mbtiles to webp format
822    #[command(
823        name = "webpify",
824        about = "Convert raster mbtiles to webp format",
825        hide = true
826    )]
827    Webpify(WebpifyArgs),
828
829    /// Convert raster mbtiles to webp format
830    #[command(
831        name = "optimize",
832        about = "Optimize tiles-db",
833        aliases = ["opt"],
834        hide = true
835    )]
836    Optimize(OptimizeArgs),
837
838    /// utiles server (wip)
839    #[command(name = "serve", hide = true)]
840    Serve(ServeArgs),
841
842    /// Development/Playground command (hidden)
843    #[command(name = "dev", hide = true)]
844    Dev(DevArgs),
845
846    // ========================================================================
847    // UNIMPLEMENTED
848    // ========================================================================
849    /// UNIMPLEMENTED
850    #[command(name = "addo", hide = true)]
851    Addo,
852
853    /// UNIMPLEMENTED
854    #[command(name = "translate", hide = true)]
855    Translate,
856}
857
858#[derive(Debug, Parser, Clone)]
859#[command(name = "rimraf", about = "rm-rf dirpath")]
860pub struct RimrafArgs {
861    /// dirpath to nuke
862    #[arg(required = true)]
863    pub dirpath: String,
864
865    /// collect and print file sizes
866    #[arg(required = false, long, action = clap::ArgAction::SetTrue)]
867    pub(crate) size: bool,
868
869    /// dryrun (don't actually rm)
870    #[arg(required = false, short = 'n', long, action = clap::ArgAction::SetTrue)]
871    pub(crate) dryrun: bool,
872
873    #[arg(required = false, short, long, action = clap::ArgAction::SetTrue)]
874    pub(crate) verbose: bool,
875}
876
877#[derive(Args, Debug)]
878#[group(required = false, multiple = false, id = "minmaxzoom")]
879pub struct MinMaxZoom {
880    /// min zoom level (0-30)
881    #[arg(long)]
882    minzoom: Option<u8>,
883
884    /// max zoom level (0-30)
885    #[arg(long)]
886    maxzoom: Option<u8>,
887}
888
889#[derive(Debug, Parser)]
890pub struct ZoomArgGroup {
891    /// Zoom level (0-30)
892    #[arg(short, long, required = false, value_delimiter = ',', value_parser = zoom::parse_zooms)]
893    pub zoom: Option<Vec<Vec<u8>>>,
894
895    /// min zoom level (0-30)
896    #[arg(long, conflicts_with = "zoom", aliases = ["min-zoom", "min-z"])]
897    pub minzoom: Option<u8>,
898
899    /// max zoom level (0-30)
900    #[arg(long, conflicts_with = "zoom", aliases = ["max-zoom", "max-z"])]
901    pub maxzoom: Option<u8>,
902}
903
904impl ZoomArgGroup {
905    #[must_use]
906    pub fn zooms(&self) -> Option<Vec<u8>> {
907        match &self.zoom {
908            Some(zooms) => Some(zooms.iter().flatten().copied().collect()),
909            None => match (self.minzoom, self.maxzoom) {
910                (Some(minzoom), Some(maxzoom)) => Some((minzoom..=maxzoom).collect()),
911                (Some(minzoom), None) => Some((minzoom..=31).collect()),
912                (None, Some(maxzoom)) => Some((0..=maxzoom).collect()),
913                (None, None) => None,
914            },
915        }
916    }
917}
918
919#[derive(
920    Debug, Copy, Parser, Clone, clap::ValueEnum, strum::EnumString, AsRefStr, Default,
921)]
922#[strum(serialize_all = "kebab-case")]
923pub enum ConflictStrategy {
924    #[default]
925    Undefined,
926    Ignore,
927    Replace,
928    Abort,
929    Fail,
930}
931
932impl From<ConflictStrategy> for InsertStrategy {
933    fn from(cs: ConflictStrategy) -> Self {
934        match cs {
935            ConflictStrategy::Undefined => Self::None,
936            ConflictStrategy::Ignore => Self::Ignore,
937            ConflictStrategy::Replace => Self::Replace,
938            ConflictStrategy::Abort => Self::Abort,
939            ConflictStrategy::Fail => Self::Fail,
940        }
941    }
942}
943
944#[derive(Debug, Parser)]
945#[command(name = "copy", about = "Copy tiles from src -> dst")]
946pub struct CopyArgs {
947    /// source dataset fspath (mbtiles, dirpath)
948    #[arg(required = true)]
949    pub src: String,
950
951    /// destination dataset fspath (mbtiles, dirpath)
952    #[arg(required = true)]
953    pub dst: String,
954
955    /// dryrun (don't actually copy)
956    #[arg(required = false, long, short = 'n', action = clap::ArgAction::SetTrue)]
957    pub dryrun: bool,
958
959    /// force overwrite dst
960    #[arg(required = false, long, short, action = clap::ArgAction::SetTrue)]
961    pub force: bool,
962
963    #[command(flatten)]
964    pub zoom: Option<ZoomArgGroup>,
965
966    /// bbox (west, south, east, north)
967    #[arg(required = false, long, value_parser = parse_bbox_ext, allow_hyphen_values = true)]
968    pub bbox: Option<BBox>,
969
970    /// conflict strategy when copying tiles
971    #[arg(required = false, long, short, default_value = "undefined")]
972    pub conflict: ConflictStrategy,
973
974    /// db-type to copy to (flat, hash, norm) defaults to dbtype of src
975    #[arg(
976          required = false,
977          long = "dst-type",
978          aliases = ["dbtype", "dsttype", "mbtype", "mbt-type", "mbtiles-type"]
979    )]
980    pub dst_type: Option<DbtypeOption>,
981
982    /// hash to use for blob-id if copying to normal/hash db type
983    #[arg(required = false, long)]
984    pub hash: Option<HashType>,
985
986    /// n-jobs ~ 0=ncpus (default: max(4, ncpus))
987    #[arg(required = false, long, short)]
988    pub jobs: Option<u8>,
989
990    /// sqlite fast writing mode (default: false) WIP to use streams
991    #[arg(required = false, long, hide = true, action = clap::ArgAction::SetTrue)]
992    pub stream: bool,
993}
994
995impl CopyArgs {
996    #[must_use]
997    pub fn zooms(&self) -> Option<Vec<u8>> {
998        match &self.zoom {
999            Some(zoom) => zoom.zooms(),
1000            None => None,
1001        }
1002    }
1003
1004    #[must_use]
1005    pub fn zoom_set(&self) -> Option<ZoomSet> {
1006        self.zooms().map(|zooms| ZoomSet::from_zooms(&zooms))
1007    }
1008
1009    #[must_use]
1010    pub fn bboxes(&self) -> Option<Vec<BBox>> {
1011        self.bbox.as_ref().map(|bbox| vec![*bbox])
1012    }
1013
1014    #[must_use]
1015    pub fn bounds(&self) -> Option<String> {
1016        if let Some(bboxes) = self.bboxes() {
1017            let new_bbox = geobbox_merge(&bboxes);
1018            Some(new_bbox.mbt_bounds())
1019        } else {
1020            None
1021        }
1022    }
1023}
1024
1025impl From<&CopyArgs> for CopyConfig {
1026    fn from(args: &CopyArgs) -> Self {
1027        let dbtype = args.dst_type.as_ref().map(|dbtype| dbtype.into());
1028        Self {
1029            src: PathBuf::from(&args.src),
1030            dst: PathBuf::from(&args.dst),
1031            zset: args.zoom_set(),
1032            zooms: args.zooms(),
1033            verbose: true,
1034            bboxes: args.bboxes(),
1035            bounds_string: args.bounds(),
1036            force: false,
1037            dryrun: false,
1038            jobs: args.jobs,
1039            istrat: InsertStrategy::from(args.conflict),
1040            hash: args.hash,
1041            dst_type: dbtype,
1042            stream: args.stream,
1043        }
1044    }
1045}