diff --git a/Sources/Turf/BoundingBox.swift b/Sources/Turf/BoundingBox.swift index 01a7b555..0dc30ff6 100644 --- a/Sources/Turf/BoundingBox.swift +++ b/Sources/Turf/BoundingBox.swift @@ -3,8 +3,21 @@ import Foundation import CoreLocation #endif -public struct BoundingBox: Codable { +/** + 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 @@ -22,11 +35,24 @@ public struct BoundingBox: Codable { 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 @@ -40,9 +66,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 +89,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 } 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 4e3197a1..a5236497 100644 --- a/Sources/Turf/Feature.swift +++ b/Sources/Turf/Feature.swift @@ -3,35 +3,43 @@ 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: Equatable { public var identifier: FeatureIdentifier? - public var properties: [String : Any?]? - public var geometry: Geometry + public var properties: JSONObject? + public var geometry: Geometry? + public init(geometry: Geometry?) { + self.geometry = geometry + } +} + +extension Feature: Codable { private enum CodingKeys: String, CodingKey { - case type - case geometry - case properties - case identifier = "id" + 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) - geometry = try container.decode(Geometry.self, forKey: .geometry) - properties = try container.decodeIfPresent([String: Any?].self, forKey: .properties) + _ = try container.decode(Kind.self, forKey: .kind) + geometry = try container.decodeIfPresent(Geometry.self, forKey: .geometry) + properties = try container.decodeIfPresent(JSONObject.self, forKey: .properties) identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type.rawValue, forKey: .type) - try container.encodeIfPresent(geometry, forKey: .geometry) + try container.encode(Kind.Feature, forKey: .kind) + try container.encode(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..0213d6d1 100644 --- a/Sources/Turf/FeatureCollection.swift +++ b/Sources/Turf/FeatureCollection.swift @@ -1,32 +1,35 @@ import Foundation - -public struct FeatureCollection: GeoJSONObject { - public let type: FeatureType = .featureCollection - public var identifier: FeatureIdentifier? +/** + A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects. + */ +public struct FeatureCollection: Equatable { 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 properties + case kind = "type" 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) } 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/FeatureIdentifier.swift b/Sources/Turf/FeatureIdentifier.swift index 2e81f51a..041bda52 100644 --- a/Sources/Turf/FeatureIdentifier.swift +++ b/Sources/Turf/FeatureIdentifier.swift @@ -1,55 +1,78 @@ import Foundation -public enum Number: Equatable { - case int(Int) - case double(Double) +/** + A [feature identifier](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) identifies a `Feature` object. + */ +public enum FeatureIdentifier: Equatable { + /// A string. + case string(_ string: String) - public var value: Any? { - switch self { - case .int(let value): - return value - case .double(let value): - return value - } + /** + 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)) } } -extension Number: Codable { - enum CodingKeys: String, CodingKey { - case int, double - } +extension FeatureIdentifier: RawRepresentable { + public typealias RawValue = Any - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if let value = try? container.decode(Int.self) { - self = .int(value) + 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 { - self = .double(try container.decode(Double.self)) + return nil } } - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() + public var rawValue: Any { switch self { - case .int(let value): - try container.encode(value) - case .double(let value): - try container.encode(value) + case let .string(value): + return value + case let .number(value): + return value } } } -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: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self = .init(value) + } +} + +extension FeatureIdentifier: ExpressibleByIntegerLiteral { + public init(integerLiteral value: IntegerLiteralType) { + self = .init(value) + } +} + +extension FeatureIdentifier: ExpressibleByFloatLiteral { + public init(floatLiteral value: FloatLiteralType) { + self = .number(value) } } @@ -63,7 +86,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 3f7c17d4..1aecb7c5 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: Equatable { + /** + 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/Geometries/GeometryCollection.swift b/Sources/Turf/Geometries/GeometryCollection.swift index b96cdc14..e59c138a 100644 --- a/Sources/Turf/Geometries/GeometryCollection.swift +++ b/Sources/Turf/Geometries/GeometryCollection.swift @@ -3,14 +3,29 @@ import Foundation import CoreLocation #endif - -public struct GeometryCollection { +/** + 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 ? @@ -19,3 +34,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..49b251d5 100644 --- a/Sources/Turf/Geometries/LineString.swift +++ b/Sources/Turf/Geometries/LineString.swift @@ -3,19 +3,56 @@ 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 } } +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..db3d87ec 100644 --- a/Sources/Turf/Geometries/MultiLineString.swift +++ b/Sources/Turf/Geometries/MultiLineString.swift @@ -3,15 +3,52 @@ 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 } } + +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..258ea22b 100644 --- a/Sources/Turf/Geometries/MultiPoint.swift +++ b/Sources/Turf/Geometries/MultiPoint.swift @@ -3,11 +3,43 @@ 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 } } + +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..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 @@ -18,6 +31,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..ef70a75e 100644 --- a/Sources/Turf/Geometries/Point.swift +++ b/Sources/Turf/Geometries/Point.swift @@ -3,14 +3,47 @@ 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 } } + +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..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 } } @@ -38,20 +52,44 @@ 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 + /// 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 { @@ -77,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 b5fc876c..caaf5520 100644 --- a/Sources/Turf/Geometry.swift +++ b/Sources/Turf/Geometry.swift @@ -3,121 +3,85 @@ 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 - case coordinates - case geometries - } - +/** + 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) +} + +extension Geometry: Codable { + private enum CodingKeys: String, CodingKey { + case kind = "type" + } - public var type: GeometryType { - 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 + enum Kind: String, Codable, CaseIterable { + case Point + case LineString + case Polygon + case MultiPoint + case MultiLineString + case MultiPolygon + case GeometryCollection + } + + public init(from decoder: Decoder) throws { + let kindContainer = try decoder.container(keyedBy: CodingKeys.self) + let container = try decoder.singleValueContainer() + switch try kindContainer.decode(Kind.self, forKey: .kind) { + case .Point: + self = .point(try container.decode(Point.self)) + case .LineString: + self = .lineString(try container.decode(LineString.self)) + case .Polygon: + self = .polygon(try container.decode(Polygon.self)) + case .MultiPoint: + self = .multiPoint(try container.decode(MultiPoint.self)) + case .MultiLineString: + self = .multiLineString(try container.decode(MultiLineString.self)) + case .MultiPolygon: + self = .multiPolygon(try container.decode(MultiPolygon.self)) + case .GeometryCollection: + self = .geometryCollection(try container.decode(GeometryCollection.self)) } } - public var value: Any? { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() 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 + 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) } } } - - -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)) - } - } - - 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) - } - } -} diff --git a/Sources/Turf/JSON.swift b/Sources/Turf/JSON.swift new file mode 100644 index 00000000..bf6cbe8c --- /dev/null +++ b/Sources/Turf/JSON.swift @@ -0,0 +1,211 @@ +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 + + /// 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) + } +} + +extension JSONValue: RawRepresentable { + public typealias RawValue = Any + + 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 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 + } + } + + 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.rawValue + case let .array(value): + return value.rawValue + } + } +} + +/** + A JSON array of `JSONValue` instances. + */ +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) + } +} + +extension JSONValue: ExpressibleByIntegerLiteral { + public init(integerLiteral value: IntegerLiteralType) { + self = .init(value) + } +} + +extension JSONValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: FloatLiteralType) { + self = .init(value) + } +} + +extension JSONValue: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: BooleanLiteralType) { + self = .init(value) + } +} + +extension JSONValue: ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = JSONValue? + + public init(arrayLiteral elements: ArrayLiteralElement...) { + self = .init(elements) + } +} + +extension JSONValue: ExpressibleByDictionaryLiteral { + public typealias Key = String + public typealias Value = JSONValue? + + public init(dictionaryLiteral elements: (Key, Value)...) { + self = .init(.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/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 } diff --git a/Tests/TurfTests/FeatureCollectionTests.swift b/Tests/TurfTests/FeatureCollectionTests.swift index 4a6970d9..34371b00 100644 --- a/Tests/TurfTests/FeatureCollectionTests.swift +++ b/Tests/TurfTests/FeatureCollectionTests.swift @@ -8,90 +8,116 @@ 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() } - 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 = 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 } 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) - let polygonFeature = geojson.features[1] + let polygonFeature = featureCollection.features[1] guard case let .polygon(polygonCoordinates) = polygonFeature.geometry else { 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) - let pointFeature = geojson.features[3] + let pointFeature = featureCollection.features[3] guard case let .point(pointCoordinates) = pointFeature.geometry else { 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) 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() } - 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 = 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 } 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) - let decodedPolygonFeature = decoded.features[1] + let decodedPolygonFeature = decodedFeatureCollection.features[1] guard case let .polygon(decodedPolygonCoordinates) = decodedPolygonFeature.geometry else { 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) - let decodedPointFeature = decoded.features[3] + let decodedPointFeature = decodedFeatureCollection.features[3] guard case let .point(decodedPointCoordinates) = decodedPointFeature.geometry else { 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) } 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 +125,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 +146,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 +154,16 @@ 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) + for feature in featureCollection.features { + if case let .number(tolerance) = feature.properties?["tolerance"] { + XCTAssertEqual(tolerance, 0.01) + } else { + XCTFail() + } + } } } 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/GeoJSONTests.swift b/Tests/TurfTests/GeoJSONTests.swift index 7eb118bb..af0269c6 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,82 @@ 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) + } + + 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") + } 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 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))) + 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/GeometryCollectionTests.swift b/Tests/TurfTests/GeometryCollectionTests.swift index c40fae02..f2807e60 100644 --- a/Tests/TurfTests/GeometryCollectionTests.swift +++ b/Tests/TurfTests/GeometryCollectionTests.swift @@ -12,25 +12,19 @@ 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 } - 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 @@ -42,29 +36,23 @@ 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 } - 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/JSONTests.swift b/Tests/TurfTests/JSONTests.swift new file mode 100644 index 00000000..fc4d946c --- /dev/null +++ b/Tests/TurfTests/JSONTests.swift @@ -0,0 +1,233 @@ +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": [:], + ])) + + 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 { + 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/LineStringTests.swift b/Tests/TurfTests/LineStringTests.swift index c660fe35..6c98a261 100644 --- a/Tests/TurfTests/LineStringTests.swift +++ b/Tests/TurfTests/LineStringTests.swift @@ -8,9 +8,8 @@ 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) - XCTAssert(geojson.geometry.type == .LineString) guard case let .lineString(lineStringCoordinates) = geojson.geometry else { XCTFail() return @@ -21,17 +20,26 @@ 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) + let decoded = try! JSONDecoder().decode(Feature.self, from: encodedData) guard case let .lineString(decodedLineStringCoordinates) = decoded.geometry else { XCTFail() return } 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..2a2fe77c 100644 --- a/Tests/TurfTests/MultiLineStringTests.swift +++ b/Tests/TurfTests/MultiLineStringTests.swift @@ -11,9 +11,8 @@ 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) - XCTAssert(geojson.geometry.type == .MultiLineString) guard case let .multiLineString(multiLineStringCoordinates) = geojson.geometry else { XCTFail() return @@ -22,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 244a1462..d5aa06cc 100644 --- a/Tests/TurfTests/MultiPointTests.swift +++ b/Tests/TurfTests/MultiPointTests.swift @@ -11,9 +11,8 @@ 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) - - XCTAssert(geojson.geometry.type == .MultiPoint) + let geojson = try! JSONDecoder().decode(Feature.self, from: data) + guard case let .multiPoint(multipointCoordinates) = geojson.geometry else { XCTFail() return @@ -22,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 0aa7637c..5facc1b3 100644 --- a/Tests/TurfTests/MultiPolygonTests.swift +++ b/Tests/TurfTests/MultiPolygonTests.swift @@ -14,9 +14,8 @@ 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) - XCTAssert(geojson.geometry.type == .MultiPolygon) guard case let .multiPolygon(multipolygonCoordinates) = geojson.geometry else { XCTFail() return @@ -26,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 @@ -74,20 +73,19 @@ 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) - 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 } - 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..007f93d2 100644 --- a/Tests/TurfTests/PointTests.swift +++ b/Tests/TurfTests/PointTests.swift @@ -8,30 +8,57 @@ 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) - XCTAssert((geojson.identifier!.value as! Number).value! as! Int == 1) + if case let .number(number) = feature.identifier { + XCTAssertEqual(number, 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) + let decoded = try! JSONDecoder().decode(GeoJSONObject.self, from: encodedData) + + guard case let .feature(decodedFeature) = decoded, + case let .point(decodedPoint) = decodedFeature.geometry else { + return XCTFail() + } + + XCTAssertEqual(point, decodedPoint) + + if case let .number(number) = feature.identifier, + case let .number(decodedNumber) = decodedFeature.identifier { + XCTAssertEqual(number, decodedNumber) + } else { + XCTFail() + } } 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(XCTUnwrap(feature.geometry)))) + XCTAssertNotNil(encodedData) + + var decoded: GeoJSONObject? + XCTAssertNoThrow(decoded = try JSONDecoder().decode(GeoJSONObject.self, from: encodedData!)) + XCTAssertNotNil(decoded) - XCTAssert(geojson.decoded is Feature) - XCTAssert(geojson.decodedFeature?.geometry.type == .Point) + 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 f8874076..737cee84 100644 --- a/Tests/TurfTests/PolygonTests.swift +++ b/Tests/TurfTests/PolygonTests.swift @@ -8,12 +8,16 @@ 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) - XCTAssert((geojson.identifier!.value as! Number).value! as! Double == 1.01) + if case let .number(number) = geojson.identifier { + XCTAssertEqual(number, 1.01) + } else { + XCTFail() + } guard case let .polygon(polygon) = geojson.geometry else { XCTFail() @@ -25,14 +29,19 @@ 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 } 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) diff --git a/Tests/TurfTests/TurfTests.swift b/Tests/TurfTests/TurfTests.swift index 0e77139d..260005eb 100644 --- a/Tests/TurfTests/TurfTests.swift +++ b/Tests/TurfTests/TurfTests.swift @@ -163,14 +163,25 @@ 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 properties = input.decodedFeature?.properties - let tolerance = (properties?["tolerance"] as? NSNumber)?.doubleValue ?? 0.01 - let highQuality = (properties?["highQuality"] as? NSNumber)?.boolValue ?? false + let input = try JSONDecoder().decode(GeoJSONObject.self, from: inputData) + let output = try JSONDecoder().decode(GeoJSONObject.self, from: outputData) for (input, output) in zip(input.features, output.features) { + let properties = input.properties + 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): break // nothing to simplify @@ -194,14 +205,15 @@ class TurfTests: XCTestCase { } } -extension GeoJSON { +extension GeoJSONObject { 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 [] } } } 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 */,