Skip to main content

mvt_wrangler/
lib.rs

1use anyhow::Result;
2use clap::Parser;
3use pmtiles::AsyncPmTilesReader;
4use std::{fs::File, path::PathBuf};
5use tokio::fs;
6
7mod filtering;
8mod metadata;
9mod processing;
10mod transform;
11
12#[derive(Parser)]
13#[command(author, version, about)]
14pub struct Args {
15    /// Input PMTiles file
16    pub input: PathBuf,
17
18    /// Output PMTiles file (will be overwritten if exists)
19    pub output: PathBuf,
20
21    /// Optional? GeoJSON file to filter features. Honestly, why are you using this tool if you don't want to filter?
22    /// See FILTERING.md for details on the syntax.
23    #[arg(short, long)]
24    pub filter: Option<PathBuf>,
25
26    /// Name of the tileset (for PMTiles metadata)
27    #[arg(long, short = 'n')]
28    pub name: Option<String>,
29
30    /// Description of the tileset (for PMTiles metadata)
31    #[arg(long, short = 'N')]
32    pub description: Option<String>,
33
34    /// Attribution information for the tileset (for PMTiles metadata)
35    #[arg(long, short = 'A')]
36    pub attribution: Option<String>,
37}
38
39pub async fn run(args: Args) -> Result<()> {
40    // Remove any existing output
41    if args.output.exists() {
42        fs::remove_file(&args.output).await?;
43    }
44
45    let pmtiles_path = args.input;
46    if !pmtiles_path.exists() {
47        panic!("Input file does not exist: {}", pmtiles_path.display());
48    }
49
50    // Validate filter file if provided
51    let mut fc = None;
52    if let Some(filter_path) = &args.filter {
53        if !filter_path.exists() {
54            panic!("Filter file does not exist: {}", filter_path.display());
55        }
56        let filter_str = fs::read_to_string(filter_path).await?;
57        let filter_json: filtering::data::FilterCollection = serde_json::from_str(&filter_str)?;
58        let compiled = filter_json.compile()?;
59        fc = Some(compiled);
60    }
61
62    // Ensure output has pmtiles extension
63    if args.output.extension().and_then(|s| s.to_str()) != Some("pmtiles") {
64        panic!("Output file must have .pmtiles extension");
65    }
66
67    // Open input and new output DBs
68    let in_pmt = AsyncPmTilesReader::new_with_path(&pmtiles_path).await?;
69    let out_pmt_f = File::create(&args.output)?;
70    let header = in_pmt.get_header();
71    let in_metadata_str = in_pmt.get_metadata().await?;
72    if header.tile_type != pmtiles::TileType::Mvt {
73        panic!("Unsupported tile type: {:?}", header.tile_type);
74    }
75    // Build output metadata by merging input metadata with overrides
76    let out_metadata_str = metadata::apply_overrides(
77        &in_metadata_str,
78        args.name.as_deref(),
79        args.description.as_deref(),
80        args.attribution.as_deref(),
81    )?;
82    let out_pmt = pmtiles::PmTilesWriter::new(header.tile_type)
83        .tile_compression(header.tile_compression)
84        .min_zoom(header.min_zoom)
85        .max_zoom(header.max_zoom)
86        .bounds(
87            header.min_longitude,
88            header.min_latitude,
89            header.max_longitude,
90            header.max_latitude,
91        )
92        .center_zoom(header.center_zoom)
93        .center(header.center_longitude, header.center_latitude)
94        .metadata(&out_metadata_str)
95        .create(out_pmt_f)?;
96
97    processing::process_tiles(&pmtiles_path, out_pmt, header.tile_compression, fc).await?;
98
99    println!("✅ Wrote transformed tiles to {}", args.output.display());
100    Ok(())
101}