Skip to content

Commit d1b68c2

Browse files
committed
✨ Implement AnyBoundingBox.union
Also fix `GeometryCollection` encoding
1 parent 990a409 commit d1b68c2

File tree

7 files changed

+147
-5
lines changed

7 files changed

+147
-5
lines changed

Sources/GeoJSON/BoundingBox.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ public enum AnyBoundingBox: BoundingBox, Hashable, Codable {
4040
public static var zero: AnyBoundingBox = .twoDimensions(.zero)
4141

4242
public func union(_ other: AnyBoundingBox) -> AnyBoundingBox {
43-
#warning("Implement `AnyBoundingBox.union`")
44-
fatalError("Not implemented yet")
43+
switch (self, other) {
44+
case let (.twoDimensions(self), .twoDimensions(other)):
45+
return .twoDimensions(self.union(other))
46+
47+
case let (.twoDimensions(bbox2d), .threeDimensions(bbox3d)),
48+
let (.threeDimensions(bbox3d), .twoDimensions(bbox2d)):
49+
return .threeDimensions(bbox3d.union(bbox2d))
50+
51+
case let (.threeDimensions(self), .threeDimensions(other)):
52+
return .threeDimensions(self.union(other))
53+
}
4554
}
4655

4756
case twoDimensions(BoundingBox2D)

Sources/GeoJSON/GeoJSON+Codable.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,39 @@ extension SingleGeometry {
176176

177177
}
178178

179+
fileprivate enum GeometryCollectionCodingKeys: String, CodingKey {
180+
case geoJSONType = "type"
181+
case geometries, bbox
182+
}
183+
184+
extension GeometryCollection {
185+
186+
public init(from decoder: Decoder) throws {
187+
let container = try decoder.container(keyedBy: GeometryCollectionCodingKeys.self)
188+
189+
let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType)
190+
guard type == Self.geoJSONType else {
191+
throw DecodingError.typeMismatch(Self.self, DecodingError.Context(
192+
codingPath: container.codingPath,
193+
debugDescription: "Found GeoJSON type '\(type.rawValue)'"
194+
))
195+
}
196+
197+
let geometries = try container.decode([AnyGeometry].self, forKey: .geometries)
198+
199+
self.init(geometries: geometries)
200+
}
201+
202+
public func encode(to encoder: Encoder) throws {
203+
var container = encoder.container(keyedBy: GeometryCollectionCodingKeys.self)
204+
205+
try container.encode(Self.geoJSONType, forKey: .geoJSONType)
206+
try container.encode(self.geometries, forKey: .geometries)
207+
try container.encode(self.bbox, forKey: .bbox)
208+
}
209+
210+
}
211+
179212
fileprivate enum AnyGeometryCodingKeys: String, CodingKey {
180213
case geoJSONType = "type"
181214
}

Sources/GeoJSON/Geometries/GeometryCollection.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ public struct GeometryCollection: CodableGeometry {
1111

1212
public static var geometryType: GeoJSON.`Type`.Geometry { .geometryCollection }
1313

14-
public var _bbox: AnyBoundingBox { asAnyGeometry.bbox }
14+
public var _bbox: AnyBoundingBox { geometries.bbox }
1515

1616
public var asAnyGeometry: AnyGeometry { .geometryCollection(self) }
1717

1818
public var geometries: [AnyGeometry]
1919

20+
public init(geometries: [AnyGeometry]) {
21+
self.geometries = geometries
22+
}
23+
2024
}

Sources/GeoJSON/Objects/FeatureCollection.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ public struct FeatureCollection<
1717

1818
public var features: [Feature<ID, Geometry, Properties>]
1919

20-
// FIXME: Fix bounding box
21-
public var bbox: AnyBoundingBox? { nil }
20+
public var bbox: AnyBoundingBox? {
21+
features.compactMap(\.bbox).reduce(nil, { $0.union($1.asAny) })
22+
}
2223

2324
}
2425

Sources/GeoModels/2D/BoundingBox2D.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,9 @@ extension BoundingBox2D: BoundingBox {
145145
}
146146

147147
}
148+
149+
extension BoundingBox2D {
150+
151+
public func union(_ bbox3d: BoundingBox3D) -> BoundingBox3D { bbox3d.union(self) }
152+
153+
}

