Skip to content

Commit

Permalink
Export options for GeoJSON
Browse files Browse the repository at this point in the history
  • Loading branch information
trasch committed Aug 27, 2024
1 parent d450148 commit 61497c1
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 59 deletions.
18 changes: 10 additions & 8 deletions Sources/MVTCLI/Import.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,22 @@ extension CLI {

@Option(
name: [.customLong("oBe", withSingleDash: true), .long],
help: "Output buffer extents for tiles of size \(VectorTile.ExportOptions.extent) (only mvt). (default: 512)")
help: "Output buffer extents for tiles of size \(VectorTile.ExportOptions.extent). (default: 512)")
var bufferExtents: Int?

@Option(
name: [.customLong("oBp", withSingleDash: true), .long],
help: "Output buffer pixels for tiles of size \(VectorTile.ExportOptions.tileSize) (only mvt). Overrides 'buffer-extents'.")
help: "Output buffer pixels for tiles of size \(VectorTile.ExportOptions.tileSize). Overrides 'buffer-extents'.")
var bufferPixels: Int?

@Option(
name: [.customLong("oSe", withSingleDash: true), .long],
help: "Simplify output features using tile extents (only mvt). (default: no simplification)")
help: "Simplify output features using tile extents. (default: no simplification)")
var simplifyExtents: Int?

@Option(
name: [.customLong("oSm", withSingleDash: true), .long],
help: "Simplify output features using meters. Overrides 'simplify-extents' (only mvt).")
help: "Simplify output features using meters. Overrides 'simplify-extents'.")
var simplifyMeters: Int?

@Flag(
Expand Down Expand Up @@ -208,12 +208,14 @@ extension CLI {
print(" - Simplification: \(simplifyFeatures)")
}

let exportOptions: VectorTile.ExportOptions = .init(
bufferSize: bufferSize,
compression: compression,
simplifyFeatures: simplifyFeatures)

tile.write(
to: outputUrl,
options: .init(
bufferSize: bufferSize,
compression: compression,
simplifyFeatures: simplifyFeatures))
options: exportOptions)

if options.verbose {
print("Done.")
Expand Down
104 changes: 60 additions & 44 deletions Sources/MVTCLI/Merge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,27 @@ extension CLI {

@Option(
name: [.customLong("oC", withSingleDash: true), .long],
help: "Output file compression level, between 0=none to 9=best (only mvt).")
var compressionLevel: Int = 9
help: "Output file compression level, between 0=none to 9=best. (default: 9 for mvt, none for geojson)")
var compressionLevel: Int?

@Option(
name: [.customLong("oBe", withSingleDash: true), .long],
help: "Output buffer extents for tiles of size \(VectorTile.ExportOptions.extent) (only mvt). (default: 512)")
help: "Output buffer extents for tiles of size \(VectorTile.ExportOptions.extent). (default: 512 for mvt, none for geojson)")
var bufferExtents: Int?

@Option(
name: [.customLong("oBp", withSingleDash: true), .long],
help: "Output buffer pixels for tiles of size \(VectorTile.ExportOptions.tileSize) (only mvt). Overrides 'buffer-extents'.")
help: "Output buffer pixels for tiles of size \(VectorTile.ExportOptions.tileSize). Overrides 'buffer-extents'.")
var bufferPixels: Int?

@Option(
name: [.customLong("oSe", withSingleDash: true), .long],
help: "Simplify output features using tile extents (only mvt). (default: no simplification)")
help: "Simplify output features using tile extents. (default: no simplification)")
var simplifyExtents: Int?

@Option(
name: [.customLong("oSm", withSingleDash: true), .long],
help: "Simplify output features using meters. Overrides 'simplify-extents' (only mvt).")
help: "Simplify output features using meters. Overrides 'simplify-extents'.")
var simplifyMeters: Int?

