geoq 0.0.25

Geospatial utility CLI
#![feature(try_blocks)]
mod geoq;
use geoq::commands;
use geoq::error::Error;
use geoq::text;

use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use std::process;

fn run(matches: ArgMatches) -> Result<(), Error> {
    match matches.subcommand() {
        ("wkt", Some(_)) => commands::wkt::run(),
        ("read", Some(_)) => commands::read::run(),
        ("gj", Some(m)) => commands::geojson_cmd::run(m),
        ("gh", Some(m)) => commands::geohash::run(m),
        ("map", Some(_)) => commands::map::run(),
        ("snip", Some(_)) => commands::snip::run(),
        ("filter", Some(m)) => commands::filter::run(m),
        ("json", Some(m)) => commands::json::run(m),
        ("centroid", Some(_)) => commands::centroid::run(),
        ("whereami", Some(_)) => commands::whereami::run(),
        ("simplify", Some(m)) => commands::simplify::run(m),
        ("measure", Some(m)) => commands::measure::run(m),
        ("bbox", Some(m)) => commands::bbox::run(m),
        ("shp", Some(m)) => commands::shp::run(m),
        ("fgb", Some(m)) => commands::fgb::run(m),
        ("h3", Some(m)) => commands::h3::run(m),
        _ => Err(Error::UnknownCommand),
    }
}

const VERSION: &'static str = env!("CARGO_PKG_VERSION");

