diff --git a/Sources/GeoJSON/BoundingBox.swift b/Sources/GeoJSON/BoundingBox.swift index 17bbbfb..027ec10 100644 --- a/Sources/GeoJSON/BoundingBox.swift +++ b/Sources/GeoJSON/BoundingBox.swift @@ -40,8 +40,17 @@ public enum AnyBoundingBox: BoundingBox, Hashable, Codable { public static var zero: AnyBoundingBox = .twoDimensions(.zero) public func union(_ other: AnyBoundingBox) -> AnyBoundingBox { - #warning("Implement `AnyBoundingBox.union`") - fatalError("Not implemented yet") + switch (self, other) { + case let (.twoDimensions(self), .twoDimensions(other)): + return .twoDimensions(self.union(other)) + + case let (.twoDimensions(bbox2d), .threeDimensions(bbox3d)), + let (.threeDimensions(bbox3d), .twoDimensions(bbox2d)): + return .threeDimensions(bbox3d.union(bbox2d)) + + case let (.threeDimensions(self), .threeDimensions(other)): + return .threeDimensions(self.union(other)) + } } case twoDimensions(BoundingBox2D) diff --git a/Sources/GeoJSON/GeoJSON+Codable.swift b/Sources/GeoJSON/GeoJSON+Codable.swift index e388457..70059ec 100644 --- a/Sources/GeoJSON/GeoJSON+Codable.swift +++ b/Sources/GeoJSON/GeoJSON+Codable.swift @@ -176,6 +176,39 @@ extension SingleGeometry { } +fileprivate enum GeometryCollectionCodingKeys: String, CodingKey { + case geoJSONType = "type" + case geometries, bbox +} + +extension GeometryCollection { + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: GeometryCollectionCodingKeys.self) + + let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType) + guard type == Self.geoJSONType else { + throw DecodingError.typeMismatch(Self.self, DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Found GeoJSON type '\(type.rawValue)'" + )) + } + + let geometries = try container.decode([AnyGeometry].self, forKey: .geometries) + + self.init(geometries: geometries) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: GeometryCollectionCodingKeys.self) + + try container.encode(Self.geoJSONType, forKey: .geoJSONType) + try container.encode(self.geometries, forKey: .geometries) + try container.encode(self.bbox, forKey: .bbox) + } + +} + fileprivate enum AnyGeometryCodingKeys: String, CodingKey { case geoJSONType = "type" } diff --git a/Sources/GeoJSON/Geometries/GeometryCollection.swift b/Sources/GeoJSON/Geometries/GeometryCollection.swift index 0ffc767..cc1fdc7 100644 --- a/Sources/GeoJSON/Geometries/GeometryCollection.swift +++ b/Sources/GeoJSON/Geometries/GeometryCollection.swift @@ -11,10 +11,14 @@ public struct GeometryCollection: CodableGeometry { public static var geometryType: GeoJSON.`Type`.Geometry { .geometryCollection } - public var _bbox: AnyBoundingBox { asAnyGeometry.bbox } + public var _bbox: AnyBoundingBox { geometries.bbox } public var asAnyGeometry: AnyGeometry { .geometryCollection(self) } public var geometries: [AnyGeometry] + public init(geometries: [AnyGeometry]) { + self.geometries = geometries + } + } diff --git a/Sources/GeoJSON/Objects/FeatureCollection.swift b/Sources/GeoJSON/Objects/FeatureCollection.swift index 46bd03c..bdc26ff 100644 --- a/Sources/GeoJSON/Objects/FeatureCollection.swift +++ b/Sources/GeoJSON/Objects/FeatureCollection.swift @@ -17,8 +17,9 @@ public struct FeatureCollection< public var features: [Feature] - // FIXME: Fix bounding box - public var bbox: AnyBoundingBox? { nil } + public var bbox: AnyBoundingBox? { + features.compactMap(\.bbox).reduce(nil, { $0.union($1.asAny) }) + } } diff --git a/Sources/GeoModels/2D/BoundingBox2D.swift b/Sources/GeoModels/2D/BoundingBox2D.swift index 3661aed..741df10 100644 --- a/Sources/GeoModels/2D/BoundingBox2D.swift +++ b/Sources/GeoModels/2D/BoundingBox2D.swift @@ -145,3 +145,9 @@ extension BoundingBox2D: BoundingBox { } } + +extension BoundingBox2D { + + public func union(_ bbox3d: BoundingBox3D) -> BoundingBox3D { bbox3d.union(self) } + +} diff --git a/Sources/GeoModels/3D/BoundingBox3D.swift b/Sources/GeoModels/3D/BoundingBox3D.swift index 919dde3..4c09a73 100644 --- a/Sources/GeoModels/3D/BoundingBox3D.swift +++ b/Sources/GeoModels/3D/BoundingBox3D.swift @@ -110,3 +110,12 @@ extension BoundingBox3D: BoundingBox { } } + +extension BoundingBox3D { + + public func union(_ bbox2d: BoundingBox2D) -> BoundingBox3D { + let other = BoundingBox3D(bbox2d, lowAltitude: self.lowAltitude, zHeight: .zero) + return self.union(other) + } + +} diff --git a/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift b/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift index 66522eb..725f8fb 100644 --- a/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift +++ b/Tests/GeoJSONTests/GeoJSON+EncodableTests.swift @@ -199,4 +199,84 @@ final class GeoJSONEncodableTests: XCTestCase { XCTAssertEqual(string, expected) } + func testFeatureCollectionOfGeometryCollectionEncode() throws { + struct FeatureProperties: Hashable, Codable {} + + let featureCollection: FeatureCollection = FeatureCollection(features: [ + Feature( + geometry: GeometryCollection(geometries: [ + .point2D(Point2D(coordinates: .nantes)), + .point2D(Point2D(coordinates: .bordeaux)), + ]), + properties: FeatureProperties() + ), + Feature( + geometry: GeometryCollection(geometries: [ + .point2D(Point2D(coordinates: .paris)), + .point2D(Point2D(coordinates: .marseille)), + ]), + properties: FeatureProperties() + ), + ]) + let data: Data = try JSONEncoder().encode(featureCollection) + let string: String = try XCTUnwrap(String(data: data, encoding: .utf8)) + + let expected: String = [ + "{", + "\"type\":\"FeatureCollection\",", + // For some reason, `"bbox"` goes here 🤷 + "\"bbox\":[-1.55366,43.29868,5.36468,48.85719],", + "\"features\":[", + "{", + // For some reason, `"properties"` goes here 🤷 + "\"properties\":{},", + "\"type\":\"Feature\",", + "\"geometry\":{", + "\"type\":\"GeometryCollection\",", + // For some reason, `"bbox"` goes here 🤷 + "\"bbox\":[-1.55366,44.8378,-0.58143,47.21881],", + "\"geometries\":[", + "{", + "\"type\":\"Point\",", + "\"coordinates\":[-1.55366,47.21881],", + "\"bbox\":[-1.55366,47.21881,-1.55366,47.21881]", + "},", + "{", + "\"type\":\"Point\",", + "\"coordinates\":[-0.58143,44.8378],", + "\"bbox\":[-0.58143,44.8378,-0.58143,44.8378]", + "}", + "]", + "},", + "\"bbox\":[-1.55366,44.8378,-0.58143,47.21881]", + "},", + "{", + // For some reason, `"properties"` goes here 🤷 + "\"properties\":{},", + "\"type\":\"Feature\",", + "\"geometry\":{", + "\"type\":\"GeometryCollection\",", + // For some reason, `"bbox"` goes here 🤷 + "\"bbox\":[2.3529,43.29868,5.36468,48.85719],", + "\"geometries\":[", + "{", + "\"type\":\"Point\",", + "\"coordinates\":[2.3529,48.85719],", + "\"bbox\":[2.3529,48.85719,2.3529,48.85719]", + "},", + "{", + "\"type\":\"Point\",", + "\"coordinates\":[5.36468,43.29868],", + "\"bbox\":[5.36468,43.29868,5.36468,43.29868]", + "}", + "]", + "},", + "\"bbox\":[2.3529,43.29868,5.36468,48.85719]", + "}", + "]", + "}", + ].joined() + XCTAssertEqual(string, expected) + } + }