Skip to content

Commit

Permalink
✨ Implement AnyBoundingBox.union
Browse files Browse the repository at this point in the history
Also fix `GeometryCollection` encoding
  • Loading branch information
RemiBardon committed Feb 2, 2023
1 parent 29b746d commit 739ec70
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 5 deletions.
13 changes: 11 additions & 2 deletions Sources/GeoJSON/BoundingBox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions Sources/GeoJSON/GeoJSON+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/GeoJSON/Geometries/GeometryCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

}
5 changes: 3 additions & 2 deletions Sources/GeoJSON/Objects/FeatureCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ public struct FeatureCollection<

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

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

}

Expand Down
153 changes: 153 additions & 0 deletions Sources/GeoModels/2D/BoundingBox2D.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// BoundingBox2D.swift
// SwiftGeo
//
// Created by Rémi Bardon on 02/02/2022.
// Copyright © 2022 Rémi Bardon. All rights reserved.
//

public struct BoundingBox2D: Hashable {

public var southWest: Coordinate2D
public var width: Longitude
public var height: Latitude

public var southLatitude: Latitude {
southWest.latitude
}
public var northLatitude: Latitude {
southLatitude + height
}
public var centerLatitude: Latitude {
southLatitude + (height / 2.0)
}
public var westLongitude: Longitude {
southWest.longitude
}
public var eastLongitude: Longitude {
let longitude = westLongitude + width

if longitude > .halfRotation {
return longitude - .fullRotation
} else {
return longitude
}
}
public var centerLongitude: Longitude {
let longitude = westLongitude + (width / 2.0)

if longitude > .halfRotation {
return longitude - .fullRotation
} else {
return longitude
}
}

public var northEast: Coordinate2D {
Coordinate2D(latitude: northLatitude, longitude: eastLongitude)
}
public var northWest: Coordinate2D {
Coordinate2D(latitude: northLatitude, longitude: westLongitude)
}
public var southEast: Coordinate2D {
Coordinate2D(latitude: southLatitude, longitude: westLongitude)
}
public var center: Coordinate2D {
Coordinate2D(latitude: centerLatitude, longitude: centerLongitude)
}

public var south: Coordinate2D {
southAtLongitude(centerLongitude)
}
public var north: Coordinate2D {
northAtLongitude(centerLongitude)
}
public var west: Coordinate2D {
westAtLatitude(centerLatitude)
}
public var east: Coordinate2D {
eastAtLatitude(centerLatitude)
}

public var crosses180thMeridian: Bool {
westLongitude > eastLongitude
}

public init(
southWest: Coordinate2D,
width: Longitude,
height: Latitude
) {
self.southWest = southWest
self.width = width
self.height = height
}

public init(
southWest: Coordinate2D,
northEast: Coordinate2D
) {
self.init(
southWest: southWest,
width: northEast.longitude - southWest.longitude,
height: northEast.latitude - southWest.latitude
)
}

public func southAtLongitude(_ longitude: Longitude) -> Coordinate2D {
Coordinate2D(latitude: northEast.latitude, longitude: longitude)
}
public func northAtLongitude(_ longitude: Longitude) -> Coordinate2D {
Coordinate2D(latitude: southWest.latitude, longitude: longitude)
}
public func westAtLatitude(_ latitude: Latitude) -> Coordinate2D {
Coordinate2D(latitude: latitude, longitude: southWest.longitude)
}
public func eastAtLatitude(_ latitude: Latitude) -> Coordinate2D {
Coordinate2D(latitude: latitude, longitude: northEast.longitude)
}

public func offsetBy(dLat: Latitude = .zero, dLong: Longitude = .zero) -> BoundingBox2D {
Self.init(
southWest: southWest.offsetBy(dLat: dLat, dLong: dLong),
width: width,
height: height
)
}
public func offsetBy(dx: Coordinate2D.X = .zero, dy: Coordinate2D.Y = .zero) -> BoundingBox2D {
Self.init(
southWest: southWest.offsetBy(dx: dx, dy: dy),
width: width,
height: height
)
}

}

