Skip to content

Commit

Permalink
Command line tool options cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch committed Aug 9, 2024
1 parent b6a77fb commit be292a6
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 110 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
name: "mvt-tools",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.macOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
],
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Mapbox vector tiles (MVT) reader/writer library for Swift, together with a tool

## Requirements

This package requires Swift 5.10 or higher (at least Xcode 14), and compiles on iOS (\>= iOS 13), macOS (\>= macOS 10.15), tvOS (\>= tvOS 13), watchOS (\>= watchOS 6) as well as Linux.
This package requires Swift 5.10 or higher (at least Xcode 14), and compiles on iOS (\>= iOS 13), macOS (\>= macOS 13), tvOS (\>= tvOS 13), watchOS (\>= watchOS 6) as well as Linux.

## Installation with Swift Package Manager

Expand Down Expand Up @@ -111,19 +111,25 @@ You can install the command line tool `mvt` either
# mvt -h
OVERVIEW: A utility for inspecting and working with vector tiles.

The tile coordinate can be extracted from the path if it's either in the form '/z/x/y' or 'z_x_y'.
Examples:
- Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
- https://demotiles.maplibre.org/tiles/2/2/1.pbf
USAGE: mvt <subcommand>
OPTIONS:
--version Show the version.
-h, --help Show help information.
SUBCOMMANDS:
dump (default) Print the vector tile as GeoJSON
dump (default) Print the vector tile as GeoJSON to the console
info Print information about the vector tile
merge Merge two or more vector tiles
query Query the features in a vector tile
export Export the vector tile as GeoJSON
import Import some GeoJSONs to a vector tile
merge Merge two or more vector tiles
import Import some GeoJSONs into a vector tile
export Export the vector tile as GeoJSON to a file
See 'mvt help <subcommand>' for detailed help.
```
Expand Down Expand Up @@ -335,6 +341,6 @@ brew install protobuf swift-protobuf swiftlint
MIT
# Author
# Authors
Thomas Rasch, Outdooractive
100 changes: 58 additions & 42 deletions Sources/MVTCLI/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ import MVTTools
@main
struct CLI: AsyncParsableCommand {

static let logger = Logger(label: "mvttool")
static let logger = Logger(label: "mvt")

static let configuration = CommandConfiguration(
commandName: "mvt",
abstract: "A utility for inspecting and working with vector tiles.",
discussion: """
The tile coordinate can be extracted from the path if it's either in the form '/z/x/y' or 'z_x_y'.
Examples:
- Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
- https://demotiles.maplibre.org/tiles/2/2/1.pbf
""",
version: cliVersion,
subcommands: [Dump.self, Info.self, Merge.self, Query.self, Export.self, Import.self],
subcommands: [
Dump.self,
Info.self,
Query.self,
Merge.self,
Import.self,
Export.self,
],
defaultSubcommand: Dump.self)

}
Expand All @@ -25,49 +39,21 @@ struct CLIError: LocalizedError {
}
}

struct Options: ParsableArguments {

@Flag(name: .shortAndLong, help: "Print some debug info")
var verbose = false

@Option(name: .short, help: "Tile zoom level - if it can't be extracted from the path")
var z: Int?
struct XYZOptions: ParsableArguments {

@Option(name: .short, help: "Tile x coordinate - if it can't be extracted from the path")
@Option(name: .short, help: "Tile x coordinate, if it can't be extracted from the path")
var x: Int?

@Option(name: .short, help: "Tile y coordinate - if it can't be extracted from the path")
@Option(name: .short, help: "Tile y coordinate, if it can't be extracted from the path")
var y: Int?

@Argument(
help: "The MVT resource (file or URL). The tile coordinate can be extracted from the path if it's either in the form '/z/x/y' or 'z_x_y'",
completion: .file(extensions: ["pbf", "mvt"]))
var path: String
@Option(name: .short, help: "Tile zoom level, if it can't be extracted from the path")
var z: Int?

// Try to parse x/y/z from the path/URL
mutating func parseUrl(
extractCoordinate: Bool = true,
checkExistence: Bool = true)
throws -> URL
mutating func parseXYZ(
fromPath path: String)
throws -> (Int, Int, Int)
{
let url: URL
if path.hasPrefix("http") {
guard let parsedUrl = URL(string: path) else {
throw CLIError("\(path) is not a valid URL")
}
url = parsedUrl
}
else {
url = URL(fileURLWithPath: path)
if checkExistence {
guard try url.checkResourceIsReachable() else {
throw CLIError("The file '\(path)' doesn't exist.")
}
}
}

guard extractCoordinate else { return url }

if x == nil
|| y == nil
|| z == nil
Expand Down Expand Up @@ -98,10 +84,9 @@ struct Options: ParsableArguments {
}
}

guard let x,
let y,
let z
else { throw CLIError("Need z, x and y") }
guard let x, let y, let z else {
throw CLIError("Need z, x and y")
}

guard x >= 0 else { throw CLIError("x must be >= 0") }
guard y >= 0 else { throw CLIError("y must be >= 0") }
Expand All @@ -111,6 +96,37 @@ struct Options: ParsableArguments {
if x >= maximumTileCoordinate { throw CLIError("x at zoom \(z) must be smaller than \(maximumTileCoordinate)") }
if y >= maximumTileCoordinate { throw CLIError("y at zoom \(z) must be smaller than \(maximumTileCoordinate)") }

return (x, y, z)
}

}

struct Options: ParsableArguments {

@Flag(name: .shortAndLong, help: "Print some debug info")
var verbose = false

func parseUrl(
fromPath path: String,
checkPathExistence: Bool = true)
throws -> URL
{
let url: URL
if path.hasPrefix("http") {
guard let parsedUrl = URL(string: path) else {
throw CLIError("\(path) is not a valid URL")
}
url = parsedUrl
}
else {
url = URL(fileURLWithPath: path)
if checkPathExistence {
guard try url.checkResourceIsReachable() else {
throw CLIError("The file '\(path)' doesn't exist.")
}
}
}

return url
}

Expand Down
34 changes: 25 additions & 9 deletions Sources/MVTCLI/Dump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,38 @@ extension CLI {

struct Dump: AsyncParsableCommand {

static let configuration = CommandConfiguration(abstract: "Print the vector tile as GeoJSON")
static let configuration = CommandConfiguration(abstract: "Print the vector tile as GeoJSON to the console")

@Option(name: .shortAndLong, help: "Dump only the specified layer (can be repeated)")
var layer: [String] = []

@OptionGroup
var xyzOptions: XYZOptions

@OptionGroup
var options: Options

@Argument(
help: "The MVT resource (file or URL)",
completion: .file(extensions: ["pbf", "mvt"]))
var path: String

mutating func run() async throws {
let url = try options.parseUrl()
let (x, y, z) = try xyzOptions.parseXYZ(fromPath: path)
let url = try options.parseUrl(fromPath: path)

guard let x = options.x,
let y = options.y,
let z = options.z
else { throw CLIError("Something went wrong during argument parsing") }
let layerAllowlist = layer.nonempty

let layerWhitelist = layer.nonempty
if options.verbose {
print("Dumping tile '\(url.lastPathComponent)' [\(x),\(y)]@\(z)")

guard let tile = VectorTile(contentsOf: url, x: x, y: y, z: z, layerWhitelist: layerWhitelist, logger: options.verbose ? CLI.logger : nil) else {
throw CLIError("Failed to parse the tile at \(options.path)")
if let layerAllowlist {
print("Layers: '\(layerAllowlist.joined(separator: ","))'")
}
}

guard let tile = VectorTile(contentsOf: url, x: x, y: y, z: z, layerWhitelist: layerAllowlist, logger: options.verbose ? CLI.logger : nil) else {
throw CLIError("Failed to parse the tile at '\(path)'")
}

guard let data = tile.toGeoJson(prettyPrinted: true) else {
Expand All @@ -34,6 +46,10 @@ extension CLI {

print(String(data: data, encoding: .utf8) ?? "", terminator: "")
print()

if options.verbose {
print("Done.")
}
}

}
Expand Down
48 changes: 36 additions & 12 deletions Sources/MVTCLI/Export.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,68 @@ extension CLI {

struct Export: AsyncParsableCommand {

static let configuration = CommandConfiguration(abstract: "Export the vector tile as GeoJSON")
static let configuration = CommandConfiguration(abstract: "Export the vector tile as GeoJSON to a file")

@Option(name: .shortAndLong, help: "Output file")
var output: String

@Flag(name: .shortAndLong, help: "Force overwrite existing files")
var forceOverwrite = false

@Option(name: .shortAndLong, help: "Export only the specified layer (can be repeated)")
var layer: [String] = []

@Flag(name: .shortAndLong, help: "Format the output GeoJSON")
@Flag(name: .shortAndLong, help: "Pretty-print the output GeoJSON")
var prettyPrint = false

@OptionGroup
var xyzOptions: XYZOptions

@OptionGroup
var options: Options

mutating func run() async throws {
let url = try options.parseUrl()
@Argument(
help: "The MVT resource (file or URL)",
completion: .file(extensions: ["pbf", "mvt"]))
var path: String

guard let x = options.x,
let y = options.y,
let z = options.z
else { throw CLIError("Something went wrong during argument parsing") }
mutating func run() async throws {
let (x, y, z) = try xyzOptions.parseXYZ(fromPath: path)
let url = try options.parseUrl(fromPath: path)

let outputUrl = URL(fileURLWithPath: output)
if (try? outputUrl.checkResourceIsReachable()) ?? false {
throw CLIError("Output file must not exist")
if forceOverwrite {
print("Existing file '\(outputUrl.lastPathComponent)' will be overwritten")
}
else {
throw CLIError("Output file must not exist (use --force-overwrite to overwrite existing files)")
}
}

let layerWhitelist = layer.nonempty
let layerAllowlist = layer.nonempty

guard let tile = VectorTile(contentsOf: url, x: x, y: y, z: z, layerWhitelist: layerWhitelist, logger: options.verbose ? CLI.logger : nil) else {
throw CLIError("Failed to parse the tile at \(options.path)")
if options.verbose {
print("Dumping tile '\(url.lastPathComponent)' [\(x),\(y)]@\(z) to '\(outputUrl.lastPathComponent)'")

if let layerAllowlist {
print("Layers: '\(layerAllowlist.joined(separator: ","))'")
}
}

guard let tile = VectorTile(contentsOf: url, x: x, y: y, z: z, layerWhitelist: layerAllowlist, logger: options.verbose ? CLI.logger : nil) else {
throw CLIError("Failed to parse the tile at '\(path)'")
}

guard let data = tile.toGeoJson(prettyPrinted: prettyPrint) else {
throw CLIError("Failed to extract the tile data as GeoJSON")
}

try data.write(to: outputUrl, options: .atomic)

if options.verbose {
print("Done.")
}
}

}
Expand Down
Loading

0 comments on commit be292a6

Please sign in to comment.