Skip to content

Commit b8200fa

Browse files
committed
Codable GeoJSON
1 parent 7327ee4 commit b8200fa

24 files changed

+1259
-0
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let package = Package(
1414
],
1515
dependencies: [
1616
.package(url: "https://github.com/apple/swift-algorithms", .upToNextMajor(from: "1.0.0")),
17+
.package(url: "https://github.com/pointfreeco/swift-nonempty", .upToNextMajor(from: "0.4.0")),
1718
],
1819
targets: [
1920
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -37,5 +38,17 @@ let package = Package(
3738
name: "TurfTests",
3839
dependencies: ["Turf"]
3940
),
41+
.target(
42+
name: "GeoJSON",
43+
dependencies: [
44+
.target(name: "GeoModels"),
45+
.target(name: "Turf"),
46+
.product(name: "NonEmpty", package: "swift-nonempty"),
47+
]
48+
),
49+
.testTarget(
50+
name: "GeoJSONTests",
51+
dependencies: ["GeoJSON"]
52+
),
4053
]
4154
)

Sources/GeoJSON/BoundingBox.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// BoundingBox.swift
3+
// GeoSwift
4+
//
5+
// Created by Rémi Bardon on 04/02/2022.
6+
// Copyright © 2022 Rémi Bardon. All rights reserved.
7+
//
8+
9+
import GeoModels
10+
11+
public protocol BoundingBox: Hashable, Codable {
12+
13+
var asAny: AnyBoundingBox { get }
14+
15+
}
16+
17+
public typealias BoundingBox2D = GeoModels.BoundingBox2D
18+
19+
extension BoundingBox2D: BoundingBox {
20+
21+
public var asAny: AnyBoundingBox { .twoDimensions(self) }
22+
23+
}
24+
25+
public enum AnyBoundingBox: BoundingBox, Hashable, Codable {
26+
27+
case twoDimensions(BoundingBox2D)
28+
29+
public var asAny: AnyBoundingBox { self }
30+
31+
}

Sources/GeoJSON/Errors.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// Errors.swift
3+
// SwiftGeo
4+
//
5+
// Created by Rémi Bardon on 07/02/2022.
6+
// Copyright © 2022 Rémi Bardon. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// See [RFC 7946, section 3.1.6](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6).
12+
public enum LinearRingError: Error {
13+
case firstAndLastPositionsShouldBeEquivalent
14+
case notEnoughPoints
15+
}

