From 88d793ae9b8a5dbf0e126029e4b9f6299033db4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 24 Sep 2021 16:15:14 -0700 Subject: [PATCH 01/16] Fixed indentation --- Sources/Turf/Feature.swift | 8 ++-- Sources/Turf/Geometry.swift | 92 ++++++++++++++++++------------------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/Sources/Turf/Feature.swift b/Sources/Turf/Feature.swift index 4e3197a1..63e19da9 100644 --- a/Sources/Turf/Feature.swift +++ b/Sources/Turf/Feature.swift @@ -11,10 +11,10 @@ public struct Feature: GeoJSONObject { public var geometry: Geometry private enum CodingKeys: String, CodingKey { - case type - case geometry - case properties - case identifier = "id" + case type + case geometry + case properties + case identifier = "id" } public init(geometry: Geometry) { diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index b5fc876c..c9b2387d 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -71,53 +71,53 @@ public enum Geometry { extension Geometry: Codable { public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(GeometryType.self, forKey: .type) - - switch type { - case .Point: - let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates - self = .point(.init(coordinates)) - case .LineString: - let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates - self = .lineString(.init(coordinates)) - case .Polygon: - let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates - self = .polygon(.init(coordinates)) - case .MultiPoint: - let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates - self = .multiPoint(.init(coordinates)) - case .MultiLineString: - let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates - self = .multiLineString(.init(coordinates)) - case .MultiPolygon: - let coordinates = try container.decode([[[LocationCoordinate2DCodable]]].self, forKey: .coordinates).decodedCoordinates - self = .multiPolygon(.init(coordinates)) - case .GeometryCollection: - let geometries = try container.decode([Geometry].self, forKey: .geometries) - self = .geometryCollection(.init(geometries: geometries)) - } + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(GeometryType.self, forKey: .type) + + switch type { + case .Point: + let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates + self = .point(.init(coordinates)) + case .LineString: + let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates + self = .lineString(.init(coordinates)) + case .Polygon: + let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates + self = .polygon(.init(coordinates)) + case .MultiPoint: + let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates + self = .multiPoint(.init(coordinates)) + case .MultiLineString: + let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates + self = .multiLineString(.init(coordinates)) + case .MultiPolygon: + let coordinates = try container.decode([[[LocationCoordinate2DCodable]]].self, forKey: .coordinates).decodedCoordinates + self = .multiPolygon(.init(coordinates)) + case .GeometryCollection: + let geometries = try container.decode([Geometry].self, forKey: .geometries) + self = .geometryCollection(.init(geometries: geometries)) } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type.rawValue, forKey: .type) - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type.rawValue, forKey: .type) - - switch self { - case .point(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .lineString(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .polygon(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .multiPoint(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .multiLineString(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .multiPolygon(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .geometryCollection(let representation): - try container.encode(representation.geometries, forKey: .geometries) - } + switch self { + case .point(let representation): + try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) + case .lineString(let representation): + try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) + case .polygon(let representation): + try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) + case .multiPoint(let representation): + try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) + case .multiLineString(let representation): + try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) + case .multiPolygon(let representation): + try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) + case .geometryCollection(let representation): + try container.encode(representation.geometries, forKey: .geometries) } + } } From e7840a4ed90df829935777134655672ceb478f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 24 Sep 2021 17:24:24 -0700 Subject: [PATCH 02/16] Removed type erasure from GeoJSON enumerations Geometry, FeatureIdentifier, and Number no longer have value properties to offer type erasure. The type property has also been removed in favor of proper pattern matching. --- Sources/Turf/FeatureIdentifier.swift | 18 -------- Sources/Turf/Geometry.swift | 42 +++++-------------- Tests/TurfTests/FeatureCollectionTests.swift | 16 +++---- Tests/TurfTests/GeoJSONTests.swift | 41 ++++++++++-------- Tests/TurfTests/GeometryCollectionTests.swift | 8 ---- Tests/TurfTests/LineStringTests.swift | 14 +++++-- Tests/TurfTests/MultiLineStringTests.swift | 1 - Tests/TurfTests/MultiPointTests.swift | 3 +- Tests/TurfTests/MultiPolygonTests.swift | 2 - Tests/TurfTests/PointTests.swift | 27 ++++++++---- Tests/TurfTests/PolygonTests.swift | 13 +++++- 11 files changed, 85 insertions(+), 100 deletions(-) diff --git a/Sources/Turf/FeatureIdentifier.swift b/Sources/Turf/FeatureIdentifier.swift index 2e81f51a..6eece94e 100644 --- a/Sources/Turf/FeatureIdentifier.swift +++ b/Sources/Turf/FeatureIdentifier.swift @@ -3,15 +3,6 @@ import Foundation public enum Number: Equatable { case int(Int) case double(Double) - - public var value: Any? { - switch self { - case .int(let value): - return value - case .double(let value): - return value - } - } } extension Number: Codable { @@ -42,15 +33,6 @@ extension Number: Codable { public enum FeatureIdentifier { case string(String) case number(Number) - - public var value: Any? { - switch self { - case .number(let value): - return value - case .string(let value): - return value - } - } } extension FeatureIdentifier: Codable { diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index c9b2387d..d65f954d 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -3,17 +3,6 @@ import Foundation import CoreLocation #endif - -public enum GeometryType: String, Codable, CaseIterable { - case Point - case LineString - case Polygon - case MultiPoint - case MultiLineString - case MultiPolygon - case GeometryCollection -} - public enum Geometry { private enum CodingKeys: String, CodingKey { case type @@ -29,7 +18,17 @@ public enum Geometry { case multiPolygon(_ geometry: MultiPolygon) case geometryCollection(_ geometry: GeometryCollection) - public var type: GeometryType { + enum GeometryType: String, Codable, CaseIterable { + case Point + case LineString + case Polygon + case MultiPoint + case MultiLineString + case MultiPolygon + case GeometryCollection + } + + var type: GeometryType { switch self { case .point(_): return .Point @@ -47,25 +46,6 @@ public enum Geometry { return .GeometryCollection } } - - public var value: Any? { - switch self { - case .point(let value): - return value - case .lineString(let value): - return value - case .polygon(let value): - return value - case .multiPoint(let value): - return value - case .multiLineString(let value): - return value - case .multiPolygon(let value): - return value - case .geometryCollection(let value): - return value - } - } } diff --git a/Tests/TurfTests/FeatureCollectionTests.swift b/Tests/TurfTests/FeatureCollectionTests.swift index 4a6970d9..17aa9d0f 100644 --- a/Tests/TurfTests/FeatureCollectionTests.swift +++ b/Tests/TurfTests/FeatureCollectionTests.swift @@ -10,10 +10,10 @@ class FeatureCollectionTests: XCTestCase { let data = try! Fixture.geojsonData(from: "featurecollection")! let geojson = try! GeoJSON.parse(FeatureCollection.self, from: data) - XCTAssert(geojson.features[0].geometry.type == .LineString) - XCTAssert(geojson.features[1].geometry.type == .Polygon) - XCTAssert(geojson.features[2].geometry.type == .Polygon) - XCTAssert(geojson.features[3].geometry.type == .Point) + if case .lineString = geojson.features[0].geometry {} else { XCTFail() } + if case .polygon = geojson.features[1].geometry {} else { XCTFail() } + if case .polygon = geojson.features[2].geometry {} else { XCTFail() } + if case .point = geojson.features[3].geometry {} else { XCTFail() } let lineStringFeature = geojson.features[0] guard case let .lineString(lineStringCoordinates) = lineStringFeature.geometry else { @@ -47,10 +47,10 @@ class FeatureCollectionTests: XCTestCase { let encodedData = try! JSONEncoder().encode(geojson) let decoded = try! GeoJSON.parse(FeatureCollection.self, from: encodedData) - XCTAssert(decoded.features[0].geometry.type == .LineString) - XCTAssert(decoded.features[1].geometry.type == .Polygon) - XCTAssert(decoded.features[2].geometry.type == .Polygon) - XCTAssert(decoded.features[3].geometry.type == .Point) + if case .lineString = decoded.features[0].geometry {} else { XCTFail() } + if case .polygon = decoded.features[1].geometry {} else { XCTFail() } + if case .polygon = decoded.features[2].geometry {} else { XCTFail() } + if case .point = decoded.features[3].geometry {} else { XCTFail() } let decodedLineStringFeature = decoded.features[0] guard case let .lineString(decodedLineStringCoordinates) = decodedLineStringFeature.geometry else { diff --git a/Tests/TurfTests/GeoJSONTests.swift b/Tests/TurfTests/GeoJSONTests.swift index 7eb118bb..56ea3f13 100644 --- a/Tests/TurfTests/GeoJSONTests.swift +++ b/Tests/TurfTests/GeoJSONTests.swift @@ -9,10 +9,10 @@ class GeoJSONTests: XCTestCase { func testPoint() { let coordinate = LocationCoordinate2D(latitude: 10, longitude: 30) - let geometry = Geometry.point(Point(coordinate)) - let pointFeature = Feature(geometry: geometry) + let feature = Feature(geometry: .point(.init(coordinate))) - XCTAssertEqual((pointFeature.geometry.value as! Point).coordinates, coordinate) + guard case let .point(point) = feature.geometry else { return XCTFail() } + XCTAssertEqual(point.coordinates, coordinate) } func testLineString() { @@ -20,9 +20,10 @@ class GeoJSONTests: XCTestCase { LocationCoordinate2D(latitude: 30, longitude: 10), LocationCoordinate2D(latitude: 40, longitude: 40)] - let lineString = Geometry.lineString(.init(coordinates)) - let lineStringFeature = Feature(geometry: lineString) - XCTAssertEqual((lineStringFeature.geometry.value as! LineString).coordinates, coordinates) + let feature = Feature(geometry: .lineString(.init(coordinates))) + + guard case let .lineString(lineString) = feature.geometry else { return XCTFail() } + XCTAssertEqual(lineString.coordinates, coordinates) } func testPolygon() { @@ -42,9 +43,10 @@ class GeoJSONTests: XCTestCase { ] ] - let polygon = Geometry.polygon(.init(coordinates)) - let polygonFeature = Feature(geometry: polygon) - XCTAssertEqual((polygonFeature.geometry.value as! Turf.Polygon).coordinates, coordinates) + let feature = Feature(geometry: .polygon(.init(coordinates))) + + guard case let .polygon(polygon) = feature.geometry else { return XCTFail() } + XCTAssertEqual(polygon.coordinates, coordinates) } func testMultiPoint() { @@ -53,9 +55,10 @@ class GeoJSONTests: XCTestCase { LocationCoordinate2D(latitude: 20, longitude: 20), LocationCoordinate2D(latitude: 10, longitude: 30)] - let multiPoint = Geometry.multiPoint(.init(coordinates)) - let multiPointFeature = Feature(geometry: multiPoint) - XCTAssertEqual((multiPointFeature.geometry.value as! MultiPoint).coordinates, coordinates) + let feature = Feature(geometry: .multiPoint(.init(coordinates))) + + guard case let .multiPoint(multiPoint) = feature.geometry else { return XCTFail() } + XCTAssertEqual(multiPoint.coordinates, coordinates) } func testMultiLineString() { @@ -73,9 +76,10 @@ class GeoJSONTests: XCTestCase { ] ] - let multiLineString = Geometry.multiLineString(.init(coordinates)) - let multiLineStringFeature = Feature(geometry: multiLineString) - XCTAssertEqual((multiLineStringFeature.geometry.value as! MultiLineString).coordinates, coordinates) + let feature = Feature(geometry: .multiLineString(.init(coordinates))) + + guard case let .multiLineString(multiLineString) = feature.geometry else { return XCTFail() } + XCTAssertEqual(multiLineString.coordinates, coordinates) } func testMultiPolygon() { @@ -106,8 +110,9 @@ class GeoJSONTests: XCTestCase { ] ] - let multiPolygon = Geometry.multiPolygon(.init(coordinates)) - let multiPolygonFeature = Feature(geometry: multiPolygon) - XCTAssertEqual((multiPolygonFeature.geometry.value as! MultiPolygon).coordinates, coordinates) + let feature = Feature(geometry: .multiPolygon(.init(coordinates))) + + guard case let .multiPolygon(multiPolygon) = feature.geometry else { return XCTFail() } + XCTAssertEqual(multiPolygon.coordinates, coordinates) } } diff --git a/Tests/TurfTests/GeometryCollectionTests.swift b/Tests/TurfTests/GeometryCollectionTests.swift index c40fae02..0d8286c0 100644 --- a/Tests/TurfTests/GeometryCollectionTests.swift +++ b/Tests/TurfTests/GeometryCollectionTests.swift @@ -22,15 +22,11 @@ class GeometryCollectionTests: XCTestCase { return } - XCTAssert(geometryCollectionFeature.geometry.type == .GeometryCollection) - XCTAssert(geometryCollectionFeature.geometry.value is GeometryCollection) - guard case let .geometryCollection(geometries) = geometryCollectionFeature.geometry else { XCTFail() return } - XCTAssert(geometries.geometries[2].type == .MultiPolygon) guard case let .multiPolygon(decodedMultiPolygonCoordinate) = geometries.geometries[2] else { XCTFail() return @@ -56,15 +52,11 @@ class GeometryCollectionTests: XCTestCase { return } - XCTAssert(geometryCollectionFeature.geometry.type == .GeometryCollection) - XCTAssert(geometryCollectionFeature.geometry.value is GeometryCollection) - guard case let .geometryCollection(geometries) = geometryCollectionFeature.geometry else { XCTFail() return } - XCTAssert(geometries.geometries[2].type == .MultiPolygon) guard case let .multiPolygon(decodedMultiPolygonCoordinate) = geometries.geometries[2] else { XCTFail() return diff --git a/Tests/TurfTests/LineStringTests.swift b/Tests/TurfTests/LineStringTests.swift index c660fe35..43d8377f 100644 --- a/Tests/TurfTests/LineStringTests.swift +++ b/Tests/TurfTests/LineStringTests.swift @@ -10,7 +10,6 @@ class LineStringTests: XCTestCase { let data = try! Fixture.geojsonData(from: "simple-line")! let geojson = try! GeoJSON.parse(Feature.self, from: data) - XCTAssert(geojson.geometry.type == .LineString) guard case let .lineString(lineStringCoordinates) = geojson.geometry else { XCTFail() return @@ -21,7 +20,11 @@ class LineStringTests: XCTestCase { let last = LocationCoordinate2D(latitude: 10, longitude: 0) XCTAssert(lineStringCoordinates.coordinates.first == first) XCTAssert(lineStringCoordinates.coordinates.last == last) - XCTAssert(geojson.identifier!.value as! String == "1") + if case let .string(string) = geojson.identifier { + XCTAssertEqual(string, "1") + } else { + XCTFail() + } let encodedData = try! JSONEncoder().encode(geojson) let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) @@ -31,7 +34,12 @@ class LineStringTests: XCTestCase { } XCTAssertEqual(lineStringCoordinates, decodedLineStringCoordinates) - XCTAssertEqual(geojson.identifier!.value as! String, decoded.identifier!.value! as! String) + if case let .string(string) = geojson.identifier, + case let .string(decodedString) = decoded.identifier { + XCTAssertEqual(string, decodedString) + } else { + XCTFail() + } } func testClosestCoordinate() { diff --git a/Tests/TurfTests/MultiLineStringTests.swift b/Tests/TurfTests/MultiLineStringTests.swift index fd929761..1a905df7 100644 --- a/Tests/TurfTests/MultiLineStringTests.swift +++ b/Tests/TurfTests/MultiLineStringTests.swift @@ -13,7 +13,6 @@ class MultiLineStringTests: XCTestCase { let geojson = try! GeoJSON.parse(Feature.self, from: data) - XCTAssert(geojson.geometry.type == .MultiLineString) guard case let .multiLineString(multiLineStringCoordinates) = geojson.geometry else { XCTFail() return diff --git a/Tests/TurfTests/MultiPointTests.swift b/Tests/TurfTests/MultiPointTests.swift index 244a1462..a8d5f0d8 100644 --- a/Tests/TurfTests/MultiPointTests.swift +++ b/Tests/TurfTests/MultiPointTests.swift @@ -12,8 +12,7 @@ class MultiPointTests: XCTestCase { let lastCoordinate = LocationCoordinate2D(latitude: 24.926294766395593, longitude: 17.75390625) let geojson = try! GeoJSON.parse(Feature.self, from: data) - - XCTAssert(geojson.geometry.type == .MultiPoint) + guard case let .multiPoint(multipointCoordinates) = geojson.geometry else { XCTFail() return diff --git a/Tests/TurfTests/MultiPolygonTests.swift b/Tests/TurfTests/MultiPolygonTests.swift index 0aa7637c..67ac8913 100644 --- a/Tests/TurfTests/MultiPolygonTests.swift +++ b/Tests/TurfTests/MultiPolygonTests.swift @@ -16,7 +16,6 @@ class MultiPolygonTests: XCTestCase { let geojson = try! GeoJSON.parse(Feature.self, from: data) - XCTAssert(geojson.geometry.type == .MultiPolygon) guard case let .multiPolygon(multipolygonCoordinates) = geojson.geometry else { XCTFail() return @@ -87,7 +86,6 @@ class MultiPolygonTests: XCTestCase { return } - XCTAssert(decodedCustomMultiPolygon.geometry.type == .MultiPolygon) guard case let .multiPolygon(decodedMultipolygonCoordinates) = decodedCustomMultiPolygon.geometry else { XCTFail() return diff --git a/Tests/TurfTests/PointTests.swift b/Tests/TurfTests/PointTests.swift index eb21ad96..55c4eb23 100644 --- a/Tests/TurfTests/PointTests.swift +++ b/Tests/TurfTests/PointTests.swift @@ -16,15 +16,28 @@ class PointTests: XCTestCase { return } XCTAssertEqual(point.coordinates, coordinate) - XCTAssert((geojson.identifier!.value as! Number).value! as! Int == 1) + if case let .number(.int(int)) = geojson.identifier { + XCTAssertEqual(int, 1) + } else { + XCTFail() + } let encodedData = try! JSONEncoder().encode(geojson) let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) - - XCTAssertEqual(geojson.geometry.value as! Point, - decoded.geometry.value as! Point) - XCTAssertEqual(geojson.identifier!.value as! Number, - decoded.identifier!.value as! Number) + + if case let .point(point) = geojson.geometry, + case let .point(decodedPoint) = decoded.geometry { + XCTAssertEqual(point, decodedPoint) + } else { + XCTFail() + } + + if case let .number(number) = geojson.identifier, + case let .number(decodedNumber) = decoded.identifier { + XCTAssertEqual(number, decodedNumber) + } else { + XCTFail() + } } func testUnkownPointFeature() { @@ -32,6 +45,6 @@ class PointTests: XCTestCase { let geojson = try! GeoJSON.parse(data) XCTAssert(geojson.decoded is Feature) - XCTAssert(geojson.decodedFeature?.geometry.type == .Point) + guard case .point = geojson.decodedFeature?.geometry else { return XCTFail() } } } diff --git a/Tests/TurfTests/PolygonTests.swift b/Tests/TurfTests/PolygonTests.swift index f8874076..56ef5332 100644 --- a/Tests/TurfTests/PolygonTests.swift +++ b/Tests/TurfTests/PolygonTests.swift @@ -13,7 +13,11 @@ class PolygonTests: XCTestCase { let firstCoordinate = LocationCoordinate2D(latitude: 37.00255267215955, longitude: -109.05029296875) let lastCoordinate = LocationCoordinate2D(latitude: 40.6306300839918, longitude: -108.56689453125) - XCTAssert((geojson.identifier!.value as! Number).value! as! Double == 1.01) + if case let .number(.double(double)) = geojson.identifier { + XCTAssertEqual(double, 1.01) + } else { + XCTFail() + } guard case let .polygon(polygon) = geojson.geometry else { XCTFail() @@ -32,7 +36,12 @@ class PolygonTests: XCTestCase { } XCTAssertEqual(polygon, decodedPolygon) - XCTAssertEqual(geojson.identifier!.value as! Number, decoded.identifier!.value! as! Number) + if case let .number(number) = geojson.identifier, + case let .number(decodedNumber) = decoded.identifier { + XCTAssertEqual(number, decodedNumber) + } else { + XCTFail() + } XCTAssert(decodedPolygon.outerRing.coordinates.first == firstCoordinate) XCTAssert(decodedPolygon.innerRings.last?.coordinates.last == lastCoordinate) XCTAssert(decodedPolygon.outerRing.coordinates.count == 5) From 8e332d405ad98943fb49845427558b69ce008868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 24 Sep 2021 17:32:50 -0700 Subject: [PATCH 03/16] Renamed GeometryType to Geometry.Kind --- Sources/Turf/Geometry.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index d65f954d..5075be3e 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -5,7 +5,7 @@ import CoreLocation public enum Geometry { private enum CodingKeys: String, CodingKey { - case type + case kind = "type" case coordinates case geometries } @@ -18,7 +18,7 @@ public enum Geometry { case multiPolygon(_ geometry: MultiPolygon) case geometryCollection(_ geometry: GeometryCollection) - enum GeometryType: String, Codable, CaseIterable { + enum Kind: String, Codable, CaseIterable { case Point case LineString case Polygon @@ -28,7 +28,7 @@ public enum Geometry { case GeometryCollection } - var type: GeometryType { + var kind: Kind { switch self { case .point(_): return .Point @@ -52,9 +52,9 @@ public enum Geometry { extension Geometry: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(GeometryType.self, forKey: .type) + let kind = try container.decode(Kind.self, forKey: .kind) - switch type { + switch kind { case .Point: let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates self = .point(.init(coordinates)) @@ -81,7 +81,7 @@ extension Geometry: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type.rawValue, forKey: .type) + try container.encode(kind.rawValue, forKey: .kind) switch self { case .point(let representation): From 00e6873c33c6b6f7d8e08c07505f568eb43145f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 24 Sep 2021 19:22:05 -0700 Subject: [PATCH 04/16] Unified GeoJSON and GeoJSONObject Replaced the GeoJSONObject protocol with an enumeration of the same name with associated values for the structs that previously conformed to the protocol. Removed GeoJSON in favor of GeoJSONObject, which now conforms to Codable. Renamed type coding keys to kind for consistency. --- Sources/Turf/Feature.swift | 22 ++- Sources/Turf/FeatureCollection.swift | 26 ++-- Sources/Turf/GeoJSON.swift | 127 ++++++------------ Sources/Turf/Geometry.swift | 3 + Tests/TurfTests/FeatureCollectionTests.swift | 57 ++++---- Tests/TurfTests/GeometryCollectionTests.swift | 14 +- Tests/TurfTests/LineStringTests.swift | 4 +- Tests/TurfTests/MultiLineStringTests.swift | 4 +- Tests/TurfTests/MultiPointTests.swift | 4 +- Tests/TurfTests/MultiPolygonTests.swift | 8 +- Tests/TurfTests/PointTests.swift | 42 ++++-- Tests/TurfTests/PolygonTests.swift | 4 +- Tests/TurfTests/TurfTests.swift | 28 ++-- 13 files changed, 171 insertions(+), 172 deletions(-) diff --git a/Sources/Turf/Feature.swift b/Sources/Turf/Feature.swift index 63e19da9..06cc832f 100644 --- a/Sources/Turf/Feature.swift +++ b/Sources/Turf/Feature.swift @@ -3,26 +3,34 @@ import Foundation import CoreLocation #endif - -public struct Feature: GeoJSONObject { - public var type: FeatureType = .feature +/** + A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing. + */ +public struct Feature { public var identifier: FeatureIdentifier? public var properties: [String : Any?]? public var geometry: Geometry + public init(geometry: Geometry) { + self.geometry = geometry + } +} + +extension Feature: Codable { private enum CodingKeys: String, CodingKey { - case type + case kind = "type" case geometry case properties case identifier = "id" } - public init(geometry: Geometry) { - self.geometry = geometry + enum Kind: String, Codable { + case Feature } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) geometry = try container.decode(Geometry.self, forKey: .geometry) properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties) identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier) @@ -30,7 +38,7 @@ public struct Feature: GeoJSONObject { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type.rawValue, forKey: .type) + try container.encode(Kind.Feature, forKey: .kind) try container.encodeIfPresent(geometry, forKey: .geometry) try container.encodeIfPresent(properties, forKey: .properties) try container.encodeIfPresent(identifier, forKey: .identifier) diff --git a/Sources/Turf/FeatureCollection.swift b/Sources/Turf/FeatureCollection.swift index b90266d2..4f21da17 100644 --- a/Sources/Turf/FeatureCollection.swift +++ b/Sources/Turf/FeatureCollection.swift @@ -1,31 +1,39 @@ import Foundation - -public struct FeatureCollection: GeoJSONObject { - public let type: FeatureType = .featureCollection +/** + A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. + */ +public struct FeatureCollection { public var identifier: FeatureIdentifier? public var features: Array = [] public var properties: [String : Any?]? + public init(features: [Feature]) { + self.features = features + } +} + +extension FeatureCollection: Codable { private enum CodingKeys: String, CodingKey { - case type + case kind = "type" case properties case features } - public init(features: [Feature]) { - self.features = features + enum Kind: String, Codable { + case FeatureCollection } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.features = try container.decode([Feature].self, forKey: .features) - self.properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties) + _ = try container.decode(Kind.self, forKey: .kind) + features = try container.decode([Feature].self, forKey: .features) + properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) + try container.encode(Kind.FeatureCollection, forKey: .kind) try container.encode(features, forKey: .features) try container.encodeIfPresent(properties, forKey: .properties) } diff --git a/Sources/Turf/GeoJSON.swift b/Sources/Turf/GeoJSON.swift index 3f7c17d4..7106c3cb 100644 --- a/Sources/Turf/GeoJSON.swift +++ b/Sources/Turf/GeoJSON.swift @@ -3,103 +3,60 @@ import Foundation import CoreLocation #endif -public enum FeatureType: String, Codable { - case feature = "Feature" - case featureCollection = "FeatureCollection" -} - -struct FeatureProxy: Codable { - public var type: FeatureType - - private enum CodingKeys: String, CodingKey { - case type - } +/** + A [GeoJSON object](https://datatracker.ietf.org/doc/html/rfc7946#section-3) represents a Geometry, Feature, or collection of + Features. + */ +public enum GeoJSONObject { + /** + A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. + + - parameter geometry: The GeoJSON object as a Geometry object. + */ + case geometry(_ geometry: Geometry) - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - do { - type = try container.decode(FeatureType.self, forKey: .type) - } catch { - throw GeoJSONError.noTypeFound - } - } + /** + A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing. + + - parameter feature: The GeoJSON object as a Feature object. + */ + case feature(_ feature: Feature) - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) - } -} - -public protocol GeoJSONObject: Codable { - var type: FeatureType { get } - var identifier: FeatureIdentifier? { get set } - var properties: [String: Any?]? { get set } -} - -public enum GeoJSONError: Error { - case unknownType - case noTypeFound + /** + A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. + + - parameter featureCollection: The GeoJSON object as a FeatureCollection object. + */ + case featureCollection(_ featureCollection: FeatureCollection) } -public class GeoJSON: Codable { - - public var decoded: Codable? - public var decodedFeature: Feature? { - return decoded as? Feature - } - public var decodedFeatureCollection: FeatureCollection? { - return decoded as? FeatureCollection +extension GeoJSONObject: Codable { + private enum CodingKeys: String, CodingKey { + case kind = "type" } - public required init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { + let kindContainer = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.singleValueContainer() - let featureProxy = try container.decode(FeatureProxy.self) - - switch featureProxy.type { - case .feature: - self.decoded = try container.decode(Feature.self) - case .featureCollection: - self.decoded = try container.decode(FeatureCollection.self) + switch try kindContainer.decode(String.self, forKey: .kind) { + case Feature.Kind.Feature.rawValue: + self = .feature(try container.decode(Feature.self)) + case FeatureCollection.Kind.FeatureCollection.rawValue: + self = .featureCollection(try container.decode(FeatureCollection.self)) + default: + self = .geometry(try container.decode(Geometry.self)) } } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - - if let value = decoded as? FeatureCollection { - try container.encode(value) - } else if let value = decoded as? Feature { - try container.encode(value) - } else { - throw GeoJSONError.unknownType + switch self { + case .geometry(let geometry): + try container.encode(geometry) + case .feature(let feature): + try container.encode(feature) + case .featureCollection(let featureCollection): + try container.encode(featureCollection) } } - - /** - Parse JSON encoded data into a GeoJSON of unknown type. - - - Parameter data: the JSON encoded GeoJSON data. - - - Throws: `GeoJSONError` if the type is not compatible. - - - Returns: decoded GeoJSON of any compatible type. - */ - public static func parse(_ data: Data) throws -> GeoJSON { - return try JSONDecoder().decode(GeoJSON.self, from: data) - } - - - /** - Parse JSON encoded data into a GeoJSON of known type. - - - Parameter type: The known GeoJSON type (T). - - Parameter data: the JSON encoded GeoJSON data. - - - Throws: `GeoJSONError` if the type is not compatible. - - - Returns: decoded GeoJSON of type T. - */ - public static func parse(_ type: T.Type, from data: Data) throws -> T { - return try JSONDecoder().decode(T.self, from: data) - } } diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index 5075be3e..3ade5508 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -3,6 +3,9 @@ import Foundation import CoreLocation #endif +/** + A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. + */ public enum Geometry { private enum CodingKeys: String, CodingKey { case kind = "type" diff --git a/Tests/TurfTests/FeatureCollectionTests.swift b/Tests/TurfTests/FeatureCollectionTests.swift index 17aa9d0f..bdfa69e0 100644 --- a/Tests/TurfTests/FeatureCollectionTests.swift +++ b/Tests/TurfTests/FeatureCollectionTests.swift @@ -8,14 +8,15 @@ class FeatureCollectionTests: XCTestCase { func testFeatureCollection() { let data = try! Fixture.geojsonData(from: "featurecollection")! - let geojson = try! GeoJSON.parse(FeatureCollection.self, from: data) + let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) + guard case let .featureCollection(featureCollection) = geojson else { return XCTFail() } - if case .lineString = geojson.features[0].geometry {} else { XCTFail() } - if case .polygon = geojson.features[1].geometry {} else { XCTFail() } - if case .polygon = geojson.features[2].geometry {} else { XCTFail() } - if case .point = geojson.features[3].geometry {} else { XCTFail() } + if case .lineString = featureCollection.features[0].geometry {} else { XCTFail() } + if case .polygon = featureCollection.features[1].geometry {} else { XCTFail() } + if case .polygon = featureCollection.features[2].geometry {} else { XCTFail() } + if case .point = featureCollection.features[3].geometry {} else { XCTFail() } - let lineStringFeature = geojson.features[0] + let lineStringFeature = featureCollection.features[0] guard case let .lineString(lineStringCoordinates) = lineStringFeature.geometry else { XCTFail() return @@ -25,7 +26,7 @@ class FeatureCollectionTests: XCTestCase { XCTAssert(lineStringCoordinates.coordinates.first!.latitude == -26.17500493262446) XCTAssert(lineStringCoordinates.coordinates.first!.longitude == 27.977542877197266) - let polygonFeature = geojson.features[1] + let polygonFeature = featureCollection.features[1] guard case let .polygon(polygonCoordinates) = polygonFeature.geometry else { XCTFail() return @@ -35,7 +36,7 @@ class FeatureCollectionTests: XCTestCase { XCTAssert(polygonCoordinates.coordinates[0].first!.latitude == -26.199035448897074) XCTAssert(polygonCoordinates.coordinates[0].first!.longitude == 27.972049713134762) - let pointFeature = geojson.features[3] + let pointFeature = featureCollection.features[3] guard case let .point(pointCoordinates) = pointFeature.geometry else { XCTFail() return @@ -45,14 +46,15 @@ class FeatureCollectionTests: XCTestCase { XCTAssert(pointCoordinates.coordinates.longitude == 27.95642852783203) let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(FeatureCollection.self, from: encodedData) + let decoded = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) + guard case let .featureCollection(decodedFeatureCollection) = decoded else { return XCTFail() } - if case .lineString = decoded.features[0].geometry {} else { XCTFail() } - if case .polygon = decoded.features[1].geometry {} else { XCTFail() } - if case .polygon = decoded.features[2].geometry {} else { XCTFail() } - if case .point = decoded.features[3].geometry {} else { XCTFail() } + if case .lineString = decodedFeatureCollection.features[0].geometry {} else { XCTFail() } + if case .polygon = decodedFeatureCollection.features[1].geometry {} else { XCTFail() } + if case .polygon = decodedFeatureCollection.features[2].geometry {} else { XCTFail() } + if case .point = decodedFeatureCollection.features[3].geometry {} else { XCTFail() } - let decodedLineStringFeature = decoded.features[0] + let decodedLineStringFeature = decodedFeatureCollection.features[0] guard case let .lineString(decodedLineStringCoordinates) = decodedLineStringFeature.geometry else { XCTFail() return @@ -62,7 +64,7 @@ class FeatureCollectionTests: XCTestCase { XCTAssert(decodedLineStringCoordinates.coordinates.first!.latitude == -26.17500493262446) XCTAssert(decodedLineStringCoordinates.coordinates.first!.longitude == 27.977542877197266) - let decodedPolygonFeature = decoded.features[1] + let decodedPolygonFeature = decodedFeatureCollection.features[1] guard case let .polygon(decodedPolygonCoordinates) = decodedPolygonFeature.geometry else { XCTFail() return @@ -72,7 +74,7 @@ class FeatureCollectionTests: XCTestCase { XCTAssert(decodedPolygonCoordinates.coordinates[0].first!.latitude == -26.199035448897074) XCTAssert(decodedPolygonCoordinates.coordinates[0].first!.longitude == 27.972049713134762) - let decodedPointFeature = decoded.features[3] + let decodedPointFeature = decodedFeatureCollection.features[3] guard case let .point(decodedPointCoordinates) = decodedPointFeature.geometry else { XCTFail() return @@ -84,14 +86,14 @@ class FeatureCollectionTests: XCTestCase { func testFeatureCollectionDecodeWithoutProperties() { let data = try! Fixture.geojsonData(from: "featurecollection-no-properties")! - let geojson = try! GeoJSON.parse(data) - XCTAssert(geojson.decoded is FeatureCollection) + let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) + guard case .featureCollection = geojson else { return XCTFail() } } func testUnkownFeatureCollection() { let data = try! Fixture.geojsonData(from: "featurecollection")! - let geojson = try! GeoJSON.parse(data) - XCTAssert(geojson.decoded is FeatureCollection) + let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) + guard case .featureCollection = geojson else { return XCTFail() } } func testPerformanceDecodeFeatureCollection() { @@ -99,14 +101,14 @@ class FeatureCollectionTests: XCTestCase { measure { for _ in 0...100 { - _ = try! GeoJSON.parse(FeatureCollection.self, from: data) + _ = try! JSONDecoder().decode(FeatureCollection.self, from: data) } } } func testPerformanceEncodeFeatureCollection() { let data = try! Fixture.geojsonData(from: "featurecollection")! - let decoded = try! GeoJSON.parse(FeatureCollection.self, from: data) + let decoded = try! JSONDecoder().decode(FeatureCollection.self, from: data) measure { for _ in 0...100 { @@ -120,7 +122,7 @@ class FeatureCollectionTests: XCTestCase { measure { for _ in 0...100 { - let decoded = try! GeoJSON.parse(FeatureCollection.self, from: data) + let decoded = try! JSONDecoder().decode(FeatureCollection.self, from: data) _ = try! JSONEncoder().encode(decoded) } } @@ -128,11 +130,10 @@ class FeatureCollectionTests: XCTestCase { func testDecodedFeatureCollection() { let data = try! Fixture.geojsonData(from: "featurecollection")! - let geojson = try! GeoJSON.parse(data) + let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) - XCTAssert(geojson.decoded is FeatureCollection) - XCTAssertEqual(geojson.decodedFeatureCollection?.type, .featureCollection) - XCTAssertEqual(geojson.decodedFeatureCollection?.features.count, 4) - XCTAssertEqual(geojson.decodedFeatureCollection?.properties?["tolerance"] as? Double, 0.01) + guard case let .featureCollection(featureCollection) = geojson else { return XCTFail() } + XCTAssertEqual(featureCollection.features.count, 4) + XCTAssertEqual(featureCollection.properties?["tolerance"] as? Double, 0.01) } } diff --git a/Tests/TurfTests/GeometryCollectionTests.swift b/Tests/TurfTests/GeometryCollectionTests.swift index 0d8286c0..f2807e60 100644 --- a/Tests/TurfTests/GeometryCollectionTests.swift +++ b/Tests/TurfTests/GeometryCollectionTests.swift @@ -12,12 +12,10 @@ class GeometryCollectionTests: XCTestCase { let multiPolygonCoordinate = LocationCoordinate2D(latitude: 8.5, longitude: 1) // Act - let geoJSON = try! GeoJSON.parse(data) + let geoJSON = try! JSONDecoder().decode(GeoJSONObject.self, from: data) // Assert - XCTAssert(geoJSON.decoded is Feature) - - guard let geometryCollectionFeature = geoJSON.decoded as? Feature else { + guard case let .feature(geometryCollectionFeature) = geoJSON else { XCTFail() return } @@ -38,16 +36,14 @@ class GeometryCollectionTests: XCTestCase { // Arrange let multiPolygonCoordinate = LocationCoordinate2D(latitude: 8.5, longitude: 1) let data = try! Fixture.geojsonData(from: "geometry-collection")! - let geoJSON = try! GeoJSON.parse(data) + let geoJSON = try! JSONDecoder().decode(GeoJSONObject.self, from: data) // Act let encodedData = try! JSONEncoder().encode(geoJSON) - let encodedJSON = try! GeoJSON.parse(encodedData) + let encodedJSON = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) // Assert - XCTAssert(encodedJSON.decoded is Feature) - - guard let geometryCollectionFeature = encodedJSON.decoded as? Feature else { + guard case let .feature(geometryCollectionFeature) = encodedJSON else { XCTFail() return } diff --git a/Tests/TurfTests/LineStringTests.swift b/Tests/TurfTests/LineStringTests.swift index 43d8377f..6c98a261 100644 --- a/Tests/TurfTests/LineStringTests.swift +++ b/Tests/TurfTests/LineStringTests.swift @@ -8,7 +8,7 @@ class LineStringTests: XCTestCase { func testLineStringFeature() { let data = try! Fixture.geojsonData(from: "simple-line")! - let geojson = try! GeoJSON.parse(Feature.self, from: data) + let geojson = try! JSONDecoder().decode(Feature.self, from: data) guard case let .lineString(lineStringCoordinates) = geojson.geometry else { XCTFail() @@ -27,7 +27,7 @@ class LineStringTests: XCTestCase { } let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) + let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) guard case let .lineString(decodedLineStringCoordinates) = decoded.geometry else { XCTFail() return diff --git a/Tests/TurfTests/MultiLineStringTests.swift b/Tests/TurfTests/MultiLineStringTests.swift index 1a905df7..2a2fe77c 100644 --- a/Tests/TurfTests/MultiLineStringTests.swift +++ b/Tests/TurfTests/MultiLineStringTests.swift @@ -11,7 +11,7 @@ class MultiLineStringTests: XCTestCase { let firstCoordinate = LocationCoordinate2D(latitude: 0, longitude: 0) let lastCoordinate = LocationCoordinate2D(latitude: 6, longitude: 6) - let geojson = try! GeoJSON.parse(Feature.self, from: data) + let geojson = try! JSONDecoder().decode(Feature.self, from: data) guard case let .multiLineString(multiLineStringCoordinates) = geojson.geometry else { XCTFail() @@ -21,7 +21,7 @@ class MultiLineStringTests: XCTestCase { XCTAssert(multiLineStringCoordinates.coordinates.last?.last == lastCoordinate) let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) + let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) guard case let .multiLineString(decodedMultiLineStringCoordinates) = decoded.geometry else { XCTFail() return diff --git a/Tests/TurfTests/MultiPointTests.swift b/Tests/TurfTests/MultiPointTests.swift index a8d5f0d8..d5aa06cc 100644 --- a/Tests/TurfTests/MultiPointTests.swift +++ b/Tests/TurfTests/MultiPointTests.swift @@ -11,7 +11,7 @@ class MultiPointTests: XCTestCase { let firstCoordinate = LocationCoordinate2D(latitude: 26.194876675795218, longitude: 14.765625) let lastCoordinate = LocationCoordinate2D(latitude: 24.926294766395593, longitude: 17.75390625) - let geojson = try! GeoJSON.parse(Feature.self, from: data) + let geojson = try! JSONDecoder().decode(Feature.self, from: data) guard case let .multiPoint(multipointCoordinates) = geojson.geometry else { XCTFail() @@ -21,7 +21,7 @@ class MultiPointTests: XCTestCase { XCTAssert(multipointCoordinates.coordinates.last == lastCoordinate) let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) + let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) guard case let .multiPoint(decodedMultipointCoordinates) = decoded.geometry else { XCTFail() return diff --git a/Tests/TurfTests/MultiPolygonTests.swift b/Tests/TurfTests/MultiPolygonTests.swift index 67ac8913..b93fc869 100644 --- a/Tests/TurfTests/MultiPolygonTests.swift +++ b/Tests/TurfTests/MultiPolygonTests.swift @@ -14,7 +14,7 @@ class MultiPolygonTests: XCTestCase { let firstCoordinate = LocationCoordinate2D(latitude: 0, longitude: 0) let lastCoordinate = LocationCoordinate2D(latitude: 11, longitude: 11) - let geojson = try! GeoJSON.parse(Feature.self, from: data) + let geojson = try! JSONDecoder().decode(Feature.self, from: data) guard case let .multiPolygon(multipolygonCoordinates) = geojson.geometry else { XCTFail() @@ -25,7 +25,7 @@ class MultiPolygonTests: XCTestCase { XCTAssert(multipolygonCoordinates.coordinates.last?.last?.last == lastCoordinate) let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) + let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) guard case let .multiPolygon(decodedMultipolygonCoordinates) = decoded.geometry else { XCTFail() return @@ -77,10 +77,10 @@ class MultiPolygonTests: XCTestCase { multiPolygonFeature.properties = ["some": "var"] let encodedData = try! JSONEncoder().encode(multiPolygonFeature) - let decodedCustomMultiPolygon = try! GeoJSON.parse(Feature.self, from: encodedData) + let decodedCustomMultiPolygon = try! JSONDecoder().decode(Feature.self, from: encodedData) let data = try! Fixture.geojsonData(from: "multipolygon")! - let bundledMultiPolygon = try! GeoJSON.parse(Feature.self, from: data) + let bundledMultiPolygon = try! JSONDecoder().decode(Feature.self, from: data) guard case let .multiPolygon(bundledMultipolygonCoordinates) = bundledMultiPolygon.geometry else { XCTFail() return diff --git a/Tests/TurfTests/PointTests.swift b/Tests/TurfTests/PointTests.swift index 55c4eb23..3689a0c1 100644 --- a/Tests/TurfTests/PointTests.swift +++ b/Tests/TurfTests/PointTests.swift @@ -8,32 +8,33 @@ class PointTests: XCTestCase { func testPointFeature() { let data = try! Fixture.geojsonData(from: "point")! - let geojson = try! GeoJSON.parse(Feature.self, from: data) + let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) let coordinate = LocationCoordinate2D(latitude: 26.194876675795218, longitude: 14.765625) - guard case let .point(point) = geojson.geometry else { + guard case let .feature(feature) = geojson, + case let .point(point) = feature.geometry else { XCTFail() return } XCTAssertEqual(point.coordinates, coordinate) - if case let .number(.int(int)) = geojson.identifier { + if case let .number(.int(int)) = feature.identifier { XCTAssertEqual(int, 1) } else { XCTFail() } let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) + let decoded = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) - if case let .point(point) = geojson.geometry, - case let .point(decodedPoint) = decoded.geometry { - XCTAssertEqual(point, decodedPoint) - } else { - XCTFail() + guard case let .feature(decodedFeature) = decoded, + case let .point(decodedPoint) = decodedFeature.geometry else { + return XCTFail() } - if case let .number(number) = geojson.identifier, - case let .number(decodedNumber) = decoded.identifier { + XCTAssertEqual(point, decodedPoint) + + if case let .number(number) = feature.identifier, + case let .number(decodedNumber) = decodedFeature.identifier { XCTAssertEqual(number, decodedNumber) } else { XCTFail() @@ -42,9 +43,22 @@ class PointTests: XCTestCase { func testUnkownPointFeature() { let data = try! Fixture.geojsonData(from: "point")! - let geojson = try! GeoJSON.parse(data) + let geojson = try! JSONDecoder().decode(GeoJSONObject.self, from: data) + + guard case let .feature(feature) = geojson, + case let .point(point) = feature.geometry else { + return XCTFail() + } + + var encodedData: Data? + XCTAssertNoThrow(encodedData = try JSONEncoder().encode(GeoJSONObject.geometry(feature.geometry))) + XCTAssertNotNil(encodedData) + + var decoded: GeoJSONObject? + XCTAssertNoThrow(decoded = try JSONDecoder().decode(GeoJSONObject.self, from: encodedData!)) + XCTAssertNotNil(decoded) - XCTAssert(geojson.decoded is Feature) - guard case .point = geojson.decodedFeature?.geometry else { return XCTFail() } + guard case let .geometry(.point(decodedPoint)) = decoded else { return XCTFail() } + XCTAssertEqual(point, decodedPoint) } } diff --git a/Tests/TurfTests/PolygonTests.swift b/Tests/TurfTests/PolygonTests.swift index 56ef5332..5c3328a9 100644 --- a/Tests/TurfTests/PolygonTests.swift +++ b/Tests/TurfTests/PolygonTests.swift @@ -8,7 +8,7 @@ class PolygonTests: XCTestCase { func testPolygonFeature() { let data = try! Fixture.geojsonData(from: "polygon")! - let geojson = try! GeoJSON.parse(Feature.self, from: data) + let geojson = try! JSONDecoder().decode(Feature.self, from: data) let firstCoordinate = LocationCoordinate2D(latitude: 37.00255267215955, longitude: -109.05029296875) let lastCoordinate = LocationCoordinate2D(latitude: 40.6306300839918, longitude: -108.56689453125) @@ -29,7 +29,7 @@ class PolygonTests: XCTestCase { XCTAssert(polygon.innerRings.first?.coordinates.count == 5) let encodedData = try! JSONEncoder().encode(geojson) - let decoded = try! GeoJSON.parse(Feature.self, from: encodedData) + let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) guard case let .polygon(decodedPolygon) = decoded.geometry else { XCTFail() return diff --git a/Tests/TurfTests/TurfTests.swift b/Tests/TurfTests/TurfTests.swift index 0e77139d..f0bcc2b1 100644 --- a/Tests/TurfTests/TurfTests.swift +++ b/Tests/TurfTests/TurfTests.swift @@ -163,10 +163,10 @@ class TurfTests: XCTestCase { { try Fixture.fixtures(folder: "simplify") { name, inputData, outputData in do { - let input = try JSONDecoder().decode(GeoJSON.self, from: inputData) - let output = try JSONDecoder().decode(GeoJSON.self, from: outputData) + let input = try JSONDecoder().decode(GeoJSONObject.self, from: inputData) + let output = try JSONDecoder().decode(GeoJSONObject.self, from: outputData) - let properties = input.decodedFeature?.properties + let properties = input.properties let tolerance = (properties?["tolerance"] as? NSNumber)?.doubleValue ?? 0.01 let highQuality = (properties?["highQuality"] as? NSNumber)?.boolValue ?? false @@ -194,14 +194,26 @@ class TurfTests: XCTestCase { } } -extension GeoJSON { +extension GeoJSONObject { + var properties: [String: Any?]? { + switch self { + case .geometry: + return nil + case .feature(let feature): + return feature.properties + case .featureCollection(let featureCollection): + return featureCollection.properties + } + } + var features: [Feature] { - if let feature = self.decodedFeature { + switch self { + case .geometry: + return [] + case .feature(let feature): return [feature] - } else if let featureCollection = self.decodedFeatureCollection { + case .featureCollection(let featureCollection): return featureCollection.features - } else { - return [] } } } From 1984f2605c24b87415d9ce7d10461acd91d691ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 27 Sep 2021 09:46:57 -0700 Subject: [PATCH 05/16] Geometric types conform to Codable Pushed Codable conformance from Geometry to each individual geometric struct, so that Geometry is only a container for a geometric type with no additional smarts. --- .../Turf/Geometries/GeometryCollection.swift | 24 +++++ Sources/Turf/Geometries/LineString.swift | 24 +++++ Sources/Turf/Geometries/MultiLineString.swift | 24 +++++ Sources/Turf/Geometries/MultiPoint.swift | 24 +++++ Sources/Turf/Geometries/MultiPolygon.swift | 24 +++++ Sources/Turf/Geometries/Point.swift | 24 +++++ Sources/Turf/Geometries/Polygon.swift | 24 +++++ Sources/Turf/Geometry.swift | 97 +++++++------------ 8 files changed, 202 insertions(+), 63 deletions(-) diff --git a/Sources/Turf/Geometries/GeometryCollection.swift b/Sources/Turf/Geometries/GeometryCollection.swift index b96cdc14..a8a19500 100644 --- a/Sources/Turf/Geometries/GeometryCollection.swift +++ b/Sources/Turf/Geometries/GeometryCollection.swift @@ -19,3 +19,27 @@ public struct GeometryCollection { } } } + +extension GeometryCollection: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case geometries + } + + enum Kind: String, Codable { + case GeometryCollection + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let geometries = try container.decode([Geometry].self, forKey: .geometries) + self = .init(geometries: geometries) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.GeometryCollection, forKey: .kind) + try container.encode(geometries, forKey: .geometries) + } +} diff --git a/Sources/Turf/Geometries/LineString.swift b/Sources/Turf/Geometries/LineString.swift index 66cb4bde..57b9a7cf 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -16,6 +16,30 @@ public struct LineString: Equatable { } } +extension LineString: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + } + + enum Kind: String, Codable { + case LineString + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates + self = .init(coordinates) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.LineString, forKey: .kind) + try container.encode(coordinates.codableCoordinates, forKey: .coordinates) + } +} + extension LineString { /// Returns a new `.LineString` based on bezier transformation of the input line. /// diff --git a/Sources/Turf/Geometries/MultiLineString.swift b/Sources/Turf/Geometries/MultiLineString.swift index a5f9bb42..55944923 100644 --- a/Sources/Turf/Geometries/MultiLineString.swift +++ b/Sources/Turf/Geometries/MultiLineString.swift @@ -15,3 +15,27 @@ public struct MultiLineString: Equatable { self.coordinates = polygon.coordinates } } + +extension MultiLineString: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + } + + enum Kind: String, Codable { + case MultiLineString + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates + self = .init(coordinates) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.MultiLineString, forKey: .kind) + try container.encode(coordinates.codableCoordinates, forKey: .coordinates) + } +} diff --git a/Sources/Turf/Geometries/MultiPoint.swift b/Sources/Turf/Geometries/MultiPoint.swift index 17e8b874..cee6026c 100644 --- a/Sources/Turf/Geometries/MultiPoint.swift +++ b/Sources/Turf/Geometries/MultiPoint.swift @@ -11,3 +11,27 @@ public struct MultiPoint: Equatable { self.coordinates = coordinates } } + +extension MultiPoint: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + } + + enum Kind: String, Codable { + case MultiPoint + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates + self = .init(coordinates) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.MultiPoint, forKey: .kind) + try container.encode(coordinates.codableCoordinates, forKey: .coordinates) + } +} diff --git a/Sources/Turf/Geometries/MultiPolygon.swift b/Sources/Turf/Geometries/MultiPolygon.swift index 87d33003..8d81cc7d 100644 --- a/Sources/Turf/Geometries/MultiPolygon.swift +++ b/Sources/Turf/Geometries/MultiPolygon.swift @@ -18,6 +18,30 @@ public struct MultiPolygon: Equatable { } } +extension MultiPolygon: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + } + + enum Kind: String, Codable { + case MultiPolygon + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let coordinates = try container.decode([[[LocationCoordinate2DCodable]]].self, forKey: .coordinates).decodedCoordinates + self = .init(coordinates) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.MultiPolygon, forKey: .kind) + try container.encode(coordinates.codableCoordinates, forKey: .coordinates) + } +} + extension MultiPolygon { public var polygons: [Polygon] { diff --git a/Sources/Turf/Geometries/Point.swift b/Sources/Turf/Geometries/Point.swift index 92c41bf3..c3f7fa1d 100644 --- a/Sources/Turf/Geometries/Point.swift +++ b/Sources/Turf/Geometries/Point.swift @@ -14,3 +14,27 @@ public struct Point: Equatable { self.coordinates = coordinates } } + +extension Point: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + } + + enum Kind: String, Codable { + case Point + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates + self = .init(coordinates) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.Point, forKey: .kind) + try container.encode(coordinates.codableCoordinates, forKey: .coordinates) + } +} diff --git a/Sources/Turf/Geometries/Polygon.swift b/Sources/Turf/Geometries/Polygon.swift index edc7616a..71ccbaba 100644 --- a/Sources/Turf/Geometries/Polygon.swift +++ b/Sources/Turf/Geometries/Polygon.swift @@ -38,6 +38,30 @@ public struct Polygon: Equatable { } } +extension Polygon: Codable { + enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + } + + enum Kind: String, Codable { + case Polygon + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + _ = try container.decode(Kind.self, forKey: .kind) + let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates + self = .init(coordinates) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(Kind.Polygon, forKey: .kind) + try container.encode(coordinates.codableCoordinates, forKey: .coordinates) + } +} + extension Polygon { /// Representation of `.Polygon`s coordinates of inner rings public var innerRings: [Ring] { diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index 3ade5508..40b355fb 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -7,12 +7,6 @@ import CoreLocation A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. */ public enum Geometry { - private enum CodingKeys: String, CodingKey { - case kind = "type" - case coordinates - case geometries - } - case point(_ geometry: Point) case lineString(_ geometry: LineString) case polygon(_ geometry: Polygon) @@ -20,6 +14,15 @@ public enum Geometry { case multiLineString(_ geometry: MultiLineString) case multiPolygon(_ geometry: MultiPolygon) case geometryCollection(_ geometry: GeometryCollection) +} + + +extension Geometry: Codable { + private enum CodingKeys: String, CodingKey { + case kind = "type" + case coordinates + case geometries + } enum Kind: String, Codable, CaseIterable { case Point @@ -31,76 +34,44 @@ public enum Geometry { case GeometryCollection } - var kind: Kind { - switch self { - case .point(_): - return .Point - case .lineString(_): - return .LineString - case .polygon(_): - return .Polygon - case .multiPoint(_): - return .MultiPoint - case .multiLineString(_): - return .MultiLineString - case .multiPolygon(_): - return .MultiPolygon - case .geometryCollection(_): - return .GeometryCollection - } - } -} - - -extension Geometry: Codable { public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let kind = try container.decode(Kind.self, forKey: .kind) - - switch kind { + let kindContainer = try decoder.container(keyedBy: CodingKeys.self) + let container = try decoder.singleValueContainer() + switch try kindContainer.decode(Kind.self, forKey: .kind) { case .Point: - let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates - self = .point(.init(coordinates)) + self = .point(try container.decode(Point.self)) case .LineString: - let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates - self = .lineString(.init(coordinates)) + self = .lineString(try container.decode(LineString.self)) case .Polygon: - let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates - self = .polygon(.init(coordinates)) + self = .polygon(try container.decode(Polygon.self)) case .MultiPoint: - let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates - self = .multiPoint(.init(coordinates)) + self = .multiPoint(try container.decode(MultiPoint.self)) case .MultiLineString: - let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates - self = .multiLineString(.init(coordinates)) + self = .multiLineString(try container.decode(MultiLineString.self)) case .MultiPolygon: - let coordinates = try container.decode([[[LocationCoordinate2DCodable]]].self, forKey: .coordinates).decodedCoordinates - self = .multiPolygon(.init(coordinates)) + self = .multiPolygon(try container.decode(MultiPolygon.self)) case .GeometryCollection: - let geometries = try container.decode([Geometry].self, forKey: .geometries) - self = .geometryCollection(.init(geometries: geometries)) + self = .geometryCollection(try container.decode(GeometryCollection.self)) } } public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(kind.rawValue, forKey: .kind) - + var container = encoder.singleValueContainer() switch self { - case .point(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .lineString(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .polygon(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .multiPoint(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .multiLineString(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .multiPolygon(let representation): - try container.encode(representation.coordinates.codableCoordinates, forKey: .coordinates) - case .geometryCollection(let representation): - try container.encode(representation.geometries, forKey: .geometries) + case .point(let point): + try container.encode(point) + case .lineString(let lineString): + try container.encode(lineString) + case .polygon(let polygon): + try container.encode(polygon) + case .multiPoint(let multiPoint): + try container.encode(multiPoint) + case .multiLineString(let multiLineString): + try container.encode(multiLineString) + case .multiPolygon(let multiPolygon): + try container.encode(multiPolygon) + case .geometryCollection(let geometryCollection): + try container.encode(geometryCollection) } } } From 750cde65bd96e60c87e92a1aa2cf1af43bdb3139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 27 Sep 2021 10:57:16 -0700 Subject: [PATCH 06/16] Removed unused coding keys --- Sources/Turf/Geometry.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index 40b355fb..ecd1181e 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -20,8 +20,6 @@ public enum Geometry { extension Geometry: Codable { private enum CodingKeys: String, CodingKey { case kind = "type" - case coordinates - case geometries } enum Kind: String, Codable, CaseIterable { From 27f83f8176627b81b7878f2b0cfd4327b3671824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 27 Sep 2021 11:09:55 -0700 Subject: [PATCH 07/16] Removed ID, properties from feature collections Removed support for the id and properties foreign members on FeatureCollection objects in GeoJSON. --- Sources/Turf/FeatureCollection.swift | 5 ----- Tests/TurfTests/FeatureCollectionTests.swift | 4 +++- .../Fixtures/featurecollection.geojson | 2 +- .../simplify/in/featurecollection.geojson | 15 ++++++++------- .../simplify/out/featurecollection.geojson | 15 ++++++++------- Tests/TurfTests/TurfTests.swift | 19 ++++--------------- 6 files changed, 24 insertions(+), 36 deletions(-) diff --git a/Sources/Turf/FeatureCollection.swift b/Sources/Turf/FeatureCollection.swift index 4f21da17..ded95869 100644 --- a/Sources/Turf/FeatureCollection.swift +++ b/Sources/Turf/FeatureCollection.swift @@ -4,9 +4,7 @@ import Foundation A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. */ public struct FeatureCollection { - public var identifier: FeatureIdentifier? public var features: Array = [] - public var properties: [String : Any?]? public init(features: [Feature]) { self.features = features @@ -16,7 +14,6 @@ public struct FeatureCollection { extension FeatureCollection: Codable { private enum CodingKeys: String, CodingKey { case kind = "type" - case properties case features } @@ -28,13 +25,11 @@ extension FeatureCollection: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) _ = try container.decode(Kind.self, forKey: .kind) features = try container.decode([Feature].self, forKey: .features) - properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(Kind.FeatureCollection, forKey: .kind) try container.encode(features, forKey: .features) - try container.encodeIfPresent(properties, forKey: .properties) } } diff --git a/Tests/TurfTests/FeatureCollectionTests.swift b/Tests/TurfTests/FeatureCollectionTests.swift index bdfa69e0..3ed85de9 100644 --- a/Tests/TurfTests/FeatureCollectionTests.swift +++ b/Tests/TurfTests/FeatureCollectionTests.swift @@ -134,6 +134,8 @@ class FeatureCollectionTests: XCTestCase { guard case let .featureCollection(featureCollection) = geojson else { return XCTFail() } XCTAssertEqual(featureCollection.features.count, 4) - XCTAssertEqual(featureCollection.properties?["tolerance"] as? Double, 0.01) + for feature in featureCollection.features { + XCTAssertEqual(feature.properties?["tolerance"] as? Double, 0.01) + } } } diff --git a/Tests/TurfTests/Fixtures/featurecollection.geojson b/Tests/TurfTests/Fixtures/featurecollection.geojson index a1a8bbc2..f78b6713 100644 --- a/Tests/TurfTests/Fixtures/featurecollection.geojson +++ b/Tests/TurfTests/Fixtures/featurecollection.geojson @@ -1 +1 @@ -{"type":"FeatureCollection","properties":{"tolerance":0.01},"features":[{"type":"Feature","properties":{"id":1},"geometry":{"type":"LineString","coordinates":[[27.977542877197266,-26.17500493262446],[27.975482940673828,-26.17870225771557],[27.969818115234375,-26.177931991326645],[27.967071533203125,-26.177623883345735],[27.966899871826172,-26.1810130263384],[27.967758178710938,-26.1853263385099],[27.97290802001953,-26.1853263385099],[27.97496795654297,-26.18270756087535],[27.97840118408203,-26.1810130263384],[27.98011779785156,-26.183323749143113],[27.98011779785156,-26.18655868408986],[27.978744506835938,-26.18933141398614],[27.97496795654297,-26.19025564262006],[27.97119140625,-26.19040968001282],[27.969303131103516,-26.1899475672235],[27.96741485595703,-26.189639491012183],[27.9656982421875,-26.187945057286793],[27.965354919433594,-26.18563442612686],[27.96432495117187,-26.183015655416536]]}},{"type":"Feature","properties":{"id":2},"geometry":{"type":"Polygon","coordinates":[[[27.972049713134762,-26.199035448897074],[27.9741096496582,-26.196108920345292],[27.977371215820312,-26.197495179879635],[27.978572845458984,-26.20042167359348],[27.980976104736328,-26.200729721284862],[27.982349395751953,-26.197803235312957],[27.982177734375,-26.194414580727656],[27.982177734375,-26.19256618212382],[27.98406600952148,-26.192258112838022],[27.985267639160156,-26.191950042737417],[27.986125946044922,-26.19426054863105],[27.986984252929688,-26.196416979445644],[27.987327575683594,-26.198881422912123],[27.98715591430664,-26.201345814222698],[27.985095977783203,-26.20381015337393],[27.983036041259766,-26.20550435628209],[27.979946136474606,-26.20550435628209],[27.97719955444336,-26.20488828535003],[27.97445297241211,-26.203656133705152],[27.972564697265625,-26.201961903900578],[27.972049713134762,-26.199035448897074]]]}},{"type":"Feature","properties":{"id":3},"geometry":{"type":"Polygon","coordinates":[[[27.946643829345703,-26.170845301716803],[27.94269561767578,-26.183631842055114],[27.935657501220703,-26.183323749143113],[27.92741775512695,-26.17685360983018],[27.926902770996094,-26.171153427614488],[27.928619384765625,-26.165298896316028],[27.936859130859375,-26.161292995018652],[27.94509887695312,-26.158981835530525],[27.950420379638672,-26.161601146157146],[27.951793670654297,-26.166223315536712],[27.954025268554688,-26.173464345889972],[27.954025268554688,-26.179626570662702],[27.951278686523438,-26.187945057286793],[27.944583892822266,-26.19395248382672],[27.936172485351562,-26.194876675795218],[27.930850982666016,-26.19379845111899],[27.925701141357422,-26.190563717201886],[27.92278289794922,-26.18655868408986],[27.92072296142578,-26.180858976522302],[27.917118072509766,-26.174080583026957],[27.916603088378906,-26.16683959094609],[27.917118072509766,-26.162987816205614],[27.920207977294922,-26.162987816205614],[27.920894622802734,-26.166069246175482],[27.9217529296875,-26.17146155269785],[27.923297882080078,-26.177469829049862],[27.92673110961914,-26.184248025435295],[27.930335998535156,-26.18856121785662],[27.936687469482422,-26.18871525748988],[27.942352294921875,-26.187945057286793],[27.94647216796875,-26.184248025435295],[27.946815490722653,-26.178548204845022],[27.946643829345703,-26.170845301716803]],[[27.936859130859375,-26.16591517661071],[27.934799194335938,-26.16945872510008],[27.93497085571289,-26.173926524048102],[27.93874740600586,-26.175621161617432],[27.94149398803711,-26.17007498340995],[27.94321060180664,-26.166223315536712],[27.939090728759762,-26.164528541367826],[27.937545776367188,-26.16406632595636],[27.936859130859375,-26.16591517661071]]]}},{"type":"Feature","properties":{"id":4},"geometry":{"type":"Point","coordinates":[27.95642852783203,-26.152510345365126]}}]} +{"type":"FeatureCollection","properties":{"tolerance":0.01},"features":[{"type":"Feature","properties":{"id":1,"tolerance":0.01},"geometry":{"type":"LineString","coordinates":[[27.977542877197266,-26.17500493262446],[27.975482940673828,-26.17870225771557],[27.969818115234375,-26.177931991326645],[27.967071533203125,-26.177623883345735],[27.966899871826172,-26.1810130263384],[27.967758178710938,-26.1853263385099],[27.97290802001953,-26.1853263385099],[27.97496795654297,-26.18270756087535],[27.97840118408203,-26.1810130263384],[27.98011779785156,-26.183323749143113],[27.98011779785156,-26.18655868408986],[27.978744506835938,-26.18933141398614],[27.97496795654297,-26.19025564262006],[27.97119140625,-26.19040968001282],[27.969303131103516,-26.1899475672235],[27.96741485595703,-26.189639491012183],[27.9656982421875,-26.187945057286793],[27.965354919433594,-26.18563442612686],[27.96432495117187,-26.183015655416536]]}},{"type":"Feature","properties":{"id":2,"tolerance":0.01},"geometry":{"type":"Polygon","coordinates":[[[27.972049713134762,-26.199035448897074],[27.9741096496582,-26.196108920345292],[27.977371215820312,-26.197495179879635],[27.978572845458984,-26.20042167359348],[27.980976104736328,-26.200729721284862],[27.982349395751953,-26.197803235312957],[27.982177734375,-26.194414580727656],[27.982177734375,-26.19256618212382],[27.98406600952148,-26.192258112838022],[27.985267639160156,-26.191950042737417],[27.986125946044922,-26.19426054863105],[27.986984252929688,-26.196416979445644],[27.987327575683594,-26.198881422912123],[27.98715591430664,-26.201345814222698],[27.985095977783203,-26.20381015337393],[27.983036041259766,-26.20550435628209],[27.979946136474606,-26.20550435628209],[27.97719955444336,-26.20488828535003],[27.97445297241211,-26.203656133705152],[27.972564697265625,-26.201961903900578],[27.972049713134762,-26.199035448897074]]]}},{"type":"Feature","properties":{"id":3,"tolerance":0.01},"geometry":{"type":"Polygon","coordinates":[[[27.946643829345703,-26.170845301716803],[27.94269561767578,-26.183631842055114],[27.935657501220703,-26.183323749143113],[27.92741775512695,-26.17685360983018],[27.926902770996094,-26.171153427614488],[27.928619384765625,-26.165298896316028],[27.936859130859375,-26.161292995018652],[27.94509887695312,-26.158981835530525],[27.950420379638672,-26.161601146157146],[27.951793670654297,-26.166223315536712],[27.954025268554688,-26.173464345889972],[27.954025268554688,-26.179626570662702],[27.951278686523438,-26.187945057286793],[27.944583892822266,-26.19395248382672],[27.936172485351562,-26.194876675795218],[27.930850982666016,-26.19379845111899],[27.925701141357422,-26.190563717201886],[27.92278289794922,-26.18655868408986],[27.92072296142578,-26.180858976522302],[27.917118072509766,-26.174080583026957],[27.916603088378906,-26.16683959094609],[27.917118072509766,-26.162987816205614],[27.920207977294922,-26.162987816205614],[27.920894622802734,-26.166069246175482],[27.9217529296875,-26.17146155269785],[27.923297882080078,-26.177469829049862],[27.92673110961914,-26.184248025435295],[27.930335998535156,-26.18856121785662],[27.936687469482422,-26.18871525748988],[27.942352294921875,-26.187945057286793],[27.94647216796875,-26.184248025435295],[27.946815490722653,-26.178548204845022],[27.946643829345703,-26.170845301716803]],[[27.936859130859375,-26.16591517661071],[27.934799194335938,-26.16945872510008],[27.93497085571289,-26.173926524048102],[27.93874740600586,-26.175621161617432],[27.94149398803711,-26.17007498340995],[27.94321060180664,-26.166223315536712],[27.939090728759762,-26.164528541367826],[27.937545776367188,-26.16406632595636],[27.936859130859375,-26.16591517661071]]]}},{"type":"Feature","properties":{"id":4,"tolerance":0.01},"geometry":{"type":"Point","coordinates":[27.95642852783203,-26.152510345365126]}}]} diff --git a/Tests/TurfTests/Fixtures/simplify/in/featurecollection.geojson b/Tests/TurfTests/Fixtures/simplify/in/featurecollection.geojson index 5cf08c51..971ad1c0 100755 --- a/Tests/TurfTests/Fixtures/simplify/in/featurecollection.geojson +++ b/Tests/TurfTests/Fixtures/simplify/in/featurecollection.geojson @@ -1,13 +1,11 @@ { "type": "FeatureCollection", - "properties": { - "tolerance": 0.01 - }, "features": [ { "type": "Feature", "properties": { - "id": 1 + "id": 1, + "tolerance": 0.01 }, "geometry": { "type": "LineString", @@ -37,7 +35,8 @@ { "type": "Feature", "properties": { - "id": 2 + "id": 2, + "tolerance": 0.01 }, "geometry": { "type": "Polygon", @@ -71,7 +70,8 @@ { "type": "Feature", "properties": { - "id": 3 + "id": 3, + "tolerance": 0.01 }, "geometry": { "type": "Polygon", @@ -128,7 +128,8 @@ { "type": "Feature", "properties": { - "id": 4 + "id": 4, + "tolerance": 0.01 }, "geometry": { "type": "Point", diff --git a/Tests/TurfTests/Fixtures/simplify/out/featurecollection.geojson b/Tests/TurfTests/Fixtures/simplify/out/featurecollection.geojson index b763b50d..8049ebf8 100755 --- a/Tests/TurfTests/Fixtures/simplify/out/featurecollection.geojson +++ b/Tests/TurfTests/Fixtures/simplify/out/featurecollection.geojson @@ -1,13 +1,11 @@ { "type": "FeatureCollection", - "properties": { - "tolerance": 0.01 - }, "features": [ { "type": "Feature", "properties": { - "id": 1 + "id": 1, + "tolerance": 0.01 }, "geometry": { "type": "LineString", @@ -20,7 +18,8 @@ { "type": "Feature", "properties": { - "id": 2 + "id": 2, + "tolerance": 0.01 }, "geometry": { "type": "Polygon", @@ -37,7 +36,8 @@ { "type": "Feature", "properties": { - "id": 3 + "id": 3, + "tolerance": 0.01 }, "geometry": { "type": "Polygon", @@ -65,7 +65,8 @@ { "type": "Feature", "properties": { - "id": 4 + "id": 4, + "tolerance": 0.01 }, "geometry": { "type": "Point", diff --git a/Tests/TurfTests/TurfTests.swift b/Tests/TurfTests/TurfTests.swift index f0bcc2b1..5e63776c 100644 --- a/Tests/TurfTests/TurfTests.swift +++ b/Tests/TurfTests/TurfTests.swift @@ -166,11 +166,11 @@ class TurfTests: XCTestCase { let input = try JSONDecoder().decode(GeoJSONObject.self, from: inputData) let output = try JSONDecoder().decode(GeoJSONObject.self, from: outputData) - let properties = input.properties - let tolerance = (properties?["tolerance"] as? NSNumber)?.doubleValue ?? 0.01 - let highQuality = (properties?["highQuality"] as? NSNumber)?.boolValue ?? false - for (input, output) in zip(input.features, output.features) { + let properties = input.properties + let tolerance = (properties?["tolerance"] as? NSNumber)?.doubleValue ?? 0.01 + let highQuality = (properties?["highQuality"] as? NSNumber)?.boolValue ?? false + switch (input.geometry, output.geometry) { case (.point, .point), (.multiPoint, .multiPoint): break // nothing to simplify @@ -195,17 +195,6 @@ class TurfTests: XCTestCase { } extension GeoJSONObject { - var properties: [String: Any?]? { - switch self { - case .geometry: - return nil - case .feature(let feature): - return feature.properties - case .featureCollection(let featureCollection): - return featureCollection.properties - } - } - var features: [Feature] { switch self { case .geometry: From f2eab8ac9db827d61c7e87606acfd272263c3d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 27 Sep 2021 11:17:45 -0700 Subject: [PATCH 08/16] Conform BoundingBox to Hashable --- Sources/Turf/BoundingBox.swift | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/Turf/BoundingBox.swift b/Sources/Turf/BoundingBox.swift index 01a7b555..1e4b76f1 100644 --- a/Sources/Turf/BoundingBox.swift +++ b/Sources/Turf/BoundingBox.swift @@ -3,7 +3,9 @@ import Foundation import CoreLocation #endif -public struct BoundingBox: Codable { +public struct BoundingBox { + public var southWest: LocationCoordinate2D + public var northEast: LocationCoordinate2D public init?(from coordinates: [LocationCoordinate2D]?) { guard coordinates?.count ?? 0 > 0 else { @@ -40,9 +42,18 @@ public struct BoundingBox: Codable { && northEast.longitude >= coordinate.longitude } } - - // MARK: - Codable - +} + +extension BoundingBox: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(southWest.longitude) + hasher.combine(southWest.latitude) + hasher.combine(northEast.longitude) + hasher.combine(northEast.latitude) + } +} + +extension BoundingBox: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() try container.encode(southWest.codableCoordinates) @@ -54,9 +65,4 @@ public struct BoundingBox: Codable { southWest = try container.decode(LocationCoordinate2DCodable.self).decodedCoordinates northEast = try container.decode(LocationCoordinate2DCodable.self).decodedCoordinates } - - // MARK: - Properties - - public var southWest: LocationCoordinate2D - public var northEast: LocationCoordinate2D } From e7f0961b9167a7f1f3ddff754817a79611364127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 27 Sep 2021 18:03:34 -0700 Subject: [PATCH 09/16] JSONValue, GeoJSON Equatable conformance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a JSONValue enumeration (along with JSONArray and JSONObject type aliases) to represent JSON values with more compile-time type checking. JSONValue can be expressed by various types of literals, so developers do not need to explicitly create JSONValue when programmatically initializing a feature and its properties. When getting a property, developers now use the case let syntax instead of type casting. A rawValue property has been retained in case type casting is absolutely necessary. Deleted the custom decoding and encoding container method implementations in favor of more straightforward JSONValue Codable conformance. Deleted the Number enumeration: JSON doesn’t distinguish between the two numeric types, so any additional precision is purely cosmetic. Made the remaining GeoJSON types conform to Equatable, now that the properties object’s type is more specifically known. --- Sources/Turf/Codable.swift | 174 ---------------- Sources/Turf/Feature.swift | 12 +- Sources/Turf/FeatureCollection.swift | 2 +- Sources/Turf/FeatureIdentifier.swift | 44 ++-- Sources/Turf/GeoJSON.swift | 2 +- .../Turf/Geometries/GeometryCollection.swift | 2 +- Sources/Turf/Geometry.swift | 3 +- Sources/Turf/JSON.swift | 121 +++++++++++ Tests/TurfTests/FeatureCollectionTests.swift | 42 +++- Tests/TurfTests/GeoJSONTests.swift | 45 +++++ Tests/TurfTests/JSONTests.swift | 191 ++++++++++++++++++ Tests/TurfTests/MultiPolygonTests.swift | 2 +- Tests/TurfTests/PointTests.swift | 4 +- Tests/TurfTests/PolygonTests.swift | 4 +- Tests/TurfTests/TurfTests.swift | 15 +- Turf.xcodeproj/project.pbxproj | 18 ++ 16 files changed, 458 insertions(+), 223 deletions(-) create mode 100644 Sources/Turf/JSON.swift create mode 100644 Tests/TurfTests/JSONTests.swift diff --git a/Sources/Turf/Codable.swift b/Sources/Turf/Codable.swift index ff8eef7d..2628d73c 100644 --- a/Sources/Turf/Codable.swift +++ b/Sources/Turf/Codable.swift @@ -3,180 +3,6 @@ import Foundation import CoreLocation #endif - -struct JSONCodingKeys: CodingKey { - var stringValue: String - - init?(stringValue: String) { - self.stringValue = stringValue - } - - var intValue: Int? - - init?(intValue: Int) { - self.init(stringValue: "\(intValue)") - self.intValue = intValue - } -} - -extension KeyedDecodingContainer { - - public func decode(_ type: [String: Any?].Type, forKey key: K) throws -> [String: Any?] { - let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) - return try container.decode(type) - } - - public func decodeIfPresent(_ type: [String: Any?].Type, forKey key: K) throws -> [String: Any?]? { - guard contains(key) else { - return nil - } - return try decode(type, forKey: key) - } - - public func decode(_ type: [Any?].Type, forKey key: K) throws -> [Any?] { - var container = try self.nestedUnkeyedContainer(forKey: key) - return try container.decode(type) - } - - public func decodeIfPresent(_ type: [Any?].Type, forKey key: K) throws -> [Any?]? { - guard contains(key) else { - return nil - } - return try decode(type, forKey: key) - } - - public func decode(_ type: [String: Any?].Type) throws -> [String: Any?] { - var dictionary = [String: Any?]() - - for key in allKeys { - if let boolValue = try? decode(Bool.self, forKey: key) { - dictionary[key.stringValue] = boolValue - } else if let stringValue = try? decode(String.self, forKey: key) { - dictionary[key.stringValue] = stringValue - } else if let intValue = try? decode(Int.self, forKey: key) { - dictionary[key.stringValue] = intValue - } else if let doubleValue = try? decode(Double.self, forKey: key) { - dictionary[key.stringValue] = doubleValue - } else if let nestedDictionary = try? decode([String: Any?].self, forKey: key) { - dictionary[key.stringValue] = nestedDictionary - } else if let nestedArray = try? decode([Any?].self, forKey: key) { - dictionary[key.stringValue] = nestedArray - } else if (try? decodeNil(forKey: key)) ?? false { - dictionary[key.stringValue] = .none - } - } - return dictionary - } -} - -extension UnkeyedDecodingContainer { - - public mutating func decode(_ type: [Any?].Type) throws -> [Any?] { - var array: [Any?] = [] - while isAtEnd == false { - if let value = try? decode(Bool.self) { - array.append(value) - } else if let value = try? decode(Double.self) { - array.append(value) - } else if let value = try? decode(Int.self) { - array.append(value) - } else if let value = try? decode(String.self) { - array.append(value) - } else if let nestedDictionary = try? decode([String: Any?].self) { - array.append(nestedDictionary) - } else if let nestedArray = try? decode([Any?].self) { - array.append(nestedArray) - } else if (try? decodeNil()) ?? false { - array.append(.none) - } - } - return array - } - - public mutating func decode(_ type: [String: Any?].Type) throws -> [String: Any?] { - - let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self) - return try nestedContainer.decode(type) - } -} - -extension KeyedEncodingContainerProtocol { - - public mutating func encodeIfPresent(_ value: [String: Any?]?, forKey key: Self.Key) throws { - guard let value = value else { return } - return try encode(value, forKey: key) - } - - public mutating func encode(_ value: [String: Any?], forKey key: Key) throws { - var container = self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) - try container.encode(value) - } - - public mutating func encodeIfPresent(_ value: [Any?]?, forKey key: Self.Key) throws { - guard let value = value else { return } - return try encode(value, forKey: key) - } - - public mutating func encode(_ value: [Any?], forKey key: Key) throws { - var container = self.nestedUnkeyedContainer(forKey: key) - try container.encode(value) - } - - public mutating func encode(_ value: [String: Any?]) throws { - try value.forEach({ (key, value) in - guard let key = Key(stringValue: key) else { return } - switch value { - case let value as Bool: - try encode(value, forKey: key) - case let value as Int: - try encode(value, forKey: key) - case let value as String: - try encode(value, forKey: key) - case let value as Double: - try encode(value, forKey: key) - case let value as [String: Any?]: - try encode(value, forKey: key) - case let value as Array: - try encode(value, forKey: key) - case Optional.none: - try encodeNil(forKey: key) - default: - return - } - }) - } -} - -extension UnkeyedEncodingContainer { - public mutating func encode(_ value: [String: Any?]) throws { - var nestedContainer = self.nestedContainer(keyedBy: JSONCodingKeys.self) - try nestedContainer.encode(value) - } - - public mutating func encode(_ value: [Any?]) throws { - try value.enumerated().forEach({ (index, value) in - switch value { - case let value as Bool: - try encode(value) - case let value as Int: - try encode(value) - case let value as String: - try encode(value) - case let value as Double: - try encode(value) - case let value as [String: Any?]: - try encode(value) - case let value as Array: - try encode(value) - case Optional.none: - try encodeNil() - default: - return - } - }) - } -} - extension Ring: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/Sources/Turf/Feature.swift b/Sources/Turf/Feature.swift index 06cc832f..5fee726f 100644 --- a/Sources/Turf/Feature.swift +++ b/Sources/Turf/Feature.swift @@ -8,7 +8,7 @@ import CoreLocation */ public struct Feature { public var identifier: FeatureIdentifier? - public var properties: [String : Any?]? + public var properties: JSONObject? public var geometry: Geometry public init(geometry: Geometry) { @@ -16,6 +16,14 @@ public struct Feature { } } +extension Feature: Equatable { + public static func == (lhs: Feature, rhs: Feature) -> Bool { + return lhs.identifier == rhs.identifier && + lhs.geometry == rhs.geometry && + lhs.properties == rhs.properties + } +} + extension Feature: Codable { private enum CodingKeys: String, CodingKey { case kind = "type" @@ -32,7 +40,7 @@ extension Feature: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) _ = try container.decode(Kind.self, forKey: .kind) geometry = try container.decode(Geometry.self, forKey: .geometry) - properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties) + properties = try container.decodeIfPresent(JSONObject.self, forKey: .properties) identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier) } diff --git a/Sources/Turf/FeatureCollection.swift b/Sources/Turf/FeatureCollection.swift index ded95869..0213d6d1 100644 --- a/Sources/Turf/FeatureCollection.swift +++ b/Sources/Turf/FeatureCollection.swift @@ -3,7 +3,7 @@ import Foundation /** A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. */ -public struct FeatureCollection { +public struct FeatureCollection: Equatable { public var features: Array = [] public init(features: [Feature]) { diff --git a/Sources/Turf/FeatureIdentifier.swift b/Sources/Turf/FeatureIdentifier.swift index 6eece94e..cdf9c8ed 100644 --- a/Sources/Turf/FeatureIdentifier.swift +++ b/Sources/Turf/FeatureIdentifier.swift @@ -1,38 +1,26 @@ import Foundation -public enum Number: Equatable { - case int(Int) - case double(Double) +public enum FeatureIdentifier: Equatable { + case string(String) + case number(Double) } -extension Number: Codable { - enum CodingKeys: String, CodingKey { - case int, double - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let value = try? container.decode(Int.self) { - self = .int(value) - } else { - self = .double(try container.decode(Double.self)) - } +extension FeatureIdentifier: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self = .string(value) } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - switch self { - case .int(let value): - try container.encode(value) - case .double(let value): - try container.encode(value) - } +} + +extension FeatureIdentifier: ExpressibleByIntegerLiteral { + public init(integerLiteral value: IntegerLiteralType) { + self = .number(Double(value)) } } -public enum FeatureIdentifier { - case string(String) - case number(Number) +extension FeatureIdentifier: ExpressibleByFloatLiteral { + public init(floatLiteral value: FloatLiteralType) { + self = .number(value) + } } extension FeatureIdentifier: Codable { @@ -45,7 +33,7 @@ extension FeatureIdentifier: Codable { if let value = try? container.decode(String.self) { self = .string(value) } else { - self = .number(try container.decode(Number.self)) + self = .number(try container.decode(Double.self)) } } diff --git a/Sources/Turf/GeoJSON.swift b/Sources/Turf/GeoJSON.swift index 7106c3cb..1aecb7c5 100644 --- a/Sources/Turf/GeoJSON.swift +++ b/Sources/Turf/GeoJSON.swift @@ -7,7 +7,7 @@ import CoreLocation A [GeoJSON object](https://datatracker.ietf.org/doc/html/rfc7946#section-3) represents a Geometry, Feature, or collection of Features. */ -public enum GeoJSONObject { +public enum GeoJSONObject: Equatable { /** A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. diff --git a/Sources/Turf/Geometries/GeometryCollection.swift b/Sources/Turf/Geometries/GeometryCollection.swift index a8a19500..4b0ba990 100644 --- a/Sources/Turf/Geometries/GeometryCollection.swift +++ b/Sources/Turf/Geometries/GeometryCollection.swift @@ -4,7 +4,7 @@ import CoreLocation #endif -public struct GeometryCollection { +public struct GeometryCollection: Equatable { public var geometries: [Geometry] public init(geometries: [Geometry]) { diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index ecd1181e..19fcd109 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -6,7 +6,7 @@ import CoreLocation /** A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. */ -public enum Geometry { +public enum Geometry: Equatable { case point(_ geometry: Point) case lineString(_ geometry: LineString) case polygon(_ geometry: Polygon) @@ -16,7 +16,6 @@ public enum Geometry { case geometryCollection(_ geometry: GeometryCollection) } - extension Geometry: Codable { private enum CodingKeys: String, CodingKey { case kind = "type" diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift new file mode 100644 index 00000000..3be25ba5 --- /dev/null +++ b/Sources/Turf/JSON.swift @@ -0,0 +1,121 @@ +import Foundation + +/** + A JSON value represents an object, array, or fragment. + + This type does not represent the `null` value in JSON. Use `Optional` wherever `null` is accepted. + */ +public enum JSONValue: Equatable { + // case null would be redundant to Optional.none + case string(_ string: String) + case number(_ number: Double) + case boolean(_ bool: Bool) + case array(_ values: JSONArray) + case object(_ properties: JSONObject) + + /** + The value expressed as a Swift standard library type. + + The computed value is consistent with the return value of `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.allowFragments` specified. + */ + public var rawValue: Any { + switch self { + case let .boolean(value): + return value + case let .string(value): + return value + case let .number(value): + return value + case let .object(value): + return value.mapValues { $0?.rawValue } + case let .array(value): + return value.map { $0?.rawValue } + } + } +} + +/** + A JSON array of `JSONValue` instances. + */ +public typealias JSONArray = [JSONValue?] + +/** + A JSON object represented in memory by a dictionary with strings as keys and `JSONValue` instances as values. + */ +public typealias JSONObject = [String: JSONValue?] + +extension JSONValue: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self = .string(value) + } +} + +extension JSONValue: ExpressibleByIntegerLiteral { + public init(integerLiteral value: IntegerLiteralType) { + self = .number(Double(value)) + } +} + +extension JSONValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: FloatLiteralType) { + self = .number(value) + } +} + +extension JSONValue: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: BooleanLiteralType) { + self = .boolean(value) + } +} + +extension JSONValue: ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = JSONValue? + + public init(arrayLiteral elements: ArrayLiteralElement...) { + self = .array(elements) + } +} + +extension JSONValue: ExpressibleByDictionaryLiteral { + public typealias Key = String + public typealias Value = JSONValue? + + public init(dictionaryLiteral elements: (Key, Value)...) { + self = .object(.init(uniqueKeysWithValues: elements)) + } +} + +extension JSONValue: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let boolean = try? container.decode(Bool.self) { + self = .boolean(boolean) + } else if let number = try? container.decode(Double.self) { + self = .number(number) + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let object = try? container.decode(JSONObject.self) { + self = .object(object) + } else if let array = try? container.decode(JSONArray.self) { + self = .array(array) + } else { + throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to decode as a JSONValue.")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .boolean(value): + try container.encode(value) + case let .string(value): + try container.encode(value) + case let .number(value): + try container.encode(value) + case let .object(value): + try container.encode(value) + case let .array(value): + try container.encode(value) + } + } +} diff --git a/Tests/TurfTests/FeatureCollectionTests.swift b/Tests/TurfTests/FeatureCollectionTests.swift index 3ed85de9..34371b00 100644 --- a/Tests/TurfTests/FeatureCollectionTests.swift +++ b/Tests/TurfTests/FeatureCollectionTests.swift @@ -22,7 +22,11 @@ class FeatureCollectionTests: XCTestCase { return } XCTAssert(lineStringCoordinates.coordinates.count == 19) - XCTAssert(lineStringFeature.properties!["id"] as! Int == 1) + if case let .number(number) = lineStringFeature.properties?["id"] { + XCTAssertEqual(number, 1) + } else { + XCTFail() + } XCTAssert(lineStringCoordinates.coordinates.first!.latitude == -26.17500493262446) XCTAssert(lineStringCoordinates.coordinates.first!.longitude == 27.977542877197266) @@ -31,7 +35,11 @@ class FeatureCollectionTests: XCTestCase { XCTFail() return } - XCTAssert(polygonFeature.properties!["id"] as! Int == 2) + if case let .number(number) = polygonFeature.properties?["id"] { + XCTAssertEqual(number, 2) + } else { + XCTFail() + } XCTAssert(polygonCoordinates.coordinates[0].count == 21) XCTAssert(polygonCoordinates.coordinates[0].first!.latitude == -26.199035448897074) XCTAssert(polygonCoordinates.coordinates[0].first!.longitude == 27.972049713134762) @@ -41,7 +49,11 @@ class FeatureCollectionTests: XCTestCase { XCTFail() return } - XCTAssert(pointFeature.properties!["id"] as! Int == 4) + if case let .number(number) = pointFeature.properties?["id"] { + XCTAssertEqual(number, 4) + } else { + XCTFail() + } XCTAssert(pointCoordinates.coordinates.latitude == -26.152510345365126) XCTAssert(pointCoordinates.coordinates.longitude == 27.95642852783203) @@ -60,7 +72,11 @@ class FeatureCollectionTests: XCTestCase { return } XCTAssert(decodedLineStringCoordinates.coordinates.count == 19) - XCTAssert(decodedLineStringFeature.properties!["id"] as! Int == 1) + if case let .number(number) = decodedLineStringFeature.properties?["id"] { + XCTAssertEqual(number, 1) + } else { + XCTFail() + } XCTAssert(decodedLineStringCoordinates.coordinates.first!.latitude == -26.17500493262446) XCTAssert(decodedLineStringCoordinates.coordinates.first!.longitude == 27.977542877197266) @@ -69,7 +85,11 @@ class FeatureCollectionTests: XCTestCase { XCTFail() return } - XCTAssert(decodedPolygonFeature.properties!["id"] as! Int == 2) + if case let .number(number) = decodedPolygonFeature.properties?["id"] { + XCTAssertEqual(number, 2) + } else { + XCTFail() + } XCTAssert(decodedPolygonCoordinates.coordinates[0].count == 21) XCTAssert(decodedPolygonCoordinates.coordinates[0].first!.latitude == -26.199035448897074) XCTAssert(decodedPolygonCoordinates.coordinates[0].first!.longitude == 27.972049713134762) @@ -79,7 +99,11 @@ class FeatureCollectionTests: XCTestCase { XCTFail() return } - XCTAssert(decodedPointFeature.properties!["id"] as! Int == 4) + if case let .number(number) = decodedPointFeature.properties?["id"] { + XCTAssertEqual(number, 4) + } else { + XCTFail() + } XCTAssert(decodedPointCoordinates.coordinates.latitude == -26.152510345365126) XCTAssert(decodedPointCoordinates.coordinates.longitude == 27.95642852783203) } @@ -135,7 +159,11 @@ class FeatureCollectionTests: XCTestCase { guard case let .featureCollection(featureCollection) = geojson else { return XCTFail() } XCTAssertEqual(featureCollection.features.count, 4) for feature in featureCollection.features { - XCTAssertEqual(feature.properties?["tolerance"] as? Double, 0.01) + if case let .number(tolerance) = feature.properties?["tolerance"] { + XCTAssertEqual(tolerance, 0.01) + } else { + XCTFail() + } } } } diff --git a/Tests/TurfTests/GeoJSONTests.swift b/Tests/TurfTests/GeoJSONTests.swift index 56ea3f13..1371e923 100644 --- a/Tests/TurfTests/GeoJSONTests.swift +++ b/Tests/TurfTests/GeoJSONTests.swift @@ -115,4 +115,49 @@ class GeoJSONTests: XCTestCase { guard case let .multiPolygon(multiPolygon) = feature.geometry else { return XCTFail() } XCTAssertEqual(multiPolygon.coordinates, coordinates) } + + func testFeatureIdentifierLiterals() { + if case let FeatureIdentifier.string(string) = "Jason" { + XCTAssertEqual(string, "Jason") + } else { + XCTFail() + } + + if case let FeatureIdentifier.number(number) = 42 { + XCTAssertEqual(number, 42) + } else { + XCTFail() + } + + if case let FeatureIdentifier.number(number) = 3.1415 { + XCTAssertEqual(number, 3.1415) + } else { + XCTFail() + } + } + + func testPropertiesCoding() { + let coordinate = LocationCoordinate2D(latitude: 10, longitude: 30) + var feature = Feature(geometry: .point(.init(coordinate))) + feature.properties = [ + "string": "Jason", + "integer": 42, + "float": 3.1415, + "false": false, + "true": true, + "nil": nil, + "array": [], + "dictionary": [:], + ] + + var encodedFeature: Data? + XCTAssertNoThrow(encodedFeature = try JSONEncoder().encode(feature)) + guard let encodedData = encodedFeature else { return XCTFail() } + + var decodedFeature: Feature? + XCTAssertNoThrow(decodedFeature = try JSONDecoder().decode(Feature.self, from: encodedData)) + XCTAssertNotNil(decodedFeature) + + XCTAssertEqual(decodedFeature, feature) + } } diff --git a/Tests/TurfTests/JSONTests.swift b/Tests/TurfTests/JSONTests.swift new file mode 100644 index 00000000..2933ded0 --- /dev/null +++ b/Tests/TurfTests/JSONTests.swift @@ -0,0 +1,191 @@ +import XCTest +import Turf + +class JSONTests: XCTestCase { + func testLiterals() throws { + if case let JSONValue.string(string) = "Jason" { + XCTAssertEqual(string, "Jason") + } else { + XCTFail() + } + + if case let JSONValue.number(number) = 42 { + XCTAssertEqual(number, 42) + } else { + XCTFail() + } + + if case let JSONValue.number(number) = 3.1415 { + XCTAssertEqual(number, 3.1415) + } else { + XCTFail() + } + + if case let JSONValue.boolean(boolean) = false { + XCTAssertFalse(boolean) + } else { + XCTFail() + } + + if case let JSONValue.boolean(boolean) = true { + XCTAssertTrue(boolean) + } else { + XCTFail() + } + + if case let JSONValue.array(array) = ["Jason", 42, 3.1415, false, true, nil, [], [:]], + array.count == 8 { + if case let .string(string) = array[0] { + XCTAssertEqual(string, "Jason") + } else { + XCTFail() + } + + if case let .number(number) = array[1] { + XCTAssertEqual(number, 42) + } else { + XCTFail() + } + + if case let .number(number) = array[2] { + XCTAssertEqual(number, 3.1415) + } else { + XCTFail() + } + + if case let .boolean(boolean) = array[3] { + XCTAssertFalse(boolean) + } else { + XCTFail() + } + + if case let .boolean(boolean) = array[4] { + XCTAssertTrue(boolean) + } else { + XCTFail() + } + + if case .none = array[5] {} else { + XCTFail() + } + + if case let .array(array) = array[6] { + XCTAssertEqual(array, []) + } else { + XCTFail() + } + + if case let .object(object) = array[7] { + XCTAssertEqual(object, [:]) + } else { + XCTFail() + } + } else { + XCTFail() + } + + if case let JSONValue.object(object) = [ + "string": "Jason", + "integer": 42, + "float": 3.1415, + "false": false, + "true": true, + "nil": nil, + "array": [], + "dictionary": [:], + ], object.count == 8 { + if case let .string(string) = object["string"] { + XCTAssertEqual(string, "Jason") + } else { + XCTFail() + } + + if case let .number(number) = object["integer"] { + XCTAssertEqual(number, 42) + } else { + XCTFail() + } + + if case let .number(number) = object["float"] { + XCTAssertEqual(number, 3.1415) + } else { + XCTFail() + } + + if case let .boolean(boolean) = object["false"] { + XCTAssertFalse(boolean) + } else { + XCTFail() + } + + if case let .boolean(boolean) = object["true"] { + XCTAssertTrue(boolean) + } else { + XCTFail() + } + + // The optional from dictionary subscripting isn’t unwrapped automatically if matching Optional.none. + if case .some(.none) = object["nil"] {} else { + XCTFail() + } + + if case let .array(array) = object["array"] { + XCTAssertEqual(array, []) + } else { + XCTFail() + } + + if case let .object(object) = object["dictionary"] { + XCTAssertEqual(object, [:]) + } else { + XCTFail() + } + } else { + XCTFail() + } + } + + func testCoding() { + let rawArray = ["Jason", 42, 3.1415, false, true, nil, [], [:]] as [Any?] + let serializedArray = try! JSONSerialization.data(withJSONObject: rawArray, options: []) + var decodedValue: JSONValue? + XCTAssertNoThrow(decodedValue = try JSONDecoder().decode(JSONValue.self, from: serializedArray)) + XCTAssertNotNil(decodedValue) + + if case let .array(array) = decodedValue, + case let .string(string) = array[0] { + XCTAssertEqual(string, rawArray[0] as? String) + } else { + XCTFail() + } + + XCTAssertEqual(decodedValue?.rawValue as? NSArray, rawArray as NSArray) + + XCTAssertNoThrow(try JSONEncoder().encode(decodedValue)) + + let rawObject = [ + "string": "Jason", + "integer": 42, + "float": 3.1415, + "false": false, + "true": true, + "nil": nil, + "array": [], + "dictionary": [:], + ] as [String: Any?] + let serializedObject = try! JSONSerialization.data(withJSONObject: rawObject, options: []) + XCTAssertNoThrow(decodedValue = try JSONDecoder().decode(JSONValue.self, from: serializedObject)) + XCTAssertNotNil(decodedValue) + + if case let .object(object) = decodedValue, + case let .string(string) = object["string"] { + XCTAssertEqual(string, rawObject["string"] as? String) + } else { + XCTFail() + } + + XCTAssertEqual(decodedValue?.rawValue as? NSDictionary, rawObject as NSDictionary) + + XCTAssertNoThrow(try JSONEncoder().encode(decodedValue)) + } +} diff --git a/Tests/TurfTests/MultiPolygonTests.swift b/Tests/TurfTests/MultiPolygonTests.swift index b93fc869..5facc1b3 100644 --- a/Tests/TurfTests/MultiPolygonTests.swift +++ b/Tests/TurfTests/MultiPolygonTests.swift @@ -73,7 +73,7 @@ class MultiPolygonTests: XCTestCase { let multiPolygon = Geometry.multiPolygon(.init(coordinates)) var multiPolygonFeature = Feature(geometry: multiPolygon) - multiPolygonFeature.identifier = FeatureIdentifier.string("uniqueIdentifier") + multiPolygonFeature.identifier = "uniqueIdentifier" multiPolygonFeature.properties = ["some": "var"] let encodedData = try! JSONEncoder().encode(multiPolygonFeature) diff --git a/Tests/TurfTests/PointTests.swift b/Tests/TurfTests/PointTests.swift index 3689a0c1..865d47f8 100644 --- a/Tests/TurfTests/PointTests.swift +++ b/Tests/TurfTests/PointTests.swift @@ -17,8 +17,8 @@ class PointTests: XCTestCase { return } XCTAssertEqual(point.coordinates, coordinate) - if case let .number(.int(int)) = feature.identifier { - XCTAssertEqual(int, 1) + if case let .number(number) = feature.identifier { + XCTAssertEqual(number, 1) } else { XCTFail() } diff --git a/Tests/TurfTests/PolygonTests.swift b/Tests/TurfTests/PolygonTests.swift index 5c3328a9..737cee84 100644 --- a/Tests/TurfTests/PolygonTests.swift +++ b/Tests/TurfTests/PolygonTests.swift @@ -13,8 +13,8 @@ class PolygonTests: XCTestCase { let firstCoordinate = LocationCoordinate2D(latitude: 37.00255267215955, longitude: -109.05029296875) let lastCoordinate = LocationCoordinate2D(latitude: 40.6306300839918, longitude: -108.56689453125) - if case let .number(.double(double)) = geojson.identifier { - XCTAssertEqual(double, 1.01) + if case let .number(number) = geojson.identifier { + XCTAssertEqual(number, 1.01) } else { XCTFail() } diff --git a/Tests/TurfTests/TurfTests.swift b/Tests/TurfTests/TurfTests.swift index 5e63776c..260005eb 100644 --- a/Tests/TurfTests/TurfTests.swift +++ b/Tests/TurfTests/TurfTests.swift @@ -168,8 +168,19 @@ class TurfTests: XCTestCase { for (input, output) in zip(input.features, output.features) { let properties = input.properties - let tolerance = (properties?["tolerance"] as? NSNumber)?.doubleValue ?? 0.01 - let highQuality = (properties?["highQuality"] as? NSNumber)?.boolValue ?? false + let tolerance: Double + if case let .number(number) = properties?["tolerance"] { + tolerance = number + } else { + tolerance = 0.01 + } + + let highQuality: Bool + if case let .boolean(boolean) = properties?["highQuality"] { + highQuality = boolean + } else { + highQuality = false + } switch (input.geometry, output.geometry) { case (.point, .point), (.multiPoint, .multiPoint): diff --git a/Turf.xcodeproj/project.pbxproj b/Turf.xcodeproj/project.pbxproj index 4f4dfdb5..13671532 100644 --- a/Turf.xcodeproj/project.pbxproj +++ b/Turf.xcodeproj/project.pbxproj @@ -169,6 +169,13 @@ DA972D7A26FEB9DC009F5615 /* simplify in Resources */ = {isa = PBXBuildFile; fileRef = DA972D7926FEB9DC009F5615 /* simplify */; }; DA972D7B26FEB9DD009F5615 /* simplify in Resources */ = {isa = PBXBuildFile; fileRef = DA972D7926FEB9DC009F5615 /* simplify */; }; DA972D7C26FEB9DD009F5615 /* simplify in Resources */ = {isa = PBXBuildFile; fileRef = DA972D7926FEB9DC009F5615 /* simplify */; }; + DA972D932702763B009F5615 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972D922702763B009F5615 /* JSON.swift */; }; + DA972D942702763C009F5615 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972D922702763B009F5615 /* JSON.swift */; }; + DA972D952702763C009F5615 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972D922702763B009F5615 /* JSON.swift */; }; + DA972D962702763C009F5615 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972D922702763B009F5615 /* JSON.swift */; }; + DA972DA6270289C5009F5615 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972DA5270289C5009F5615 /* JSONTests.swift */; }; + DA972DA7270289C5009F5615 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972DA5270289C5009F5615 /* JSONTests.swift */; }; + DA972DA8270289C5009F5615 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA972DA5270289C5009F5615 /* JSONTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -252,6 +259,8 @@ DA39EB6D20101F99004D87F7 /* TurfTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TurfTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DA94249B2010283900CDB4E6 /* Turf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Turf.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DA972D7926FEB9DC009F5615 /* simplify */ = {isa = PBXFileReference; lastKnownFileType = folder; path = simplify; sourceTree = ""; }; + DA972D922702763B009F5615 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; + DA972DA5270289C5009F5615 /* JSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -338,6 +347,7 @@ 3502F790201F507500399EFE /* GeoJSON.swift */, 35ECAF2E20974A1800DC3BC3 /* Geometry.swift */, 2B74CD90248538E500EE6693 /* Geometries */, + DA972D922702763B009F5615 /* JSON.swift */, 35B56E6920DAF3C500C4923D /* RadianCoordinate2D.swift */, 35B56E6E20DAF41F00C4923D /* Ring.swift */, 3A40B38126E48262004457C4 /* Simplifier.swift */, @@ -362,6 +372,7 @@ 35B56E9620DAF82F00C4923D /* PointTests.swift */, 35B56E9E20DAF88C00C4923D /* PolygonTests.swift */, 2B5FAC0024067051008A285F /* GeometryCollectionTests.swift */, + DA972DA5270289C5009F5615 /* JSONTests.swift */, 3547ECF9200C3C82009DA062 /* TurfTests.swift */, 3547ECFA200C3C82009DA062 /* Fixtures */, 11E726F926247D3900C1890B /* LocationCoordinate2DTests.swift */, @@ -761,6 +772,7 @@ 35ECAF3020974A2000DC3BC3 /* Geometry.swift in Sources */, 35B56E7520DAF47D00C4923D /* Polygon.swift in Sources */, 7AA969AD21B98F8F009C57FE /* Spline.swift in Sources */, + DA972D942702763C009F5615 /* JSON.swift in Sources */, CE2EB999214C247100915A30 /* BoundingBox.swift in Sources */, 2B74CD792485372800EE6693 /* Point.swift in Sources */, 35B56E7020DAF41F00C4923D /* Ring.swift in Sources */, @@ -777,6 +789,7 @@ 35B56EAC20DAF8CB00C4923D /* MultiPolygonTests.swift in Sources */, 11E7270D2624840900C1890B /* RadianCoordinate2DTests.swift in Sources */, 11E726FB26247D3900C1890B /* LocationCoordinate2DTests.swift in Sources */, + DA972DA7270289C5009F5615 /* JSONTests.swift in Sources */, 35B56E9820DAF82F00C4923D /* PointTests.swift in Sources */, 2B5FAC0224067051008A285F /* GeometryCollectionTests.swift in Sources */, CE7F8165215182FF00A9D221 /* BoundingBoxTests.swift in Sources */, @@ -809,6 +822,7 @@ 35ECAF2F20974A1800DC3BC3 /* Geometry.swift in Sources */, 35B56E7420DAF47D00C4923D /* Polygon.swift in Sources */, 7AA969AC21B98F8F009C57FE /* Spline.swift in Sources */, + DA972D932702763B009F5615 /* JSON.swift in Sources */, CE2EB998214C246A00915A30 /* BoundingBox.swift in Sources */, 2B74CD782485372800EE6693 /* Point.swift in Sources */, 35B56E6F20DAF41F00C4923D /* Ring.swift in Sources */, @@ -825,6 +839,7 @@ 2B5FAC122406BADF008A285F /* GeoJSONTests.swift in Sources */, 11E7270C2624840900C1890B /* RadianCoordinate2DTests.swift in Sources */, 11E726FA26247D3900C1890B /* LocationCoordinate2DTests.swift in Sources */, + DA972DA6270289C5009F5615 /* JSONTests.swift in Sources */, 35B56EAB20DAF8CB00C4923D /* MultiPolygonTests.swift in Sources */, 35B56E9720DAF82F00C4923D /* PointTests.swift in Sources */, 2B5FAC0124067051008A285F /* GeometryCollectionTests.swift in Sources */, @@ -857,6 +872,7 @@ 35ECAF3120974A2100DC3BC3 /* Geometry.swift in Sources */, 35B56E7620DAF47D00C4923D /* Polygon.swift in Sources */, 7AA969AE21B98F8F009C57FE /* Spline.swift in Sources */, + DA972D952702763C009F5615 /* JSON.swift in Sources */, CE2EB99A214C247100915A30 /* BoundingBox.swift in Sources */, 2B74CD7A2485372800EE6693 /* Point.swift in Sources */, 35B56E7120DAF41F00C4923D /* Ring.swift in Sources */, @@ -873,6 +889,7 @@ 35B56EAD20DAF8CB00C4923D /* MultiPolygonTests.swift in Sources */, 11E7270E2624840900C1890B /* RadianCoordinate2DTests.swift in Sources */, 11E7270326247D4000C1890B /* LocationCoordinate2DTests.swift in Sources */, + DA972DA8270289C5009F5615 /* JSONTests.swift in Sources */, 35B56E9920DAF82F00C4923D /* PointTests.swift in Sources */, 2B5FAC0324067051008A285F /* GeometryCollectionTests.swift in Sources */, CE7F8166215182FF00A9D221 /* BoundingBoxTests.swift in Sources */, @@ -905,6 +922,7 @@ 35ECAF3220974A2100DC3BC3 /* Geometry.swift in Sources */, 35B56E7720DAF47D00C4923D /* Polygon.swift in Sources */, 7AA969AF21B98F8F009C57FE /* Spline.swift in Sources */, + DA972D962702763C009F5615 /* JSON.swift in Sources */, CE2EB99B214C247200915A30 /* BoundingBox.swift in Sources */, 2B74CD7B2485372800EE6693 /* Point.swift in Sources */, 35B56E7220DAF41F00C4923D /* Ring.swift in Sources */, From 2b9efa75b173aad5726dd4cbfb0ce3626dd1d8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 27 Sep 2021 19:24:13 -0700 Subject: [PATCH 10/16] Added JSONValue initializers --- Sources/Turf/FeatureIdentifier.swift | 16 +++++++++++-- Sources/Turf/JSON.swift | 36 +++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/Sources/Turf/FeatureIdentifier.swift b/Sources/Turf/FeatureIdentifier.swift index cdf9c8ed..f213e502 100644 --- a/Sources/Turf/FeatureIdentifier.swift +++ b/Sources/Turf/FeatureIdentifier.swift @@ -3,17 +3,29 @@ import Foundation public enum FeatureIdentifier: Equatable { case string(String) case number(Double) + + public init(_ string: String) { + self = .string(string) + } + + public init(_ number: Source) where Source: BinaryInteger { + self = .number(Double(number)) + } + + public init(_ number: Source) where Source: BinaryFloatingPoint { + self = .number(Double(number)) + } } extension FeatureIdentifier: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { - self = .string(value) + self = .init(value) } } extension FeatureIdentifier: ExpressibleByIntegerLiteral { public init(integerLiteral value: IntegerLiteralType) { - self = .number(Double(value)) + self = .init(value) } } diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift index 3be25ba5..5f0cc190 100644 --- a/Sources/Turf/JSON.swift +++ b/Sources/Turf/JSON.swift @@ -13,6 +13,30 @@ public enum JSONValue: Equatable { case array(_ values: JSONArray) case object(_ properties: JSONObject) + public init(_ string: String) { + self = .string(string) + } + + public init(_ number: Source) where Source: BinaryInteger { + self = .number(Double(number)) + } + + public init(_ number: Source) where Source: BinaryFloatingPoint { + self = .number(Double(number)) + } + + public init(_ bool: Bool) { + self = .boolean(bool) + } + + public init(_ values: JSONArray) { + self = .array(values) + } + + public init(_ properties: JSONObject) { + self = .object(properties) + } + /** The value expressed as a Swift standard library type. @@ -46,25 +70,25 @@ public typealias JSONObject = [String: JSONValue?] extension JSONValue: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { - self = .string(value) + self = .init(value) } } extension JSONValue: ExpressibleByIntegerLiteral { public init(integerLiteral value: IntegerLiteralType) { - self = .number(Double(value)) + self = .init(value) } } extension JSONValue: ExpressibleByFloatLiteral { public init(floatLiteral value: FloatLiteralType) { - self = .number(value) + self = .init(value) } } extension JSONValue: ExpressibleByBooleanLiteral { public init(booleanLiteral value: BooleanLiteralType) { - self = .boolean(value) + self = .init(value) } } @@ -72,7 +96,7 @@ extension JSONValue: ExpressibleByArrayLiteral { public typealias ArrayLiteralElement = JSONValue? public init(arrayLiteral elements: ArrayLiteralElement...) { - self = .array(elements) + self = .init(elements) } } @@ -81,7 +105,7 @@ extension JSONValue: ExpressibleByDictionaryLiteral { public typealias Value = JSONValue? public init(dictionaryLiteral elements: (Key, Value)...) { - self = .object(.init(uniqueKeysWithValues: elements)) + self = .init(.init(uniqueKeysWithValues: elements)) } } From 51c1981bb5207e64c27b5893d028f06819916006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 28 Sep 2021 01:01:24 -0700 Subject: [PATCH 11/16] Conform JSONValue to RawRepresentable Make it easier to convert JSONSerialization-style objects to JSONValue and back. --- Sources/Turf/JSON.swift | 54 ++++++++++++++++++++++++++++----- Tests/TurfTests/JSONTests.swift | 30 ++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift index 5f0cc190..e3eba6a2 100644 --- a/Sources/Turf/JSON.swift +++ b/Sources/Turf/JSON.swift @@ -5,7 +5,9 @@ import Foundation This type does not represent the `null` value in JSON. Use `Optional` wherever `null` is accepted. */ -public enum JSONValue: Equatable { +public enum JSONValue: Equatable, RawRepresentable { + public typealias RawValue = Any + // case null would be redundant to Optional.none case string(_ string: String) case number(_ number: Double) @@ -37,11 +39,23 @@ public enum JSONValue: Equatable { self = .object(properties) } - /** - The value expressed as a Swift standard library type. - - The computed value is consistent with the return value of `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.allowFragments` specified. - */ + public init?(rawValue: Any) { + // Like `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.fragmentsAllowed` specified. + if let bool = rawValue as? Bool { + self = .boolean(bool) + } else if let string = rawValue as? String { + self = .string(string) + } else if let number = rawValue as? NSNumber { + self = .number(number.doubleValue) + } else if let array = rawValue as? JSONArray.RawValue { + self = .array(.init(rawValue: array)) + } else if let object = rawValue as? JSONObject.RawValue { + self = .object(.init(rawValue: object)) + } else { + return nil + } + } + public var rawValue: Any { switch self { case let .boolean(value): @@ -51,9 +65,9 @@ public enum JSONValue: Equatable { case let .number(value): return value case let .object(value): - return value.mapValues { $0?.rawValue } + return value.rawValue case let .array(value): - return value.map { $0?.rawValue } + return value.rawValue } } } @@ -63,11 +77,35 @@ public enum JSONValue: Equatable { */ public typealias JSONArray = [JSONValue?] +extension JSONArray: RawRepresentable { + public typealias RawValue = [Any?] + + public init(rawValue values: RawValue) { + self = values.map(JSONValue.init(rawValue:)) + } + + public var rawValue: RawValue { + return map { $0?.rawValue } + } +} + /** A JSON object represented in memory by a dictionary with strings as keys and `JSONValue` instances as values. */ public typealias JSONObject = [String: JSONValue?] +extension JSONObject: RawRepresentable { + public typealias RawValue = [String: Any?] + + public init(rawValue: RawValue) { + self = rawValue.mapValues { $0.flatMap(JSONValue.init(rawValue:)) } + } + + public var rawValue: RawValue { + return mapValues { $0?.rawValue } + } +} + extension JSONValue: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { self = .init(value) diff --git a/Tests/TurfTests/JSONTests.swift b/Tests/TurfTests/JSONTests.swift index 2933ded0..371e7d6b 100644 --- a/Tests/TurfTests/JSONTests.swift +++ b/Tests/TurfTests/JSONTests.swift @@ -2,6 +2,36 @@ import XCTest import Turf class JSONTests: XCTestCase { + func testRawValues() { + XCTAssertEqual(JSONValue(rawValue: "Jason" as NSString), .string("Jason")) + XCTAssertEqual(JSONValue(rawValue: 42 as NSNumber), .number(42)) + XCTAssertEqual(JSONValue(rawValue: 3.1415 as NSNumber), .number(3.1415)) + XCTAssertEqual(JSONValue(rawValue: false as NSNumber), .boolean(false)) + XCTAssertEqual(JSONValue(rawValue: true as NSNumber), .boolean(true)) + XCTAssertEqual(JSONValue(rawValue: ["Jason", 42, 3.1415, false, true, nil, [], [:]] as NSArray), + .array(["Jason", 42, 3.1415, false, true, nil, [], [:]])) + XCTAssertEqual(JSONValue(rawValue: [ + "string": "Jason", + "integer": 42, + "float": 3.1415, + "false": false, + "true": true, + "nil": nil, + "array": [], + "dictionary": [:], + ] as NSDictionary), + .object([ + "string": "Jason", + "integer": 42, + "float": 3.1415, + "false": false, + "true": true, + "nil": nil, + "array": [], + "dictionary": [:], + ])) + } + func testLiterals() throws { if case let JSONValue.string(string) = "Jason" { XCTAssertEqual(string, "Jason") From dd19d887de4ee08ba0152e26dcbc102fa3ce4e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 28 Sep 2021 09:31:08 -0700 Subject: [PATCH 12/16] A feature can have no geometry The GeoJSON specification allows a Feature object to have no geometry at all. --- Sources/Turf/Feature.swift | 8 ++++---- Tests/TurfTests/GeoJSONTests.swift | 22 ++++++++++++++++++++++ Tests/TurfTests/PointTests.swift | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Sources/Turf/Feature.swift b/Sources/Turf/Feature.swift index 5fee726f..d8f6a89d 100644 --- a/Sources/Turf/Feature.swift +++ b/Sources/Turf/Feature.swift @@ -9,9 +9,9 @@ import CoreLocation public struct Feature { public var identifier: FeatureIdentifier? public var properties: JSONObject? - public var geometry: Geometry + public var geometry: Geometry? - public init(geometry: Geometry) { + public init(geometry: Geometry?) { self.geometry = geometry } } @@ -39,7 +39,7 @@ extension Feature: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) _ = try container.decode(Kind.self, forKey: .kind) - geometry = try container.decode(Geometry.self, forKey: .geometry) + geometry = try container.decodeIfPresent(Geometry.self, forKey: .geometry) properties = try container.decodeIfPresent(JSONObject.self, forKey: .properties) identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier) } @@ -47,7 +47,7 @@ extension Feature: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(Kind.Feature, forKey: .kind) - try container.encodeIfPresent(geometry, forKey: .geometry) + try container.encode(geometry, forKey: .geometry) try container.encodeIfPresent(properties, forKey: .properties) try container.encodeIfPresent(identifier, forKey: .identifier) } diff --git a/Tests/TurfTests/GeoJSONTests.swift b/Tests/TurfTests/GeoJSONTests.swift index 1371e923..a5214434 100644 --- a/Tests/TurfTests/GeoJSONTests.swift +++ b/Tests/TurfTests/GeoJSONTests.swift @@ -136,6 +136,28 @@ class GeoJSONTests: XCTestCase { } } + func testFeatureCoding() { + let feature = Feature(geometry: nil) + XCTAssertNil(feature.geometry) + + var encodedFeature: Data? + XCTAssertNoThrow(encodedFeature = try JSONEncoder().encode(feature)) + guard let encodedData = encodedFeature else { return XCTFail() } + + var deserializedFeature: JSONObject? + XCTAssertNoThrow(deserializedFeature = try JSONSerialization.jsonObject(with: encodedData, options: []) as? JSONObject) + if let geometry = deserializedFeature?["geometry"] { + XCTAssertNil(geometry) + } + + var decodedFeature: Feature? + XCTAssertNoThrow(decodedFeature = try JSONDecoder().decode(Feature.self, from: encodedData)) + XCTAssertNotNil(decodedFeature) + + XCTAssertNil(feature.geometry) + XCTAssertEqual(decodedFeature, feature) + } + func testPropertiesCoding() { let coordinate = LocationCoordinate2D(latitude: 10, longitude: 30) var feature = Feature(geometry: .point(.init(coordinate))) diff --git a/Tests/TurfTests/PointTests.swift b/Tests/TurfTests/PointTests.swift index 865d47f8..007f93d2 100644 --- a/Tests/TurfTests/PointTests.swift +++ b/Tests/TurfTests/PointTests.swift @@ -51,7 +51,7 @@ class PointTests: XCTestCase { } var encodedData: Data? - XCTAssertNoThrow(encodedData = try JSONEncoder().encode(GeoJSONObject.geometry(feature.geometry))) + XCTAssertNoThrow(encodedData = try JSONEncoder().encode(GeoJSONObject.geometry(XCTUnwrap(feature.geometry)))) XCTAssertNotNil(encodedData) var decoded: GeoJSONObject? From 2a317e42015f6052f4534a417211dd1766abac39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 28 Sep 2021 10:23:13 -0700 Subject: [PATCH 13/16] Synthesize Feature Equatable conformance --- Sources/Turf/Feature.swift | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Sources/Turf/Feature.swift b/Sources/Turf/Feature.swift index d8f6a89d..a5236497 100644 --- a/Sources/Turf/Feature.swift +++ b/Sources/Turf/Feature.swift @@ -6,7 +6,7 @@ import CoreLocation /** A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing. */ -public struct Feature { +public struct Feature: Equatable { public var identifier: FeatureIdentifier? public var properties: JSONObject? public var geometry: Geometry? @@ -16,14 +16,6 @@ public struct Feature { } } -extension Feature: Equatable { - public static func == (lhs: Feature, rhs: Feature) -> Bool { - return lhs.identifier == rhs.identifier && - lhs.geometry == rhs.geometry && - lhs.properties == rhs.properties - } -} - extension Feature: Codable { private enum CodingKeys: String, CodingKey { case kind = "type" From 7a9ea2598a9a7740daf9ba51a98e5b56ed43741b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 28 Sep 2021 11:31:04 -0700 Subject: [PATCH 14/16] Made raw value initializers failable --- Sources/Turf/JSON.swift | 14 ++++++++------ Tests/TurfTests/JSONTests.swift | 12 ++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift index e3eba6a2..a0dd38cf 100644 --- a/Sources/Turf/JSON.swift +++ b/Sources/Turf/JSON.swift @@ -47,10 +47,12 @@ public enum JSONValue: Equatable, RawRepresentable { self = .string(string) } else if let number = rawValue as? NSNumber { self = .number(number.doubleValue) - } else if let array = rawValue as? JSONArray.RawValue { - self = .array(.init(rawValue: array)) - } else if let object = rawValue as? JSONObject.RawValue { - self = .object(.init(rawValue: object)) + } else if let rawArray = rawValue as? JSONArray.RawValue, + let array = JSONArray(rawValue: rawArray) { + self = .array(array) + } else if let rawObject = rawValue as? JSONObject.RawValue, + let object = JSONObject(rawValue: rawObject) { + self = .object(object) } else { return nil } @@ -80,7 +82,7 @@ public typealias JSONArray = [JSONValue?] extension JSONArray: RawRepresentable { public typealias RawValue = [Any?] - public init(rawValue values: RawValue) { + public init?(rawValue values: RawValue) { self = values.map(JSONValue.init(rawValue:)) } @@ -97,7 +99,7 @@ public typealias JSONObject = [String: JSONValue?] extension JSONObject: RawRepresentable { public typealias RawValue = [String: Any?] - public init(rawValue: RawValue) { + public init?(rawValue: RawValue) { self = rawValue.mapValues { $0.flatMap(JSONValue.init(rawValue:)) } } diff --git a/Tests/TurfTests/JSONTests.swift b/Tests/TurfTests/JSONTests.swift index 371e7d6b..fc4d946c 100644 --- a/Tests/TurfTests/JSONTests.swift +++ b/Tests/TurfTests/JSONTests.swift @@ -30,6 +30,18 @@ class JSONTests: XCTestCase { "array": [], "dictionary": [:], ])) + + XCTAssertNil(JSONValue(rawValue: NSNull())) + XCTAssertEqual(JSONValue(rawValue: [NSNull()]), .array([nil])) + XCTAssertEqual(JSONArray(rawValue: [NSNull()]), [nil]) + XCTAssertEqual(JSONValue(rawValue: ["NSNull": NSNull()]), .object(["NSNull": nil])) + XCTAssertEqual(JSONObject(rawValue: ["NSNull": NSNull()]), ["NSNull": nil]) + + XCTAssertNil(JSONValue(rawValue: Set(["Get"]))) + XCTAssertEqual(JSONValue(rawValue: [Set(["Get"])]), .array([nil])) + XCTAssertEqual(JSONArray(rawValue: [Set(["Get"])]), [nil]) + XCTAssertEqual(JSONValue(rawValue: ["set": Set(["Get"])]), .object(["set": nil])) + XCTAssertEqual(JSONObject(rawValue: ["set": Set(["Get"])]), ["set": nil]) } func testLiterals() throws { From 3ab52fc20070cf5233cdacc7e5dceb42090b8998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 28 Sep 2021 15:09:13 -0700 Subject: [PATCH 15/16] Documented GeoJSON types and members --- Sources/Turf/BoundingBox.swift | 24 +++++++++++++++++++ Sources/Turf/FeatureIdentifier.swift | 21 ++++++++++++++-- .../Turf/Geometries/GeometryCollection.swift | 17 ++++++++++++- Sources/Turf/Geometries/LineString.swift | 15 +++++++++++- Sources/Turf/Geometries/MultiLineString.swift | 15 +++++++++++- Sources/Turf/Geometries/MultiPoint.swift | 10 +++++++- Sources/Turf/Geometries/MultiPolygon.swift | 15 +++++++++++- Sources/Turf/Geometries/Point.swift | 17 +++++++++---- Sources/Turf/Geometries/Polygon.swift | 24 +++++++++++++++---- Sources/Turf/Geometry.swift | 15 +++++++++++- Sources/Turf/JSON.swift | 24 +++++++++++++++++++ Sources/Turf/Ring.swift | 8 ++++++- 12 files changed, 187 insertions(+), 18 deletions(-) diff --git a/Sources/Turf/BoundingBox.swift b/Sources/Turf/BoundingBox.swift index 1e4b76f1..0dc30ff6 100644 --- a/Sources/Turf/BoundingBox.swift +++ b/Sources/Turf/BoundingBox.swift @@ -3,10 +3,21 @@ import Foundation import CoreLocation #endif +/** + A [bounding box](https://datatracker.ietf.org/doc/html/rfc7946#section-5) indicates the extremes of a `GeoJSONObject` along the x- and y-axes (longitude and latitude, respectively). + */ public struct BoundingBox { + /// The southwesternmost position contained in the bounding box. public var southWest: LocationCoordinate2D + + /// The northeasternmost position contained in the bounding box. public var northEast: LocationCoordinate2D + /** + Initializes the smallest bounding box that contains all the given coordinates. + + - parameter coordinates: The coordinates to fit in the bounding box. + */ public init?(from coordinates: [LocationCoordinate2D]?) { guard coordinates?.count ?? 0 > 0 else { return nil @@ -24,11 +35,24 @@ public struct BoundingBox { northEast = LocationCoordinate2D(latitude: maxLat, longitude: maxLon) } + /** + Initializes a bounding box defined by its southwesternmost and northeasternmost positions. + + - parameter southWest: The southwesternmost position contained in the bounding box. + - parameter northEast: The northeasternmost position contained in the bounding box. + */ public init(southWest: LocationCoordinate2D, northEast: LocationCoordinate2D) { self.southWest = southWest self.northEast = northEast } + /** + Returns a Boolean value indicating whether the bounding box contains the given position. + + - parameter coordinate: The coordinate that may or may not be contained by the bounding box. + - parameter ignoreBoundary: A Boolean value indicating whether a position lying exactly on the edge of the bounding box should be considered to be contained in the bounding box. + - returns: `true` if the bounding box contains the position; `false` otherwise. + */ public func contains(_ coordinate: LocationCoordinate2D, ignoreBoundary: Bool = true) -> Bool { if ignoreBoundary { return southWest.latitude < coordinate.latitude diff --git a/Sources/Turf/FeatureIdentifier.swift b/Sources/Turf/FeatureIdentifier.swift index f213e502..9d461bdf 100644 --- a/Sources/Turf/FeatureIdentifier.swift +++ b/Sources/Turf/FeatureIdentifier.swift @@ -1,17 +1,34 @@ import Foundation +/** + A [feature identifier](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) identifies a `Feature` object. + */ public enum FeatureIdentifier: Equatable { - case string(String) - case number(Double) + /// A string. + case string(_ string: String) + /** + A floating-point number. + + - parameter number: A floating-point number. JSON does not distinguish numeric types of different precisions. If you need integer precision, cast this associated value to an `Int`. + */ + case number(_ number: Double) + + /// Initializes a feature identifier representing the given string. public init(_ string: String) { self = .string(string) } + /** + Initializes a feature identifier representing the given integer. + + - parameter number: An integer. JSON does not distinguish numeric types of different precisions, so the integer is stored as a floating-point number. + */ public init(_ number: Source) where Source: BinaryInteger { self = .number(Double(number)) } + /// Initializes a feature identifier representing the given floating-point number. public init(_ number: Source) where Source: BinaryFloatingPoint { self = .number(Double(number)) } diff --git a/Sources/Turf/Geometries/GeometryCollection.swift b/Sources/Turf/Geometries/GeometryCollection.swift index 4b0ba990..e59c138a 100644 --- a/Sources/Turf/Geometries/GeometryCollection.swift +++ b/Sources/Turf/Geometries/GeometryCollection.swift @@ -3,14 +3,29 @@ import Foundation import CoreLocation #endif - +/** + A [GeometryCollection geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.8) is a heterogeneous collection of `Geometry` objects that are related. + */ public struct GeometryCollection: Equatable { + /// The geometries contained by the geometry collection. public var geometries: [Geometry] + /** + Initializes a geometry collection defined by the given geometries. + + - parameter geometries: The geometries contained by the geometry collection. + */ public init(geometries: [Geometry]) { self.geometries = geometries } + /** + Initializes a geometry collection coincident to the given multipolygon. + + You should only use this initializer if you intend to add geometries other than multipolygons to the geometry collection after initializing it. + + - parameter multiPolygon: The multipolygon that is coincident to the geometry collection. + */ public init(_ multiPolygon: MultiPolygon) { self.geometries = multiPolygon.coordinates.map { $0.count > 1 ? diff --git a/Sources/Turf/Geometries/LineString.swift b/Sources/Turf/Geometries/LineString.swift index 57b9a7cf..49b251d5 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -3,14 +3,27 @@ import Foundation import CoreLocation #endif - +/** + A [LineString geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.4) is a collection of two or more positions, each position connected to the next position linearly. + */ public struct LineString: Equatable { + /// The positions at which the line string is located. public var coordinates: [LocationCoordinate2D] + /** + Initializes a line string defined by given positions. + + - parameter coordinates: The positions at which the line string is located. + */ public init(_ coordinates: [LocationCoordinate2D]) { self.coordinates = coordinates } + /** + Initializes a line string coincident to the given linear ring. + + - parameter ring: The linear ring coincident to the line string. + */ public init(_ ring: Ring) { self.coordinates = ring.coordinates } diff --git a/Sources/Turf/Geometries/MultiLineString.swift b/Sources/Turf/Geometries/MultiLineString.swift index 55944923..db3d87ec 100644 --- a/Sources/Turf/Geometries/MultiLineString.swift +++ b/Sources/Turf/Geometries/MultiLineString.swift @@ -3,14 +3,27 @@ import Foundation import CoreLocation #endif - +/** + A [MultiLineString geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.5) is a collection of `LineString` geometries that are disconnected but related. + */ public struct MultiLineString: Equatable { + /// The positions at which the multi–line string is located. Each nested array corresponds to one line string. public var coordinates: [[LocationCoordinate2D]] + /** + Initializes a multi–line string defined by the given positions. + + - parameter coordinates: The positions at which the multi–line string is located. Each nested array corresponds to one line string. + */ public init(_ coordinates: [[LocationCoordinate2D]]) { self.coordinates = coordinates } + /** + Initializes a multi–line string coincident to the given polygon’s linear rings. + + - parameter polygon: The polygon whose linear rings are coincident to the multi–line string. + */ public init(_ polygon: Polygon) { self.coordinates = polygon.coordinates } diff --git a/Sources/Turf/Geometries/MultiPoint.swift b/Sources/Turf/Geometries/MultiPoint.swift index cee6026c..258ea22b 100644 --- a/Sources/Turf/Geometries/MultiPoint.swift +++ b/Sources/Turf/Geometries/MultiPoint.swift @@ -3,10 +3,18 @@ import Foundation import CoreLocation #endif - +/** + A [MultiPoint geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.3) represents a collection of disconnected but related positions. + */ public struct MultiPoint: Equatable { + /// The positions at which the multipoint is located. public var coordinates: [LocationCoordinate2D] + /** + Initializes a multipoint defined by the given positions. + + - parameter coordinates: The positions at which the multipoint is located. + */ public init(_ coordinates: [LocationCoordinate2D]) { self.coordinates = coordinates } diff --git a/Sources/Turf/Geometries/MultiPolygon.swift b/Sources/Turf/Geometries/MultiPolygon.swift index 8d81cc7d..14bf40d7 100644 --- a/Sources/Turf/Geometries/MultiPolygon.swift +++ b/Sources/Turf/Geometries/MultiPolygon.swift @@ -3,14 +3,27 @@ import Foundation import CoreLocation #endif - +/** + A [MultiPolygon geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.7) is a collection of `Polygon` geometries that are disconnected but related. + */ public struct MultiPolygon: Equatable { + /// The positions at which the multipolygon is located. Each nested array corresponds to one polygon. public var coordinates: [[[LocationCoordinate2D]]] + /** + Initializes a multipolygon defined by the given positions. + + - parameter coordinates: The positions at which the multipolygon is located. Each nested array corresponds to one polygon. + */ public init(_ coordinates: [[[LocationCoordinate2D]]]) { self.coordinates = coordinates } + /** + Initializes a multipolygon coincident to the given polygons. + + - parameter polygons: The polygons that together are coincident to the multipolygon. + */ public init(_ polygons: [Polygon]) { self.coordinates = polygons.map { (polygon) -> [[LocationCoordinate2D]] in return polygon.coordinates diff --git a/Sources/Turf/Geometries/Point.swift b/Sources/Turf/Geometries/Point.swift index c3f7fa1d..ef70a75e 100644 --- a/Sources/Turf/Geometries/Point.swift +++ b/Sources/Turf/Geometries/Point.swift @@ -3,13 +3,22 @@ import Foundation import CoreLocation #endif - +/** + A [Point geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.2) represents a single position. + */ public struct Point: Equatable { - /** Note: The pluralization of `coordinates` is defined - in the GeoJSON RFC, so we've kept it for consistency. - https://tools.ietf.org/html/rfc7946#section-1.5 */ + /** + The position at which the point is located. + + This property has a plural name for consistency with [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.2). For convenience, it is represented by a `LocationCoordinate2D` instead of a dedicated `Position` type. + */ public var coordinates: LocationCoordinate2D + /** + Initializes a point defined by the given position. + + - parameter coordinates: The position at which the point is located. + */ public init(_ coordinates: LocationCoordinate2D) { self.coordinates = coordinates } diff --git a/Sources/Turf/Geometries/Polygon.swift b/Sources/Turf/Geometries/Polygon.swift index 71ccbaba..16546dd1 100644 --- a/Sources/Turf/Geometries/Polygon.swift +++ b/Sources/Turf/Geometries/Polygon.swift @@ -3,14 +3,28 @@ import Foundation import CoreLocation #endif - +/** + A [Polygon geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6) is conceptually a collection of `Ring`s that form a single connected geometry. + */ public struct Polygon: Equatable { + /// The positions at which the polygon is located. Each nested array corresponds to one linear ring. public var coordinates: [[LocationCoordinate2D]] + /** + Initializes a polygon defined by the given positions. + + - parameter coordinates: The positions at which the polygon is located. Each nested array corresponds to one linear ring. + */ public init(_ coordinates: [[LocationCoordinate2D]]) { self.coordinates = coordinates } + /** + Initializes a polygon defined by the given linear rings. + + - parameter outerRing: The outer linear ring. + - parameter innerRings: The inner linear rings that define “holes” in the polygon. + */ public init(outerRing: Ring, innerRings: [Ring] = []) { self.coordinates = ([outerRing] + innerRings).map { $0.coordinates } } @@ -63,19 +77,19 @@ extension Polygon: Codable { } extension Polygon { - /// Representation of `.Polygon`s coordinates of inner rings + /// Representation of `Polygon`s coordinates of inner rings public var innerRings: [Ring] { return Array(coordinates.suffix(from: 1)).map { Ring(coordinates: $0) } } - /// Representation of `.Polygon`s coordinates of outer ring + /// Representation of `Polygon`s coordinates of outer ring public var outerRing: Ring { get { return Ring(coordinates: coordinates.first! ) } } - /// An area of current `.Polygon` + /// An area of current `Polygon` /// /// Ported from https://github.com/Turfjs/turf/blob/a94151418cb969868fdb42955a19a133512da0fd/packages/turf-area/index.js public var area: Double { @@ -101,7 +115,7 @@ extension Polygon { return true } - /// Smooths a `.Polygon`. Based on [Chaikin's algorithm](http://graphics.cs.ucdavis.edu/education/CAGDNotes/Chaikins-Algorithm/Chaikins-Algorithm.html). + /// Smooths a `Polygon`. Based on [Chaikin's algorithm](http://graphics.cs.ucdavis.edu/education/CAGDNotes/Chaikins-Algorithm/Chaikins-Algorithm.html). /// Warning: may create degenerate polygons. /// /// Ported from https://github.com/Turfjs/turf/blob/402716a29f6ae16bf3d0220e213e5380cc5a50c4/packages/turf-polygon-smooth/index.js diff --git a/Sources/Turf/Geometry.swift b/Sources/Turf/Geometry.swift index 19fcd109..caaf5520 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -4,15 +4,28 @@ import CoreLocation #endif /** - A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. + A [Geometry object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1) represents points, curves, and surfaces in coordinate space. Use an instance of this enumeration whenever a value could be any kind of Geometry object. */ public enum Geometry: Equatable { + /// A single position. case point(_ geometry: Point) + + /// A collection of two or more positions, each position connected to the next position linearly. case lineString(_ geometry: LineString) + + /// Conceptually, a collection of `Ring`s that form a single connected geometry. case polygon(_ geometry: Polygon) + + /// A collection of positions that are disconnected but related. case multiPoint(_ geometry: MultiPoint) + + /// A collection of `LineString` geometries that are disconnected but related. case multiLineString(_ geometry: MultiLineString) + + /// A collection of `Polygon` geometries that are disconnected but related. case multiPolygon(_ geometry: MultiPolygon) + + /// A heterogeneous collection of geometries that are related. case geometryCollection(_ geometry: GeometryCollection) } diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift index a0dd38cf..099a65fe 100644 --- a/Sources/Turf/JSON.swift +++ b/Sources/Turf/JSON.swift @@ -9,32 +9,56 @@ public enum JSONValue: Equatable, RawRepresentable { public typealias RawValue = Any // case null would be redundant to Optional.none + + /// A string. case string(_ string: String) + + /** + A floating-point number. + + JSON does not distinguish numeric types of different precisions. If you need integer precision, cast the value to an `Int`. + */ case number(_ number: Double) + + /// A Boolean value. case boolean(_ bool: Bool) + + /// A heterogeneous array of JSON values and `null` values. case array(_ values: JSONArray) + + /// An object containing JSON values and `null` values keyed by strings. case object(_ properties: JSONObject) + /// Initializes a JSON value representing the given string. public init(_ string: String) { self = .string(string) } + /** + Initializes a JSON value representing the given integer. + + - parameter number: An integer. JSON does not distinguish numeric types of different precisions, so the integer is stored as a floating-point number. + */ public init(_ number: Source) where Source: BinaryInteger { self = .number(Double(number)) } + /// Initializes a JSON value representing the given floating-point number. public init(_ number: Source) where Source: BinaryFloatingPoint { self = .number(Double(number)) } + /// Initializes a JSON value representing the given Boolean value. public init(_ bool: Bool) { self = .boolean(bool) } + /// Initializes a JSON value representing the given JSON array. public init(_ values: JSONArray) { self = .array(values) } + /// Initializes a JSON value representing the given JSON object. public init(_ properties: JSONObject) { self = .object(properties) } diff --git a/Sources/Turf/Ring.swift b/Sources/Turf/Ring.swift index e91864cd..ec0831b2 100644 --- a/Sources/Turf/Ring.swift +++ b/Sources/Turf/Ring.swift @@ -4,11 +4,17 @@ import CoreLocation #endif /** - Creates a `Ring` struct that represents a closed figure that is bounded by three or more straight line segments. + A [linear ring](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6) is a closed figure bounded by three or more straight line segments. */ public struct Ring { + /// The positions at which the linear ring is located. public var coordinates: [LocationCoordinate2D] + /** + Initializes a linear ring defined by the given positions. + + - parameter coordinates: The positions at which the linear ring is located. + */ public init(coordinates: [LocationCoordinate2D]) { self.coordinates = coordinates } From 4e7a3878c4b215b3c39bd0376be8faaf01ce9a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 28 Sep 2021 15:14:23 -0700 Subject: [PATCH 16/16] Conform FeatureIdentifier to RawRepresentable --- Sources/Turf/FeatureIdentifier.swift | 24 ++++++++++++++++++++++++ Sources/Turf/JSON.swift | 8 +++++--- Tests/TurfTests/GeoJSONTests.swift | 6 ++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Sources/Turf/FeatureIdentifier.swift b/Sources/Turf/FeatureIdentifier.swift index 9d461bdf..041bda52 100644 --- a/Sources/Turf/FeatureIdentifier.swift +++ b/Sources/Turf/FeatureIdentifier.swift @@ -34,6 +34,30 @@ public enum FeatureIdentifier: Equatable { } } +extension FeatureIdentifier: RawRepresentable { + public typealias RawValue = Any + + public init?(rawValue: Any) { + // Like `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.fragmentsAllowed` specified. + if let string = rawValue as? String { + self = .string(string) + } else if let number = rawValue as? NSNumber { + self = .number(number.doubleValue) + } else { + return nil + } + } + + public var rawValue: Any { + switch self { + case let .string(value): + return value + case let .number(value): + return value + } + } +} + extension FeatureIdentifier: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { self = .init(value) diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift index 099a65fe..bf6cbe8c 100644 --- a/Sources/Turf/JSON.swift +++ b/Sources/Turf/JSON.swift @@ -5,9 +5,7 @@ import Foundation This type does not represent the `null` value in JSON. Use `Optional` wherever `null` is accepted. */ -public enum JSONValue: Equatable, RawRepresentable { - public typealias RawValue = Any - +public enum JSONValue: Equatable { // case null would be redundant to Optional.none /// A string. @@ -62,6 +60,10 @@ public enum JSONValue: Equatable, RawRepresentable { public init(_ properties: JSONObject) { self = .object(properties) } +} + +extension JSONValue: RawRepresentable { + public typealias RawValue = Any public init?(rawValue: Any) { // Like `JSONSerialization.jsonObject(with:options:)` with `JSONSerialization.ReadingOptions.fragmentsAllowed` specified. diff --git a/Tests/TurfTests/GeoJSONTests.swift b/Tests/TurfTests/GeoJSONTests.swift index a5214434..af0269c6 100644 --- a/Tests/TurfTests/GeoJSONTests.swift +++ b/Tests/TurfTests/GeoJSONTests.swift @@ -116,6 +116,12 @@ class GeoJSONTests: XCTestCase { XCTAssertEqual(multiPolygon.coordinates, coordinates) } + func testRawFeatureIdentifierValues() { + XCTAssertEqual(FeatureIdentifier(rawValue: "Jason" as NSString)?.rawValue as? String, "Jason") + XCTAssertEqual(FeatureIdentifier(rawValue: 42 as NSNumber)?.rawValue as? Double, 42) + XCTAssertEqual(FeatureIdentifier(rawValue: 3.1415 as NSNumber)?.rawValue as? Double, 3.1415) + } + func testFeatureIdentifierLiterals() { if case let FeatureIdentifier.string(string) = "Jason" { XCTAssertEqual(string, "Jason")