From a383f71672f323e701f4112406504e633be3bbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Bardon?= Date: Sun, 4 Dec 2022 19:25:13 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20some=20genericity=20fro?= =?UTF-8?q?m=20`TurfCore`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xcshareddata/xcschemes/Turf.xcscheme | 35 +- Sources/Geodesy/CoordinateComponent.swift | 39 +- Sources/Geodesy/Coordinates.swift | 243 +++++++++++ Sources/Geodesy/Geodesy.swift | 230 +---------- .../GeodeticDisplay/Coordinate+Display.swift | 2 +- .../Concepts/BoundingBox.swift | 35 +- .../GeodeticGeometry/Concepts/Vector.swift | 55 ++- Sources/GeodeticGeometry/Shapes/Line.swift | 7 + .../GeodeticGeometry/Shapes/LineString.swift | 7 + .../GeodeticGeometry/Shapes/LinearRing.swift | 12 +- .../GeodeticGeometry/Shapes/MultiLine.swift | 21 +- .../GeodeticGeometry/Shapes/MultiPoint.swift | 9 +- Sources/GeodeticGeometry/Shapes/Point.swift | 28 ++ .../GeodeticGeometry/Toolbox/Iterable.swift | 6 +- Sources/TurfCore/Boundable.swift | 60 --- Sources/TurfCore/CubicBezierSpline.swift | 60 +-- Sources/TurfCore/Features/BBox.swift | 246 ++++++++++++ Sources/TurfCore/Features/Bezier.swift | 41 ++ Sources/TurfCore/Features/Center.swift | 84 ++++ Sources/TurfCore/Features/CenterOfMass.swift | 78 ++++ Sources/TurfCore/Features/Centroid.swift | 111 +++++ .../TurfCore/Features/GeographicBBox.swift | 216 ++++++++++ Sources/TurfCore/Features/IsClockwise.swift | 33 ++ Sources/TurfCore/Features/PlannarArea.swift | 40 ++ Sources/TurfCore/Features/PointAlong.swift | 19 + .../TurfCore/GeodeticGeometry+Iterable.swift | 14 - Sources/TurfCore/GeodeticGeometry+Turf.swift | 378 ------------------ Sources/TurfCore/GeometricSystemAlgebra.swift | 116 ------ Sources/TurfCore/Helpers.swift | 16 - Sources/TurfCore/Logger.swift | 11 + Sources/WGS84Turf/WGS84+Turf.swift | 28 +- Tests/GeodeticGeometryTests/VectorTests.swift | 31 ++ .../CubicBezierSplineTests.swift | 40 +- Tests/TurfCoreTests/Toolbox/MapSnapshot.swift | 1 + Tests/TurfCoreTests/TurfTests.swift | 60 ++- 35 files changed, 1471 insertions(+), 941 deletions(-) create mode 100644 Sources/Geodesy/Coordinates.swift delete mode 100644 Sources/TurfCore/Boundable.swift create mode 100644 Sources/TurfCore/Features/BBox.swift create mode 100644 Sources/TurfCore/Features/Bezier.swift create mode 100644 Sources/TurfCore/Features/Center.swift create mode 100644 Sources/TurfCore/Features/CenterOfMass.swift create mode 100644 Sources/TurfCore/Features/Centroid.swift create mode 100644 Sources/TurfCore/Features/GeographicBBox.swift create mode 100644 Sources/TurfCore/Features/IsClockwise.swift create mode 100644 Sources/TurfCore/Features/PlannarArea.swift create mode 100644 Sources/TurfCore/Features/PointAlong.swift delete mode 100644 Sources/TurfCore/GeodeticGeometry+Iterable.swift delete mode 100644 Sources/TurfCore/GeodeticGeometry+Turf.swift delete mode 100644 Sources/TurfCore/GeometricSystemAlgebra.swift delete mode 100644 Sources/TurfCore/Helpers.swift create mode 100644 Sources/TurfCore/Logger.swift create mode 100644 Tests/GeodeticGeometryTests/VectorTests.swift 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 )) }