Skip to content

Commit

Permalink
cli tool accepts GeoJSON as input for some commands
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch committed Aug 12, 2024
1 parent 009066d commit 952ace7
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 134 deletions.
6 changes: 3 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "5d734874e4d66aa5e42e2e73f545fbb1c3e33e7f08847495a19cf43690d8a3e5",
"originHash" : "e36efc60ff6513f9fd73eec6246018b16a077ae5221e7e54d785cf0a8c172a1a",
"pins" : [
{
"identity" : "gis-tools",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Outdooractive/gis-tools",
"state" : {
"revision" : "831a87ebd1ae3bb013c46b1d1752400dbc7bc352",
"version" : "1.7.0"
"revision" : "a8118bcd5e715a69f640476446fbf058fe39973f",
"version" : "1.8.3"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ let package = Package(
targets: ["MVTTools"]),
],
dependencies: [
.package(url: "https://github.com/Outdooractive/gis-tools", from: "1.7.0"),
.package(url: "https://github.com/Outdooractive/gis-tools", from: "1.8.3"),
.package(url: "https://github.com/1024jp/GzipSwift.git", from: "5.2.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.4.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.1"),
Expand Down
6 changes: 5 additions & 1 deletion Sources/MVTCLI/CLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ struct CLI: AsyncParsableCommand {
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'.
The tile coordinate of vector tiles can be extracted from the path
if it's either in the form '/z/x/y' or 'z_x_y'.
Tile coordinates are not necessary for GeoJSON input files.
Examples:
- Tests/MVTToolsTests/TestData/14_8716_8015.vector.mvt
Expand Down Expand Up @@ -50,6 +52,8 @@ struct XYZOptions: ParsableArguments {
@Option(name: .short, help: "Tile zoom level, if it can't be extracted from the path")
var z: Int?

/// Try to extract x, y and z tile coordinates from some file paths or URLs,
/// if the were not given on the command line
mutating func parseXYZ(
fromPaths paths: [String])
throws -> (Int, Int, Int)
Expand Down
25 changes: 15 additions & 10 deletions Sources/MVTCLI/Dump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extension CLI {

struct Dump: AsyncParsableCommand {

static let configuration = CommandConfiguration(abstract: "Print the vector tile as pretty-printed GeoJSON to the console")
static let configuration = CommandConfiguration(abstract: "Print the input file (mvt or GeoJSON) as pretty-printed GeoJSON to the console")

@Option(name: .shortAndLong, help: "Dump only the specified layer (can be repeated)")
var layer: [String] = []
Expand All @@ -18,28 +18,33 @@ extension CLI {
var options: Options

@Argument(
help: "The vector tile (file or URL)",
completion: .file(extensions: ["pbf", "mvt"]))
help: "The vector tile or GeoJSON (file or URL)",
completion: .file(extensions: ["pbf", "mvt", "geojson", "json"]))
var path: String

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

let layerAllowlist = layer.nonempty
var tile = VectorTile(contentsOfGeoJson: url, layerWhitelist: layerAllowlist, logger: options.verbose ? CLI.logger : nil)
if tile == nil,
let (x, y, z) = try? xyzOptions.parseXYZ(fromPaths: [path])
{
tile = VectorTile(contentsOf: url, x: x, y: y, z: z, layerWhitelist: layerAllowlist, logger: options.verbose ? CLI.logger : nil)
}

guard let tile else {
throw CLIError("Failed to parse the resource at '\(path)'")
}

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

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 resource at '\(path)'")
}

guard let data = tile.toGeoJson(prettyPrinted: true) else {
throw CLIError("Failed to extract the tile data as GeoJSON")
}
Expand Down
18 changes: 9 additions & 9 deletions Sources/MVTCLI/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,21 @@ extension CLI {

struct Info: AsyncParsableCommand {

static let configuration = CommandConfiguration(abstract: "Print information about the vector tile")
static let configuration = CommandConfiguration(abstract: "Print information about the input file (mvt or GeoJSON)")

@OptionGroup
var options: Options

@Argument(
help: "The vector tile (file or URL)",
completion: .file(extensions: ["pbf", "mvt"]))
help: "The vector tile or GeoJSON (file or URL)",
completion: .file(extensions: ["pbf", "mvt", "geojson", "json"]))
var path: String

mutating func run() async throws {
let url = try options.parseUrl(fromPath: path)

if options.verbose {
print("Info for tile '\(url.lastPathComponent)'")
}

guard let tileInfo = VectorTile.tileInfo(at: url),
var layers = tileInfo["layers"] as? [[String: Any]]
guard var layers = VectorTile.tileInfo(at: url)
?? VectorTile(contentsOfGeoJson: url)?.tileInfo()
else { throw CLIError("Error retreiving the tile info for '\(path)'") }

layers.sort { first, second in
Expand All @@ -46,6 +42,10 @@ extension CLI {
layers.compactMap({ ($0["version"] as? Int)?.toString }),
]

if options.verbose {
print("Info for tile '\(url.lastPathComponent)'")
}

let result = dumpSideBySide(
table,
asTableWithHeaders: tableHeader)
Expand Down
64 changes: 47 additions & 17 deletions Sources/MVTCLI/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,47 @@ extension CLI {

struct Query: AsyncParsableCommand {

static let configuration = CommandConfiguration(abstract: "Query the features in a vector tile")
static let configuration = CommandConfiguration(abstract: "Query the features in the input file (mvt or GeoJSON)")

@Option(name: .shortAndLong, help: "Output GeoJSON file (optional, default is console)")
var output: String?

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

@Option(name: .shortAndLong, help: "Search only in this layer (can be repeated)")
var layer: [String] = []

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

@OptionGroup
var xyzOptions: XYZOptions

@OptionGroup
var options: Options

@Argument(
help: "The vector tile (file or URL)",
completion: .file(extensions: ["pbf", "mvt"]))
help: "The vector tile or GeoJSON (file or URL)",
completion: .file(extensions: ["pbf", "mvt", "geojson", "json"]))
var path: String

@Argument(help: "Search term, can be a string or a coordinate in the form 'latitude,longitude,tolerance(meters)'")
var searchTerm: String

mutating func run() async throws {
if let output {
let outputUrl = URL(fileURLWithPath: output)
if (try? outputUrl.checkResourceIsReachable()) ?? false {
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)")
}
}
}

var coordinate: Coordinate3D?
var tolerance: CLLocationDistance?

Expand All @@ -47,17 +68,22 @@ extension CLI {
}
}

let (x, y, z) = try xyzOptions.parseXYZ(fromPaths: [path])
let layerAllowlist = layer.nonempty
let url = try options.parseUrl(fromPath: path)

let layerAllowlist = layer.nonempty
var tile = VectorTile(contentsOfGeoJson: url, layerWhitelist: layerAllowlist, logger: options.verbose ? CLI.logger : nil)
if tile == nil,
let (x, y, z) = try? xyzOptions.parseXYZ(fromPaths: [path])
{
tile = VectorTile(contentsOf: url, x: x, y: y, z: z, layerWhitelist: layerAllowlist, logger: options.verbose ? CLI.logger : nil)
}

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 resource at \(path)")
guard let tile else {
throw CLIError("Failed to parse the resource at '\(path)'")
}

if options.verbose {
print("Searching in tile '\(url.lastPathComponent)' [\(x),\(y)]@\(z)")
print("Searching in tile '\(url.lastPathComponent)' [\(tile.x),\(tile.y)]@\(tile.z)")

if let layerAllowlist {
print("Layers: '\(layerAllowlist.joined(separator: ","))'")
Expand All @@ -80,15 +106,19 @@ extension CLI {
result = search(term: searchTerm, in: tile)
}

if let result,
let output = result.asJsonString(prettyPrinted: true)
{
print(output, terminator: "")
print()

if options.verbose {
let count = result.features.count
print("Found \(count) \(count == 1 ? "result" : "results").")
if let result {
if let output {
let outputUrl = URL(fileURLWithPath: output)
try result.asJsonData(prettyPrinted: prettyPrint)?.write(to: outputUrl, options: .atomic)
}
else if let resultGeoJson = result.asJsonString(prettyPrinted: prettyPrint) {
print(resultGeoJson, terminator: "")
print()

if options.verbose {
let count = result.features.count
print("Found \(count) \(count == 1 ? "result" : "results").")
}
}
}
else {
Expand Down
10 changes: 8 additions & 2 deletions Sources/MVTTools/GeoJson.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ extension VectorTile {
public mutating func addGeoJson(
geoJson: GeoJson,
layerName: String? = nil,
propertyName: String? = nil)
propertyName: String? = nil,
layerAllowList: Set<String>? = nil)
{
guard let features = geoJson.flattened?.features else { return }

Expand All @@ -83,10 +84,12 @@ extension VectorTile {
return mapping
},
onKey: { key, features in
if let layerAllowList, !layerAllowList.contains(key) { return }
appendFeatures(features, to: key)
})
}
else {
if let layerAllowList, !layerAllowList.contains(layerName) { return }
appendFeatures(features, to: layerName)
}
}
Expand All @@ -95,7 +98,8 @@ extension VectorTile {
public mutating func setGeoJson(
geoJson: GeoJson,
layerName: String? = nil,
propertyName: String? = nil)
propertyName: String? = nil,
layerAllowList: Set<String>? = nil)
{
guard let features = geoJson.flattened?.features else { return }

Expand All @@ -108,10 +112,12 @@ extension VectorTile {
return mapping
},
onKey: { key, features in
if let layerAllowList, !layerAllowList.contains(key) { return }
setFeatures(features, for: key)
})
}
else {
if let layerAllowList, !layerAllowList.contains(layerName) { return }
setFeatures(features, for: layerName)
}
}
Expand Down
74 changes: 52 additions & 22 deletions Sources/MVTTools/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import Foundation
import GISTools

// MARK: Static functions
// MARK: Info functions

extension VectorTile {

Expand All @@ -20,22 +20,55 @@ extension VectorTile {
return layerNames(from: data)
}

// { layers:
// [ { name: 'world',
// features: 1,
// point_features: 0,
// linestring_features: 0,
// polygon_features: 1,
// unknown_features: 0,
// raster_features: 0,
// version: 2 } ],
// errors: false }

/// Read a tile from `data` and return some information about the tile
public static func tileInfo(from data: Data) -> [String: Any]? {
// [{
// name: 'world',
// features: 1,
// point_features: 0,
// linestring_features: 0,
// polygon_features: 1,
// unknown_features: 0,
// raster_features: 0,
// version: 2
// }]

/// Information about the features in a tile, per layer.
public func tileInfo() -> [[String: Any]]? {
var result: [[String: Any]] = []

for (layerName, layerContainer) in layers {
var pointFeatures = 0
var lineStringFeatures = 0
var polygonFeatures = 0
var unknownFeatures = 0

for feature in layerContainer.features {
switch feature.geometry.type {
case .point, .multiPoint: pointFeatures += 1
case .lineString, .multiLineString: lineStringFeatures += 1
case .polygon, .multiPolygon: polygonFeatures += 1
default: unknownFeatures += 1
}
}

let info: [String: Any] = [
"name": layerName,
"features": pointFeatures + lineStringFeatures + polygonFeatures + unknownFeatures,
"point_features": pointFeatures,
"linestring_features": lineStringFeatures,
"polygon_features": polygonFeatures,
"unknown_features": unknownFeatures,
]
result.append(info)
}

return result
}

/// Read a tile from `data` and return some information about the tile per layer.
public static func tileInfo(from data: Data) -> [[String: Any]]? {
guard let tile = MVTDecoder.vectorTile(from: data) else { return nil }

var layers: [[String: Any]] = []
var result: [[String: Any]] = []

for layer in tile.layers {
var pointFeatures = 0
Expand All @@ -61,17 +94,14 @@ extension VectorTile {
"polygon_features": polygonFeatures,
"unknown_features": unknownFeatures,
]
layers.append(info)
result.append(info)
}

return [
"layers": layers,
"errors": false,
]
return result
}

/// Read a tile from `url` and return some information about the tile
public static func tileInfo(at url: URL) -> [String: Any]? {
/// Read a tile from `url` and return some information about the tile per layer.
public static func tileInfo(at url: URL) -> [[String: Any]]? {
guard let data = try? Data(contentsOf: url) else { return nil }
return tileInfo(from: data)
}
Expand Down
Loading

0 comments on commit 952ace7

Please sign in to comment.