@Flag(
Expand Down Expand Up @@ -270,59 +270,75 @@ extension CLI {
tile.merge(otherTile, ignoreTileCoordinateMismatch: true)
}

// Export

let bufferSize: VectorTile.ExportOptions.BufferSizeOptions = if let bufferPixels, bufferPixels > 0 {
.pixel(bufferPixels)
}
else if let bufferExtents, bufferExtents > 0 {
.extent(bufferExtents)
}
else if outputFormatToUse == .geojson {
.extent(0)
}
else {
.extent(512)
}

var compression: VectorTile.ExportOptions.CompressionOptions = .no
if outputUrl != nil { // don't gzip output to the console
if let compressionLevel {
if compressionLevel > 0 {
compression = .level(max(0, min(9, compressionLevel)))
}
}
else if outputFormatToUse == .mvt {
compression = .level(9)
}
}

let simplifyFeatures: VectorTile.ExportOptions.SimplifyFeaturesOptions = if let simplifyMeters, simplifyMeters > 0 {
.meters(Double(simplifyMeters))
}
else if let simplifyExtents, simplifyExtents > 0 {
.extent(simplifyExtents)
}
else {
.no
}

if options.verbose {
print("Output options:")
print(" - Buffer size: \(bufferSize)")
print(" - Compression: \(compression)")
print(" - Simplification: \(simplifyFeatures)")
}

let exportOptions: VectorTile.ExportOptions = .init(
bufferSize: bufferSize,
compression: compression,
simplifyFeatures: simplifyFeatures)

if let outputUrl {
if outputFormatToUse == .geojson {
if let data = tile.toGeoJson(
prettyPrinted: prettyPrint,
layerProperty: disableOutputLayerProperty ? nil : propertyName)
layerProperty: disableOutputLayerProperty ? nil : propertyName,
options: exportOptions)
{
try data.write(to: outputUrl, options: .atomic)
}
}
else {
let bufferSize: VectorTile.ExportOptions.BufferSizeOptions = if let bufferPixels, bufferPixels > 0 {
.pixel(bufferPixels)
}
else if let bufferExtents, bufferExtents > 0 {
.extent(bufferExtents)
}
else {
.extent(512)
}

var compression: VectorTile.ExportOptions.CompressionOptions = .no
if compressionLevel > 0 {
compression = .level(max(0, min(9, compressionLevel)))
}

let simplifyFeatures: VectorTile.ExportOptions.SimplifyFeaturesOptions = if let simplifyMeters, simplifyMeters > 0 {
.meters(Double(simplifyMeters))
}
else if let simplifyExtents, simplifyExtents > 0 {
.extent(simplifyExtents)
}
else {
.no
}

if options.verbose {
print("Output options:")
print(" - Buffer size: \(bufferSize)")
print(" - Compression: \(compression)")
print(" - Simplification: \(simplifyFeatures)")
}

tile.write(
to: outputUrl,
options: .init(
bufferSize: bufferSize,
compression: compression,
simplifyFeatures: simplifyFeatures))
options: exportOptions)
}
}
else if let resultGeoJson = tile.toGeoJson(
prettyPrinted: prettyPrint,
layerProperty: disableOutputLayerProperty ? nil : propertyName)
layerProperty: disableOutputLayerProperty ? nil : propertyName,
options: exportOptions)
{
print(resultGeoJson, terminator: "")
print()
Expand Down
89 changes: 82 additions & 7 deletions Sources/MVTTools/GeoJson.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#endif
import Foundation
import GISTools
import Gzip

// MARK: GeoJSON write support

Expand All @@ -13,15 +14,71 @@ extension VectorTile {
layerNames: [String] = [],
additionalFeatureProperties: [String: Sendable]? = nil,
prettyPrinted: Bool = false,
layerProperty: String? = nil)
layerProperty: String? = nil,
options: VectorTile.ExportOptions? = nil)
-> Data?
{
var simplifyDistance: CLLocationDistance = 0.0
var clipBoundingBox: BoundingBox?

if let options {
var bufferSize = 0

switch options.bufferSize {
case let .extent(extent):
bufferSize = extent
case let .pixel(pixel):
bufferSize = Int((Double(pixel) / Double(VectorTile.ExportOptions.tileSize)) * Double(VectorTile.ExportOptions.extent))
}

switch options.simplifyFeatures {
case .no:
simplifyDistance = 0.0
case let .extent(extent):
let tileBoundsInMeters = MapTile(x: x, y: y, z: z).boundingBox(projection: .epsg3857)
simplifyDistance = (tileBoundsInMeters.southEast.longitude - tileBoundsInMeters.southWest.longitude) / Double(VectorTile.ExportOptions.extent) * Double(extent)
case let .meters(meters):
simplifyDistance = meters
}

if bufferSize != 0 {
clipBoundingBox = MapTile(x: x, y: y, z: z).boundingBox(projection: .epsg4326)

if let boundingBoxToExpand = clipBoundingBox {
let sqrt2 = 2.0.squareRoot()
let diagonal = Double(VectorTile.ExportOptions.extent) * sqrt2
let bufferDiagonal = Double(bufferSize) * sqrt2
let factor = bufferDiagonal / diagonal

let diagonalLength = boundingBoxToExpand.southWest.distance(from: boundingBoxToExpand.northEast)
let distance = diagonalLength * factor

clipBoundingBox = boundingBoxToExpand.expanded(byDistance: distance)
}
}
}

var allFeatures: [Feature] = []

for (layerName, layerContainer) in layers {
if !layerNames.isEmpty, !layerNames.contains(layerName) { continue }

for feature in layerContainer.features {
let layerFeatures: [Feature] = if let clipBoundingBox {
if simplifyDistance > 0.0 {
layerContainer.features.compactMap({ $0.clipped(to: clipBoundingBox)?.simplified(tolerance: simplifyDistance) })
}
else {
layerContainer.features.compactMap({ $0.clipped(to: clipBoundingBox) })
}
}
else if simplifyDistance > 0.0 {
layerContainer.features.compactMap({ $0.simplified(tolerance: simplifyDistance) })
}
else {
layerContainer.features
}

for feature in layerFeatures {
var feature = feature
if let layerProperty {
feature.setProperty(layerName, for: layerProperty)
Expand All @@ -35,11 +92,27 @@ extension VectorTile {

let json = FeatureCollection(allFeatures).asJson

var options: JSONSerialization.WritingOptions = []
var jsonOptions: JSONSerialization.WritingOptions = []
if prettyPrinted {
options.insert(.prettyPrinted)
jsonOptions.insert(.prettyPrinted)
}

let serializedData = try? JSONSerialization.data(withJSONObject: json, options: jsonOptions)

if let options,
options.compression != .no,
let serializedData
{
var value = 6 // default
if case let .level(compressionLevel) = options.compression {
value = max(0, min(9, compressionLevel))
}
let level = CompressionLevel(rawValue: Int32(value))
return (try? serializedData.gzipped(level: level)) ?? serializedData
}
else {
return serializedData
}
return try? JSONSerialization.data(withJSONObject: json, options: options)
}

/// Write the tile's content as GeoJSON to `url`
Expand All @@ -49,14 +122,16 @@ extension VectorTile {
layerNames: [String] = [],
additionalFeatureProperties: [String: Sendable]? = nil,
prettyPrinted: Bool = false,
layerProperty: String? = nil)
layerProperty: String? = nil,
options: VectorTile.ExportOptions? = nil)
-> Bool
{
guard let data: Data = toGeoJson(
layerNames: layerNames,
additionalFeatureProperties: additionalFeatureProperties,
prettyPrinted: prettyPrinted,
layerProperty: layerProperty)
layerProperty: layerProperty,
options: options)
else { return false }

do {
Expand Down

0 comments on commit 61497c1

Please sign in to comment.