From f264491eccf0c4b93d6297f26f495bbbb8b4d5e9 Mon Sep 17 00:00:00 2001 From: Thomas Rasch Date: Tue, 27 Aug 2024 14:48:39 +0200 Subject: [PATCH] #26: Output export options (#29) --- Sources/MVTCLI/Dump.swift | 27 +++++-- Sources/MVTCLI/Export.swift | 56 ++++++++++---- Sources/MVTCLI/Import.swift | 83 +++++++++++++++++++-- Sources/MVTCLI/Merge.swift | 95 ++++++++++++++++++++---- Sources/MVTCLI/Query.swift | 57 ++++++++++++-- Sources/MVTCLI/Version.swift | 2 +- Sources/MVTTools/Coders/MVTEncoder.swift | 8 +- Sources/MVTTools/ExportOptions.swift | 12 +-- Sources/MVTTools/GeoJson.swift | 90 ++++++++++++++++++++-- 9 files changed, 364 insertions(+), 66 deletions(-) diff --git a/Sources/MVTCLI/Dump.swift b/Sources/MVTCLI/Dump.swift index 8953eb4..1d118c9 100644 --- a/Sources/MVTCLI/Dump.swift +++ b/Sources/MVTCLI/Dump.swift @@ -22,12 +22,17 @@ extension CLI { @Flag( name: [.customLong("Di", withSingleDash: true), .long], help: "Don't parse the layer name (option 'property-name') from Feature properties in the input GeoJSONs. Might speed up GeoJSON parsing considerably.") - var disableInputLayerProperty: Bool = false + var disableInputLayerProperty = false @Flag( name: [.customLong("Do", withSingleDash: true), .long], help: "Don't add the layer name (option 'property-name') as a Feature property in the output GeoJSONs.") - var disableOutputLayerProperty: Bool = false + var disableOutputLayerProperty = false + + @Option( + name: [.customLong("oSm", withSingleDash: true), .long], + help: "Simplify output features using meters.") + var simplifyMeters: Int? @OptionGroup var xyzOptions: XYZOptions @@ -77,10 +82,15 @@ extension CLI { } } + var exportOptions = VectorTile.ExportOptions() + if let simplifyMeters, simplifyMeters > 0 { + exportOptions.simplifyFeatures = .meters(Double(simplifyMeters)) + } + if options.verbose { print("Dumping \(tile.origin) tile '\(url.lastPathComponent)' [\(tile.x),\(tile.y)]@\(tile.z)") - print("Property name: \(propertyName)") + print("Layer property name: \(propertyName)") if disableInputLayerProperty { print(" - disable input layer property") } @@ -96,15 +106,22 @@ extension CLI { if tile.origin == .mvt || !disableInputLayerProperty, - let layerAllowlist + let layerAllowlist { print("Layers: '\(layerAllowlist.joined(separator: ","))'") } + + print("Output options:") + print(" - Pretty print: true") + print(" - Simplification: \(exportOptions.simplifyFeatures)") + + print("GeoJSON:") } guard let data = tile.toGeoJson( prettyPrinted: true, - layerProperty: disableOutputLayerProperty ? nil : propertyName) + layerProperty: disableOutputLayerProperty ? nil : propertyName, + options: exportOptions) else { throw CLIError("Failed to extract the tile data as GeoJSON") } print(String(data: data, encoding: .utf8) ?? "", terminator: "") diff --git a/Sources/MVTCLI/Export.swift b/Sources/MVTCLI/Export.swift index dbd12f3..77b07e2 100644 --- a/Sources/MVTCLI/Export.swift +++ b/Sources/MVTCLI/Export.swift @@ -15,6 +15,16 @@ extension CLI { completion: .file(extensions: ["geojson", "json"])) var outputFile: String + @Option( + name: [.customLong("oC", withSingleDash: true), .long], + help: "Output file compression level, between 0=none to 9=best. (default: none)") + var compressionLevel: Int? + + @Option( + name: [.customLong("oSm", withSingleDash: true), .long], + help: "Simplify output features using meters.") + var simplifyMeters: Int? + @Flag( name: .shortAndLong, help: "Overwrite existing files.") @@ -31,9 +41,9 @@ extension CLI { var propertyName: String = VectorTile.defaultLayerPropertyName @Flag( - name: [.customLong("Do", withSingleDash: true), .long], + name: [.customLong("Do", withSingleDash: true), .long], help: "Don't add the layer name (option 'property-name') as a Feature property in the output GeoJSONs.") - var disableOutputLayerProperty: Bool = false + var disableOutputLayerProperty = false @Flag( name: .shortAndLong, @@ -57,19 +67,6 @@ extension CLI { let layerAllowlist = layer.nonempty let outputUrl = URL(fileURLWithPath: outputFile) - if options.verbose { - print("Dumping tile '\(url.lastPathComponent)' [\(x),\(y)]@\(z) to '\(outputUrl.lastPathComponent)'") - print("Property name: \(propertyName)") - - if disableOutputLayerProperty { - print(" - disable output layer property") - } - - if let layerAllowlist { - print("Layers: '\(layerAllowlist.joined(separator: ","))'") - } - } - if (try? outputUrl.checkResourceIsReachable()) ?? false { if forceOverwrite { if options.verbose { @@ -90,9 +87,36 @@ extension CLI { logger: options.verbose ? CLI.logger : nil) else { throw CLIError("Failed to parse the resource at '\(path)'") } + var exportOptions = VectorTile.ExportOptions() + if let simplifyMeters, simplifyMeters > 0 { + exportOptions.simplifyFeatures = .meters(Double(simplifyMeters)) + } + if let compressionLevel, compressionLevel > 0 { + exportOptions.compression = .level(max(0, min(9, compressionLevel))) + } + + if options.verbose { + print("Dumping tile '\(url.lastPathComponent)' [\(x),\(y)]@\(z) to '\(outputUrl.lastPathComponent)'") + + print("Layer property name: \(propertyName)") + if disableOutputLayerProperty { + print(" - disable output layer property") + } + + if let layerAllowlist { + print("Layers: '\(layerAllowlist.joined(separator: ","))'") + } + + print("Output options:") + print(" - Pretty print: \(prettyPrint)") + print(" - Compression: \(exportOptions.compression)") + print(" - Simplification: \(exportOptions.simplifyFeatures)") + } + guard let data = tile.toGeoJson( prettyPrinted: prettyPrint, - layerProperty: disableOutputLayerProperty ? nil : propertyName) + layerProperty: disableOutputLayerProperty ? nil : propertyName, + options: exportOptions) else { throw CLIError("Failed to extract the tile data as GeoJSON") } try data.write(to: outputUrl, options: .atomic) diff --git a/Sources/MVTCLI/Import.swift b/Sources/MVTCLI/Import.swift index 36938d9..f56c013 100644 --- a/Sources/MVTCLI/Import.swift +++ b/Sources/MVTCLI/Import.swift @@ -16,6 +16,31 @@ extension CLI { completion: .file(extensions: ["pbf", "mvt"])) var outputFile: String + @Option( + name: [.customLong("oC", withSingleDash: true), .long], + help: "Output file compression level, between 0=none to 9=best.") + var compressionLevel = 9 + + @Option( + name: [.customLong("oBe", withSingleDash: true), .long], + 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). Overrides 'buffer-extents'.") + var bufferPixels: Int? + + @Option( + name: [.customLong("oSe", withSingleDash: true), .long], + 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'.") + var simplifyMeters: Int? + @Flag( name: .shortAndLong, help: "Overwrite an existing 'output' file.") @@ -44,7 +69,7 @@ extension CLI { @Flag( name: [.customLong("Di", withSingleDash: true), .long], help: "Don't parse the layer name (option 'property-name') from Feature properties in the input GeoJSONs, just use 'layer-name' or a default. Might speed up GeoJSON parsing considerably.") - var disableInputLayerProperty: Bool = false + var disableInputLayerProperty = false @OptionGroup var xyzOptions: XYZOptions @@ -101,9 +126,9 @@ extension CLI { guard var tile else { throw CLIError("Failed to create a tile [\(x),\(y)]@\(z)") } if options.verbose { - print("Import into \(tile.origin) tile '\(outputUrl.lastPathComponent)' [\(x),\(y)]@\(z)") - print("Property name: \(propertyName)") + print("Import into \(tile.origin == .none ? "new" : tile.origin.rawValue) tile '\(outputUrl.lastPathComponent)' [\(x),\(y)]@\(z)") + print("Layer property name: \(propertyName)") if disableInputLayerProperty { print(" - disable input layer property") } @@ -111,6 +136,12 @@ extension CLI { if let layerName { print("Fallback layer name: \(layerName)") } + + if !disableInputLayerProperty, + let layerAllowlist + { + print("Layers: '\(layerAllowlist.joined(separator: ","))'") + } } for path in other { @@ -132,7 +163,7 @@ extension CLI { throw CLIError("Failed to parse the GeoJSON at '\(path)'") } - print("- \(otherUrl.lastPathComponent)") + print("- \(otherUrl.lastPathComponent) (geojson)") if !disableInputLayerProperty, let layerAllowlist @@ -149,12 +180,48 @@ extension CLI { layerProperty: disableInputLayerProperty ? nil : propertyName) } + // Export + + 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 + } + + let exportOptions: VectorTile.ExportOptions = .init( + bufferSize: bufferSize, + compression: compression, + simplifyFeatures: simplifyFeatures) + + if options.verbose { + print("Output options:") + print(" - Buffer size: \(exportOptions.bufferSize)") + print(" - Compression: \(exportOptions.compression)") + print(" - Simplification: \(exportOptions.simplifyFeatures)") + } + tile.write( to: outputUrl, - options: .init( - bufferSize: .extent(512), - compression: .level(9), - simplifyFeatures: .no)) + options: exportOptions) if options.verbose { print("Done.") diff --git a/Sources/MVTCLI/Merge.swift b/Sources/MVTCLI/Merge.swift index af4786e..0505b15 100644 --- a/Sources/MVTCLI/Merge.swift +++ b/Sources/MVTCLI/Merge.swift @@ -27,6 +27,31 @@ extension CLI { help: "Output file format (optional, one of 'auto', 'geojson', 'mvt').") var outputFormat: OutputFormat = .auto + @Option( + name: [.customLong("oC", withSingleDash: true), .long], + 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). (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). Overrides 'buffer-extents'.") + var bufferPixels: Int? + + @Option( + name: [.customLong("oSe", withSingleDash: true), .long], + 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'.") + var simplifyMeters: Int? + @Flag( name: .shortAndLong, help: "Force overwrite an existing 'output' file.") @@ -160,14 +185,13 @@ extension CLI { if options.verbose { if let outputUrl { - print("Merging into \(tile.origin) tile '\(outputUrl.lastPathComponent)' [\(tile.x),\(tile.y)]@\(tile.z)") + print("Merging into \(tile.origin == .none ? "new" : tile.origin.rawValue) tile '\(outputUrl.lastPathComponent)' [\(tile.x),\(tile.y)]@\(tile.z)") } else { print("Dumping the merged tile to the console") } - print("Property name: \(propertyName)") - + print("Layer property name: \(propertyName)") if disableInputLayerProperty { print(" - disable input layer property") } @@ -187,8 +211,6 @@ extension CLI { { print("Layers: '\(layerAllowlist.joined(separator: ","))'") } - - print("Output format: \(outputFormatToUse)") } for path in other { @@ -215,16 +237,14 @@ extension CLI { x: x, y: y, z: z, - layerWhitelist: layerAllowlist, - logger: options.verbose ? CLI.logger : nil) + layerWhitelist: layerAllowlist) { otherTile = other } else if let other = VectorTile( contentsOfGeoJson: otherUrl, layerProperty: disableInputLayerProperty ? nil : propertyName, - layerWhitelist: disableInputLayerProperty ? nil : layerAllowlist, - logger: options.verbose ? CLI.logger : nil) + layerWhitelist: disableInputLayerProperty ? nil : layerAllowlist) { otherTile = other } @@ -245,11 +265,58 @@ extension CLI { tile.merge(otherTile, ignoreTileCoordinateMismatch: true) } + // Export + + var exportOptions = VectorTile.ExportOptions() + + if let bufferPixels, bufferPixels > 0 { + exportOptions.bufferSize = .pixel(bufferPixels) + } + else if let bufferExtents, bufferExtents > 0 { + exportOptions.bufferSize = .extent(bufferExtents) + } + else if outputFormatToUse == .geojson { + exportOptions.bufferSize = .extent(0) + } + else { + exportOptions.bufferSize = .extent(512) + } + + if outputUrl != nil { // don't gzip output to the console + if let compressionLevel { + if compressionLevel > 0 { + exportOptions.compression = .level(max(0, min(9, compressionLevel))) + } + } + else if outputFormatToUse == .mvt { + exportOptions.compression = .level(9) + } + } + + if let simplifyMeters, simplifyMeters > 0 { + exportOptions.simplifyFeatures = .meters(Double(simplifyMeters)) + } + else if let simplifyExtents, simplifyExtents > 0 { + exportOptions.simplifyFeatures = .extent(simplifyExtents) + } + + if options.verbose { + print("Output options:") + if outputFormatToUse == .geojson || outputUrl == nil { + print(" - Pretty print: \(prettyPrint)") + } + print(" - File format: \(outputFormatToUse)") + print(" - Buffer size: \(exportOptions.bufferSize)") + print(" - Compression: \(exportOptions.compression)") + print(" - Simplification: \(exportOptions.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) } @@ -257,15 +324,13 @@ extension CLI { else { tile.write( to: outputUrl, - options: .init( - bufferSize: .extent(512), - compression: .level(9), - simplifyFeatures: .no)) + options: exportOptions) } } else if let resultGeoJson = tile.toGeoJson( prettyPrinted: prettyPrint, - layerProperty: disableOutputLayerProperty ? nil : propertyName) + layerProperty: disableOutputLayerProperty ? nil : propertyName, + options: exportOptions) { print(resultGeoJson, terminator: "") print() diff --git a/Sources/MVTCLI/Query.swift b/Sources/MVTCLI/Query.swift index 54206f9..d0baa56 100644 --- a/Sources/MVTCLI/Query.swift +++ b/Sources/MVTCLI/Query.swift @@ -1,9 +1,10 @@ import ArgumentParser #if !os(Linux) -import CoreLocation + import CoreLocation #endif import Foundation import GISTools +import Gzip import MVTTools extension CLI { @@ -19,6 +20,16 @@ extension CLI { completion: .file(extensions: ["geojson", "json"])) var outputFile: String? + @Option( + name: [.customLong("oC", withSingleDash: true), .long], + help: "Output file compression level, between 0=none to 9=best. (default: none)") + var compressionLevel: Int? + + @Option( + name: [.customLong("oSm", withSingleDash: true), .long], + help: "Simplify output features using meters.") + var simplifyMeters: Int? + @Flag( name: .shortAndLong, help: "Overwrite existing files.") @@ -37,12 +48,12 @@ extension CLI { @Flag( name: [.customLong("Di", withSingleDash: true), .long], help: "Don't parse the layer name (option 'property-name') from Feature properties in the input GeoJSONs. Might speed up GeoJSON parsing considerably.") - var disableInputLayerProperty: Bool = false + var disableInputLayerProperty = false @Flag( name: [.customLong("Do", withSingleDash: true), .long], help: "Don't add the layer name (option 'property-name') as a Feature property in the output GeoJSONs.") - var disableOutputLayerProperty: Bool = false + var disableOutputLayerProperty = false @Flag( name: .shortAndLong, @@ -120,8 +131,8 @@ extension CLI { if options.verbose { print("Searching in tile '\(url.lastPathComponent)' [\(tile.x),\(tile.y)]@\(tile.z)") - print("Property name: \(propertyName)") + print("Layer property name: \(propertyName)") if disableInputLayerProperty { print(" - disable input layer property") } @@ -137,7 +148,7 @@ extension CLI { if tile.origin == .mvt || !disableInputLayerProperty, - let layerAllowlist + let layerAllowlist { print("Layers: '\(layerAllowlist.joined(separator: ","))'") } @@ -166,10 +177,44 @@ extension CLI { in: tile) } + if options.verbose { + print("Output options:") + print(" - Pretty print: \(prettyPrint)") + } + + var exportCompressionLevel: VectorTile.ExportOptions.CompressionOptions = .no + if outputFile != nil { // don't gzip output to the console + if let compressionLevel, compressionLevel > 0 { + exportCompressionLevel = .level(max(0, min(9, compressionLevel))) + } + + if options.verbose { + print(" - Compression: \(exportCompressionLevel)") + } + } + + if let simplifyMeters, simplifyMeters > 0 { + if options.verbose { + print(" - Simplification: \(simplifyMeters > 0 ? ".meters(\(simplifyMeters)" : ".no")") + } + result = result?.simplified(tolerance: Double(simplifyMeters)) + } + if let result { if let outputFile { let outputUrl = URL(fileURLWithPath: outputFile) - try result.asJsonData(prettyPrinted: prettyPrint)?.write(to: outputUrl, options: .atomic) + var serializedData = result.asJsonData(prettyPrinted: prettyPrint) + + if exportCompressionLevel != .no { + var value = 6 // default + if case let .level(compressionLevel) = exportCompressionLevel { + value = max(0, min(9, compressionLevel)) + } + let level = CompressionLevel(rawValue: Int32(value)) + serializedData = (try? serializedData?.gzipped(level: level)) ?? serializedData + } + + try serializedData?.write(to: outputUrl, options: .atomic) } else if let resultGeoJson = result.asJsonString(prettyPrinted: prettyPrint) { print(resultGeoJson, terminator: "") diff --git a/Sources/MVTCLI/Version.swift b/Sources/MVTCLI/Version.swift index 9065df7..299b6ac 100644 --- a/Sources/MVTCLI/Version.swift +++ b/Sources/MVTCLI/Version.swift @@ -1,4 +1,4 @@ // This file will be overwritten by the // homebrew Github action. // DO NOT change this file. -public let cliVersion = "v1.7.0" +public let cliVersion = "v1.8.1" diff --git a/Sources/MVTTools/Coders/MVTEncoder.swift b/Sources/MVTTools/Coders/MVTEncoder.swift index 087baca..132b0cc 100644 --- a/Sources/MVTTools/Coders/MVTEncoder.swift +++ b/Sources/MVTTools/Coders/MVTEncoder.swift @@ -21,7 +21,7 @@ enum MVTEncoder { { var tile = VectorTile_Tile() - let extent = UInt32(options.extent) + let extent = UInt32(VectorTile.ExportOptions.extent) let projectionFunction: ((Coordinate3D) -> (x: Int, y: Int)) var clipBoundingBox: BoundingBox? @@ -38,10 +38,12 @@ enum MVTEncoder { var bufferSize = 0 switch options.bufferSize { + case .no: + bufferSize = 0 case let .extent(extent): bufferSize = extent case let .pixel(pixel): - bufferSize = Int((Double(pixel) / Double(options.tileSize)) * Double(options.extent)) + bufferSize = Int((Double(pixel) / Double(VectorTile.ExportOptions.tileSize)) * Double(VectorTile.ExportOptions.extent)) } var simplifyDistance: CLLocationDistance = 0.0 @@ -50,7 +52,7 @@ enum MVTEncoder { 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(options.extent) * Double(extent) + simplifyDistance = (tileBoundsInMeters.southEast.longitude - tileBoundsInMeters.southWest.longitude) / Double(VectorTile.ExportOptions.extent) * Double(extent) case let .meters(meters): simplifyDistance = meters } diff --git a/Sources/MVTTools/ExportOptions.swift b/Sources/MVTTools/ExportOptions.swift index 45735f6..eed7d33 100644 --- a/Sources/MVTTools/ExportOptions.swift +++ b/Sources/MVTTools/ExportOptions.swift @@ -14,6 +14,8 @@ extension VectorTile { /// Options for the buffer around tiles. public enum BufferSizeOptions { + /// No buffering. + case no /// Use the same dimension as ``ExportOptions.extent``. case extent(Int) /// Use pixels (see ``ExportOptions.tileSize``). @@ -41,13 +43,13 @@ extension VectorTile { } /// The grid width and height of one tile. Always 4096. - public let extent = 4096 + public static let extent = 4096 /// The tile size in pixels. Always 256. - public let tileSize = 256 + public static let tileSize = 256 - /// The buffer around the tile, either in pixels (see ``tileSize``) or in the same dimension as ``extent`` (default: **0**). - public var bufferSize: BufferSizeOptions = .extent(0) + /// The buffer around the tile, either in pixels (see ``tileSize``) or in the same dimension as ``extent`` (default: **no**). + public var bufferSize: BufferSizeOptions = .no /// Whether to enable compression or not (default: **no**) /// @@ -58,7 +60,7 @@ extension VectorTile { public var simplifyFeatures: SimplifyFeaturesOptions = .no public init( - bufferSize: BufferSizeOptions = .extent(0), + bufferSize: BufferSizeOptions = .no, compression: CompressionOptions = .no, simplifyFeatures: SimplifyFeaturesOptions = .no) { diff --git a/Sources/MVTTools/GeoJson.swift b/Sources/MVTTools/GeoJson.swift index 5e548bd..5b499a5 100644 --- a/Sources/MVTTools/GeoJson.swift +++ b/Sources/MVTTools/GeoJson.swift @@ -3,6 +3,7 @@ #endif import Foundation import GISTools +import Gzip // MARK: GeoJSON write support @@ -13,15 +14,72 @@ 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 .no: + bufferSize = 0 + 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) @@ -35,11 +93,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` @@ -49,14 +123,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 {