fn main() {
    let geojson = SubCommand::with_name("gj")
        .about("Output features as GeoJSON")
        .subcommand(SubCommand::with_name("geom").about("Output entity as a GeoJSON geometry"))
        .subcommand(SubCommand::with_name("f").about("Output entity as a GeoJSON Feature"))
        .subcommand(
            SubCommand::with_name("fc")
                .about("Collect all given entities into a GeoJSON Feature Collection"),
        );

    let geohash = SubCommand::with_name("gh")
        .about("Work with geohashes")
        .subcommand(
            SubCommand::with_name("point")
                .about("Output base 32 Geohash for a given Lat,Lon")
                .arg(
                    Arg::with_name("level")
                        .help("Characters of geohash precision")
                        .required(true)
                        .index(1),
                ),
        )
        .subcommand(
            SubCommand::with_name("covering")
                .about("Output the set of geohashes at the given level which covers the given entity.")
                .arg(
                    Arg::with_name("level")
                        .help("Characters of geohash precision")
                        .required(true)
                        .index(1),
                ).arg(Arg::with_name("original")
                      .long("original")
                      .short("o")
                      .help("Also print the query entity in the output.\nUseful for mapping a geometry along with its covering Geohashes.")),
        )
        .subcommand(SubCommand::with_name("children").about("Get children for the given geohash"))
        .subcommand(SubCommand::with_name("roots").about("List the Base32 Geohash root characters"))
        .subcommand(SubCommand::with_name("encode-long").about("Convert a 64 bit geohash from Base 10 numeric representation to Base 32."))
        .subcommand(SubCommand::with_name("neighbors")
                    .about("Get neighbors of the given Geohash")
                    .arg(Arg::with_name("exclude")
                         .long("exclude")
                         .short("e")
                         .help("Exclude the given geohash from its neighbors.\nBy default it will be included in the output,\ngiving a 3x3 grid centered on the provided geohash.")));

    let filter = SubCommand::with_name("filter")
        .about("Select features based on geospatial predicates")
        .after_help(text::FILTER_AFTER_HELP)
        .arg(Arg::with_name("query-file")
             .help("Input file for reading query feature(s).")
             .takes_value(true)
             .global(true)
             .long("query-file")
             .short("q"))
        .arg(Arg::with_name("negate")
             .help("Negate the filter, so intersects becomes 'not intersects', etc.")
             .global(true)
             .long("negate")
             .short("n"))
        .subcommand(
            SubCommand::with_name("intersects")
                .about("Output only entities (from STDIN) which intersect a QUERY entity (as command-line ARG)")
                .arg(Arg::with_name("query")
                     .help("Entity to check intersections.\nMust be Lat/Lon, Geohash, WKT, or GeoJSON.")
                     .index(1))
        )
        .subcommand(
            SubCommand::with_name("contains")
                .about("Output only entities (from STDIN) which fall within a QUERY entity (as command-line ARG)")
                .arg(
                    Arg::with_name("query")
                        .help("Entity to check intersections.\nMust be Geohash, WKT, or GeoJSON.\nMust be a POLYGON or MULTIPOLYGON.")
                        .index(1)
                )
        )
        .subcommand(
            SubCommand::with_name("dwithin")
                .about("Output only points (from STDIN) which fall within a QUERY entity (as command-line ARG)")
                .after_help(text::FILTER_DWITHIN_AFTER_HELP)
                .arg(
                    Arg::with_name("query")
                        .help("Feature(s) to check intersections.\nMust be Geohash, WKT, or GeoJSON.")
                        .index(1)
                )
                .arg(
                    Arg::with_name("radius")
                        .help("Radius in meters")
                        .takes_value(true)
                        .required(true)
                        .long("radius")
                        .short("r")
                )
        );

    let json = SubCommand::with_name("json")
        .about("Best-guess conversions from geo-oriented JSON to GeoJSON")
        .subcommand(
            SubCommand::with_name("munge")
                .about("Attempt to convert arbitrary JSON to a GeoJSON Feature.")
                .after_help(text::JSON_MUNGE_AFTER_HELP),
        );

    let read = SubCommand::with_name("read")
        .about("Information about reading inputs with geoq")
        .after_help(text::READ_AFTER_HELP);

    let centroid = SubCommand::with_name("centroid")
        .about(text::CENTROID_ABOUT)
        .after_help(text::CENTROID_AFTER_HELP);

    let whereami = SubCommand::with_name("whereami")
        .about(text::WHEREAMI_ABOUT)
        .after_help(text::WHEREAMI_AFTER_HELP);

    let measure = SubCommand::with_name("measure")
        .about(text::MEASURE_ABOUT)
        .subcommand(
            SubCommand::with_name("distance")
                .about(text::DISTANCE_ABOUT)
                .after_help(text::DISTANCE_AFTER_HELP)
                .arg(
                    Arg::with_name("query")
                        .help(text::DISTANCE_QUERY_ARG_HELP)
                        .required(true)
                        .index(1),
                ),
        )
        .subcommand(
            SubCommand::with_name("coord-count")
                .about(text::MEASURE_COORDS_ABOUT)
                .arg(
                    Arg::with_name("geojson")
                        .long("geojson")
                        .required(false)
                        .takes_value(false)
                        .help(text::MEASURE_COORDS_GEOJSON_ARG_HELP),
                ),
        );

    let simplify = SubCommand::with_name("simplify")
        .about(text::SIMPLIFY_ABOUT)
        .after_help(text::SIMPLIFY_AFTER_HELP)
        .arg(
            Arg::with_name("epsilon")
                .help(text::SIMPLIFY_EPSILON_ARG_HELP)
                .required(true)
                .index(1),
        )
        .arg(
            Arg::with_name("to_coord_count")
                .long("to-coord-count")
                .required(false)
                .takes_value(true)
                .help(text::SIMPLIFY_TO_COORD_COUNT_ARG_HELP),
        );

    let bbox = SubCommand::with_name("bbox")
        .about("Generate bounding boxes for geometries")
        .arg(Arg::with_name("embed").long("embed").short("e").help(
            "Print inputs as GeoJSON features and include the bbox in the GeoJSON bbox field",
        ))
        .arg(
            Arg::with_name("all")
                .long("all")
                .short("a")
                .help("Give a single bbox for all input geometries rather than 1 bbox per input"),
        )
        .after_help(text::BBOX_AFTER_HELP);

    let shp = SubCommand::with_name("shp")
        .about("Read a shapefile and convert to GeoJSON")
        .arg(
            Arg::with_name("path")
                .help("output file, e.g. data.fgb")
                .required(true)
                .index(1),
        );

    let fgb = SubCommand::with_name("fgb")
        .about("Reading and Writing FlatGeoBuf")
        .subcommand(
            SubCommand::with_name("write")
                .about("Write GeoJSON data to a binary flatgeobuf file")
                .arg(
                    Arg::with_name("path")
                        .help("output path")
                        .required(true)
                        .index(1),
                ),
        )
        .subcommand(
            SubCommand::with_name("read")
                .about("Read a binary flatgeobuf file to GeoJSON")
                .arg(
                    Arg::with_name("path")
                        .help("input path to .fgb file")
                        .required(true)
                        .index(1),
                )
                .arg(
                    Arg::with_name("bbox")
                        .allow_hyphen_values(true)
                        .long("bbox")
                        .required(false)
                        .takes_value(true)
                        .help("Comma-separated bounding box: minLon,minLat,maxLon,maxLat"),
                ),
        );

    let h3 = SubCommand::with_name("h3")
        .about("Work with H3")
        .subcommand(
            SubCommand::with_name("point")
                .about("Output hexadecimal encoded Cell ID for a given Lat,Lon at requested resolution")
                .arg(
                    Arg::with_name("resolution")
                        .help("H3 cell resolution (0-15)")
                        .required(true)
                        .index(1),
                ),
        )
        .subcommand(
            SubCommand::with_name("hierarchy")
                .about("Output all hexadecimal encoded Cells for a given Lat,Lon, from res 0 to 15")
        )
        .subcommand(
            SubCommand::with_name("to-str")
                .about("Convert an h3 numeric index (64-bit integer representation) to its hexadecimal string representation")
        ).subcommand(
            SubCommand::with_name("from-str")
                .about("Convert an h3 string index (15-character hexadecimal representation) to its 64-bit integer numeric representation")
        ).subcommand(
            SubCommand::with_name("children")
                .about("Get children for given cells at given resolution. If no resolution is given, the cell's resolution + 1 is used.")
                .arg(
                    Arg::with_name("resolution")
                        .help("H3 cell resolution (0-15)")
                        .index(1),
                )
        ).subcommand(
            SubCommand::with_name("parent")
                .about("Get parent (or ancestor) for given cells at given resolution. If no resolution is given, the cell's resolution - 1 (immediate parent) is used.")
                .arg(
                    Arg::with_name("resolution")
                        .help("H3 cell resolution (0-15)")
                        .index(1),
                )
        ).subcommand(SubCommand::with_name("resolution").about("Get resolution for an H3 cell"))
        .subcommand(
            SubCommand::with_name("grid-disk")
                .about("Get disk of given radius around given cells. Default radius is 1.")
                .arg(
                    Arg::with_name("radius")
                        .help("https://h3geo.org/docs/api/traversal/#griddisk")
                        .index(1),
                )
        ).subcommand(
            SubCommand::with_name("covering")
                .about("Generate set of H3 cells covering a geometry.")
                .arg(
                    Arg::with_name("resolution")
                        .help("H3 cell resolution (0-15)")
                        .index(1),
                )
                .arg(Arg::with_name("original")
                     .long("original")
                     .short("o")
                     .help("Also print the query entity in the output.\nUseful for mapping a geometry along with its covering H3 Cells."))
        );

    let matches = App::new("geoq")
        .version(VERSION)
        .setting(AppSettings::SubcommandRequiredElseHelp)
        .about("geoq - GeoSpatial utility belt")
        .after_help(text::MAIN_AFTER_HELP)
        .subcommand(SubCommand::with_name("wkt").about("Output features as Well-Known Text"))
        .subcommand(SubCommand::with_name("map").about("View features on a map using geojson.io"))
        .subcommand(read)
        .subcommand(geohash)
        .subcommand(geojson)
        .subcommand(json)
        .subcommand(filter)
        .subcommand(centroid)
        .subcommand(whereami)
        .subcommand(measure)
        .subcommand(simplify)
        .subcommand(bbox)
        .subcommand(shp)
        .subcommand(fgb)
        .subcommand(h3)
        .get_matches();

    if let Err(e) = run(matches) {
        eprintln!("geoq exited with error: {:?}", e);
        process::exit(1);
    }
}