diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Turf.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Turf.xcscheme
index 193c88a..672a900 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Turf.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Turf.xcscheme
@@ -20,20 +20,6 @@
ReferencedContainer = "container:">
-
-
-
-
+
+
+
+
-
-
-
-
diff --git a/Sources/Geodesy/CoordinateComponent.swift b/Sources/Geodesy/CoordinateComponent.swift
index 63bab38..65af2f4 100644
--- a/Sources/Geodesy/CoordinateComponent.swift
+++ b/Sources/Geodesy/CoordinateComponent.swift
@@ -14,27 +14,24 @@ import SwiftGeoToolbox
public typealias CoordinateComponent = ValueWithUnit.Value
-// // MARK: CustomStringConvertible
-//
-// var description: String {
-// let formatter = NumberFormatter()
-// formatter.numberStyle = .decimal
-// formatter.maximumFractionDigits = 9
-// formatter.locale = .en
-// return formatter.string(for: Double(self))
-// ?? "\(self.rawValue.rawValue)"
-// }
-
-// // MARK: CustomDebugStringConvertible
-//
-// var debugDescription: String {
-// let formatter = NumberFormatter()
-// formatter.numberStyle = .decimal
-// formatter.maximumFractionDigits = 99
-// formatter.locale = .en
-// return formatter.string(for: Double(self))
-// ?? "\(self.rawValue.rawValue)"
-// }
+// MARK: CustomStringConvertible & CustomDebugStringConvertible
+
+public extension CoordinateComponent {
+ var description: String {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .decimal
+ formatter.maximumFractionDigits = 9
+ formatter.locale = .en
+ return formatter.string(for: Double(self.rawValue)) ?? String(describing: self.rawValue)
+ }
+ var debugDescription: String {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .decimal
+ formatter.locale = .en
+ formatter.maximumFractionDigits = 99
+ return formatter.string(for: Double(self.rawValue)) ?? String(reflecting: self.rawValue)
+ }
+}
// MARK: - ValidatableCoordinateComponent
diff --git a/Sources/Geodesy/Coordinates.swift b/Sources/Geodesy/Coordinates.swift
new file mode 100644
index 0000000..8c11f01
--- /dev/null
+++ b/Sources/Geodesy/Coordinates.swift
@@ -0,0 +1,243 @@
+//
+// Coordinates.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 01/10/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import SwiftGeoToolbox
+
+// MARK: - Coordinates
+
+public protocol Coordinates:
+ Hashable,
+ Zeroable,
+ AdditiveArithmetic,
+ MultiplicativeArithmetic,
+ InitializableByNumber,
+ CustomStringConvertible,
+ CustomDebugStringConvertible
+{
+ associatedtype CRS: Geodesy.CoordinateReferenceSystem
+ associatedtype Components
+
+ var components: Components { get set }
+
+ init(components: Components)
+}
+
+// CustomStringConvertible & CustomDebugStringConvertible
+public extension Coordinates {
+ var description: String { String(describing: self.components) }
+ var debugDescription: String {
+ "<\(Self.CRS.epsgName)>\(String(reflecting: self.components))"
+ }
+}
+
+// MARK: 2D Coordinates
+
+public protocol AtLeastTwoDimensionalCoordinates: Geodesy.Coordinates
+where CRS: AtLeastTwoDimensionalCRS {
+ associatedtype X: CoordinateComponent
+ associatedtype Y: CoordinateComponent
+
+ var x: X { get set }
+ var y: Y { get set }
+}
+
+public extension AtLeastTwoDimensionalCoordinates where X == Geodesy.Latitude {
+ typealias Latitude = Geodesy.Latitude
+ var latitude: Latitude { self.x }
+}
+
+public extension AtLeastTwoDimensionalCoordinates where Y == Geodesy.Longitude {
+ typealias Longitude = Geodesy.Longitude
+ var longitude: Longitude { self.y }
+}
+
+public protocol TwoDimensionalCoordinates: AtLeastTwoDimensionalCoordinates
+where Components == (X, Y) {
+ init(x: X, y: Y)
+}
+
+public struct Coordinates2DOf: TwoDimensionalCoordinates
+where CRS: TwoDimensionalCRS
+{
+ public typealias X = CRS.CoordinateSystem.Axis1.Value
+ public typealias Y = CRS.CoordinateSystem.Axis2.Value
+
+ public var x: X
+ public var y: Y
+
+ public var components: (X, Y) {
+ get { (self.x, self.y) }
+ set {
+ self.x = newValue.0
+ self.y = newValue.1
+ }
+ }
+
+ public init(x: X, y: Y) {
+ #warning("TODO: Perform validations")
+ self.x = x
+ self.y = y
+ }
+ public init(components: (X, Y)) {
+ self.init(x: components.0, y: components.1)
+ }
+}
+
+// Zeroable
+public extension Coordinates2DOf {
+ static var zero: Self { Self.init(x: .zero, y: .zero) }
+}
+
+// AdditiveArithmetic
+public extension Coordinates2DOf {
+ static func + (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
+ }
+ static func - (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
+ }
+}
+// MultiplicativeArithmetic
+public extension Coordinates2DOf {
+ static func * (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x * rhs.x, y: lhs.y * rhs.y)
+ }
+ static func / (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x / rhs.x, y: lhs.y / rhs.y)
+ }
+}
+
+// InitializableByInteger
+public extension Coordinates2DOf {
+ init(_ value: Source) {
+ self.init(x: .init(value), y: .init(value))
+ }
+}
+// InitializableByFloatingPoint
+public extension Coordinates2DOf {
+ init(_ value: Source) {
+ self.init(x: .init(value), y: .init(value))
+ }
+}
+
+public extension TwoDimensionalCoordinates where X == Latitude {
+ var latitude: X { self.x }
+}
+public extension TwoDimensionalCoordinates where Y == Longitude {
+ var longitude: Y { self.y }
+ var withPositiveLongitude: Self {
+ Self.init(x: self.x, y: self.longitude.positive)
+ }
+}
+public extension TwoDimensionalCoordinates
+where X == Latitude, Y == Longitude
+{
+ init(latitude: X, longitude: Y) {
+ self.init(x: latitude, y: longitude)
+ }
+}
+
+// MARK: 3D Coordinates
+
+public protocol AtLeastThreeDimensionalCoordinates: AtLeastTwoDimensionalCoordinates
+where CRS: AtLeastTwoDimensionalCRS {
+ associatedtype Z: CoordinateComponent
+
+ var z: Z { get set }
+}
+public protocol ThreeDimensionalCoordinates: AtLeastThreeDimensionalCoordinates
+where Components == (X, Y, Z) {
+ init(x: X, y: Y, z: Z)
+}
+
+public struct Coordinates3DOf: ThreeDimensionalCoordinates {
+ public typealias X = CRS.CoordinateSystem.Axis1.Value
+ public typealias Y = CRS.CoordinateSystem.Axis2.Value
+ public typealias Z = CRS.CoordinateSystem.Axis3.Value
+
+ public var x: X
+ public var y: Y
+ public var z: Z
+
+ public var components: (X, Y, Z) {
+ get { (self.x, self.y, self.z) }
+ set {
+ self.x = newValue.0
+ self.y = newValue.1
+ self.z = newValue.2
+ }
+ }
+
+ public init(x: X, y: Y, z: Z) {
+ #warning("TODO: Perform validations")
+ self.x = x
+ self.y = y
+ self.z = z
+ }
+ public init(components: (X, Y, Z)) {
+ self.init(x: components.0, y: components.1, z: components.2)
+ }
+}
+
+// Zeroable
+public extension Coordinates3DOf {
+ static var zero: Self { Self.init(x: .zero, y: .zero, z: .zero) }
+}
+
+// AdditiveArithmetic
+public extension Coordinates3DOf {
+ static func + (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
+ }
+ static func - (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z)
+ }
+}
+// MultiplicativeArithmetic
+public extension Coordinates3DOf {
+ static func * (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x * rhs.x, y: lhs.y * rhs.y, z: lhs.z * rhs.z)
+ }
+ static func / (lhs: Self, rhs: Self) -> Self {
+ Self.init(x: lhs.x / rhs.x, y: lhs.y / rhs.y, z: lhs.z / rhs.z)
+ }
+}
+
+// InitializableByInteger
+public extension Coordinates3DOf {
+ init(_ value: Source) {
+ self.init(x: .init(value), y: .init(value), z: .init(value))
+ }
+}
+// InitializableByFloatingPoint
+public extension Coordinates3DOf {
+ init(_ value: Source) {
+ self.init(x: .init(value), y: .init(value), z: .init(value))
+ }
+}
+
+public extension ThreeDimensionalCoordinates where X == Latitude {
+ var latitude: X { self.x }
+}
+public extension ThreeDimensionalCoordinates where Y == Longitude {
+ var longitude: Y { self.y }
+ var withPositiveLongitude: Self {
+ Self.init(x: self.x, y: self.longitude.positive, z: self.z)
+ }
+}
+public extension ThreeDimensionalCoordinates where Z == Altitude {
+ var altitude: Z { self.z }
+}
+
+public extension ThreeDimensionalCoordinates
+where X == Latitude, Y == Longitude, Z == Altitude
+{
+ init(latitude: X, longitude: Y, altitude: Z) {
+ self.init(x: latitude, y: longitude, z: altitude)
+ }
+}
diff --git a/Sources/Geodesy/Geodesy.swift b/Sources/Geodesy/Geodesy.swift
index 37aa17b..5887a8d 100644
--- a/Sources/Geodesy/Geodesy.swift
+++ b/Sources/Geodesy/Geodesy.swift
@@ -17,230 +17,6 @@ public protocol EPSGItem {
static var epsgCode: Int { get }
}
-// MARK: - Coordinates
-
-public protocol Coordinates:
- Hashable,
- Zeroable,
- AdditiveArithmetic,
- MultiplicativeArithmetic,
- InitializableByNumber,
- CustomStringConvertible,
- CustomDebugStringConvertible
-{
- associatedtype CRS: Geodesy.CoordinateReferenceSystem
- associatedtype Components
-
- var components: Components { get set }
-
- init(components: Components)
-}
-
-// CustomStringConvertible & CustomDebugStringConvertible
-public extension Coordinates {
- var description: String { String(describing: self.components) }
- var debugDescription: String {
- "<\(Self.CRS.epsgName)>\(String(reflecting: self.components))"
- }
-}
-
-// MARK: 2D Coordinates
-
-public protocol AtLeastTwoDimensionalCoordinates: Geodesy.Coordinates
-where CRS: AtLeastTwoDimensionalCRS {
- associatedtype X: CoordinateComponent
- associatedtype Y: CoordinateComponent
-
- var x: X { get set }
- var y: Y { get set }
-}
-public protocol TwoDimensionalCoordinates: AtLeastTwoDimensionalCoordinates
-where Components == (X, Y) {
- init(x: X, y: Y)
-}
-
-public struct Coordinates2DOf: TwoDimensionalCoordinates
-where CRS: TwoDimensionalCRS
-{
- public typealias X = CRS.CoordinateSystem.Axis1.Value
- public typealias Y = CRS.CoordinateSystem.Axis2.Value
-
- public var x: X
- public var y: Y
-
- public var components: (X, Y) {
- get { (self.x, self.y) }
- set {
- self.x = newValue.0
- self.y = newValue.1
- }
- }
-
- public init(x: X, y: Y) {
- #warning("TODO: Perform validations")
- self.x = x
- self.y = y
- }
- public init(components: (X, Y)) {
- self.init(x: components.0, y: components.1)
- }
-}
-
-// Zeroable
-public extension Coordinates2DOf {
- static var zero: Self { Self.init(x: .zero, y: .zero) }
-}
-
-// AdditiveArithmetic
-public extension Coordinates2DOf {
- static func + (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
- }
- static func - (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
- }
-}
-// MultiplicativeArithmetic
-public extension Coordinates2DOf {
- static func * (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x * rhs.x, y: lhs.y * rhs.y)
- }
- static func / (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x / rhs.x, y: lhs.y / rhs.y)
- }
-}
-
-// InitializableByInteger
-public extension Coordinates2DOf {
- init(_ value: Source) {
- self.init(x: .init(value), y: .init(value))
- }
-}
-// InitializableByFloatingPoint
-public extension Coordinates2DOf {
- init(_ value: Source) {
- self.init(x: .init(value), y: .init(value))
- }
-}
-
-public extension Coordinates2DOf where CRS: GeographicCRS {
- var latitude: X { self.x }
- var longitude: Y { self.y }
- init(latitude: X, longitude: Y) {
- self.init(x: latitude, y: longitude)
- }
-}
-
-public extension Coordinates2DOf
-where CRS: GeographicCRS,
- Y: AngularCoordinateComponent
-{
- var withPositiveLongitude: Self {
- Self.init(latitude: self.latitude, longitude: self.longitude.positive)
- }
-}
-
-// MARK: 3D Coordinates
-
-public protocol AtLeastThreeDimensionalCoordinates: AtLeastTwoDimensionalCoordinates
-where CRS: AtLeastTwoDimensionalCRS {
- associatedtype Z: CoordinateComponent
-
- var z: Z { get set }
-}
-public protocol ThreeDimensionalCoordinates: AtLeastThreeDimensionalCoordinates
-where Components == (X, Y, Z) {
- init(x: X, y: Y, z: Z)
-}
-
-public struct Coordinates3DOf: ThreeDimensionalCoordinates {
- public typealias X = CRS.CoordinateSystem.Axis1.Value
- public typealias Y = CRS.CoordinateSystem.Axis2.Value
- public typealias Z = CRS.CoordinateSystem.Axis3.Value
-
- public var x: X
- public var y: Y
- public var z: Z
-
- public var components: (X, Y, Z) {
- get { (self.x, self.y, self.z) }
- set {
- self.x = newValue.0
- self.y = newValue.1
- self.z = newValue.2
- }
- }
-
- public init(x: X, y: Y, z: Z) {
- #warning("TODO: Perform validations")
- self.x = x
- self.y = y
- self.z = z
- }
- public init(components: (X, Y, Z)) {
- self.init(x: components.0, y: components.1, z: components.2)
- }
-}
-
-// Zeroable
-public extension Coordinates3DOf {
- static var zero: Self { Self.init(x: .zero, y: .zero, z: .zero) }
-}
-
-// AdditiveArithmetic
-public extension Coordinates3DOf {
- static func + (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
- }
- static func - (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z)
- }
-}
-// MultiplicativeArithmetic
-public extension Coordinates3DOf {
- static func * (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x * rhs.x, y: lhs.y * rhs.y, z: lhs.z * rhs.z)
- }
- static func / (lhs: Self, rhs: Self) -> Self {
- Self.init(x: lhs.x / rhs.x, y: lhs.y / rhs.y, z: lhs.z / rhs.z)
- }
-}
-
-// InitializableByInteger
-public extension Coordinates3DOf {
- init(_ value: Source) {
- self.init(x: .init(value), y: .init(value), z: .init(value))
- }
-}
-// InitializableByFloatingPoint
-public extension Coordinates3DOf {
- init(_ value: Source) {
- self.init(x: .init(value), y: .init(value), z: .init(value))
- }
-}
-
-public extension ThreeDimensionalCoordinates where CRS: GeographicCRS {
- var latitude: X { self.x }
- var longitude: Y { self.y }
- var altitude: Z { self.z }
- init(latitude: X, longitude: Y, altitude: Z) {
- self.init(x: latitude, y: longitude, z: altitude)
- }
-}
-
-public extension ThreeDimensionalCoordinates
-where CRS: GeographicCRS,
- Y: AngularCoordinateComponent
-{
- var withPositiveLongitude: Self {
- Self.init(
- latitude: self.latitude,
- longitude: self.longitude.positive,
- altitude: self.altitude
- )
- }
-}
-
// MARK: - Coordinate Reference System (CRS/SRS)
public protocol CoordinateReferenceSystem: EPSGItem {
@@ -342,7 +118,10 @@ public protocol ThreeDimensionalCS: AtLeastThreeDimensionalCS
where Axes == (Axis1, Axis2, Axis3) {}
public protocol GeocentricCS: ThreeDimensionalCS {}
-public protocol GeographicCS: CoordinateSystem {}
+public protocol GeographicCS: AtLeastTwoDimensionalCS
+where Axis1.Value: AngularCoordinateComponent,
+ Axis2.Value: AngularCoordinateComponent
+{}
public enum GeocentricCartesian3DCS: ThreeDimensionalCS, GeocentricCS {
public typealias Axis1 = GeocentricX
@@ -495,6 +274,7 @@ public enum EllipsoidalHeight: Axis {
public struct Value: CoordinateComponent {
public typealias Unit = Meter
+ public typealias RawValue = DoubleOf
public var rawValue: DoubleOf
public init(rawValue: RawValue) {
self.rawValue = rawValue
diff --git a/Sources/GeodeticDisplay/Coordinate+Display.swift b/Sources/GeodeticDisplay/Coordinate+Display.swift
index dc3dd50..42e4f3a 100644
--- a/Sources/GeodeticDisplay/Coordinate+Display.swift
+++ b/Sources/GeodeticDisplay/Coordinate+Display.swift
@@ -17,7 +17,7 @@ extension CoordinateComponent {
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = Int(maxDigits)
formatter.locale = .en
- return formatter.string(for: self.rawValue) ?? String(describing: self.rawValue)
+ return formatter.string(for: Double(self.rawValue)) ?? String(describing: self.rawValue)
}
}
diff --git a/Sources/GeodeticGeometry/Concepts/BoundingBox.swift b/Sources/GeodeticGeometry/Concepts/BoundingBox.swift
index 98ed914..130bf31 100644
--- a/Sources/GeodeticGeometry/Concepts/BoundingBox.swift
+++ b/Sources/GeodeticGeometry/Concepts/BoundingBox.swift
@@ -27,10 +27,6 @@ public struct BoundingBox {
public init(min: Self.Coordinates, max: Self.Coordinates) {
self.init(origin: min, size: .init(from: min, to: max))
}
-
- #warning("TODO: Reimplement `BoundingBox.union(_ other: Self)`")
- /// The union of bounding boxes gives a new bounding box that encloses the given two.
-// public func union(_ other: Self) -> Self
}
extension BoundingBox: Hashable {}
@@ -38,3 +34,34 @@ extension BoundingBox: Hashable {}
extension BoundingBox: Zeroable {
public static var zero: Self { Self.init(origin: .zero, size: .zero) }
}
+
+extension BoundingBox: CustomStringConvertible {
+ public var description: String {
+ "(origin: \(String(describing: self.origin)), size: \(String(describing: self.size)))"
+ }
+}
+extension BoundingBox: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ "(origin: \(String(describing: self.origin)), size: \(String(describing: self.size)))"
+ }
+}
+
+public extension BoundingBox
+where Self.Coordinates: TwoDimensionalCoordinates,
+ Self.Coordinates.X == Geodesy.Latitude
+{
+ var southLatitude: Latitude { origin.latitude }
+ var northLatitude: Latitude { southLatitude + size.dLat }
+ var centerLatitude: Latitude { southLatitude + (size.dLat / 2) }
+}
+
+public extension BoundingBox
+where Self.Coordinates: TwoDimensionalCoordinates,
+ Self.Coordinates.Y == Geodesy.Longitude
+{
+ var westLongitude: Longitude { origin.longitude }
+ var eastLongitude: Longitude { westLongitude + size.dLong }
+ var centerLongitude: Longitude { westLongitude + (size.dLong / 2) }
+
+ var crosses180thMeridian: Bool { !eastLongitude.isValid }
+}
diff --git a/Sources/GeodeticGeometry/Concepts/Vector.swift b/Sources/GeodeticGeometry/Concepts/Vector.swift
index aa80639..b23f089 100644
--- a/Sources/GeodeticGeometry/Concepts/Vector.swift
+++ b/Sources/GeodeticGeometry/Concepts/Vector.swift
@@ -26,14 +26,18 @@ public struct Vector:
public init(rawValue: CRS.Coordinates) {
self.rawValue = rawValue
}
- init(from: CRS.Coordinates, to: CRS.Coordinates) {
+ public init(from: CRS.Coordinates, to: CRS.Coordinates) {
self.init(rawValue: to - from)
}
- init(from: Point, to: Point) {
+ public init(from: Point, to: Point) {
self.init(from: from.coordinates, to: to.coordinates)
}
}
+public extension Vector {
+ var half: Self { self / 2 }
+}
+
// Zeroable
public extension Vector {
static var zero: Self { Self.init(rawValue: .zero) }
@@ -71,6 +75,17 @@ public extension Vector {
}
}
+extension Vector: CustomStringConvertible {
+ public var description: String {
+ String(describing: self.rawValue)
+ }
+}
+extension Vector: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ "\(String(describing: self.rawValue))"
+ }
+}
+
// MARK: - 2D
public extension Vector where RawValue: AtLeastTwoDimensionalCoordinates {
@@ -90,10 +105,38 @@ public extension Vector where RawValue: AtLeastTwoDimensionalCoordinates {
var horizontalDelta: DY { self.dy }
}
+public extension Vector
+where Self.RawValue: TwoDimensionalCoordinates,
+ Self.RawValue.X == Geodesy.Latitude
+{
+ var dLat: Geodesy.Latitude { self.dx }
+}
+
+public extension Vector
+where Self.RawValue: TwoDimensionalCoordinates,
+ Self.RawValue.Y == Geodesy.Longitude
+{
+ var dLong: Geodesy.Longitude { self.dy }
+}
+
+public extension Vector where RawValue: TwoDimensionalCoordinates {
+ init(dx: DX, dy: DY) {
+ self.init(rawValue: .init(x: dx, y: dy))
+ }
+}
+
// MARK: - 3D
public extension Vector where RawValue: AtLeastThreeDimensionalCoordinates {
- var dz: RawValue.Z { self.rawValue.z }
+ typealias DZ = Self.RawValue.Z
+
+ var dz: DZ { self.rawValue.z }
+}
+
+public extension Vector where RawValue: ThreeDimensionalCoordinates {
+ init(dx: DX, dy: DY, dz: DZ) {
+ self.init(rawValue: .init(x: dx, y: dy, z: dz))
+ }
}
// MARK: - Extensions
@@ -103,3 +146,9 @@ public extension Coordinates where CRS.Coordinates == Self {
lhs + rhs.rawValue
}
}
+
+public extension Point {
+ static func + (lhs: Self, rhs: Vector) -> Self {
+ Self.init(coordinates: lhs.coordinates + rhs)
+ }
+}
diff --git a/Sources/GeodeticGeometry/Shapes/Line.swift b/Sources/GeodeticGeometry/Shapes/Line.swift
index 2f5b263..4761ca6 100644
--- a/Sources/GeodeticGeometry/Shapes/Line.swift
+++ b/Sources/GeodeticGeometry/Shapes/Line.swift
@@ -45,3 +45,10 @@ extension Line: GeodeticGeometry.MultiPointProtocol {
}
}
+
+extension Line: Iterable {
+ public typealias Element = Self.Point
+ public func makeIterator() -> NonEmptyIterator {
+ NonEmptyIterator(base: self.points)
+ }
+}
diff --git a/Sources/GeodeticGeometry/Shapes/LineString.swift b/Sources/GeodeticGeometry/Shapes/LineString.swift
index c30cd21..a98761e 100644
--- a/Sources/GeodeticGeometry/Shapes/LineString.swift
+++ b/Sources/GeodeticGeometry/Shapes/LineString.swift
@@ -95,3 +95,10 @@ extension LineString: CustomStringConvertible, CustomDebugStringConvertible {
return "[\(descriptions.joined(separator: ","))]"
}
}
+
+extension LineString: Iterable {
+ public typealias Element = Self.Line
+ public func makeIterator() -> NonEmptyIterator {
+ NonEmptyIterator(base: self.lines)
+ }
+}
diff --git a/Sources/GeodeticGeometry/Shapes/LinearRing.swift b/Sources/GeodeticGeometry/Shapes/LinearRing.swift
index a6962fb..6c72ec4 100644
--- a/Sources/GeodeticGeometry/Shapes/LinearRing.swift
+++ b/Sources/GeodeticGeometry/Shapes/LinearRing.swift
@@ -12,14 +12,11 @@ import NonEmpty
// MARK: - Protocol
-public protocol LinearRingProtocol: GeodeticGeometry.LineStringProtocol
-//where Lines.Collection: RangeReplaceableCollection
-{}
+public protocol LinearRingProtocol: GeodeticGeometry.LineStringProtocol {}
// MARK: - Implementation
public struct LinearRing: GeodeticGeometry.LinearRingProtocol
-//where Lines.Collection: RangeReplaceableCollection
where CRS: CoordinateReferenceSystem
{
public typealias Point = Self.Line.Point
@@ -65,3 +62,10 @@ where CRS: CoordinateReferenceSystem
self.points.append(point)
}
}
+
+extension LinearRing: Iterable {
+ public typealias Element = Self.Line
+ public func makeIterator() -> NonEmptyIterator {
+ NonEmptyIterator(base: self.lines)
+ }
+}
diff --git a/Sources/GeodeticGeometry/Shapes/MultiLine.swift b/Sources/GeodeticGeometry/Shapes/MultiLine.swift
index a039307..9267766 100644
--- a/Sources/GeodeticGeometry/Shapes/MultiLine.swift
+++ b/Sources/GeodeticGeometry/Shapes/MultiLine.swift
@@ -22,6 +22,16 @@ where Points == AtLeast2<[Self.Point]> {
init(lines: Self.Lines)
}
+public extension MultiLineProtocol {
+ var points: Self.Points {
+ Points(
+ lines.first.points.first,
+ lines.first.points.second,
+ tail: lines.dropFirst(Lines.minimumCount).flatMap(\.points)
+ )
+ }
+}
+
// MARK: - Implementation
public struct MultiLine: GeodeticGeometry.MultiLineProtocol
@@ -40,12 +50,9 @@ where CRS: Geodesy.CoordinateReferenceSystem {
}
}
-public extension MultiLineProtocol {
- var points: Self.Points {
- Points(
- lines.first.points.first,
- lines.first.points.second,
- tail: lines.dropFirst(Lines.minimumCount).flatMap(\.points)
- )
+extension MultiLine: Iterable {
+ public typealias Element = Self.Line
+ public func makeIterator() -> NonEmptyIterator {
+ NonEmptyIterator(base: self.lines)
}
}
diff --git a/Sources/GeodeticGeometry/Shapes/MultiPoint.swift b/Sources/GeodeticGeometry/Shapes/MultiPoint.swift
index 42e01f2..a5c6687 100644
--- a/Sources/GeodeticGeometry/Shapes/MultiPoint.swift
+++ b/Sources/GeodeticGeometry/Shapes/MultiPoint.swift
@@ -11,7 +11,7 @@ import NonEmpty
// MARK: - Protocol
-public protocol MultiPointProtocol: Hashable {
+public protocol MultiPointProtocol: Hashable, Iterable {
associatedtype CRS: Geodesy.CoordinateReferenceSystem
typealias Point = GeodeticGeometry.Point
associatedtype Points: NonEmptyProtocol
@@ -39,3 +39,10 @@ where CRS: Geodesy.CoordinateReferenceSystem {
self.init(points: try! Self.Points(points))
}
}
+
+extension MultiPoint: Iterable {
+ public typealias Element = Self.Point
+ public func makeIterator() -> NonEmptyIterator {
+ NonEmptyIterator(base: self.points)
+ }
+}
diff --git a/Sources/GeodeticGeometry/Shapes/Point.swift b/Sources/GeodeticGeometry/Shapes/Point.swift
index c29dd26..6fb46ee 100644
--- a/Sources/GeodeticGeometry/Shapes/Point.swift
+++ b/Sources/GeodeticGeometry/Shapes/Point.swift
@@ -40,3 +40,31 @@ extension Point: MultiplicativeArithmetic {
Self.init(coordinates: lhs.coordinates / rhs.coordinates)
}
}
+
+extension Point: CustomStringConvertible {
+ public var description: String {
+ String(describing: self.coordinates)
+ }
+}
+extension Point: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ "\(String(describing: self.coordinates))"
+ }
+}
+
+extension Point
+where Coordinates: TwoDimensionalCoordinates,
+ Coordinates.Y == Geodesy.Longitude
+{
+ var withPositiveLongitude: Self {
+ Self.init(coordinates: self.coordinates.withPositiveLongitude)
+ }
+}
+extension Point
+where Coordinates: ThreeDimensionalCoordinates,
+ Coordinates.Y == Geodesy.Longitude
+{
+ var withPositiveLongitude: Self {
+ Self.init(coordinates: self.coordinates.withPositiveLongitude)
+ }
+}
diff --git a/Sources/GeodeticGeometry/Toolbox/Iterable.swift b/Sources/GeodeticGeometry/Toolbox/Iterable.swift
index 59d26e0..a6e9081 100644
--- a/Sources/GeodeticGeometry/Toolbox/Iterable.swift
+++ b/Sources/GeodeticGeometry/Toolbox/Iterable.swift
@@ -6,7 +6,7 @@
// Copyright © 2022 Rémi Bardon. All rights reserved.
//
-import protocol NonEmpty.NonEmptyProtocol
+import NonEmpty
public protocol Iterable {
associatedtype Element
@@ -52,3 +52,7 @@ public struct NonEmptyIterator: IteratorProtocol {
extension Array: Iterable {}
extension Set: Iterable {}
extension Slice: Iterable {}
+
+// MARK: - Third-party type conformances
+
+extension NonEmpty: Iterable {}
diff --git a/Sources/TurfCore/Boundable.swift b/Sources/TurfCore/Boundable.swift
deleted file mode 100644
index e5fc843..0000000
--- a/Sources/TurfCore/Boundable.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-//
-// Boundable.swift
-// SwiftGeo
-//
-// Created by Rémi Bardon on 04/02/2022.
-// Copyright © 2022 Rémi Bardon. All rights reserved.
-//
-
-import GeodeticGeometry
-
-public protocol Boundable {
- associatedtype GeometricSystem: GeodeticGeometry.GeometricSystem
- typealias BoundingBox = GeodeticGeometry.BoundingBox
-
- var bbox: BoundingBox { get }
-
- func union(_ other: Self) -> Self
-}
-
-// MARK: - Default implementations
-
-// MARK: Bounding box
-
-extension GeodeticGeometry.BoundingBox {
- public var bbox: Self { self }
-}
-
-// MARK: Shapes
-
-extension GeodeticGeometry.Point where Self.Coordinates: Boundable {
- public var bbox: Self.Coordinates.BoundingBox { self.coordinates.bbox }
-}
-
-// MARK: Iterable types
-
-extension Iterable
-where Self.Element: Boundable,
- Self.Element.GeometricSystem: GeometricSystemAlgebra,
- Self.Element.BoundingBox == Self.Element.BoundingBox.GeometricSystem.BoundingBox
-{
- public var bbox: Self.Element.BoundingBox? {
- Self.Element.BoundingBox.GeometricSystem.bbox(forIterable: self)
- }
-}
-
-extension NonEmptyIterable
-where Self.Element: Boundable,
- Self.Element.BoundingBox.GeometricSystem: GeometricSystemAlgebra,
- Self.Element.BoundingBox == Self.Element.BoundingBox.GeometricSystem.BoundingBox
-{
- public var bbox: Self.Element.BoundingBox {
- Self.Element.BoundingBox.GeometricSystem.bbox(forNonEmptyIterable: self)
- }
-}
-
-extension Sequence where Self.Element: Boundable, Self.Element.BoundingBox: Boundable {
- public var bbox: Self.Element.BoundingBox {
- self.reduce(.zero, { $0.union($1.bbox) })
- }
-}
diff --git a/Sources/TurfCore/CubicBezierSpline.swift b/Sources/TurfCore/CubicBezierSpline.swift
index bb02199..35a2772 100644
--- a/Sources/TurfCore/CubicBezierSpline.swift
+++ b/Sources/TurfCore/CubicBezierSpline.swift
@@ -7,58 +7,62 @@
//
import Algorithms
+import Geodesy
import GeodeticGeometry
import NonEmpty
import SwiftGeoToolbox
-struct CubicBezierSpline
+struct CubicBezierSpline
+where CRS: Geodesy.CoordinateReferenceSystem,
+ CRS.Coordinates: BoundableCoordinates
{
- typealias Coordinates = GeometricSystem.Coordinates
- typealias Point = GeometricSystem.Point
- typealias Line = GeometricSystem.Line
+ typealias Coordinates = CRS.Coordinates
+ typealias Line = GeodeticGeometry.Line
let controls: [(p0: Coordinates, c0: Coordinates, c1: Coordinates, p1: Coordinates)]
- init(
- points: AtLeast2,
- sharpness: Double
- )
- where Points: BidirectionalCollection,
- Points.Element == Point,
- Points.Index == Int
+ init(coordinates: AtLeast2, sharpness: Double)
+ where C: BidirectionalCollection,
+ C.Element == Self.Coordinates,
+ C.Index == Int
{
precondition(sharpness >= 0, "Sharpness must be >= 0 (was \(sharpness))")
precondition(sharpness <= 1, "Sharpness must be <= 1 (was \(sharpness))")
- let lastLine = Line.init(from: points[points.count - 2], to: points[points.count - 1])
let lineAfterEnd: Line
- var previousLine: Line
+ var previousCenter: Coordinates
do {
- if points.last == points.first {
- // If the points form a ring, take last line
- // NOTE: We don't need to check that `points.count > 1`, as points is `AtLeast2`
- lineAfterEnd = Line(from: points.first, to: points.second)
- previousLine = lastLine
+ let firstLine = Line(from: coordinates.first, to: coordinates.second)
+ // NOTE: We don't need to check that `coordinates.count > 1`, as coordinates is `AtLeast2`
+ let lastLine = Line(
+ from: coordinates[coordinates.count - 2],
+ to: coordinates[coordinates.count - 1]
+ )
+ if coordinates.last == coordinates.first {
+ // If the coordinates form a ring, take the first line as the line after the last
+ // and the last line as the one before the first.
+ lineAfterEnd = firstLine
+ previousCenter = lastLine.center
} else {
- // If the points do not form a ring, create a 0-length line
- lineAfterEnd = Line(from: points.last, to: points.last)
- previousLine = Line(from: points.first, to: points.first)
+ // If the coordinates do not form a ring, use first and last lines.
+ // This means control directions will equal `Vector.zero`,
+ // generating a better bezier interpolation.
+ lineAfterEnd = lastLine
+ previousCenter = firstLine.center
}
}
- var lines = points.adjacentPairs().map(Line.init(from:to:))
+ var lines = coordinates.adjacentPairs().map(Line.init(from:to:))
lines.append(lineAfterEnd)
self.controls = lines.adjacentPairs().map { (line: Line, nextLine: Line) in
- defer { previousLine = line }
-
- let previousCenter = previousLine.center
let center = line.center
+ defer { previousCenter = center }
let nextCenter = nextLine.center
let point = line.start.coordinates
let nextPoint = line.end.coordinates
- let dir1 = Line(from: previousCenter, to: center).vector.center
- let dir2 = Line(from: nextCenter, to: center).vector.center
+ let dir1 = Vector(from: previousCenter, to: center).half
+ let dir2 = Vector(from: nextCenter, to: center).half
let ratio: Double = (1 - sharpness)
let control1 = point + ratio * dir1
@@ -92,7 +96,7 @@ struct CubicBezierSpline
fraction: Double
) -> Coordinates {
precondition((Double(0.0)...Double(1.0)).contains(fraction))
- return p1 + Coordinates(fraction) * (p2 - p1)
+ return p1 + fraction * Vector(from: p1, to: p2)
}
static func pointInQuadCurve(
diff --git a/Sources/TurfCore/Features/BBox.swift b/Sources/TurfCore/Features/BBox.swift
new file mode 100644
index 0000000..3bef54f
--- /dev/null
+++ b/Sources/TurfCore/Features/BBox.swift
@@ -0,0 +1,246 @@
+//
+// BBox.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import GeodeticGeometry
+import SwiftGeoToolbox
+
+// MARK: - Protocols
+
+public protocol Boundable {
+ associatedtype CRS: CoordinateReferenceSystem
+ var bbox: BoundingBox { get }
+}
+
+public protocol BoundableCoordinates: Coordinates, Boundable {
+ /// Returns new coordinates made with the minimum coordinate components.
+ ///
+ /// Example:
+ /// ```swift
+ /// let min = Coordinate2D.makeMin(
+ /// Coordinate2D(5, 10),
+ /// Coordinate2D(3, 12)
+ /// )
+ /// assert(min == Coordinate2D(3, 10))
+ /// ```
+ static func makeMin(_ lhs: Self, _ rhs: Self) -> Self
+ /// Returns new coordinates made with the maximum coordinate components.
+ ///
+ /// Example:
+ /// ```swift
+ /// let max = Coordinate2D.makeMax(
+ /// Coordinate2D(5, 10),
+ /// Coordinate2D(3, 12)
+ /// )
+ /// assert(max == Coordinate2D(5, 12))
+ /// ```
+ static func makeMax(_ lhs: Self, _ rhs: Self) -> Self
+ /// Returns new coordinates made with the minimum and maximum coordinate components.
+ ///
+ /// Example:
+ /// ```swift
+ /// let (min, max) = Coordinate2D.makeMinAndMax([
+ /// Coordinate2D(5, 10),
+ /// Coordinate2D(3, 12),
+ /// Coordinate2D(4, 15),
+ /// Coordinate2D(2, 14),
+ /// ])!
+ /// assert(min == Coordinate2D(2, 10))
+ /// assert(max == Coordinate2D(5, 15))
+ /// ```
+ static func makeMinAndMax>(_ coordinates: S) -> (Self, Self)?
+}
+
+public extension BoundableCoordinates {
+ static func makeMinAndMax>(_ coordinates: S) -> (Self, Self)? {
+ guard let first = coordinates.first else { return nil }
+
+ var (min, max) = (first, first)
+ for c in coordinates.dropFirst() {
+ min = Self.makeMin(min, c)
+ max = Self.makeMax(max, c)
+ }
+
+ return (min, max)
+ }
+}
+
+extension BoundingBox where Self.Coordinates: BoundableCoordinates {
+ /// The union of bounding boxes gives a new bounding box that encloses the given two.
+ public func union(_ other: Self) -> Self {
+ if let (min, max) = Self.Coordinates.makeMinAndMax([
+ self.origin,
+ other.origin,
+ self.origin + self.size,
+ other.origin + other.size
+ ]) {
+ return Self.init(min: min, max: max)
+ } else {
+ return self
+ }
+ }
+}
+
+// MARK: - Functions
+
+public func bbox(forPoint point: Point) -> BoundingBox {
+ return BoundingBox(origin: point.coordinates, size: .zero)
+}
+
+public func bbox(forIterator iterator: inout Iterator) -> BoundingBox?
+where Iterator: IteratorProtocol,
+ Iterator.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ guard let element = iterator.next() else {
+ return nil
+ }
+ var bbox = element.bbox
+ while let element = iterator.next() {
+ bbox = bbox.union(element.bbox)
+ }
+ return bbox
+}
+
+public func bbox(
+ forNonEmptyIterator iterator: inout NonEmptyIterator
+) -> BoundingBox
+where S.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ var bbox = iterator.first().bbox
+ while let element = iterator.next() {
+ bbox = bbox.union(element.bbox)
+ }
+ return bbox
+}
+
+public func bbox(forIterable elements: C) -> BoundingBox?
+where C: Iterable,
+ C.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ var iterator = elements.makeIterator()
+ return bbox(forIterator: &iterator)
+}
+
+public func bbox(forNonEmptyIterable elements: C) -> BoundingBox
+where C: NonEmptyIterable,
+ C.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ var iterator = elements.makeIterator()
+ return bbox(forIterator: &iterator) ?? iterator.first().bbox
+}
+
+// MARK: - Helpers
+
+// MARK: Concepts
+
+extension BoundingBox: Boundable {
+ public var bbox: Self { self }
+}
+
+extension Coordinates {
+ public var bbox: BoundingBox {
+ guard let origin = self as? CRS.Coordinates else {
+ logger.error("""
+ \(Self.self) was not \(CRS.Coordinates.self). \
+ Make sure your `CoordinateReferenceSystem` was defined correctly.
+ Recovering fatal error by returning `BoundingBox.zero`.
+ """)
+ return BoundingBox(origin: .zero, size: .zero)
+ }
+ return BoundingBox(origin: origin, size: .zero)
+ }
+}
+extension Coordinates2DOf: Boundable {}
+extension Coordinates3DOf: Boundable {}
+
+// MARK: Shapes
+
+extension Point: Boundable {
+ public var bbox: BoundingBox { TurfCore.bbox(forPoint: self) }
+}
+
+extension MultiPointProtocol
+where Self.Points: Iterable,
+ Self.CRS.Coordinates: BoundableCoordinates
+{
+ public var bbox: BoundingBox {
+ TurfCore.bbox(forIterable: self.points) ?? self.points.first.bbox
+ }
+}
+
+extension MultiPoint: Boundable where Self.CRS.Coordinates: BoundableCoordinates {}
+extension Line: Boundable where Self.CRS.Coordinates: BoundableCoordinates {}
+extension MultiLine: Boundable where Self.CRS.Coordinates: BoundableCoordinates {}
+extension LineString: Boundable where Self.CRS.Coordinates: BoundableCoordinates {}
+extension LinearRing: Boundable where Self.CRS.Coordinates: BoundableCoordinates {}
+
+// MARK: Iterable types
+
+public extension Iterable
+where Self.Element: Boundable,
+ Self.Element.CRS.Coordinates: BoundableCoordinates
+{
+ var bbox: BoundingBox? {
+ TurfCore.bbox(forIterable: self)
+ }
+}
+
+public extension NonEmptyIterable
+where Self.Element: Boundable,
+ Self.Element.CRS.Coordinates: BoundableCoordinates
+{
+ var bbox: BoundingBox {
+ TurfCore.bbox(forNonEmptyIterable: self)
+ }
+}
+
+public extension Collection
+where Self.Element: Boundable,
+ Self.Element.CRS.Coordinates: BoundableCoordinates
+{
+ var bbox: BoundingBox? {
+ self.isEmpty ? nil : self.reduce(.zero, { $0.union($1.bbox) })
+ }
+}
+
+// MARK: - Implementations
+
+extension Coordinates2DOf: BoundableCoordinates {
+ public static func makeMin(_ lhs: Self, _ rhs: Self) -> Self {
+ Self.init(
+ x: min(lhs.x, rhs.x),
+ y: min(lhs.y, rhs.y)
+ )
+ }
+ public static func makeMax(_ lhs: Self, _ rhs: Self) -> Self {
+ Self.init(
+ x: max(lhs.x, rhs.x),
+ y: max(lhs.y, rhs.y)
+ )
+ }
+}
+
+extension Coordinates3DOf: BoundableCoordinates {
+ public static func makeMin(_ lhs: Self, _ rhs: Self) -> Self {
+ Self.init(
+ x: min(lhs.x, rhs.x),
+ y: min(lhs.y, rhs.y),
+ z: min(lhs.z, rhs.z)
+ )
+ }
+ public static func makeMax(_ lhs: Self, _ rhs: Self) -> Self {
+ Self.init(
+ x: max(lhs.x, rhs.x),
+ y: max(lhs.y, rhs.y),
+ z: max(lhs.z, rhs.z)
+ )
+ }
+}
diff --git a/Sources/TurfCore/Features/Bezier.swift b/Sources/TurfCore/Features/Bezier.swift
new file mode 100644
index 0000000..9422da9
--- /dev/null
+++ b/Sources/TurfCore/Features/Bezier.swift
@@ -0,0 +1,41 @@
+//
+// Bezier.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import GeodeticGeometry
+import NonEmpty
+
+// MARK: - Functions
+
+public func bezier(
+ forLineString lineString: LineString,
+ sharpness: Double,
+ resolution: Double
+) -> LineString
+where CRS.Coordinates: BoundableCoordinates
+{
+ let coordinates = try! AtLeast2<[CRS.Coordinates]>(lineString.points.map(\.coordinates))
+ let spline = CubicBezierSpline(coordinates: coordinates, sharpness: sharpness)
+ let points = AtLeast2<[Point]>(
+ rawValue: spline.curve(resolution: resolution).map(Point.init(coordinates:))
+ )!
+ return LineString(points: points)
+}
+
+// MARK: - Helpers
+
+// MARK: Shape
+
+public extension LineString where CRS.Coordinates: BoundableCoordinates {
+ func bezier(sharpness: Double, resolution: Double) -> Self {
+ TurfCore.bezier(
+ forLineString: self,
+ sharpness: sharpness,
+ resolution: resolution
+ )
+ }
+}
diff --git a/Sources/TurfCore/Features/Center.swift b/Sources/TurfCore/Features/Center.swift
new file mode 100644
index 0000000..855ea5b
--- /dev/null
+++ b/Sources/TurfCore/Features/Center.swift
@@ -0,0 +1,84 @@
+//
+// Center.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import GeodeticGeometry
+import SwiftGeoToolbox
+
+// MARK: - Functions
+
+public func center(forIterator iterator: inout Iterator) -> CRS.Coordinates?
+where Iterator: IteratorProtocol,
+ Iterator.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ TurfCore.bbox(forIterator: &iterator)?.center
+}
+
+public func center(
+ forNonEmptyIterator iterator: inout NonEmptyIterator
+) -> CRS.Coordinates
+where S.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ TurfCore.bbox(forNonEmptyIterator: &iterator).center
+}
+
+public func center(forIterable elements: C) -> CRS.Coordinates?
+where C: Iterable,
+ C.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ TurfCore.bbox(forIterable: elements)?.center
+}
+
+public func center(forNonEmptyIterable elements: C) -> CRS.Coordinates
+where C: NonEmptyIterable,
+ C.Element: Boundable,
+ CRS.Coordinates: BoundableCoordinates
+{
+ TurfCore.bbox(forNonEmptyIterable: elements).center
+}
+
+// MARK: - Helpers
+
+// MARK: Concepts
+
+public extension Vector {
+ var center: CRS.Coordinates { self.half.rawValue }
+}
+
+// MARK: Shapes
+
+public extension Line {
+ var center: CRS.Coordinates {
+ self.start.coordinates + Vector(from: self.start, to: self.end).half
+ }
+}
+
+// MARK: Iterable types
+
+extension Iterable
+where Self.Element: Boundable,
+ Self.Element.CRS.Coordinates: BoundableCoordinates
+{
+ public var center: Self.Element.CRS.Coordinates? { self.bbox?.center }
+}
+
+extension NonEmptyIterable
+where Self.Element: Boundable,
+ Self.Element.CRS.Coordinates: BoundableCoordinates
+{
+ public var center: Self.Element.CRS.Coordinates { self.bbox.center }
+}
+
+extension Collection
+where Self.Element: Boundable,
+ Self.Element.CRS.Coordinates: BoundableCoordinates
+{
+ public var center: Self.Element.CRS.Coordinates? { self.bbox?.center }
+}
diff --git a/Sources/TurfCore/Features/CenterOfMass.swift b/Sources/TurfCore/Features/CenterOfMass.swift
new file mode 100644
index 0000000..cba535c
--- /dev/null
+++ b/Sources/TurfCore/Features/CenterOfMass.swift
@@ -0,0 +1,78 @@
+//
+// CenterOfMass.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import Geodesy
+import GeodeticGeometry
+import SwiftGeoToolbox
+
+// MARK: - Functions
+
+/// Returns the [center of mass](https://en.wikipedia.org/wiki/Center_of_mass) of a polygon.
+///
+/// Ported from
+///
+/// - Note: This code is not very clean, but Compile Time Optimizations have been added to reduce
+/// type checking from `>500ms` to `<50ms`.
+public func centerOfMass(
+ forCollection points: Points
+) -> CRS.Coordinates?
+where CRS: TwoDimensionalCRS,
+ Points.Element == Point,
+ CRS.Coordinates.X: AngularCoordinateComponent,
+ CRS.Coordinates.Y: AngularCoordinateComponent
+{
+ // First, we neutralize the feature (set it around coordinates [0,0]) to prevent rounding errors
+ // We take any point to translate all the points around 0
+ guard let centre = TurfCore.centroid(forCollection: points.map(\.coordinates)) else { return nil }
+ let translation = centre
+ var sx: CRS.Coordinates.X = 0
+ var sy: CRS.Coordinates.Y = 0
+ var sArea: Double = 0
+
+ let neutralizedPoints: [CRS.Coordinates] = points.map { $0.coordinates - translation }
+
+ for i in 0..(
+ forIterator iterator: inout Iterator,
+ crs: CRS.Type
+) -> CRS.Coordinates?
+where Iterator: IteratorProtocol,
+ CRS: CoordinateReferenceSystem,
+ Iterator.Element == CRS.Coordinates
+{
+ guard let element = iterator.next() else { return nil }
+ var sum: CRS.Coordinates = element
+ var count: Int = 0
+ while let element = iterator.next() {
+ sum += element
+ count += 1
+ }
+ return sum / count
+}
+
+public func centroid(
+ forNonEmptyIterator iterator: inout NonEmptyIterator,
+ crs: CRS.Type
+) -> CRS.Coordinates
+where CRS: CoordinateReferenceSystem,
+ S.Element == CRS.Coordinates
+{
+ var sum: CRS.Coordinates = iterator.first()
+ var count: Int = 0
+ while let element = iterator.next() {
+ sum += element
+ count += 1
+ }
+ return sum / count
+}
+
+public func centroid(
+ forIterable elements: C,
+ crs: CRS.Type
+) -> CRS.Coordinates?
+where C: Iterable,
+ CRS: CoordinateReferenceSystem,
+ C.Element == CRS.Coordinates
+{
+ var iterator = elements.makeIterator()
+ return TurfCore.centroid(forIterator: &iterator, crs: crs)
+}
+
+public func centroid(
+ forNonEmptyIterable elements: C,
+ crs: CRS.Type
+) -> CRS.Coordinates
+where C: NonEmptyIterable,
+ CRS: CoordinateReferenceSystem,
+ C.Element == CRS.Coordinates
+{
+ var iterator = elements.makeIterator()
+ return TurfCore.centroid(forIterator: &iterator, crs: CRS.self) ?? iterator.first()
+}
+
+public func centroid(forCollection elements: C) -> C.Element?
+where C: Collection,
+ C.Element: Coordinates,
+ C.Element == C.Element.CRS.Coordinates
+{
+ var iterator = elements.makeIterator()
+ return TurfCore.centroid(forIterator: &iterator, crs: C.Element.CRS.self)
+}
+
+// MARK: - Helpers
+
+// MARK: Iterable types
+
+extension Iterable
+where Self.Element: Coordinates,
+ Self.Element == Self.Element.CRS.Coordinates
+{
+ public var centroid: Self.Element.CRS.Coordinates? {
+ TurfCore.centroid(forIterable: self, crs: Self.Element.CRS.self)
+ }
+}
+
+extension NonEmptyIterable
+where Self.Element: Coordinates,
+ Self.Element == Self.Element.CRS.Coordinates
+{
+ public var centroid: Self.Element.CRS.Coordinates {
+ TurfCore.centroid(forNonEmptyIterable: self, crs: Self.Element.CRS.self)
+ }
+}
+
+extension Collection
+where Self.Element: Coordinates,
+ Self.Element == Self.Element.CRS.Coordinates
+{
+ public var centroid: Self.Element.CRS.Coordinates? {
+ var iterator = self.makeIterator()
+ return TurfCore.centroid(forIterator: &iterator, crs: Self.Element.CRS.self)
+ }
+}
diff --git a/Sources/TurfCore/Features/GeographicBBox.swift b/Sources/TurfCore/Features/GeographicBBox.swift
new file mode 100644
index 0000000..03385ac
--- /dev/null
+++ b/Sources/TurfCore/Features/GeographicBBox.swift
@@ -0,0 +1,216 @@
+//
+// GeographicBBox.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import GeodeticGeometry
+import NonEmpty
+import SwiftGeoToolbox
+import ValueWithUnit
+
+//// MARK: - Implementations
+//
+//public func geographicBBox(forIterator iterator: inout Iterator) -> BoundingBox?
+//where Iterator: IteratorProtocol, Iterator.Element: Boundable
+//{
+// return bbox(forIterator: &iterator)
+//}
+//
+//public func bbox(
+// forNonEmptyIterator iterator: inout NonEmptyIterator
+//) -> BoundingBox
+//where S.Element: Boundable
+//{
+// var bbox = iterator.first().bbox
+// while let element = iterator.next() {
+// bbox = bbox.union(element.bbox)
+// }
+// return bbox
+//}
+//
+//public func bbox(forIterable elements: C) -> BoundingBox?
+//where C: Iterable, C.Element: Boundable
+//{
+// var iterator = elements.makeIterator()
+// return bbox(forIterator: &iterator)
+//}
+//
+//public func bbox(forNonEmptyIterable elements: C) -> BoundingBox
+//where C: NonEmptyIterable, C.Element: Boundable
+//{
+// var iterator = elements.makeIterator()
+// return bbox(forIterator: &iterator) ?? iterator.first().bbox
+//}
+//
+//public func geographicBBox(
+// forCollection coordinates: C
+//) -> BoundingBox?
+//where C.Element == CRS.Coordinates
+//{
+// return geographicBBox(forCollection: coordinates.map(Point.init(coordinates:)))
+//}
+//
+//public func geographicBBox(
+// forMultiPoint multiPoint: MultiPoint
+//) -> BoundingBox
+//where MultiPoint: MultiPointProtocol,
+// MultiPoint.Point == Point
+//{
+// return geographicBBox(forIterable: multiPoint.points)
+// ?? bbox(forPoint: multiPoint.points.first)
+//}
+//
+//// MARK: - Helpers
+//
+//// MARK: Shapes
+//
+//extension Point where Self.Coordinates: Boundable {
+// public var geographicBBox: BoundingBox { self.coordinates.bbox }
+//}
+//
+//extension MultiPointProtocol: Boundable {
+// public var geographicBBox: BoundingBox {
+// TurfCore.geographicBBox(forIterable: self.points)
+// }
+//}
+//
+//public extension LineString {
+// var bbox: BoundingBox {
+// bbox(forMultiPoint: self)
+// }
+//}
+//
+//// MARK: Iterable types
+//
+//extension Iterable where Self.Element: Boundable {
+// public var bbox: BoundingBox? {
+// TurfCore.bbox(forIterable: self)
+// }
+//}
+//
+//extension NonEmptyIterable where Self.Element: Boundable {
+// public var bbox: BoundingBox {
+// TurfCore.bbox(forNonEmptyIterable: self)
+// }
+//}
+//
+//extension Sequence where Self.Element: Boundable, BoundingBox: Boundable {
+// public var bbox: BoundingBox {
+// self.reduce(.zero, { $0.union($1.bbox) })
+// }
+//}
+
+// MARK: - Implementations
+
+public extension MultiPoint
+where CRS.Coordinates: TwoDimensionalCoordinates & BoundableCoordinates,
+ CRS.Coordinates.Y == Geodesy.Longitude,
+ Point.Coordinates == CRS.Coordinates
+{
+ var geographicBBox: BoundingBox? {
+ guard let bbox = self.bbox else { return nil }
+
+ if bbox.size.horizontalDelta > .halfRotation {
+ let offsetCoords: [CRS.Coordinates] = self.points.map(\.coordinates.withPositiveLongitude)
+ return TurfCore.bbox(forIterable: offsetCoords)
+ } else {
+ return bbox
+ }
+ }
+}
+
+public extension MultiPoint
+where CRS.Coordinates: ThreeDimensionalCoordinates & BoundableCoordinates,
+ CRS.Coordinates.Y == Geodesy.Longitude,
+ Point.Coordinates == CRS.Coordinates
+{
+ var geographicBBox: BoundingBox? {
+ guard let bbox = self.bbox else { return nil }
+
+ if bbox.size.horizontalDelta > .halfRotation {
+ let offsetCoords: [CRS.Coordinates] = self.points.map(\.coordinates.withPositiveLongitude)
+ return TurfCore.bbox(forIterable: offsetCoords)
+ } else {
+ return bbox
+ }
+ }
+}
+
+//public extension TwoDimensionalGeometricSystem {
+// static func bbox(forCollection points: Points) -> Self.BoundingBox?
+// where Points.Element == Self.Point
+// {
+// guard let (minX, maxX) = points.map(\.x).minAndMax(),
+// let (minY, maxY) = points.map(\.y).minAndMax()
+// else { return nil }
+//
+// return Self.BoundingBox(
+// min: .init(x: minX, y: minY),
+// max: .init(x: maxX, y: maxY)
+// )
+// }
+//}
+//
+//#warning("TODO: Merge this implementations with the 3D version.")
+//public extension TwoDimensionalGeometricSystem
+//where Self.CRS: GeographicCRS,
+//// NOTE: For some reason, replacing `CRS.CoordinateSystem.Axis2.Value` by `Self.Coordinates.Y`
+//// results in a compiler error.
+//Self.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent,
+//Self.Size.DY: AngularCoordinateComponent
+//{
+// static func geographicBBox(forCollection points: Points) -> Self.BoundingBox?
+// where Points.Element == Self.Point,
+// Self.BoundingBox.Size.RawValue: TwoDimensionalCoordinate
+// {
+// guard let bbox = Self.bbox(forCollection: points) else { return nil }
+// if bbox.size.horizontalDelta > .halfRotation {
+// let offsetCoords: [Point] = points.map(\.withPositiveLongitude)
+//
+// return Self.bbox(forCollection: offsetCoords)
+// } else {
+// return bbox
+// }
+// }
+//}
+//
+//extension ThreeDimensionalGeometricSystem {
+// public static func bbox(forCollection points: Points) -> Self.BoundingBox?
+// where Points.Element == Self.Point
+// {
+// guard let (minX, maxX) = points.map(\.x).minAndMax(),
+// let (minY, maxY) = points.map(\.y).minAndMax(),
+// let (minZ, maxZ) = points.map(\.z).minAndMax()
+// else { return nil }
+//
+// return Self.BoundingBox(
+// min: Self.Coordinates(rawValue: (minX, minY, minZ)),
+// max: Self.Coordinates(rawValue: (maxX, maxY, maxZ))
+// )
+// }
+//}
+//
+//public extension ThreeDimensionalGeometricSystem
+//where Self.CRS: GeographicCRS,
+//// NOTE: For some reason, replacing `CRS.CoordinateSystem.Axis2.Value` by `Self.Coordinates.Y`
+//// results in a compiler error.
+//Self.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent,
+//Self.Size.DY: AngularCoordinateComponent
+//{
+// static func geographicBBox(forCollection points: Points) -> Self.BoundingBox?
+// where Points.Element == Self.Point,
+// Self.BoundingBox.Size.RawValue: ThreeDimensionalCoordinate
+// {
+// guard let bbox = Self.bbox(forCollection: points) else { return nil }
+// if bbox.size.horizontalDelta > .halfRotation {
+// let offsetCoords: [Point] = points.map(\.withPositiveLongitude)
+//
+// return Self.bbox(forCollection: offsetCoords)
+// } else {
+// return bbox
+// }
+// }
+//}
diff --git a/Sources/TurfCore/Features/IsClockwise.swift b/Sources/TurfCore/Features/IsClockwise.swift
new file mode 100644
index 0000000..1dc256a
--- /dev/null
+++ b/Sources/TurfCore/Features/IsClockwise.swift
@@ -0,0 +1,33 @@
+//
+// IsClockwise.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import Geodesy
+import GeodeticGeometry
+import SwiftGeoToolbox
+
+/// Calculates if a given polygon is clockwise or counter-clockwise.
+///
+/// ```swift
+/// let clockwiseRing = LineString2D(Point2D(0, 0), Point2D(1, 1), Point2D(1, 0), Point2D(0, 0))
+/// let counterClockwiseRing = LineString2D(Point2D(0, 0), Point2D(1, 0), Point2D(1, 1), Point2D(0, 0))
+///
+/// isClockwise(clockwiseRing) // true
+/// isClockwise(counterClockwiseRing) // false
+/// ```
+///
+/// Ported from [Turf](https://github.com/Turfjs/turf/blob/d72985ce1a577b42340fed5fc70efe8e4bc8b062/packages/turf-boolean-clockwise/index.ts#L19-L35).
+public func isClockwise(
+ forCollection points: Points
+) -> Bool
+where CRS: TwoDimensionalCRS,
+ Points.Element == Point,
+ CRS.Coordinates.X: AngularCoordinateComponent,
+ CRS.Coordinates.Y: AngularCoordinateComponent
+{
+ return TurfCore.plannarArea(forCollection: points).sign == .plus
+}
diff --git a/Sources/TurfCore/Features/PlannarArea.swift b/Sources/TurfCore/Features/PlannarArea.swift
new file mode 100644
index 0000000..f0004ff
--- /dev/null
+++ b/Sources/TurfCore/Features/PlannarArea.swift
@@ -0,0 +1,40 @@
+//
+// CenterOfMass.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import Geodesy
+import GeodeticGeometry
+import SwiftGeoToolbox
+
+/// Calculates the signed area of a planar non-self-intersecting polygon
+/// (not taking into account the curvature of the Earth).
+///
+/// Formula from .
+public func plannarArea(
+ forCollection points: Points
+) -> Double
+where CRS: TwoDimensionalCRS,
+ Points.Element == Point,
+ CRS.Coordinates.X: AngularCoordinateComponent,
+ CRS.Coordinates.Y: AngularCoordinateComponent
+{
+ guard let first = points.first else { return 0 }
+
+ var ring = Array(points)
+ // Close the ring
+ if ring.last != first {
+ ring.append(first)
+ }
+
+ var area: Double = 0
+ for (c1, c2) in ring.adjacentPairs() {
+ area += c1.coordinates.x.decimalDegrees * c2.coordinates.y.decimalDegrees
+ - c2.coordinates.x.decimalDegrees * c1.coordinates.y.decimalDegrees
+ }
+
+ return area / 2.0
+}
diff --git a/Sources/TurfCore/Features/PointAlong.swift b/Sources/TurfCore/Features/PointAlong.swift
new file mode 100644
index 0000000..a46d254
--- /dev/null
+++ b/Sources/TurfCore/Features/PointAlong.swift
@@ -0,0 +1,19 @@
+//
+// PointAlong.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 26/03/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import Geodesy
+import GeodeticGeometry
+import SwiftGeoToolbox
+
+// MARK: - Functions
+
+public func pointAlong(line: Line, fraction: Double) -> CRS.Coordinates
+where CRS: CoordinateReferenceSystem {
+ precondition((Double(0.0)...Double(1.0)).contains(fraction))
+ return line.start.coordinates + fraction * line.vector
+}
diff --git a/Sources/TurfCore/GeodeticGeometry+Iterable.swift b/Sources/TurfCore/GeodeticGeometry+Iterable.swift
deleted file mode 100644
index ed4f3ac..0000000
--- a/Sources/TurfCore/GeodeticGeometry+Iterable.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// File.swift
-//
-//
-// Created by Rémi Bardon on 28/11/2022.
-//
-
-import GeodeticGeometry
-
-public extension MultiPointProtocol {
- func makeIterator() -> NonEmptyIterator {
- NonEmptyIterator(base: self.points)
- }
-}
diff --git a/Sources/TurfCore/GeodeticGeometry+Turf.swift b/Sources/TurfCore/GeodeticGeometry+Turf.swift
deleted file mode 100644
index edbb74b..0000000
--- a/Sources/TurfCore/GeodeticGeometry+Turf.swift
+++ /dev/null
@@ -1,378 +0,0 @@
-//
-// GeodeticGeometry+Turf.swift
-// SwiftGeo
-//
-// Created by Rémi Bardon on 26/03/2022.
-// Copyright © 2022 Rémi Bardon. All rights reserved.
-//
-
-import GeodeticGeometry
-import NonEmpty
-import SwiftGeoToolbox
-import ValueWithUnit
-
-// MARK: - Default `GeometricSystemAlgebra` implementations
-
-public extension GeometricSystemAlgebra {
-
- static func bbox(forPoint point: Self.Point) -> Self.BoundingBox {
- Self.BoundingBox(origin: point.coordinates, size: .zero)
- }
-
- static func bbox(forIterator iterator: inout Iterator) -> Self.BoundingBox?
- where Iterator: IteratorProtocol, Iterator.Element: Boundable
- {
- guard let element = iterator.next() else {
- return nil
- }
- var bbox: Self.BoundingBox = element.bbox
- while let element = iterator.next() {
- bbox = bbox.union(element.bbox)
- }
- return bbox
- }
-
- static func bbox(
- forNonEmptyIterator iterator: inout NonEmptyIterator
- ) -> Self.BoundingBox
- where S.Element: Boundable
- {
- var bbox: Self.BoundingBox = iterator.first().bbox
- while let element = iterator.next() {
- bbox = bbox.union(element.bbox)
- }
- return bbox
- }
-
- static func bbox(forIterable elements: C) -> Self.BoundingBox?
- where C: Iterable, C.Element: Boundable
- {
- var iterator = elements.makeIterator()
- return Self.bbox(forIterator: &iterator)
- }
-
- static func bbox(forNonEmptyIterable elements: C) -> Self.BoundingBox
- where C: NonEmptyIterable, C.Element: Boundable
- {
- var iterator = elements.makeIterator()
- return Self.bbox(forIterator: &iterator) ?? iterator.first().bbox
- }
-
- static func geographicBBox(forCollection coordinates: C) -> Self.BoundingBox?
- where C.Element == Self.Coordinates
- {
- return Self.geographicBBox(forCollection: coordinates.map(Self.Point.init(coordinates:)))
- }
-
- static func geographicBBox(forMultiPoint multiPoint: MultiPoint) -> Self.BoundingBox
- where MultiPoint: GeodeticGeometry.MultiPointProtocol,
- MultiPoint.Point == Self.Point
- {
- return self.geographicBBox(forCollection: multiPoint.points)
- ?? self.bbox(forPoint: multiPoint.points.first)
- }
-
-
- static func center(forIterable elements: C) -> Self.Coordinates?
- where C: Iterable, C.Element == Self.Point
- {
- Self.bbox(forIterable: elements).flatMap(Self.center(forBBox:))
- }
-
- static func centroid(forCollection points: Points) -> Self.Coordinates?
- where Points.Element == Self.Point
- {
- guard !points.isEmpty else { return nil }
- return points.sum().coordinates / .init(points.count)
- }
-
- static func pointAlong(line: Self.Line, fraction: Double) -> Self.Coordinates {
- precondition((Double(0.0)...Double(1.0)).contains(fraction))
- return line.start.coordinates + fraction * line.vector.end.coordinates
- }
-
- static func bezier(
- forLineString lineString: Self.LineString,
- sharpness: Double,
- resolution: Double
- ) -> Self.LineString {
- let spline = CubicBezierSpline(points: lineString.points, sharpness: sharpness)
- let points = AtLeast2<[Self.Point]>(
- rawValue: spline.curve(resolution: resolution).map(Self.Point.init(coordinates:))
- )!
- return Self.LineString(points: points)
- }
-
-}
-
-// MARK: Helper functions
-
-public extension GeodeticGeometry.Point where GeometricSystem: GeometricSystemAlgebra {
-
- var bbox: Self.GeometricSystem.BoundingBox {
- Self.GeometricSystem.bbox(forPoint: self)
- }
-
-}
-
-public extension GeodeticGeometry.Line where GeometricSystem: GeometricSystemAlgebra {
-
- var bbox: Self.GeometricSystem.BoundingBox {
- Self.GeometricSystem.bbox(forMultiPoint: self)
- }
-
- var geographicBBox: Self.GeometricSystem.BoundingBox {
- Self.GeometricSystem.geographicBBox(forMultiPoint: self)
- }
-
- var center: Self.GeometricSystem.Coordinates {
- Self.GeometricSystem.center(forBBox: self.bbox)
- }
-
- var centerPoint: Self.GeometricSystem.Point {
- .init(coordinates: self.center)
- }
-
-}
-
-public extension GeodeticGeometry.LineString where GeometricSystem: GeometricSystemAlgebra {
-
- var bbox: Self.GeometricSystem.BoundingBox {
- Self.GeometricSystem.bbox(forMultiPoint: self)
- }
-
- func bezier(sharpness: Double, resolution: Double) -> Self {
- // NOTE: For some reason, this does not compile without type casts.
- // Cannot convert return expression of type 'Self.GeometricSystem.LineString' to return type 'Self'
- // Insert ' as! Self'
- // Cannot convert value of type 'Self' to expected argument type 'Self.GeometricSystem.LineString'
- // Insert ' as! Self.GeometricSystem.LineString'
- Self.GeometricSystem.bezier(
- forLineString: self as! Self.GeometricSystem.LineString,
- sharpness: sharpness,
- resolution: resolution
- ) as! Self
- }
-
-}
-
-// MARK: - 2D
-
-// MARK: Required methods
-
-public extension TwoDimensionalGeometricSystem {
-
- static func bbox(forCollection points: Points) -> Self.BoundingBox?
- where Points.Element == Self.Point
- {
- guard let (minX, maxX) = points.map(\.x).minAndMax(),
- let (minY, maxY) = points.map(\.y).minAndMax()
- else { return nil }
-
- return Self.BoundingBox(
- min: .init(x: minX, y: minY),
- max: .init(x: maxX, y: maxY)
- )
- }
-
-}
-
-#warning("TODO: Merge this implementations with the 3D version.")
-public extension TwoDimensionalGeometricSystem
-where Self.CRS: GeographicCRS,
- // NOTE: For some reason, replacing `CRS.CoordinateSystem.Axis2.Value` by `Self.Coordinates.Y`
- // results in a compiler error.
- Self.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent,
- Self.Size.DY: AngularCoordinateComponent
-{
- static func geographicBBox(forCollection points: Points) -> Self.BoundingBox?
- where Points.Element == Self.Point,
- Self.BoundingBox.Size.RawValue: TwoDimensionalCoordinate
- {
- guard let bbox = Self.bbox(forCollection: points) else { return nil }
- if bbox.size.horizontalDelta > .halfRotation {
- let offsetCoords: [Point] = points.map(\.withPositiveLongitude)
-
- return Self.bbox(forCollection: offsetCoords)
- } else {
- return bbox
- }
- }
-}
-
-// MARK: Specific methods
-
-extension TwoDimensionalGeometricSystem where Self: GeometricSystemAlgebra {
-
- /// Returns the [center of mass](https://en.wikipedia.org/wiki/Center_of_mass) of a polygon.
- ///
- /// Ported from
- ///
- /// - Note: This code is not very clean, but Compile Time Optimizations have been added to reduce
- /// type checking from `>500ms` to `<50ms`.
- public static func centerOfMass(
- forCollection points: Points
- ) -> Self.Coordinates?
- where Points.Element == Self.Point,
- Self.Point.CRS.CoordinateSystem: TwoDimensionalCS,
- Self.Point.CRS.CoordinateSystem.Axis1.Value: AngularCoordinateComponent,
- Self.Point.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent
- {
- // First, we neutralize the feature (set it around coordinates [0,0]) to prevent rounding errors
- // We take any point to translate all the points around 0
- guard let centre: Self.Coordinates = Self.centroid(forCollection: points) else { return nil }
- let translation: Self.Coordinates = centre
- var sx: Self.Coordinates.X = 0
- var sy: Self.Coordinates.Y = 0
- var sArea: Double = 0
-
- let neutralizedPoints: [Self.Coordinates] = points.map { $0.coordinates - translation }
-
- for i in 0...
- public static func plannarArea(forCollection points: Points) -> Double
- where Points.Element == Self.Point,
- // NOTE: For some reason, replacing `Self.CRS.CoordinateSystem.Axis1.Value`
- // by `Self.Coordinates.X` results in a compiler error.
- Self.CRS.CoordinateSystem.Axis1.Value: AngularCoordinateComponent,
- // NOTE: For some reason, replacing `Self.CRS.CoordinateSystem.Axis2.Value`
- // by `Self.Coordinates.Y` results in a compiler error.
- Self.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent
- {
- guard let first = points.first else { return 0 }
-
- var ring = Array(points)
- // Close the ring
- if ring.last != first {
- ring.append(first)
- }
-
- var area: Double = 0
- for (c1, c2) in ring.adjacentPairs() {
- area += c1.x.decimalDegrees * c2.y.decimalDegrees - c2.x.decimalDegrees * c1.y.decimalDegrees
- }
-
- return area / 2.0
- }
-
- /// Calculates if a given polygon is clockwise or counter-clockwise.
- ///
- /// ```swift
- /// let clockwiseRing = LineString2D(Point2D(0, 0), Point2D(1, 1), Point2D(1, 0), Point2D(0, 0))
- /// let counterClockwiseRing = LineString2D(Point2D(0, 0), Point2D(1, 0), Point2D(1, 1), Point2D(0, 0))
- ///
- /// Turf.isClockwise(clockwiseRing) // true
- /// Turf.isClockwise(counterClockwiseRing) // false
- /// ```
- ///
- /// Ported from [Turf](https://github.com/Turfjs/turf/blob/d72985ce1a577b42340fed5fc70efe8e4bc8b062/packages/turf-boolean-clockwise/index.ts#L19-L35).
- public static func isClockwise(collection points: Points) -> Bool
- where Points.Element == Self.Point,
- // NOTE: For some reason, replacing `Self.CRS.CoordinateSystem.Axis1.Value`
- // by `Self.Coordinates.X` results in a compiler error.
- Self.CRS.CoordinateSystem.Axis1.Value: AngularCoordinateComponent,
- // NOTE: For some reason, replacing `Self.CRS.CoordinateSystem.Axis2.Value`
- // by `Self.Coordinates.Y` results in a compiler error.
- Self.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent
- {
- return Self.plannarArea(forCollection: points) > 0
- }
-
-}
-
-// MARK: - 3D
-
-// MARK: Required methods
-
-extension ThreeDimensionalGeometricSystem {
-
- public static func bbox(forCollection points: Points) -> Self.BoundingBox?
- where Points.Element == Self.Point
- {
- guard let (minX, maxX) = points.map(\.x).minAndMax(),
- let (minY, maxY) = points.map(\.y).minAndMax(),
- let (minZ, maxZ) = points.map(\.z).minAndMax()
- else { return nil }
-
- return Self.BoundingBox(
- min: Self.Coordinates(rawValue: (minX, minY, minZ)),
- max: Self.Coordinates(rawValue: (maxX, maxY, maxZ))
- )
- }
-
- public static func center(forBBox bbox: Self.BoundingBox) -> Self.Coordinates
- where Self.Size.RawValue == Self.Coordinates
- {
- return bbox.origin.offsetBy(
- dx: bbox.size.dx / 2,
- dy: bbox.size.dy / 2,
- dz: bbox.size.dz / 2
- )
- }
-
-}
-
-public extension ThreeDimensionalGeometricSystem
-where Self.CRS: GeographicCRS,
- // NOTE: For some reason, replacing `CRS.CoordinateSystem.Axis2.Value` by `Self.Coordinates.Y`
- // results in a compiler error.
- Self.CRS.CoordinateSystem.Axis2.Value: AngularCoordinateComponent,
- Self.Size.DY: AngularCoordinateComponent
-{
- static func geographicBBox(forCollection points: Points) -> Self.BoundingBox?
- where Points.Element == Self.Point,
- Self.BoundingBox.Size.RawValue: ThreeDimensionalCoordinate
- {
- guard let bbox = Self.bbox(forCollection: points) else { return nil }
- if bbox.size.horizontalDelta > .halfRotation {
- let offsetCoords: [Point] = points.map(\.withPositiveLongitude)
-
- return Self.bbox(forCollection: offsetCoords)
- } else {
- return bbox
- }
- }
-}
diff --git a/Sources/TurfCore/GeometricSystemAlgebra.swift b/Sources/TurfCore/GeometricSystemAlgebra.swift
deleted file mode 100644
index 2b6cf6f..0000000
--- a/Sources/TurfCore/GeometricSystemAlgebra.swift
+++ /dev/null
@@ -1,116 +0,0 @@
-//
-// GeodeticGeometry+Turf.swift
-// SwiftGeo
-//
-// Created by Rémi Bardon on 26/03/2022.
-// Copyright © 2022 Rémi Bardon. All rights reserved.
-//
-
-import GeodeticGeometry
-import NonEmpty
-import SwiftGeoToolbox
-import ValueWithUnit
-
-#warning("TODO: Replace all the `bbox` by one or two using `Boundable`")
-
-public protocol GeometricSystemAlgebra: GeodeticGeometry.GeometricSystem
-where Self.BoundingBox: Boundable,
- Self.Point: Boundable
-{
-
- // MARK: Bounding box
-
- /// Returns a naive [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of geometrical elements.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Warning: This is a naive implementation, not taking into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 0°E).
- static func bbox(forIterator iterator: inout Iterator) -> Self.BoundingBox?
- where Iterator: IteratorProtocol, Iterator.Element: Boundable
-
- /// Returns a naive [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of geometrical elements.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Warning: This is a naive implementation, not taking into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 0°E).
- static func bbox(
- forNonEmptyIterator iterator: inout NonEmptyIterator
- ) -> Self.BoundingBox
- where Base.Element: Boundable
-
- /// Returns a naive [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of geometrical elements.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Warning: This is a naive implementation, not taking into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 0°E).
- static func bbox(forIterable elements: C) -> Self.BoundingBox?
- where C: Iterable, C.Element: Boundable
-
- /// Returns a naive [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of geometrical elements.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Warning: This is a naive implementation, not taking into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 0°E).
- static func bbox(forNonEmptyIterable elements: C) -> Self.BoundingBox
- where C: NonEmptyIterable, C.Element: Boundable
-
- /// Returns a naive [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of points.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Warning: This is a naive implementation, not taking into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 0°E).
- static func bbox(forMultiPoint multiPoint: MultiPoint) -> Self.BoundingBox
- where MultiPoint: GeodeticGeometry.MultiPointProtocol,
- MultiPoint.Point == Self.Point
-
- /// Returns the [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of points.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Note: This implementation takes into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 180°E).
- static func geographicBBox(forCollection coordinates: C) -> Self.BoundingBox?
- where C.Element == Self.Coordinates
-
- /// Returns the [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of points.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Note: This implementation takes into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 180°E).
- static func geographicBBox(forCollection points: Points) -> Self.BoundingBox?
- where Points.Element == Self.Point
-
- /// Returns the [bounding box](https://en.wikipedia.org/wiki/Minimum_bounding_box)
- /// enclosing a cluster of points.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Note: This implementation takes into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a bounding box around 0°N 180°E).
- static func geographicBBox(forMultiPoint multiPoint: MultiPoint) -> Self.BoundingBox
- where MultiPoint: GeodeticGeometry.MultiPointProtocol,
- MultiPoint.Point == Self.Point
-
- // MARK: Center
-
- /// Returns the linear center of a cluster of points.
- /// - Warning: This does not take into account the curvature of the Earth.
- /// - Warning: This is a naive implementation, not taking into account the angular coordinate system
- /// (i.e. a cluster around 0°N 180°E will have a center near 0°N 0°E).
- static func center(forCollection points: Points) -> Self.Coordinates?
- where Points.Element == Self.Point
-
- static func center(forBBox bbox: Self.BoundingBox) -> Self.Coordinates
-
- // MARK: Centroid
-
- /// Calculates the centroid of a polygon using the mean of all vertices.
- static func centroid(forCollection points: Points) -> Self.Coordinates?
- where Points.Element == Self.Point
-
- // MARK: Bézier
-
- static func bezier(
- forLineString: Self.LineString,
- sharpness: Double,
- resolution: Double
- ) -> Self.LineString
-
-}
diff --git a/Sources/TurfCore/Helpers.swift b/Sources/TurfCore/Helpers.swift
deleted file mode 100644
index a5d5556..0000000
--- a/Sources/TurfCore/Helpers.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// Helpers.swift
-// SwiftGeo
-//
-// Created by Rémi Bardon on 29/03/2022.
-// Copyright © 2022 Rémi Bardon. All rights reserved.
-//
-
-import Foundation
-
-extension Sequence where Element: AdditiveArithmetic {
- func sum() -> Element {
- return reduce(.zero, +)
- }
-}
-
diff --git a/Sources/TurfCore/Logger.swift b/Sources/TurfCore/Logger.swift
new file mode 100644
index 0000000..8058bcd
--- /dev/null
+++ b/Sources/TurfCore/Logger.swift
@@ -0,0 +1,11 @@
+//
+// Logger.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 01/12/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import OSLog
+
+let logger = Logger(subsystem: "name.remibardon.swift-geo", category: "turf")
diff --git a/Sources/WGS84Turf/WGS84+Turf.swift b/Sources/WGS84Turf/WGS84+Turf.swift
index a7c6da1..ac4eb8d 100644
--- a/Sources/WGS84Turf/WGS84+Turf.swift
+++ b/Sources/WGS84Turf/WGS84+Turf.swift
@@ -6,20 +6,14 @@
// Copyright © 2022 Rémi Bardon. All rights reserved.
//
-import SwiftGeoToolbox
-import TurfCore
-import protocol Turf.GeometricSystemAlgebra
-import enum WGS84Geometry.WGS842D
-import enum WGS84Geometry.WGS843D
-
-extension WGS842D.BoundingBox: Boundable {}
-extension WGS842D.Point: Boundable {}
-
-extension WGS842D: GeometricSystemAlgebra {
- public static func center(forBBox bbox: Self.BoundingBox) -> Self.Coordinates {
- let offset: Self.Size = bbox.size / 2
- return bbox.origin.offsetBy(offset.rawValue)
- }
-}
-
-extension WGS843D: GeometricSystemAlgebra {}
+//import SwiftGeoToolbox
+//import TurfCore
+//
+//extension WGS842D: GeometricSystemAlgebra {
+// public static func center(forBBox bbox: Self.BoundingBox) -> Self.Coordinates {
+// let offset: Self.Size = bbox.size / 2
+// return bbox.origin.offsetBy(offset.rawValue)
+// }
+//}
+//
+//extension WGS843D: GeometricSystemAlgebra {}
diff --git a/Tests/GeodeticGeometryTests/VectorTests.swift b/Tests/GeodeticGeometryTests/VectorTests.swift
new file mode 100644
index 0000000..79cad02
--- /dev/null
+++ b/Tests/GeodeticGeometryTests/VectorTests.swift
@@ -0,0 +1,31 @@
+//
+// VectorTests.swift
+// SwiftGeo
+//
+// Created by Rémi Bardon on 01/12/2022.
+// Copyright © 2022 Rémi Bardon. All rights reserved.
+//
+
+import GeodeticGeometry
+import SwiftGeoToolbox
+import WGS84Geometry
+import XCTest
+
+final class VectorTests: XCTestCase {
+
+ func testVectorArithmetic() {
+ XCTAssertEqual(
+ Vector2D(from: .init(x: 10, y: 15), to: .init(x: 12, y: 18)),
+ Vector2D(dx: 2, dy: 3)
+ )
+ XCTAssertEqual(
+ Vector2D(dx: 4, dy: 6).half,
+ Vector2D(dx: 2, dy: 3)
+ )
+ XCTAssertEqual(
+ 2 * Vector2D(dx: 4, dy: 6),
+ Vector2D(dx: 8, dy: 12)
+ )
+ }
+
+}
diff --git a/Tests/TurfCoreTests/CubicBezierSplineTests.swift b/Tests/TurfCoreTests/CubicBezierSplineTests.swift
index c1b9b1a..049e696 100644
--- a/Tests/TurfCoreTests/CubicBezierSplineTests.swift
+++ b/Tests/TurfCoreTests/CubicBezierSplineTests.swift
@@ -6,17 +6,20 @@
// Copyright © 2022 Rémi Bardon. All rights reserved.
//
-import SwiftGeoToolbox
+import typealias NonEmpty.AtLeast2
@testable import struct TurfCore.CubicBezierSpline
-import WGS84Turf
+import WGS84Core
import XCTest
final class CubicBezierSplineTests: XCTestCase {
+ typealias CRS2D = EPSG4326
+ typealias Spline = CubicBezierSpline
+
func testPointBetween() {
let a = Coordinate2D(x: 10, y: 10)
let b = Coordinate2D(x: 20, y: 20)
- let res = CubicBezierSpline.pointBetween(a, and: b, fraction: 0.5)
+ let res = Spline.pointBetween(a, and: b, fraction: 0.5)
XCTAssertEqual(res, Coordinate2D(x: 15, y: 15))
}
@@ -24,8 +27,37 @@ final class CubicBezierSplineTests: XCTestCase {
let a = Coordinate2D(x: 10, y: 10)
let b = Coordinate2D(x: 20, y: 20)
let c = Coordinate2D(x: 30, y: 10)
- let res = CubicBezierSpline.pointInQuadCurve(a, b, c, fraction: 0.5)
+ let res = Spline.pointInQuadCurve(a, b, c, fraction: 0.5)
XCTAssertEqual(res, Coordinate2D(x: 20, y: 15))
}
+ func testControlPoints() {
+ let a = Coordinate2D(x: 10, y: 10)
+ let b = Coordinate2D(x: 20, y: 20)
+ let c = Coordinate2D(x: 30, y: 10)
+ let res1 = Spline(coordinates: AtLeast2<[Coordinate2D]>(a, b, c), sharpness: 1)
+ XCTAssertEqual(res1.controls.map(controlsToString), [
+ "((10, 10),(10, 10),(20, 20),(20, 20))",
+ "((20, 20),(20, 20),(30, 10),(30, 10))"
+ ])
+ let res2 = Spline(coordinates: AtLeast2<[Coordinate2D]>(a, b, c), sharpness: 0)
+ XCTAssertEqual(res2.controls.map(controlsToString), [
+ "((10, 10),(10, 10),(15, 20),(20, 20))",
+ "((20, 20),(25, 20),(30, 10),(30, 10))"
+ ])
+ }
+
+ func controlsToString(
+ _ controls: (Coordinate2D, Coordinate2D, Coordinate2D, Coordinate2D)
+ ) -> String {
+ """
+ (\
+ \(String(describing: controls.0)),\
+ \(String(describing: controls.1)),\
+ \(String(describing: controls.2)),\
+ \(String(describing: controls.3))\
+ )
+ """
+ }
+
}
diff --git a/Tests/TurfCoreTests/Toolbox/MapSnapshot.swift b/Tests/TurfCoreTests/Toolbox/MapSnapshot.swift
index 709944b..b037142 100644
--- a/Tests/TurfCoreTests/Toolbox/MapSnapshot.swift
+++ b/Tests/TurfCoreTests/Toolbox/MapSnapshot.swift
@@ -46,6 +46,7 @@ func snapshot(
mapView.addOverlay(polyline, level: .aboveRoads)
while (!delegate.renderingFinished) {
+ // Wait 100ms
try await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
}
return mapView.snapshot
diff --git a/Tests/TurfCoreTests/TurfTests.swift b/Tests/TurfCoreTests/TurfTests.swift
index 7e2eff3..72ab2c6 100644
--- a/Tests/TurfCoreTests/TurfTests.swift
+++ b/Tests/TurfCoreTests/TurfTests.swift
@@ -7,16 +7,26 @@
//
import Geodesy
+import GeodeticDisplay
+import GeodeticGeometry
import TurfCore
import WGS84Core
import WGS84Geometry
import XCTest
final class TurfTests: XCTestCase {
-
+
+ typealias CRS2D = EPSG4326
+
func testNaiveBBoxNeverCrosses180thMeridian() throws {
- func test(coordinates: [Coordinate2D], crosses: Bool) throws {
- let bbox = try XCTUnwrap(WGS842D.bbox(forCollection: coordinates))
+ func test(
+ coordinates: [Coordinate2D],
+ crosses: Bool,
+ file: StaticString = #filePath,
+ line: UInt = #line
+ ) throws {
+ let multiPoint = MultiPoint(coordinates: try! .init(coordinates))
+ let bbox = try XCTUnwrap(multiPoint.bbox)
XCTAssertEqual(bbox.crosses180thMeridian, crosses, String(reflecting: bbox))
}
@@ -36,11 +46,17 @@ final class TurfTests: XCTestCase {
}
func testGeographicBBoxCrosses180thMeridian() throws {
- func test(coordinates: [Coordinate2D], crosses: Bool) throws {
- let bbox = try XCTUnwrap(WGS842D.geographicBBox(forCollection: coordinates))
- XCTAssertEqual(bbox.crosses180thMeridian, crosses, String(reflecting: bbox))
+ func test(
+ coordinates: [Coordinate2D],
+ crosses: Bool,
+ _file: StaticString = #filePath,
+ _line: UInt = #line
+ ) throws {
+ let multiPoint = MultiPoint(coordinates: try! .init(coordinates))
+ let bbox = try XCTUnwrap(multiPoint.geographicBBox)
+ XCTAssertEqual(bbox.crosses180thMeridian, crosses, String(reflecting: bbox), file: _file, line: _line)
}
-
+
// Green: Across the world
try test(coordinates: [
.init(latitude: -65, longitude: 175),
@@ -63,16 +79,17 @@ final class TurfTests: XCTestCase {
leftLong: Coordinate2D.Y,
latDelta: Coordinate2D.X,
longDelta: Coordinate2D.Y,
- file: StaticString = #filePath,
- line: UInt = #line
+ _file: StaticString = #filePath,
+ _line: UInt = #line
) throws {
- let bbox = try XCTUnwrap(WGS842D.geographicBBox(forCollection: coordinates))
+ let multiPoint = MultiPoint(coordinates: try! .init(coordinates))
+ let bbox = try XCTUnwrap(multiPoint.geographicBBox, file: _file, line: _line)
let expectedOrigin = Coordinate2D(latitude: bottomLat, longitude: leftLong)
- XCTAssertEqual(bbox.origin, expectedOrigin, "Origin", file: file, line: line)
- XCTAssertEqual(bbox.dLat, latDelta, "Latitude delta", file: file, line: line)
- XCTAssertEqual(bbox.dLong, longDelta, "Longitude delta", file: file, line: line)
+ XCTAssertEqual(bbox.origin, expectedOrigin, "Origin", file: _file, line: _line)
+ XCTAssertEqual(bbox.size.dLat, latDelta, "Latitude delta", file: _file, line: _line)
+ XCTAssertEqual(bbox.size.dLong, longDelta, "Longitude delta", file: _file, line: _line)
}
// Blue: Positive latitudes, positive longitudes
@@ -170,18 +187,23 @@ final class TurfTests: XCTestCase {
}
func testLineBBox() throws {
- func test(line: Line2D, bbox expected: BoundingBox2D) throws {
+ func test(
+ line: Line2D,
+ bbox expected: BoundingBox2D,
+ _file: StaticString = #filePath,
+ _line: UInt = #line
+ ) throws {
let bbox = try XCTUnwrap(line.bbox)
- XCTAssertEqual(bbox, expected)
+ XCTAssertEqual(bbox, expected, file: _file, line: _line)
}
let line1 = Line2D(
- start: Point2D(latitude: .min + 30, longitude: .min + 50),
- end: Point2D(latitude: .max - 10, longitude: .max - 10)
+ start: Point2D(coordinates: .init(latitude: .min + 30, longitude: .min + 50)),
+ end: Point2D(coordinates: .init(latitude: .max - 10, longitude: .max - 10))
)
try test(line: line1, bbox: BoundingBox2D(
- southWest: line1.start.coordinates,
- northEast: line1.end.coordinates
+ min: line1.start.coordinates,
+ max: line1.end.coordinates
))
}