Sources/GeoModels/3D/BoundingBox3D.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,12 @@ extension BoundingBox3D: BoundingBox {
110110
}
111111

112112
}
113+
114+
extension BoundingBox3D {
115+
116+
public func union(_ bbox2d: BoundingBox2D) -> BoundingBox3D {
117+
let other = BoundingBox3D(bbox2d, lowAltitude: self.lowAltitude, zHeight: .zero)
118+
return self.union(other)
119+
}
120+
121+
}

Tests/GeoJSONTests/GeoJSON+EncodableTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,84 @@ final class GeoJSONEncodableTests: XCTestCase {
199199
XCTAssertEqual(string, expected)
200200
}
201201

202+
func testFeatureCollectionOfGeometryCollectionEncode() throws {
203+
struct FeatureProperties: Hashable, Codable {}
204+
205+
let featureCollection: FeatureCollection = FeatureCollection(features: [
206+
Feature(
207+
geometry: GeometryCollection(geometries: [
208+
.point2D(Point2D(coordinates: .nantes)),
209+
.point2D(Point2D(coordinates: .bordeaux)),
210+
]),
211+
properties: FeatureProperties()
212+
),
213+
Feature(
214+
geometry: GeometryCollection(geometries: [
215+
.point2D(Point2D(coordinates: .paris)),
216+
.point2D(Point2D(coordinates: .marseille)),
217+
]),
218+
properties: FeatureProperties()
219+
),
220+
])
221+
let data: Data = try JSONEncoder().encode(featureCollection)
222+
let string: String = try XCTUnwrap(String(data: data, encoding: .utf8))
223+
224+
let expected: String = [
225+
"{",
226+
"\"type\":\"FeatureCollection\",",
227+
// For some reason, `"bbox"` goes here 🤷
228+
"\"bbox\":[-1.55366,43.29868,5.36468,48.85719],",
229+
"\"features\":[",
230+
"{",
231+
// For some reason, `"properties"` goes here 🤷
232+
"\"properties\":{},",
233+
"\"type\":\"Feature\",",
234+
"\"geometry\":{",
235+
"\"type\":\"GeometryCollection\",",
236+
// For some reason, `"bbox"` goes here 🤷
237+
"\"bbox\":[-1.55366,44.8378,-0.58143,47.21881],",
238+
"\"geometries\":[",
239+
"{",
240+
"\"type\":\"Point\",",
241+
"\"coordinates\":[-1.55366,47.21881],",
242+
"\"bbox\":[-1.55366,47.21881,-1.55366,47.21881]",
243+
"},",
244+
"{",
245+
"\"type\":\"Point\",",
246+
"\"coordinates\":[-0.58143,44.8378],",
247+
"\"bbox\":[-0.58143,44.8378,-0.58143,44.8378]",
248+
"}",
249+
"]",
250+
"},",
251+
"\"bbox\":[-1.55366,44.8378,-0.58143,47.21881]",
252+
"},",
253+
"{",
254+
// For some reason, `"properties"` goes here 🤷
255+
"\"properties\":{},",
256+
"\"type\":\"Feature\",",
257+
"\"geometry\":{",
258+
"\"type\":\"GeometryCollection\",",
259+
// For some reason, `"bbox"` goes here 🤷
260+
"\"bbox\":[2.3529,43.29868,5.36468,48.85719],",
261+
"\"geometries\":[",
262+
"{",
263+
"\"type\":\"Point\",",
264+
"\"coordinates\":[2.3529,48.85719],",
265+
"\"bbox\":[2.3529,48.85719,2.3529,48.85719]",
266+
"},",
267+
"{",
268+
"\"type\":\"Point\",",
269+
"\"coordinates\":[5.36468,43.29868],",
270+
"\"bbox\":[5.36468,43.29868,5.36468,43.29868]",
271+
"}",
272+
"]",
273+
"},",
274+
"\"bbox\":[2.3529,43.29868,5.36468,48.85719]",
275+
"}",
276+
"]",
277+
"}",
278+
].joined()
279+
XCTAssertEqual(string, expected)
280+
}
281+
202282
}

0 commit comments

Comments
 (0)