Skip to content

Commit 0aa9cda

Browse files
committed
✨ Add "id" to Feature
1 parent c3f4594 commit 0aa9cda

File tree

9 files changed

+127
-23
lines changed

9 files changed

+127
-23
lines changed

Sources/GeoJSON/FeatureProperties.swift

Lines changed: 0 additions & 10 deletions
This file was deleted.

Sources/GeoJSON/GeoJSON+Codable.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,12 +375,13 @@ extension AnyBoundingBox {
375375

376376
fileprivate enum FeatureCodingKeys: String, CodingKey {
377377
case geoJSONType = "type"
378-
case geometry, properties, bbox
378+
case id, geometry, properties, bbox
379379
}
380380

381381
extension Feature {
382382

383383
public init(from decoder: Decoder) throws {
384+
print(String(describing: ID.self))
384385
let container = try decoder.container(keyedBy: FeatureCodingKeys.self)
385386

386387
let type = try container.decode(GeoJSON.`Type`.self, forKey: .geoJSONType)
@@ -391,16 +392,18 @@ extension Feature {
391392
))
392393
}
393394

395+
let id = try container.decodeIfPresent(ID.self, forKey: .id)
394396
let geometry = try container.decodeIfPresent(Geometry.self, forKey: .geometry)
395397
let properties = try container.decode(Properties.self, forKey: .properties)
396398

397-
self.init(geometry: geometry, properties: properties)
399+
self.init(id: id, geometry: geometry, properties: properties)
398400
}
399401

