diff --git a/README.md b/README.md index 102a924..aa8d185 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,6 @@ brew install protobuf swift-protobuf swiftlint - Documentation (!) - Tests -- Decode V1 tiles - Locking (when updating/deleting features, indexing) - Query option: within/intersects diff --git a/Sources/MVTCLI/Extensions/ArrayExtensions.swift b/Sources/MVTCLI/Extensions/ArrayExtensions.swift index bb532bf..6e37286 100644 --- a/Sources/MVTCLI/Extensions/ArrayExtensions.swift +++ b/Sources/MVTCLI/Extensions/ArrayExtensions.swift @@ -2,9 +2,7 @@ import Foundation extension Array { - var nonempty: Self? { - isEmpty ? nil : self - } + var nonempty: Self? { isEmpty ? nil : self } var isNotEmpty: Bool { !isEmpty } @@ -25,13 +23,9 @@ extension Array { extension Array where Element: Hashable { - var asSet: Set { - Set(self) - } + var asSet: Set { Set(self) } - var uniqued: Self { - Array(Set(self)) - } + var uniqued: Self { Array(Set(self)) } } diff --git a/Sources/MVTCLI/Extensions/IntExtensions.swift b/Sources/MVTCLI/Extensions/IntExtensions.swift index 6fe9263..cc0cb5a 100644 --- a/Sources/MVTCLI/Extensions/IntExtensions.swift +++ b/Sources/MVTCLI/Extensions/IntExtensions.swift @@ -2,8 +2,6 @@ import Foundation extension Int { - var toString: String { - String(self) - } + var toString: String { String(self) } } diff --git a/Sources/MVTCLI/Extensions/OptionalProtocol.swift b/Sources/MVTCLI/Extensions/OptionalProtocol.swift index 9d72dee..bf98b57 100644 --- a/Sources/MVTCLI/Extensions/OptionalProtocol.swift +++ b/Sources/MVTCLI/Extensions/OptionalProtocol.swift @@ -13,8 +13,6 @@ public protocol OptionalProtocol { extension Optional: OptionalProtocol { - public var optional: Wrapped? { - self - } + public var optional: Wrapped? { self } } diff --git a/Sources/MVTCLI/Extensions/SetExtensions.swift b/Sources/MVTCLI/Extensions/SetExtensions.swift index 90836ab..4b17920 100644 --- a/Sources/MVTCLI/Extensions/SetExtensions.swift +++ b/Sources/MVTCLI/Extensions/SetExtensions.swift @@ -2,8 +2,8 @@ import Foundation extension Set { - var asArray: [Element] { - Array(self) - } + var isNotEmpty: Bool { !isEmpty } + + var asArray: [Element] { Array(self) } } diff --git a/Sources/MVTCLI/Info.swift b/Sources/MVTCLI/Info.swift index c5aca68..3875fce 100644 --- a/Sources/MVTCLI/Info.swift +++ b/Sources/MVTCLI/Info.swift @@ -6,11 +6,6 @@ extension CLI { struct Info: AsyncParsableCommand { - enum InfoTables: String, CaseIterable { - case features - case properties - } - static let configuration = CommandConfiguration( abstract: "Print information about the input file (MVT or GeoJSON)", discussion: """ @@ -19,14 +14,22 @@ extension CLI { in the input file. - properties: Counts of all Feature properties for each layer in the input file. + - property=: Count the values for '' across all layers + and features (can be repeated). Note: This doesn't + work for Array and Dictionary values. """) @Option( name: .shortAndLong, help: "The tables to print, comma separated list of '\(InfoTables.allCases.map(\.rawValue).joined(separator: ","))'.", - transform: { $0.components(separatedBy: ",").compactMap(InfoTables.init(rawValue:)) }) + transform: InfoTables.parse) var infoTables: [InfoTables] = [.features, .properties] + @Option( + name: .shortAndLong, + help: "Shortcut for -i property= (can be repeated).") + var property: [String] = [] + @OptionGroup var options: Options @@ -39,13 +42,17 @@ extension CLI { let url = try options.parseUrl(fromPath: path) guard var layers = VectorTile.tileInfo(at: url) - ?? VectorTile(contentsOfGeoJson: url)?.tileInfo() + ?? VectorTile(contentsOfGeoJson: url, layerProperty: nil)?.tileInfo() else { throw CLIError("Error retreiving the tile info for '\(path)'") } if options.verbose { print("Info for tile '\(url.lastPathComponent)'") } + if property.isNotEmpty { + infoTables = [.property(property.uniqued)] + } + layers.sort { first, second in first.name.compare(second.name) == .orderedAscending } @@ -56,6 +63,8 @@ extension CLI { dumpFeatures(layers) case .properties: dumpProperties(layers) + case let .property(names): + dumpProperty(layers, names: names) } if index < infoTables.count - 1 { @@ -116,6 +125,39 @@ extension CLI { print(result) } + func dumpProperty(_ layers: [VectorTile.LayerInfo], names: [String]) { + let propertyValues: [String: [String: Int]] = layers.reduce(into: [:]) { result, layer in + for name in names { + guard let values = layer.propertyValues[name] else { continue } + + var thisNameValues = result[name] ?? [:] + thisNameValues.merge(values) { $0 + $1 } + result[name] = thisNameValues + } + } + let propertyNames = propertyValues.flatMap({ $1.keys }).sorted() + + var tableHeader = ["Name"] + tableHeader.append(contentsOf: propertyNames) + + var table: [[String]] = [] + table.append(names) + + for propertyName in propertyNames { + var column: [String] = [] + for name in names { + column.append((propertyValues[name]?[propertyName] ?? 0).toString) + } + table.append(column) + } + + let result = dumpSideBySide( + table, + asTableWithHeaders: tableHeader) + + print(result) + } + private func dumpSideBySide( _ strings: [[String]], asTableWithHeaders headers: [String]) @@ -173,4 +215,55 @@ extension CLI { } + // MARK: - InfoTables + + enum InfoTables: CaseIterable { + case features + case properties + case property([String]) + + static var allCases: [InfoTables] { + [.features, .properties, .property([])] + } + + @Sendable + static func parse(_ rawValue: String) -> [InfoTables] { + var result: [InfoTables] = [] + var properties: Set = [] + + let components = rawValue.components(separatedBy: ",") + for component in components { + if component == "features" { + result.append(.features) + continue + } + if component == "properties" { + result.append(.properties) + continue + } + + let propertyParts = component.components(separatedBy: "=") + guard propertyParts.count == 2, + propertyParts[0] == "property" + else { continue } + + properties.insert(propertyParts[1]) + } + + if properties.isNotEmpty { + result.append(.property(properties.sorted())) + } + + return result + } + + var rawValue: String { + switch self { + case .features: "features" + case .properties: "properties" + case .property: "property" + } + } + } + } diff --git a/Sources/MVTTools/Info.swift b/Sources/MVTTools/Info.swift index e55f617..2d53ed4 100644 --- a/Sources/MVTTools/Info.swift +++ b/Sources/MVTTools/Info.swift @@ -16,6 +16,7 @@ extension VectorTile { public let polygonFeatures: Int public let unknownFeatures: Int public let propertyNames: [String: Int] + public let propertyValues: [String: [String: Int]] public let version: Int? } @@ -31,23 +32,13 @@ extension VectorTile { return layerNames(from: data) } - // [{ - // name: 'world', - // features: 1, - // point_features: 0, - // linestring_features: 0, - // polygon_features: 1, - // unknown_features: 0, - // property_names: [:], - // version: 2 - // }] - /// Information about the features in a tile, per layer. public func tileInfo() -> [LayerInfo]? { var result: [LayerInfo] = [] for (layerName, layerContainer) in layers { var propertyNames: [String: Int] = [:] + var propertyValues: [String: [String: Int]] = [:] var pointFeatures = 0 var lineStringFeatures = 0 @@ -62,8 +53,14 @@ extension VectorTile { default: unknownFeatures += 1 } - for key in feature.properties.keys { + for (key, value) in feature.properties { propertyNames[key, default: 0] += 1 + + if let value = value as? CustomStringConvertible { + var thisKeyValues = propertyValues[key] ?? [:] + thisKeyValues[value.description, default: 0] += 1 + propertyValues[key] = thisKeyValues + } } } @@ -75,6 +72,7 @@ extension VectorTile { polygonFeatures: polygonFeatures, unknownFeatures: unknownFeatures, propertyNames: propertyNames, + propertyValues: propertyValues, version: nil)) } @@ -88,8 +86,9 @@ extension VectorTile { var result: [LayerInfo] = [] for layer in tile.layers { - let keys: [String] = layer.keys + let (keys, values) = MVTDecoder.keysAndValues(forLayer: layer) var propertyNames: [String: Int] = [:] + var propertyValues: [String: [String: Int]] = [:] var pointFeatures = 0 var lineStringFeatures = 0 @@ -105,9 +104,17 @@ extension VectorTile { } for tags in feature.tags.pairs() { - guard let key: String = keys.get(at: Int(tags.first)) else { continue } + guard let key: String = keys.get(at: Int(tags.first)), + let value: Sendable = values.get(at: Int(tags.second)) + else { continue } propertyNames[key, default: 0] += 1 + + if let value = value as? CustomStringConvertible { + var thisKeyValues = propertyValues[key] ?? [:] + thisKeyValues[value.description, default: 0] += 1 + propertyValues[key] = thisKeyValues + } } } @@ -119,6 +126,7 @@ extension VectorTile { polygonFeatures: polygonFeatures, unknownFeatures: unknownFeatures, propertyNames: propertyNames, + propertyValues: propertyValues, version: Int(layer.version))) }