extension BoundingBox2D: BoundingBox {

public static var zero: BoundingBox2D {
Self.init(southWest: .zero, width: .zero, height: .zero)
}

/// The union of bounding boxes gives a new bounding box that encloses the given two.
public func union(_ other: BoundingBox2D) -> BoundingBox2D {
Self.init(
southWest: Coordinate2D(
latitude: min(self.southLatitude, other.southLatitude),
longitude: min(self.westLongitude, other.westLongitude)
),
northEast: Coordinate2D(
latitude: max(self.northLatitude, other.northLatitude),
longitude: max(self.eastLongitude, other.eastLongitude)
)
)
}

}

extension BoundingBox2D {

public func union(_ bbox3d: BoundingBox3D) -> BoundingBox3D { bbox3d.union(self) }

}
121 changes: 121 additions & 0 deletions Sources/GeoModels/3D/BoundingBox3D.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// BoundingBox3D.swift
// SwiftGeo
//
// Created by Rémi Bardon on 08/02/2022.
// Copyright © 2022 Rémi Bardon. All rights reserved.
//

public struct BoundingBox3D: Hashable {

public var twoDimensions: BoundingBox2D
public var lowAltitude: Altitude
public var zHeight: Altitude

public var highAltitude: Altitude {
lowAltitude + zHeight
}
public var centerAltitude: Altitude {
lowAltitude + (zHeight / 2.0)
}

public var southWestLow: Coordinate3D {
Coordinate3D(twoDimensions.southWest, altitude: lowAltitude)
}
public var northEastHigh: Coordinate3D {
Coordinate3D(twoDimensions.northEast, altitude: highAltitude)
}
public var center: Coordinate3D {
Coordinate3D(twoDimensions.center, altitude: centerAltitude)
}

public var crosses180thMeridian: Bool {
twoDimensions.crosses180thMeridian
}

public init(_ boundingBox2d: BoundingBox2D, lowAltitude: Altitude, zHeight: Altitude) {
self.twoDimensions = boundingBox2d
self.lowAltitude = lowAltitude
self.zHeight = zHeight
}

public init(
southWestLow: Coordinate3D,
width: Longitude,
height: Latitude,
zHeight: Altitude
) {
self.init(
BoundingBox2D(southWest: southWestLow.twoDimensions, width: width, height: height),
lowAltitude: southWestLow.altitude,
zHeight: zHeight
)
}

public init(
southWestLow: Coordinate3D,
northEastHigh: Coordinate3D
) {
self.init(
southWestLow: southWestLow,
width: northEastHigh.longitude - southWestLow.longitude,
height: northEastHigh.latitude - southWestLow.latitude,
zHeight: northEastHigh.altitude - southWestLow.altitude
)
}

public func offsetBy(
dLat: Latitude = .zero,
dLong: Longitude = .zero,
dAlt: Altitude = .zero
) -> BoundingBox3D {
Self.init(
twoDimensions.offsetBy(dLat: dLat, dLong: dLong),
lowAltitude: lowAltitude + dAlt,
zHeight: zHeight
)
}
public func offsetBy(
dx: Coordinate3D.X = .zero,
dy: Coordinate3D.Y = .zero,
dz: Coordinate3D.Z = .zero
) -> BoundingBox3D {
Self.init(
twoDimensions.offsetBy(dx: dx, dy: dy),
lowAltitude: lowAltitude + dz,
zHeight: zHeight
)
}

}

extension BoundingBox3D: BoundingBox {

public static var zero: BoundingBox3D {
BoundingBox3D(.zero, lowAltitude: .zero, zHeight: .zero)
}

/// The union of bounding boxes gives a new bounding box that encloses the given two.
public func union(_ other: BoundingBox3D) -> BoundingBox3D {
BoundingBox3D(
southWestLow: Coordinate3D(
self.twoDimensions.union(other.twoDimensions).southWest,
altitude: min(self.lowAltitude, other.lowAltitude)
),
northEastHigh: Coordinate3D(
self.twoDimensions.union(other.twoDimensions).northEast,
altitude: max(self.highAltitude, other.highAltitude)
)
)
}

}

extension BoundingBox3D {

public func union(_ bbox2d: BoundingBox2D) -> BoundingBox3D {
let other = BoundingBox3D(bbox2d, lowAltitude: self.lowAltitude, zHeight: .zero)
return self.union(other)
}

}
Loading

0 comments on commit 739ec70

Please sign in to comment.