Skip to content

Commit cbbb513

Browse files
authored
Improve boundingBox handling (#68)
1 parent c653afc commit cbbb513

14 files changed

+466
-96
lines changed

Sources/GISTools/Algorithms/BooleanPointInPolygon.swift

+9-5
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ extension Polygon {
8888
ignoreBoundary: Bool = false)
8989
-> Bool
9090
{
91-
if let boundingBox = boundingBox, !boundingBox.contains(coordinate) {
91+
if let boundingBox, !boundingBox.contains(coordinate) {
9292
return false
9393
}
9494

95-
guard let outerRing = outerRing, outerRing.contains(coordinate, ignoreBoundary: ignoreBoundary) else {
96-
return false
97-
}
95+
guard let outerRing = outerRing,
96+
outerRing.contains(coordinate, ignoreBoundary: ignoreBoundary)
97+
else { return false }
9898

99-
if let innerRings = innerRings {
99+
if let innerRings {
100100
for ring in innerRings {
101101
if ring.contains(coordinate, ignoreBoundary: ignoreBoundary) {
102102
return false
@@ -140,6 +140,10 @@ extension MultiPolygon {
140140
ignoreBoundary: Bool = false)
141141
-> Bool
142142
{
143+
if let boundingBox, !boundingBox.contains(coordinate) {
144+
return false
145+
}
146+
143147
for polygon in polygons {
144148
if polygon.contains(coordinate, ignoreBoundary: ignoreBoundary) {
145149
return true

Sources/GISTools/GeoJson/BoundingBoxRepresentable.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extension BoundingBoxRepresentable {
3131

3232
@discardableResult
3333
public mutating func updateBoundingBox(onlyIfNecessary ifNecessary: Bool = true) -> BoundingBox? {
34-
if boundingBox != nil && ifNecessary { return nil }
34+
if boundingBox != nil && ifNecessary { return boundingBox }
3535
boundingBox = calculateBoundingBox()
3636
return boundingBox
3737
}

Sources/GISTools/GeoJson/Feature.swift

+17-7
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public struct Feature:
7979
}
8080

8181
public var type: GeoJsonType {
82-
return .feature
82+
.feature
8383
}
8484

8585
public var projection: Projection {
@@ -90,7 +90,7 @@ public struct Feature:
9090
public var id: Identifier?
9191

9292
/// The `Feature`s geometry object.
93-
public let geometry: GeoJsonGeometry
93+
public private(set) var geometry: GeoJsonGeometry
9494

9595
public var allCoordinates: [Coordinate3D] {
9696
geometry.allCoordinates
@@ -115,7 +115,7 @@ public struct Feature:
115115
self.properties = properties
116116

117117
if calculateBoundingBox {
118-
self.boundingBox = self.calculateBoundingBox()
118+
self.updateBoundingBox()
119119
}
120120
}
121121

@@ -134,8 +134,8 @@ public struct Feature:
134134
self.properties = (geoJson["properties"] as? [String: Sendable]) ?? [:]
135135
self.boundingBox = Feature.tryCreate(json: geoJson["bbox"])
136136

137-
if calculateBoundingBox, self.boundingBox == nil {
138-
self.boundingBox = self.calculateBoundingBox()
137+
if calculateBoundingBox {
138+
self.updateBoundingBox()
139139
}
140140

141141
if geoJson.count > 3 {
@@ -170,8 +170,18 @@ public struct Feature:
170170

171171
extension Feature {
172172

173+
@discardableResult
174+
public mutating func updateBoundingBox(onlyIfNecessary ifNecessary: Bool = true) -> BoundingBox? {
175+
geometry.updateBoundingBox(onlyIfNecessary: ifNecessary)
176+
177+
if boundingBox != nil && ifNecessary { return boundingBox }
178+
179+
boundingBox = calculateBoundingBox()
180+
return boundingBox
181+
}
182+
173183
public func calculateBoundingBox() -> BoundingBox? {
174-
return geometry.boundingBox ?? geometry.calculateBoundingBox()
184+
geometry.boundingBox ?? geometry.calculateBoundingBox()
175185
}
176186

177187
public func intersects(_ otherBoundingBox: BoundingBox) -> Bool {
@@ -227,7 +237,7 @@ extension Feature {
227237

228238
/// Returns a property by key.
229239
public func property<T: Sendable>(for key: String) -> T? {
230-
return properties[key] as? T
240+
properties[key] as? T
231241
}
232242

233243
/// Set a property key/value pair.

Sources/GISTools/GeoJson/FeatureCollection.swift

+27-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public struct FeatureCollection:
77
{
88

99
public var type: GeoJsonType {
10-
return .featureCollection
10+
.featureCollection
1111
}
1212

1313
public var projection: Projection {
@@ -39,7 +39,7 @@ public struct FeatureCollection:
3939
self.features = features
4040

4141
if calculateBoundingBox {
42-
self.boundingBox = self.calculateBoundingBox()
42+
self.updateBoundingBox()
4343
}
4444
}
4545

@@ -48,7 +48,7 @@ public struct FeatureCollection:
4848
self.features = geometries.compactMap { Feature($0) }
4949

5050
if calculateBoundingBox {
51-
self.boundingBox = self.calculateBoundingBox()
51+
self.updateBoundingBox()
5252
}
5353
}
5454

@@ -74,8 +74,8 @@ public struct FeatureCollection:
7474
return nil
7575
}
7676

77-
if calculateBoundingBox, self.boundingBox == nil {
78-
self.boundingBox = self.calculateBoundingBox()
77+
if calculateBoundingBox {
78+
self.updateBoundingBox()
7979
}
8080
}
8181

@@ -102,8 +102,8 @@ public struct FeatureCollection:
102102
self.features = features
103103
self.boundingBox = FeatureCollection.tryCreate(json: geoJson["bbox"])
104104

105-
if calculateBoundingBox, self.boundingBox == nil {
106-
self.boundingBox = self.calculateBoundingBox()
105+
if calculateBoundingBox {
106+
self.updateBoundingBox()
107107
}
108108

109109
if geoJson.count > 2 {
@@ -133,6 +133,20 @@ public struct FeatureCollection:
133133

134134
extension FeatureCollection {
135135

136+
@discardableResult
137+
public mutating func updateBoundingBox(onlyIfNecessary ifNecessary: Bool = true) -> BoundingBox? {
138+
mapFeatures { feature in
139+
var feature = feature
140+
feature.updateBoundingBox(onlyIfNecessary: ifNecessary)
141+
return feature
142+
}
143+
144+
if boundingBox != nil && ifNecessary { return boundingBox }
145+
146+
boundingBox = calculateBoundingBox()
147+
return boundingBox
148+
}
149+
136150
public func calculateBoundingBox() -> BoundingBox? {
137151
let featureBoundingBoxes: [BoundingBox] = features.compactMap({ $0.boundingBox ?? $0.calculateBoundingBox() })
138152
guard !featureBoundingBoxes.isEmpty else { return nil}
@@ -201,7 +215,7 @@ extension FeatureCollection {
201215
}
202216

203217
if boundingBox != nil {
204-
boundingBox = calculateBoundingBox()
218+
updateBoundingBox(onlyIfNecessary: false)
205219
}
206220
}
207221

@@ -214,17 +228,19 @@ extension FeatureCollection {
214228
features.append(feature)
215229

216230
if boundingBox != nil {
217-
boundingBox = calculateBoundingBox()
231+
updateBoundingBox(onlyIfNecessary: false)
218232
}
219233
}
220234

221235
/// Remove a Feature from the receiver.
222236
@discardableResult
223-
public mutating func removeFeature(at index: Int) -> Feature {
237+
public mutating func removeFeature(at index: Int) -> Feature? {
238+
guard index >= 0, index < features.count else { return nil }
239+
224240
let removedFeature = features.remove(at: index)
225241

226242
if boundingBox != nil {
227-
boundingBox = calculateBoundingBox()
243+
updateBoundingBox(onlyIfNecessary: false)
228244
}
229245

230246
return removedFeature

Sources/GISTools/GeoJson/GeoJson.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ extension GeoJson {
9292

9393
/// Any foreign member by key.
9494
public func foreignMember<T: Sendable>(for key: String) -> T? {
95-
return foreignMembers[key] as? T
95+
foreignMembers[key] as? T
9696
}
9797

9898
/// Set a foreign member key/value pair.

Sources/GISTools/GeoJson/GeoJsonConvertible.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ extension Sequence where Self.Iterator.Element: GeoJsonWritable {
100100
///
101101
/// - important: Always projected to EPSG:4326, unless the coordinate has no SRID.
102102
public var asJson: [[String: Sendable]] {
103-
return self.map({ $0.asJson })
103+
self.map({ $0.asJson })
104104
}
105105

106106
}

Sources/GISTools/GeoJson/GeometryCollection.swift

+86-5
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import Foundation
44
public struct GeometryCollection: GeoJsonGeometry {
55

66
public var type: GeoJsonType {
7-
return .geometryCollection
7+
.geometryCollection
88
}
99

1010
public var projection: Projection {
1111
geometries.first?.projection ?? .noSRID
1212
}
1313

1414
/// The GeometryCollection's geometry objects.
15-
public let geometries: [GeoJsonGeometry]
15+
public private(set) var geometries: [GeoJsonGeometry]
1616

1717
public var allCoordinates: [Coordinate3D] {
1818
geometries.flatMap(\.allCoordinates)
@@ -32,7 +32,7 @@ public struct GeometryCollection: GeoJsonGeometry {
3232
self.geometries = geometries
3333

3434
if calculateBoundingBox {
35-
self.boundingBox = self.calculateBoundingBox()
35+
self.updateBoundingBox()
3636
}
3737
}
3838

@@ -49,8 +49,8 @@ public struct GeometryCollection: GeoJsonGeometry {
4949
self.geometries = geometries
5050
self.boundingBox = GeometryCollection.tryCreate(json: geoJson["bbox"])
5151

52-
if calculateBoundingBox, self.boundingBox == nil {
53-
self.boundingBox = self.calculateBoundingBox()
52+
if calculateBoundingBox {
53+
self.updateBoundingBox()
5454
}
5555

5656
if geoJson.count > 2 {
@@ -80,6 +80,20 @@ public struct GeometryCollection: GeoJsonGeometry {
8080

8181
extension GeometryCollection {
8282

83+
@discardableResult
84+
public mutating func updateBoundingBox(onlyIfNecessary ifNecessary: Bool = true) -> BoundingBox? {
85+
mapGeometries { geometry in
86+
var geometry = geometry
87+
geometry.updateBoundingBox(onlyIfNecessary: ifNecessary)
88+
return geometry
89+
}
90+
91+
if boundingBox != nil && ifNecessary { return boundingBox }
92+
93+
boundingBox = calculateBoundingBox()
94+
return boundingBox
95+
}
96+
8397
public func calculateBoundingBox() -> BoundingBox? {
8498
let geometryBoundingBoxes: [BoundingBox] = geometries.compactMap({ $0.boundingBox ?? $0.calculateBoundingBox() })
8599
guard !geometryBoundingBoxes.isEmpty else { return nil }
@@ -130,3 +144,70 @@ extension GeometryCollection {
130144
}
131145

132146
}
147+
148+
// MARK: - Geometries
149+
150+
extension GeometryCollection {
151+
152+
/// Insert a GeoJsonGeometry into the receiver.
153+
///
154+
/// - note: `geometry` must be in the same projection as the receiver.
155+
public mutating func insertGeometry(_ geometry: GeoJsonGeometry, atIndex index: Int) {
156+
guard geometries.count == 0 || projection == geometry.projection else { return }
157+
158+
if index < geometries.count {
159+
geometries.insert(geometry, at: index)
160+
}
161+
else {
162+
geometries.append(geometry)
163+
}
164+
165+
if boundingBox != nil {
166+
updateBoundingBox(onlyIfNecessary: false)
167+
}
168+
}
169+
170+
/// Append a GeoJsonGeometry to the receiver.
171+
///
172+
/// - note: `geometry` must be in the same projection as the receiver.
173+
public mutating func appendGeometry(_ geometry: GeoJsonGeometry) {
174+
guard geometries.count == 0 || projection == geometry.projection else { return }
175+
176+
geometries.append(geometry)
177+
178+
if boundingBox != nil {
179+
updateBoundingBox(onlyIfNecessary: false)
180+
}
181+
}
182+
183+
/// Remove a GeoJsonGeometry from the receiver.
184+
@discardableResult
185+
public mutating func removeGeometry(at index: Int) -> GeoJsonGeometry? {
186+
guard index >= 0, index < geometries.count else { return nil }
187+
188+
let removedGeometry = geometries.remove(at: index)
189+
190+
if boundingBox != nil {
191+
updateBoundingBox(onlyIfNecessary: false)
192+
}
193+
194+
return removedGeometry
195+
}
196+
197+
/// Map Geometries in-place.
198+
public mutating func mapGeometries(_ transform: (GeoJsonGeometry) -> GeoJsonGeometry) {
199+
geometries = geometries.map(transform)
200+
}
201+
202+
/// Map Geometries in-place, removing *nil* values.
203+
public mutating func compactMapGeometries(_ transform: (GeoJsonGeometry) -> GeoJsonGeometry?) {
204+
geometries = geometries.compactMap(transform)
205+
}
206+
207+
/// Filter Geometries in-place.
208+
public mutating func filterGeometries(_ isIncluded: (GeoJsonGeometry) -> Bool) {
209+
geometries = geometries.filter(isIncluded)
210+
}
211+
212+
213+
}

Sources/GISTools/GeoJson/LineSegment.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ extension LineSegment {
3939

4040
/// The receiver's two coordinates.
4141
public var coordinates: [Coordinate3D] {
42-
return [first, second]
42+
[first, second]
4343
}
4444

4545
}
@@ -70,7 +70,9 @@ extension LineSegment {
7070
second: CLLocationCoordinate2D,
7171
calculateBoundingBox: Bool = false)
7272
{
73-
self.init(first: Coordinate3D(first), second: Coordinate3D(second), calculateBoundingBox: calculateBoundingBox)
73+
self.init(first: Coordinate3D(first),
74+
second: Coordinate3D(second),
75+
calculateBoundingBox: calculateBoundingBox)
7476
}
7577

7678
/// Initialize a LineSegment with two locations.
@@ -79,7 +81,9 @@ extension LineSegment {
7981
second: CLLocation,
8082
calculateBoundingBox: Bool = false)
8183
{
82-
self.init(first: Coordinate3D(first), second: Coordinate3D(second), calculateBoundingBox: calculateBoundingBox)
84+
self.init(first: Coordinate3D(first),
85+
second: Coordinate3D(second),
86+
calculateBoundingBox: calculateBoundingBox)
8387
}
8488

8589
}
@@ -90,7 +94,7 @@ extension LineSegment {
9094
extension LineSegment: BoundingBoxRepresentable {
9195

9296
public func calculateBoundingBox() -> BoundingBox? {
93-
return BoundingBox(coordinates: coordinates)
97+
BoundingBox(coordinates: coordinates)
9498
}
9599

96100
public func intersects(_ otherBoundingBox: BoundingBox) -> Bool {

0 commit comments

Comments
 (0)