Sources/GeoJSON/GeoJSON+Codable.swift

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
//
2+
// GeoJSON+Codable.swift
3+
// SwiftGeo
4+
//
5+
// Created by Rémi Bardon on 07/02/2022.
6+
// Copyright © 2022 Rémi Bardon. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import GeoModels
11+
12+
//extension BinaryFloatingPoint {
13+
//
14+
// /// Rounds the double to decimal places value
15+
// fileprivate func roundedToPlaces(_ places: Int) -> Self {
16+
// let divisor = pow(10.0, Double(places))
17+
// return Self((Double(self) * divisor).rounded() / divisor)
18+
// }
19+
//
20+
//}
21+
22+
extension Double {
23+
24+
/// Rounds the double to decimal places value
25+
func roundedToPlaces(_ places: Int) -> Decimal {
26+
let divisor = pow(10.0, Double(places))
27+
return Decimal((self * divisor).rounded()) / Decimal(divisor)
28+
}
29+
30+
}
31+
32+
extension Latitude: Codable {
33+
34+
public init(from decoder: Decoder) throws {
35+
let container = try decoder.singleValueContainer()
36+
37+
let decimalDegrees = try container.decode(Double.self)
38+
39+
self.init(decimalDegrees: decimalDegrees)
40+
}
41+
42+
public func encode(to encoder: Encoder) throws {
43+
var container = encoder.singleValueContainer()
44+
45+
try container.encode(self.decimalDegrees.roundedToPlaces(6))
46+
}
47+
48+
}
49+
50+
extension Longitude: Codable {
51+
52+
public init(from decoder: Decoder) throws {
53+
let container = try decoder.singleValueContainer()
54+
55+
let decimalDegrees = try container.decode(Double.self)
56+
57+
self.init(decimalDegrees: decimalDegrees)
58+
}
59+
60+
public func encode(to encoder: Encoder) throws {
61+
var container = encoder.singleValueContainer()
62+
63+
try container.encode(self.decimalDegrees.roundedToPlaces(6))
64+
}
65+
66+
}
67+
68+
extension Position2D: Codable {
69+
70+
public init(from decoder: Decoder) throws {
71+
var container = try decoder.unkeyedContainer()
72+
73+
let longitude = try container.decode(Longitude.self)
74+
let latitude = try container.decode(Latitude.self)
75+
76+
self.init(latitude: latitude, longitude: longitude)
77+
}
78+
79+
public func encode(to encoder: Encoder) throws {
80+
var container = encoder.unkeyedContainer()
81+
82+
try container.encode(self.longitude)
83+
try container.encode(self.latitude)
84+
}
85+
86+
}
87+
88+
extension LinearRingCoordinates {
89+
90+
public init(from decoder: Decoder) throws {
91+
let container = try decoder.singleValueContainer()
92+
93+
let coordinates = try container.decode(Self.RawValue.self)
94+
95+
try self.init(rawValue: coordinates)
96+
}
97+
98+
public func encode(to encoder: Encoder) throws {
99+
var container = encoder.singleValueContainer()
100+
101+
try container.encode(self.rawValue)
102+
}
103+
104+
}
105+
106+
fileprivate enum SingleGeometryCodingKeys: String, CodingKey {
107+
case geoJSONType = "type"
108+
case coordinates
109+
}
110+
111+
extension SingleGeometry {
112+
113+
public init(from decoder: Decoder) throws {
114+
let container = try decoder.container(keyedBy: SingleGeometryCodingKeys.self)
115+
116+
let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType)
117+
guard type == Self.geoJSONType else {
118+
throw DecodingError.typeMismatch(Self.self, DecodingError.Context(
119+
codingPath: container.codingPath,
120+
debugDescription: "Found GeoJSON type '\(type.rawValue)'"
121+
))
122+
}
123+
124+
let coordinates = try container.decode(Self.Coordinates.self, forKey: .coordinates)
125+
126+
self.init(coordinates: coordinates)
127+
}
128+
129+
public func encode(to encoder: Encoder) throws {
130+
var container = encoder.container(keyedBy: SingleGeometryCodingKeys.self)
131+
132+
try container.encode(Self.geoJSONType, forKey: .geoJSONType)
133+
try container.encode(self.coordinates, forKey: .coordinates)
134+
}
135+
136+
}
137+
138+
fileprivate enum AnyGeometryCodingKeys: String, CodingKey {
139+
case geoJSONType = "type"
140+
}
141+
142+
extension AnyGeometry {
143+
144+
public init(from decoder: Decoder) throws {
145+
let typeContainer = try decoder.container(keyedBy: SingleGeometryCodingKeys.self)
146+
let type = try typeContainer.decode(GeoJSON.`Type`.Geometry.self, forKey: .geoJSONType)
147+
148+
let container = try decoder.singleValueContainer()
149+
150+
// FIXME: Fix 2D/3D performance by checking the number of values in `bbox`
151+
switch type {
152+
case .geometryCollection:
153+
let geometryCollection = try container.decode(GeometryCollection.self)
154+
self = .geometryCollection(geometryCollection)
155+
case .point:
156+
// do {
157+
// let point3D = try container.decode(Point3D.self)
158+
// self = .point3D(point3D)
159+
// } catch {
160+
let point2D = try container.decode(Point2D.self)
161+
self = .point2D(point2D)
162+
// }
163+
case .multiPoint:
164+
let multiPoint2D = try container.decode(MultiPoint2D.self)
165+
self = .multiPoint2D(multiPoint2D)
166+
case .lineString:
167+
let lineString2D = try container.decode(LineString2D.self)
168+
self = .lineString2D(lineString2D)
169+
case .multiLineString:
170+
let multiLineString2D = try container.decode(MultiLineString2D.self)
171+
self = .multiLineString2D(multiLineString2D)
172+
case .polygon:
173+
let polygon2D = try container.decode(Polygon2D.self)
174+
self = .polygon2D(polygon2D)
175+
case .multiPolygon:
176+
let multiPolygon2D = try container.decode(MultiPolygon2D.self)
177+
self = .multiPolygon2D(multiPolygon2D)
178+
}
179+
}
180+
181+
public func encode(to encoder: Encoder) throws {
182+
var container = encoder.singleValueContainer()
183+
184+
switch self {
185+
case .geometryCollection(let geometryCollection):
186+
try container.encode(geometryCollection)
187+
case .point2D(let point2D):
188+
try container.encode(point2D)
189+
case .multiPoint2D(let multiPoint2D):
190+
try container.encode(multiPoint2D)
191+
case .lineString2D(let lineString2D):
192+
try container.encode(lineString2D)
193+
case .multiLineString2D(let multiLineString2D):
194+
try container.encode(multiLineString2D)
195+
case .polygon2D(let polygon2D):
196+
try container.encode(polygon2D)
197+
case .multiPolygon2D(let multiPolygon2D):
198+
try container.encode(multiPolygon2D)
199+
}
200+
}
201+
202+
}
203+
204+
extension BoundingBox2D {
205+
206+
public init(from decoder: Decoder) throws {
207+
var container = try decoder.unkeyedContainer()
208+
209+
let westLongitude = try container.decode(Longitude.self)
210+
let southLatitude = try container.decode(Latitude.self)
211+
let eastLongitude = try container.decode(Longitude.self)
212+
let northLatitude = try container.decode(Latitude.self)
213+
214+
self.init(
215+
southWest: Coordinate2D(latitude: southLatitude, longitude: westLongitude),
216+
northEast: Coordinate2D(latitude: northLatitude, longitude: eastLongitude)
217+
)
218+
}
219+
220+
public func encode(to encoder: Encoder) throws {
221+
var container = encoder.unkeyedContainer()
222+
223+
try container.encode(self.westLongitude)
224+
try container.encode(self.southLatitude)
225+
try container.encode(self.eastLongitude)
226+
try container.encode(self.northLatitude)
227+
}
228+
229+
}
230+
231+
extension AnyBoundingBox {
232+
233+
public init(from decoder: Decoder) throws {
234+
let container = try decoder.singleValueContainer()
235+
236+
let boundingBox2D = try container.decode(BoundingBox2D.self)
237+
self = .twoDimensions(boundingBox2D)
238+
}
239+
240+
public func encode(to encoder: Encoder) throws {
241+
var container = encoder.singleValueContainer()
242+
243+
switch self {
244+
case .twoDimensions(let boundingBox2D):
245+
try container.encode(boundingBox2D)
246+
}
247+
}
248+
249+
}
250+
251+
fileprivate enum FeatureCodingKeys: String, CodingKey {
252+
case geoJSONType = "type"
253+
case geometry, properties, bbox
254+
}
255+
256+
extension Feature {
257+
258+
public init(from decoder: Decoder) throws {
259+
let container = try decoder.container(keyedBy: FeatureCodingKeys.self)
260+
261+
let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType)
262+
guard type == Self.geoJSONType else {
263+
throw DecodingError.typeMismatch(Self.self, DecodingError.Context(
264+
codingPath: container.codingPath,
265+
debugDescription: "Found GeoJSON type '\(type.rawValue)'"
266+
))
267+
}
268+
269+
let geometry = try container.decodeIfPresent(AnyGeometry.self, forKey: .geometry)
270+
let properties = try container.decode(Properties.self, forKey: .properties)
271+
272+
self.init(geometry: geometry, properties: properties)
273+
}
274+
275+
public func encode(to encoder: Encoder) throws {
276+
var container = encoder.container(keyedBy: FeatureCodingKeys.self)
277+
278+
try container.encode(Self.geoJSONType, forKey: .geoJSONType)
279+
try container.encodeIfPresent(self.geometry, forKey: .geometry)
280+
try container.encode(self.properties, forKey: .properties)
281+
// TODO: Create GeoJSONEncoder that allows setting "export bboxes" to a boolean value
282+
// TODO: Memoize bboxes not to recompute them all the time (bboxes tree)
283+
try container.encodeIfPresent(self.bbox, forKey: .bbox)
284+
}
285+
286+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// GeometryCollection.swift
3+
// GeoSwift
4+
//
5+
// Created by Rémi Bardon on 04/02/2022.
6+
// Copyright © 2022 Rémi Bardon. All rights reserved.
7+
//
8+
9+
public struct GeometryCollection: Geometry {
10+
11+
public static var geometryType: GeoJSON.`Type`.Geometry { .geometryCollection}
12+
13+
public var bbox: AnyBoundingBox?
14+
15+
public var asAnyGeometry: AnyGeometry { .geometryCollection(self) }
16+
17+
public var geometries: [AnyGeometry]
18+
19+
}

0 commit comments

Comments
 (0)