Skip to content

Commit

Permalink
Round-trip GeoJSON foreign members (#175)
Browse files Browse the repository at this point in the history
* Round-trip GeoJSON foreign members

Any concrete GeoJSON object type can now store and round-trip foreign members back to JSON.

* Inlined foreignMemberKeys

* Test round-tripping GeoJSON-T
  • Loading branch information
1ec5 authored Dec 15, 2021
1 parent 132aaf7 commit 3150c69
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 23 deletions.
24 changes: 13 additions & 11 deletions Sources/Turf/Codable.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import Foundation
#if !os(Linux)
import CoreLocation
#endif

extension Ring: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = Ring(coordinates: try container.decode([LocationCoordinate2DCodable].self).decodedCoordinates)
/**
A coding key as an extensible enumeration.
*/
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int?

init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(coordinates.codableCoordinates)
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}

6 changes: 5 additions & 1 deletion Sources/Turf/Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import CoreLocation
/**
A [Feature object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2) represents a spatially bounded thing.
*/
public struct Feature: Equatable {
public struct Feature: Equatable, ForeignMemberContainer {
/**
A string or number that commonly identifies the feature in the context of a data set.

Expand All @@ -20,6 +20,8 @@ public struct Feature: Equatable {
/// The geometry at which the feature is located.
public var geometry: Geometry?

public var foreignMembers: JSONObject = [:]

/**
Initializes a feature located at the given geometry.

Expand Down Expand Up @@ -57,6 +59,7 @@ extension Feature: Codable {
geometry = try container.decodeIfPresent(Geometry.self, forKey: .geometry)
properties = try container.decodeIfPresent(JSONObject.self, forKey: .properties)
identifier = try container.decodeIfPresent(FeatureIdentifier.self, forKey: .identifier)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -65,5 +68,6 @@ extension Feature: Codable {
try container.encode(geometry, forKey: .geometry)
try container.encodeIfPresent(properties, forKey: .properties)
try container.encodeIfPresent(identifier, forKey: .identifier)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
6 changes: 5 additions & 1 deletion Sources/Turf/FeatureCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import Foundation
/**
A [FeatureCollection object](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3) is a collection of Feature objects.
*/
public struct FeatureCollection: Equatable {
public struct FeatureCollection: Equatable, ForeignMemberContainer {
/// The features that the collection contains.
public var features: [Feature] = []

public var foreignMembers: JSONObject = [:]

/**
Initializes a feature collection containing the given features.

Expand All @@ -31,11 +33,13 @@ extension FeatureCollection: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self)
_ = try container.decode(Kind.self, forKey: .kind)
features = try container.decode([Feature].self, forKey: .features)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(Kind.FeatureCollection, forKey: .kind)
try container.encode(features, forKey: .features)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
35 changes: 35 additions & 0 deletions Sources/Turf/GeoJSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,38 @@ extension Feature: GeoJSONObjectConvertible {
extension FeatureCollection: GeoJSONObjectConvertible {
public var geoJSONObject: GeoJSONObject { return .featureCollection(self) }
}

/**
A GeoJSON object that can contain [foreign members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) in arbitrary keys.
*/
public protocol ForeignMemberContainer {
/// [Foreign members](https://datatracker.ietf.org/doc/html/rfc7946#section-6.1) to round-trip to JSON.
var foreignMembers: JSONObject { get set }
}

extension ForeignMemberContainer {
/**
Decodes any foreign members using the given decoder.
*/
mutating func decodeForeignMembers<WellKnownCodingKeys>(notKeyedBy _: WellKnownCodingKeys.Type, with decoder: Decoder) throws where WellKnownCodingKeys: CodingKey {
let foreignMemberContainer = try decoder.container(keyedBy: AnyCodingKey.self)
for key in foreignMemberContainer.allKeys {
if WellKnownCodingKeys(stringValue: key.stringValue) == nil {
foreignMembers[key.stringValue] = try foreignMemberContainer.decode(JSONValue?.self, forKey: key)
}
}
}

/**
Encodes any foreign members using the given encoder.
*/
func encodeForeignMembers<WellKnownCodingKeys>(notKeyedBy _: WellKnownCodingKeys.Type, to encoder: Encoder) throws where WellKnownCodingKeys: CodingKey {
var foreignMemberContainer = encoder.container(keyedBy: AnyCodingKey.self)
for (key, value) in foreignMembers {
if let key = AnyCodingKey(stringValue: key),
WellKnownCodingKeys(stringValue: key.stringValue) == nil {
try foreignMemberContainer.encode(value, forKey: key)
}
}
}
}
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/GeometryCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CoreLocation
/**
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 {
public struct GeometryCollection: Equatable, ForeignMemberContainer {
/// The geometries contained by the geometry collection.
public var geometries: [Geometry]

public var foreignMembers: JSONObject = [:]

/**
Initializes a geometry collection defined by the given geometries.

Expand Down Expand Up @@ -50,11 +52,13 @@ extension GeometryCollection: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let geometries = try container.decode([Geometry].self, forKey: .geometries)
self = .init(geometries: geometries)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/LineString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CoreLocation
/**
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 {
public struct LineString: Equatable, ForeignMemberContainer {
/// The positions at which the line string is located.
public var coordinates: [LocationCoordinate2D]

public var foreignMembers: JSONObject = [:]

/**
Initializes a line string defined by given positions.

Expand Down Expand Up @@ -55,12 +57,14 @@ extension LineString: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates
self = .init(coordinates)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}

Expand Down
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/MultiLineString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CoreLocation
/**
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 {
public struct MultiLineString: Equatable, ForeignMemberContainer {
/// The positions at which the multi–line string is located. Each nested array corresponds to one line string.
public var coordinates: [[LocationCoordinate2D]]

public var foreignMembers: JSONObject = [:]

/**
Initializes a multi–line string defined by the given positions.

Expand Down Expand Up @@ -46,11 +48,13 @@ extension MultiLineString: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates
self = .init(coordinates)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/MultiPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CoreLocation
/**
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 {
public struct MultiPoint: Equatable, ForeignMemberContainer {
/// The positions at which the multipoint is located.
public var coordinates: [LocationCoordinate2D]

public var foreignMembers: JSONObject = [:]

/**
Initializes a multipoint defined by the given positions.

Expand All @@ -35,11 +37,13 @@ extension MultiPoint: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let coordinates = try container.decode([LocationCoordinate2DCodable].self, forKey: .coordinates).decodedCoordinates
self = .init(coordinates)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/MultiPolygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CoreLocation
/**
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 {
public struct MultiPolygon: Equatable, ForeignMemberContainer {
/// The positions at which the multipolygon is located. Each nested array corresponds to one polygon.
public var coordinates: [[[LocationCoordinate2D]]]

public var foreignMembers: JSONObject = [:]

/// The polygon geometries that conceptually form the multipolygon.
public var polygons: [Polygon] {
return coordinates.map { (coordinates) -> Polygon in
Expand Down Expand Up @@ -53,12 +55,14 @@ extension MultiPolygon: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let coordinates = try container.decode([[[LocationCoordinate2DCodable]]].self, forKey: .coordinates).decodedCoordinates
self = .init(coordinates)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}

Expand Down
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/Point.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import CoreLocation
/**
A [Point geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.2) represents a single position.
*/
public struct Point: Equatable {
public struct Point: Equatable, ForeignMemberContainer {
/**
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

public var foreignMembers: JSONObject = [:]

/**
Initializes a point defined by the given position.

Expand All @@ -39,11 +41,13 @@ extension Point: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let coordinates = try container.decode(LocationCoordinate2DCodable.self, forKey: .coordinates).decodedCoordinates
self = .init(coordinates)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
6 changes: 5 additions & 1 deletion Sources/Turf/Geometries/Polygon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import CoreLocation
/**
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 {
public struct Polygon: Equatable, ForeignMemberContainer {
/// The positions at which the polygon is located. Each nested array corresponds to one linear ring.
public var coordinates: [[LocationCoordinate2D]]

public var foreignMembers: JSONObject = [:]

/**
Initializes a polygon defined by the given positions.

Expand Down Expand Up @@ -71,12 +73,14 @@ extension Polygon: Codable {
_ = try container.decode(Kind.self, forKey: .kind)
let coordinates = try container.decode([[LocationCoordinate2DCodable]].self, forKey: .coordinates).decodedCoordinates
self = .init(coordinates)
try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

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)
try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}

Expand Down
16 changes: 13 additions & 3 deletions Sources/Turf/Ring.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ public struct Ring {
}
return area
}
}

extension Ring {

/**
* Determines if the given point falls within the ring.
* The optional parameter `ignoreBoundary` will result in the method returning true if the given point
Expand Down Expand Up @@ -105,3 +103,15 @@ extension Ring {
return isInside
}
}

extension Ring: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = Ring(coordinates: try container.decode([LocationCoordinate2DCodable].self).decodedCoordinates)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(coordinates.codableCoordinates)
}
}
Loading

0 comments on commit 3150c69

Please sign in to comment.