400402
public func encode(to encoder: Encoder) throws {
401403
var container = encoder.container(keyedBy: FeatureCodingKeys.self)
402404

403405
try container.encode(Self.geoJSONType, forKey: .geoJSONType)
406+
try container.encodeIfPresent(self.id, forKey: .id)
404407
try container.encodeIfPresent(self.geometry, forKey: .geometry)
405408
try container.encode(self.properties, forKey: .properties)
406409
// TODO: Create GeoJSONEncoder that allows setting "export bboxes" to a boolean value
@@ -429,7 +432,7 @@ extension FeatureCollection {
429432
}
430433

431434
let features = try container.decodeIfPresent(
432-
[Feature<Geometry, Properties>].self,
435+
[Feature<ID, Geometry, Properties>].self,
433436
forKey: .features
434437
) ?? []
435438

Sources/GeoJSON/Helpers/NonID.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//
2+
// NonID.swift
3+
// GeoSwift
4+
//
5+
// Created by Rémi Bardon on 09/02/2022.
6+
// Copyright © 2022 Rémi Bardon. All rights reserved.
7+
//
8+
9+
public typealias NonID = Bool

Sources/GeoJSON/Objects/Feature.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,53 @@
88

99
/// A [GeoJSON Feature](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2).
1010
public struct Feature<
11+
ID: Codable,
1112
Geometry: GeoJSON.Geometry & Codable,
12-
Properties: GeoJSON.FeatureProperties
13+
Properties: Codable
1314
>: CodableObject {
1415

1516
public static var geoJSONType: GeoJSON.`Type` { .feature }
1617

1718
public var bbox: Geometry.BoundingBox? { geometry?.bbox }
1819

20+
public var id: ID?
1921
public var geometry: Geometry?
22+
/// The `"properties"` field of a [GeoJSON Feature](https://datatracker.ietf.org/doc/html/rfc7946#section-3.2).
2023
public var properties: Properties
2124

22-
public init(geometry: Geometry?, properties: Properties) {
25+
public init(id: ID?, geometry: Geometry?, properties: Properties) {
26+
self.id = id
2327
self.geometry = geometry
2428
self.properties = properties
2529
}
2630

31+
public init(geometry: Geometry?, properties: Properties) where ID == NonID {
32+
self.id = nil
33+
self.geometry = geometry
34+
self.properties = properties
35+
}
36+
37+
}
38+
39+
extension Feature: Identifiable where ID: Hashable {}
40+
extension Feature: Equatable where ID: Equatable, Properties: Equatable {
41+
42+
public static func == (lhs: Self, rhs: Self) -> Bool {
43+
return lhs.id == rhs.id
44+
&& lhs.geometry == rhs.geometry
45+
&& lhs.properties == rhs.properties
46+
}
47+
48+
}
49+
extension Feature: Hashable where ID: Hashable, Properties: Hashable {
50+
51+
public func hash(into hasher: inout Hasher) {
52+
hasher.combine(id)
53+
hasher.combine(geometry)
54+
hasher.combine(properties)
55+
}
56+
2757
}
2858

2959
/// A (half) type-erased ``Feature``.
30-
public typealias AnyFeature<Properties: GeoJSON.FeatureProperties> = Feature<AnyGeometry, Properties>
60+
public typealias AnyFeature<Properties: Codable> = Feature<NonID, AnyGeometry, Properties>

Sources/GeoJSON/Objects/FeatureCollection.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,37 @@
88

99
/// A [GeoJSON FeatureCollection](https://datatracker.ietf.org/doc/html/rfc7946#section-3.3).
1010
public struct FeatureCollection<
11+
ID: Codable,
1112
Geometry: GeoJSON.Geometry & Codable,
12-
Properties: GeoJSON.FeatureProperties
13+
Properties: Codable
1314
>: CodableObject {
1415

1516
public static var geoJSONType: GeoJSON.`Type` { .featureCollection }
1617

17-
public var features: [Feature<Geometry, Properties>]
18+
public var features: [Feature<ID, Geometry, Properties>]
1819

1920
// FIXME: Fix bounding box
2021
public var bbox: AnyBoundingBox? { nil }
2122

2223
}
2324

25+
extension FeatureCollection: Equatable where ID: Equatable, Properties: Equatable {
26+
27+
public static func == (lhs: Self, rhs: Self) -> Bool {
28+
return lhs.features == rhs.features
29+
}
30+
31+
}
32+
33+
extension FeatureCollection: Hashable where ID: Hashable, Properties: Hashable {
34+
35+
public func hash(into hasher: inout Hasher) {
36+
hasher.combine(features)
37+
}
38+
39+
}
40+
2441
/// A (half) type-erased ``FeatureCollection``.
2542
public typealias AnyFeatureCollection<
26-
Properties: GeoJSON.FeatureProperties
27-
> = FeatureCollection<AnyGeometry, Properties>
43+
Properties: Codable
44+
> = FeatureCollection<NonID, AnyGeometry, Properties>

Sources/GeoJSON/Objects/Geometry.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Turf
1010

1111
/// A [GeoJSON Geometry](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1).
12-
public protocol Geometry: GeoJSON.Object {
12+
public protocol Geometry: GeoJSON.Object, Hashable {
1313

1414
var bbox: BoundingBox? { get }
1515

Sources/GeoJSON/Objects/Object.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//
88

99
/// A [GeoJSON Object](https://datatracker.ietf.org/doc/html/rfc7946#section-3).
10-
public protocol Object: Hashable {
10+
public protocol Object {
1111

1212
associatedtype BoundingBox: GeoJSON.BoundingBox
1313

Tests/GeoJSONTests/GeoJSON+DecodableTests.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ final class GeoJSONDecodableTests: XCTestCase {
170170
].joined()
171171

172172
let data: Data = try XCTUnwrap(string.data(using: .utf8))
173-
let feature = try JSONDecoder().decode(Feature<Point2D, FeatureProperties>.self, from: data)
173+
let feature = try JSONDecoder().decode(Feature<NonID, Point2D, FeatureProperties>.self, from: data)
174174

175175
let expected: Feature = Feature(
176176
geometry: Point2D(coordinates: .nantes),
@@ -179,6 +179,33 @@ final class GeoJSONDecodableTests: XCTestCase {
179179
XCTAssertEqual(feature, expected)
180180
}
181181

182+
func testFeature2DWithIDDecode() throws {
183+
struct FeatureProperties: Hashable, Codable {}
184+
185+
let string: String = [
186+
"{",
187+
"\"type\":\"Feature\",",
188+
"\"id\":\"feature_id\",",
189+
"\"geometry\":{",
190+
"\"type\":\"Point\",",
191+
"\"coordinates\":[-1.55366,47.21881]",
192+
"},",
193+
"\"properties\":{},",
194+
"\"bbox\":[-1.55366,47.21881,-1.55366,47.21881]",
195+
"}",
196+
].joined()
197+
198+
let data: Data = try XCTUnwrap(string.data(using: .utf8))
199+
let feature = try JSONDecoder().decode(Feature<String, Point2D, FeatureProperties>.self, from: data)
200+
201+
let expected: Feature = Feature(
202+
id: "feature_id",
203+
geometry: Point2D(coordinates: .nantes),
204+
properties: FeatureProperties()
205+
)
206+
XCTAssertEqual(feature, expected)
207+
}
208+
182209
// MARK: Real-world use cases
183210

184211
func testDecodeFeatureProperties() throws {

Tests/GeoJSONTests/GeoJSON+EncodableTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,32 @@ final class GeoJSONEncodableTests: XCTestCase {
163163
XCTAssertEqual(string, expected)
164164
}
165165

166+
func testFeature2DWithIDEncode() throws {
167+
struct FeatureProperties: Hashable, Codable {}
168+
169+
let feature: Feature = Feature(
170+
id: "feature_id",
171+
geometry: Point2D(coordinates: .nantes),
172+
properties: FeatureProperties()
173+
)
174+
let data: Data = try JSONEncoder().encode(feature)
175+
let string: String = try XCTUnwrap(String(data: data, encoding: .utf8))
176+
177+
let expected: String = [
178+
"{",
179+
// For some reason, `"id"` goes here 🤷
180+
"\"id\":\"feature_id\",",
181+
// For some reason, `"properties"` goes here 🤷
182+
"\"properties\":{},",
183+
"\"type\":\"Feature\",",
184+
"\"geometry\":{",
185+
"\"type\":\"Point\",",
186+
"\"coordinates\":[-1.55366,47.21881]",
187+
"},",
188+
"\"bbox\":[-1.55366,47.21881,-1.55366,47.21881]",
189+
"}",
190+
].joined()
191+
XCTAssertEqual(string, expected)
192+
}
193+
166194
}

0 commit comments

Comments
 (0)