diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/GeodeticDisplayTests.xcbaseline/9AAF5982-4101-4C27-8140-52ECB503EFD7.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/GeodeticDisplayTests.xcbaseline/9AAF5982-4101-4C27-8140-52ECB503EFD7.plist new file mode 100644 index 0000000..b233fc7 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/GeodeticDisplayTests.xcbaseline/9AAF5982-4101-4C27-8140-52ECB503EFD7.plist @@ -0,0 +1,32 @@ + + + + + classNames + + GeodeticDisplayTests + + testNumberFormatterStringForPerformance() + + com.apple.dt.XCTMetric_Clock.time.monotonic + + baselineAverage + 0.000011 + baselineIntegrationDisplayName + Local Baseline + + + testNumberFormatterStringFromPerformance() + + com.apple.dt.XCTMetric_Clock.time.monotonic + + baselineAverage + 0.000013 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/GeodeticDisplayTests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/GeodeticDisplayTests.xcbaseline/Info.plist new file mode 100644 index 0000000..2cba2a9 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/GeodeticDisplayTests.xcbaseline/Info.plist @@ -0,0 +1,33 @@ + + + + + runDestinationsByUUID + + 9AAF5982-4101-4C27-8140-52ECB503EFD7 + + localComputer + + busSpeedInMHz + 400 + cpuCount + 1 + cpuKind + 6-Core Intel Core i7 + cpuSpeedInMHz + 2600 + logicalCPUCoresPerPackage + 12 + modelCode + MacBookPro16,1 + physicalCPUCoresPerPackage + 6 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + x86_64 + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GeoModels.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GeoModels.xcscheme deleted file mode 100644 index 82b7eea..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/GeoModels.xcscheme +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme index 48aa5bb..f17f7c6 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/swift-geo-Package.xcscheme @@ -118,6 +118,48 @@ ReferencedContainer = "container:"> + + + + + + + + + + + + + + + + + + + + Self { - return Self.random(in: -halfRotation...halfRotation) - } - -} diff --git a/Sources/GeoCoordinates/Longitude.swift b/Sources/GeoCoordinates/Longitude.swift deleted file mode 100644 index 7561e52..0000000 --- a/Sources/GeoCoordinates/Longitude.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Longitude.swift -// GeoSwift -// -// Created by Rémi Bardon on 04/02/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -/// The longitude component of a geographical coordinate. -public struct Longitude: AngularCoordinate { - - public static let positiveDirectionChar: Character = "E" - public static let negativeDirectionChar: Character = "W" - - public static var fullRotation: Longitude = 360 - public static var halfRotation: Longitude = 180 - - public var decimalDegrees: Double - - public var positive: Longitude { - if decimalDegrees < .zero { - // `degrees` is negative, so we end up with `360 - |degrees|` - return self + Self.fullRotation - } else { - return self - } - } - - public init(decimalDegrees: Double) { - self.decimalDegrees = decimalDegrees - } - - public static func random() -> Self { - return Self.random(in: -halfRotation...halfRotation) - } - -} diff --git a/Sources/GeoCoordinates/Protocols/AngularCoordinate.swift b/Sources/GeoCoordinates/Protocols/AngularCoordinate.swift deleted file mode 100644 index 998da49..0000000 --- a/Sources/GeoCoordinates/Protocols/AngularCoordinate.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AngularCoordinate.swift -// GeoSwift -// -// Created by Rémi Bardon on 04/02/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -/// - Todo: Create custom struct that handles cycling in an interval. -public protocol AngularCoordinate: ValidatableCoordinate, GeographicNotation { - - static var fullRotation: Self { get } - static var halfRotation: Self { get } - - /// "N" or "E" depending on axis - static var positiveDirectionChar: Character { get } - - /// "S" or "W" depending on axis - static var negativeDirectionChar: Character { get } - - var decimalDegrees: Double { get set } - - init(decimalDegrees: Double) - -} - -extension AngularCoordinate { - - public var value: Double { - get { decimalDegrees } - set { decimalDegrees = newValue } - } - - public init(value: Double) { - self.init(decimalDegrees: value) - } - -} - -extension AngularCoordinate { - - public static var min: Self { -halfRotation } - public static var max: Self { halfRotation } - - public var valid: Self { - if isValid { - return self - } else { - let remainder = self.truncatingRemainder(dividingBy: Self.fullRotation) - - if abs(remainder) > .halfRotation { - let base = self < .zero ? Self.fullRotation : -Self.fullRotation - return base + remainder - } else { - return remainder - } - } - } - -} diff --git a/Sources/GeoCoordinates/Protocols/Coordinate.swift b/Sources/GeoCoordinates/Protocols/Coordinate.swift deleted file mode 100644 index 1d2d540..0000000 --- a/Sources/GeoCoordinates/Protocols/Coordinate.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// Coordinate.swift -// SwiftGeo -// -// Created by Rémi Bardon on 03/02/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -import Foundation - -public protocol Coordinate: BinaryFloatingPoint, CustomStringConvertible, CustomDebugStringConvertible, Zeroable { - - var value: Double { get set } - - init(value: Double) - -} - -extension Coordinate { - - // MARK: ExpressibleByFloatLiteral - - public init(floatLiteral value: Double) { - self.init(value: value) - } - - // MARK: ExpressibleByIntegerLiteral - - public init(integerLiteral value: Int) { - self.init(value: Double(value)) - } - - // MARK: FloatingPoint - - public static var nan: Self { Self.init(value: Double.nan) } - public static var signalingNaN: Self { Self.init(value: Double.signalingNaN) } - public static var infinity: Self { Self.init(value: Double.infinity) } - public static var greatestFiniteMagnitude: Self { Self.init(value: Double.greatestFiniteMagnitude) } - public static var pi: Self { Self.init(value: Double.pi) } - - public static var leastNormalMagnitude: Self { Self.init(value: Double.leastNormalMagnitude) } - public static var leastNonzeroMagnitude: Self { Self.init(value: Double.leastNonzeroMagnitude) } - - public var exponent: Double.Exponent { self.value.exponent } - public var sign: FloatingPointSign { self.value.sign } - public var significand: Self { Self.init(value: self.value.significand) } - public var ulp: Self { Self.init(value: self.value.ulp) } - - public var nextUp: Self { Self.init(value: self.value.nextUp) } - - public var isNormal: Bool { self.value.isNormal } - public var isFinite: Bool { self.value.isFinite } - public var isZero: Bool { self.value.isZero } - public var isSubnormal: Bool { self.value.isSubnormal } - public var isInfinite: Bool { self.value.isInfinite } - public var isNaN: Bool { self.value.isNaN } - public var isSignalingNaN: Bool { self.value.isSignalingNaN } - public var isCanonical: Bool { self.value.isCanonical } - - public init(sign: FloatingPointSign, exponent: Int, significand: Self) { - self.init(value: Double(sign: sign, exponent: exponent, significand: significand.value)) - } - - public static func / (lhs: Self, rhs: Self) -> Self { - Self.init(value: lhs.value / rhs.value) - } - public static func /= (lhs: inout Self, rhs: Self) { - lhs.value /= rhs.value - } - - public func isEqual(to other: Self) -> Bool { self.value == other.value } - public func isLess(than other: Self) -> Bool { self.value < other.value } - public func isLessThanOrEqualTo(_ other: Self) -> Bool { self.value <= other.value } - - public mutating func addProduct(_ lhs: Self, _ rhs: Self) { - self.value += lhs.value * rhs.value - } - public mutating func formRemainder(dividingBy other: Self) { - self.value.formRemainder(dividingBy: other.value) - } - public mutating func formTruncatingRemainder(dividingBy other: Self) { - self.value.formTruncatingRemainder(dividingBy: other.value) - } - public mutating func formSquareRoot() { - self.value.formSquareRoot() - } - public mutating func round(_ rule: FloatingPointRoundingRule) { - self.value.round(rule) - } - - // MARK: Numeric - - public static func * (lhs: Self, rhs: Self) -> Self { - return Self.init(value: lhs.value * rhs.value) - } - public static func *= (lhs: inout Self, rhs: Self) { - lhs.value *= rhs.value - } - - public var magnitude: Self { Self.init(value: self.value.magnitude) } - - public init?(exactly source: T) where T: BinaryInteger { - guard let value = Double(exactly: source) else { return nil } - self.init(value: value) - } - - // MARK: AdditiveArithmetic - - public static var zero: Self { Self.init(value: .zero) } - - // MARK: Strideable - - public static func + (lhs: Self, rhs: Self) -> Self { - return Self.init(value: lhs.value + rhs.value) - } - - public static func - (lhs: Self, rhs: Self) -> Self { - return Self.init(value: lhs.value - rhs.value) - } - - public func advanced(by n: Double.Stride) -> Self { - return Self.init(value: self.value.advanced(by: n)) - } - - public func distance(to other: Self) -> Double.Stride { - return self.value.distance(to: other.value) - } - - // MARK: BinaryFloatingPoint - - public static var exponentBitCount: Int { Double.exponentBitCount } - public static var significandBitCount: Int { Double.significandBitCount } - - public var binade: Self { Self.init(value: self.value.binade) } - public var exponentBitPattern: Double.RawExponent { self.value.exponentBitPattern } - public var significandBitPattern: Double.RawSignificand { self.value.significandBitPattern } - public var significandWidth: Int { self.value.significandWidth } - - public init( - sign: FloatingPointSign, - exponentBitPattern: Double.RawExponent, - significandBitPattern: Double.RawSignificand - ) { - self.init(value: Double( - sign: sign, - exponentBitPattern: exponentBitPattern, - significandBitPattern: significandBitPattern - )) - } - - public init(_ value: Source) where Source: BinaryFloatingPoint { - self.init(value: Double(value)) - } - - public init(_ value: Source) where Source: BinaryInteger { - self.init(value: Double(value)) - } - - // MARK: CustomStringConvertible - - public var description: String { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - return formatter.string(from: NSNumber(value: self.value)) ?? "\(self.value)" - } - - // MARK: CustomDebugStringConvertible - - public var debugDescription: String { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 3 - formatter.locale = .en - return formatter.string(from: NSNumber(value: self.value)) ?? "\(self.value)" - } - -} diff --git a/Sources/GeoCoordinates/Protocols/ValidatableCoordinate.swift b/Sources/GeoCoordinates/Protocols/ValidatableCoordinate.swift deleted file mode 100644 index 0b98752..0000000 --- a/Sources/GeoCoordinates/Protocols/ValidatableCoordinate.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// ValidatableCoordinate.swift -// GeoSwift -// -// Created by Rémi Bardon on 04/02/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -import Foundation - -public protocol ValidatableCoordinate: Coordinate { - - static var min: Self { get } - static var max: Self { get } - - /// Tells if this value is in valid bounds or not. - var isValid: Bool { get } - - /// A copy of this value, in valid bounds. - /// - /// In angular coordinate system, it means rotating the value - /// to put it in the valid range. - /// - /// In linear coordinate system, it can mean making the value positive. - var valid: Self { get } - -} - -extension ValidatableCoordinate { - - public static var validRange: ClosedRange { min...max } - - public var isValid: Bool { - Self.validRange.contains(self) - } - -} diff --git a/Sources/GeoModels/Interoperability/GeoModels+MapKit.swift b/Sources/GeoModels/Interoperability/GeoModels+MapKit.swift deleted file mode 100644 index 173845a..0000000 --- a/Sources/GeoModels/Interoperability/GeoModels+MapKit.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// GeoModels+MapKit.swift -// SwiftGeo -// -// Created by Rémi Bardon on 02/02/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -#if canImport(MapKit) -import MapKit - -extension WGS842D.BoundingBox { - - public var mkCoordinateSpan: MKCoordinateSpan { - MKCoordinateSpan(latitudeDelta: height.decimalDegrees, longitudeDelta: width.decimalDegrees) - } - public var mkCoordinateRegion: MKCoordinateRegion { - MKCoordinateRegion(center: center.clLocationCoordinate2D, span: mkCoordinateSpan) - } - - public var mkMapWidth: Double { - mkMapWidthAtLatitude(center.latitude) - } - public var mkMapHeight: Double { - mkMapHeightAtLongitude(center.longitude) - } - - public var mkMapRect: MKMapRect { - // Avoid center recalculation - let center = self.center - - return MKMapRect( - origin: northWest.mkMapPoint, - size: MKMapSize( - width: mkMapWidthAtLatitude(center.latitude), - height: mkMapHeightAtLongitude(center.longitude) - ) - ) - } - - public func mkMapWidthAtLatitude(_ latitude: Latitude) -> Double { - let east = eastAtLatitude(latitude).mkMapPoint - let west = westAtLatitude(latitude).mkMapPoint - - // `east.x > west.x` - return east.x - west.x - } - public func mkMapHeightAtLongitude(_ longitude: Longitude) -> Double { - let south = southAtLongitude(longitude).mkMapPoint - let north = northAtLongitude(longitude).mkMapPoint - - // `south.y > north.y` - return south.y - north.y - } - -} - -extension WGS842D.LineString { - - public var mkPolyline: MKPolyline { - var points: [CLLocationCoordinate2D] = self.points.map(\.clLocationCoordinate2D) - return MKPolyline(coordinates: &points, count: points.count) - } - -} -#endif diff --git a/Sources/GeoModels/Protocols/CoordinateSystem.swift b/Sources/GeoModels/Protocols/CoordinateSystem.swift deleted file mode 100644 index 3fc1b62..0000000 --- a/Sources/GeoModels/Protocols/CoordinateSystem.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// CoordinateSystem.swift -// SwiftGeo -// -// Created by Rémi Bardon on 20/03/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -import GeoCoordinates - -public protocol CoordinateSystem { - - associatedtype Point: GeoCoordinates.Coordinates//: GeoModels.Point - associatedtype Size: GeoModels.Size -// associatedtype MultiPoint: GeoModels.MultiPoint - associatedtype Line: GeoModels.Line -// associatedtype MultiLine: GeoModels.MultiLine - associatedtype LineString: GeoModels.LineString -// associatedtype LinearRing: GeoModels.LinearRing - - associatedtype BoundingBox: GeoModels.BoundingBox - -} diff --git a/Sources/GeoModels/WGS843D.swift b/Sources/GeoModels/WGS843D.swift deleted file mode 100644 index 242b449..0000000 --- a/Sources/GeoModels/WGS843D.swift +++ /dev/null @@ -1,246 +0,0 @@ -// -// WGS843D.swift -// SwiftGeo -// -// Created by Rémi Bardon on 20/03/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -import GeoCoordinates -import NonEmpty - -public enum WGS843D: GeoModels.CoordinateSystem { - - public typealias Point = WGS84Coordinate3D - - public struct Size: GeoModels.Size { - - public typealias CoordinateSystem = WGS843D - public typealias RawValue = WGS843D.Point - - public let rawValue: Self.RawValue - - public var width: Self.RawValue.X { self.rawValue.x } - public var height: Self.RawValue.Y { self.rawValue.y } - public var zHeight: Self.RawValue.Z { self.rawValue.z } - - public init(rawValue: Self.RawValue) { - self.rawValue = rawValue - } - - public init(width: Self.RawValue.X, height: Self.RawValue.Y, zHeight: Self.RawValue.Z) { - self.init(rawValue: Self.RawValue.init(x: width, y: height, z: zHeight)) - } - - } - -// public struct MultiPoint - - public struct Line: GeoModels.Line, Hashable { - - public typealias CoordinateSystem = WGS843D - public typealias Point = WGS843D.Point - - public let start: Point - public let end: Point - - public var altitudeDelta: Altitude { - end.altitude - start.altitude - } - - public init(start: Point, end: Point) { - self.start = start - self.end = end - } - - } - -// public struct MultiLine - - public struct LineString: GeoModels.LineString, Hashable { - - public typealias CoordinateSystem = WGS843D - public typealias Points = AtLeast2<[CoordinateSystem.Point]> - public typealias Lines = NonEmpty<[CoordinateSystem.Line]> - - public internal(set) var points: Points - - public init(points: Points) { - self.points = points - } - - public mutating func append(_ point: Self.Point) { - self.points.append(point) - } - - } - -// public struct LinearRing - - public struct BoundingBox: Hashable { - - public var twoDimensions: LowerDimension - public var lowAltitude: Altitude - public var zHeight: Altitude - - public var highAltitude: Altitude { - lowAltitude + zHeight - } - public var centerAltitude: Altitude { - lowAltitude + (zHeight / 2.0) - } - - public var southWestLow: Self.Point { - Self.Point.init(twoDimensions.southWest, altitude: lowAltitude) - } - public var northEastHigh: Self.Point { - Self.Point.init(twoDimensions.northEast, altitude: highAltitude) - } - public var center: Self.Point { - Self.Point.init(twoDimensions.center, altitude: centerAltitude) - } - - public var crosses180thMeridian: Bool { - twoDimensions.crosses180thMeridian - } - - public init(_ boundingBox2d: LowerDimension, lowAltitude: Altitude, zHeight: Altitude) { - self.twoDimensions = boundingBox2d - self.lowAltitude = lowAltitude - self.zHeight = zHeight - } - - public init( - southWestLow: Self.Point, - width: Longitude, - height: Latitude, - zHeight: Altitude - ) { - self.init( - Self.LowerDimension( - southWest: southWestLow.lowerDimension, - width: width, - height: height - ), - lowAltitude: southWestLow.altitude, - zHeight: zHeight - ) - } - - public init( - southWestLow: Self.Point, - northEastHigh: Self.Point - ) { - self.init( - southWestLow: southWestLow, - width: northEastHigh.longitude - southWestLow.longitude, - height: northEastHigh.latitude - southWestLow.latitude, - zHeight: northEastHigh.altitude - southWestLow.altitude - ) - } - - public func offsetBy( - dLat: Latitude = .zero, - dLong: Longitude = .zero, - dAlt: Altitude = .zero - ) -> Self { - Self.init( - twoDimensions.offsetBy(dLat: dLat, dLong: dLong), - lowAltitude: lowAltitude + dAlt, - zHeight: zHeight - ) - } - public func offsetBy( - dx: Self.Point.X = .zero, - dy: Self.Point.Y = .zero, - dz: Self.Point.Z = .zero - ) -> Self { - Self.init( - twoDimensions.offsetBy(dx: dx, dy: dy), - lowAltitude: lowAltitude + dz, - zHeight: zHeight - ) - } - - } - -} - -extension WGS843D.Size: GeoCoordinates.CompoundDimension { - - public typealias LowerDimension = WGS842D.Size - - public var lowerDimension: Self.LowerDimension { - LowerDimension(rawValue: self.rawValue.lowerDimension) - } - - public init(_ twoDimensions: Self.LowerDimension, zHeight: Self.RawValue.Z) { - self.init(rawValue: Self.RawValue.init( - x: twoDimensions.width, - y: twoDimensions.height, - z: zHeight - )) - } - -} - -extension WGS843D.Line: GeoCoordinates.CompoundDimension { - - public typealias LowerDimension = WGS842D.Line - - public var lowerDimension: LowerDimension { - Self.LowerDimension(start: self.start.lowerDimension, end: self.end.lowerDimension) - } - -} - -extension WGS843D.BoundingBox: GeoModels.BoundingBox { - - public typealias CoordinateSystem = WGS843D - - public var origin: Self.Point { self.southWestLow } - public var size: Self.Size { Self.Size(self.twoDimensions.size, zHeight: self.zHeight) } - - public init(origin: Self.Point, size: Self.Size) { - self.init( - southWestLow: origin, - width: size.width, - height: size.height, - zHeight: size.zHeight - ) - } - - /// The union of bounding boxes gives a new bounding box that encloses the given two. - public func union(_ other: Self) -> Self { - // FIXME: Use width, height and zHeight, because `eastLongitude` can cross the antimeridian - Self.init( - southWestLow: Self.Point( - self.twoDimensions.union(other.twoDimensions).southWest, - altitude: min(self.lowAltitude, other.lowAltitude) - ), - northEastHigh: Self.Point( - self.twoDimensions.union(other.twoDimensions).northEast, - altitude: max(self.highAltitude, other.highAltitude) - ) - ) - } - -} - -extension WGS843D.BoundingBox: GeoCoordinates.CompoundDimension { - - public typealias LowerDimension = WGS842D.BoundingBox - - public var lowerDimension: LowerDimension { - self.twoDimensions - } - -} - -extension WGS843D.BoundingBox: CustomDebugStringConvertible { - - public var debugDescription: String { - "BBox3D(southWestLow: \(String(reflecting: self.southWestLow)), northEastHigh: \(String(reflecting: self.northEastHigh)))" - } - -} diff --git a/Sources/Geodesy/Conversions.swift b/Sources/Geodesy/Conversions.swift index d8b0b92..0dd9106 100644 --- a/Sources/Geodesy/Conversions.swift +++ b/Sources/Geodesy/Conversions.swift @@ -15,11 +15,11 @@ public protocol Conversion { // static var name: String { get } // static var code: Int { get } - static func apply, C2: Coordinate>(on coordinate: C1) -> C2 + static func apply, C2: Coordinates>(on coordinate: C1) -> C2 } public protocol ReversibleConversion: Conversion { - static func unapply, C2: Coordinate>(from coordinate: C2) -> C1 + static func unapply, C2: Coordinates>(from coordinate: C2) -> C1 } // MARK: EPSG 9659 (Geographic 3D to 2D conversions) @@ -36,14 +36,14 @@ where OldCRS: GeographicCRS & ThreeDimensionsCRS, /// Geographic 3D to 2D conversion. /// /// section 4.1.4 - public static func apply, C2: Coordinate>(on coordinate: C1) -> C2 { + public static func apply, C2: Coordinates>(on coordinate: C1) -> C2 { return C2.init(x: .init(coordinate.x), y: .init(coordinate.y)) } /// Geographic 2D to 3D conversion. /// /// section 4.1.1 - public static func unapply, C2: Coordinate>(from coordinate: C2) -> C1 { + public static func unapply, C2: Coordinates>(from coordinate: C2) -> C1 { return C1.init(x: .init(Double(coordinate.x)), y: .init(coordinate.y), z: .init(0.0)) } } @@ -83,7 +83,7 @@ where OldCRS: GeographicCRS & ThreeDimensionsCRS, /// /// /// page 101 - public static func apply, C2: Coordinate>(on coordinate: C1) -> C2 { + public static func apply, C2: Coordinates>(on coordinate: C1) -> C2 { // φ and λ are respectively the latitude and longitude (related to Greenwich) of the point let φ = Double(coordinate.x).toRadians let λ = Double(coordinate.y).toRadians @@ -107,7 +107,7 @@ where OldCRS: GeographicCRS & ThreeDimensionsCRS, /// /// /// page 101 - public static func unapply, C2: Coordinate>(from coordinate: C2) -> C1 { + public static func unapply, C2: Coordinates>(from coordinate: C2) -> C1 { let x = Double(coordinate.x) let y = Double(coordinate.y) let z = Double(coordinate.z) diff --git a/Sources/Geodesy/CoordinateComponent.swift b/Sources/Geodesy/CoordinateComponent.swift new file mode 100644 index 0000000..52ab745 --- /dev/null +++ b/Sources/Geodesy/CoordinateComponent.swift @@ -0,0 +1,279 @@ +// +// CoordinateComponent.swift +// SwiftGeo +// +// Created by Rémi Bardon on 03/02/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import Foundation +import ValueWithUnit + +// MARK: - CoordinateComponent + +public protocol CoordinateComponent: + RawRepresentable, + BinaryFloatingPoint, + CustomStringConvertible, + CustomDebugStringConvertible, + Zeroable +where RawValue: Value +{ + init(rawValue: RawValue) +} + +extension CoordinateComponent { + + internal var _rawValue: RawValue.RawValue { self.rawValue.rawValue } + + // MARK: ExpressibleByFloatLiteral + + public init(floatLiteral value: Double) { + self.init(value) + } + + // MARK: ExpressibleByIntegerLiteral + + public init(integerLiteral value: Int) { + self.init(Double(value)) + } + + // MARK: FloatingPoint + + public static var nan: Self { Self(Double.nan) } + public static var signalingNaN: Self { Self(Double.signalingNaN) } + public static var infinity: Self { Self(Double.infinity) } + public static var greatestFiniteMagnitude: Self { Self(Double.greatestFiniteMagnitude) } + public static var pi: Self { Self(Double.pi) } + + public static var leastNormalMagnitude: Self { Self(Double.leastNormalMagnitude) } + public static var leastNonzeroMagnitude: Self { Self(Double.leastNonzeroMagnitude) } + + public var exponent: Double.Exponent { Double(self._rawValue).exponent } + public var sign: FloatingPointSign { Double(self._rawValue).sign } + public var significand: Self { Self(Double(self._rawValue).significand) } + public var ulp: Self { Self(Double(self._rawValue).ulp) } + + public var nextUp: Self { Self(Double(self._rawValue).nextUp) } + + public var isNormal: Bool { Double(self._rawValue).isNormal } + public var isFinite: Bool { Double(self._rawValue).isFinite } + public var isZero: Bool { Double(self._rawValue).isZero } + public var isSubnormal: Bool { Double(self._rawValue).isSubnormal } + public var isInfinite: Bool { Double(self._rawValue).isInfinite } + public var isNaN: Bool { Double(self._rawValue).isNaN } + public var isSignalingNaN: Bool { Double(self._rawValue).isSignalingNaN } + public var isCanonical: Bool { Double(self._rawValue).isCanonical } + + public init(sign: FloatingPointSign, exponent: Int, significand: Self) { + self.init(Double(sign: sign, exponent: exponent, significand: Double(significand._rawValue))) + } + + public static func / (lhs: Self, rhs: Self) -> Self { + Self(Double(lhs._rawValue) / Double(rhs._rawValue)) + } + public static func /= (lhs: inout Self, rhs: Self) { + lhs = lhs / rhs + } + + public func isEqual(to other: Self) -> Bool { + Double(self._rawValue) == Double(other._rawValue) + } + public func isLess(than other: Self) -> Bool { + Double(self._rawValue) < Double(other._rawValue) + } + public func isLessThanOrEqualTo(_ other: Self) -> Bool { + Double(self._rawValue) <= Double(other._rawValue) + } + + public mutating func addProduct(_ lhs: Self, _ rhs: Self) { + self += Self(Double(lhs._rawValue) * Double(rhs._rawValue)) + } + public mutating func formRemainder(dividingBy other: Self) { + self = Self(Double(self._rawValue).remainder(dividingBy: Double(other._rawValue))) + } + public mutating func formTruncatingRemainder(dividingBy other: Self) { + self = Self(Double(self._rawValue).truncatingRemainder(dividingBy: Double(other._rawValue))) + } + public mutating func formSquareRoot() { + self = Self(Double(self._rawValue).squareRoot()) + } + public mutating func round(_ rule: FloatingPointRoundingRule) { + self = Self(Double(self._rawValue).rounded(rule)) + } + + // MARK: Numeric + + public static func * (lhs: Self, rhs: Self) -> Self { + Self(Double(lhs._rawValue) * Double(rhs._rawValue)) + } + public static func *= (lhs: inout Self, rhs: Self) { + lhs = lhs * rhs + } + + public var magnitude: Self { Self(Double(self._rawValue).magnitude) } + + public init?(exactly source: T) where T: BinaryInteger { + guard let value = Double(exactly: source) else { return nil } + self.init(value) + } + + // MARK: AdditiveArithmetic + + public static var zero: Self { Self(Double.zero) } + + // MARK: Strideable + + public static func + (lhs: Self, rhs: Self) -> Self { + return Self(Double(lhs._rawValue) + Double(rhs._rawValue)) + } + + public static func - (lhs: Self, rhs: Self) -> Self { + return Self(Double(lhs._rawValue) - Double(rhs._rawValue)) + } + + public func advanced(by n: Double.Stride) -> Self { + return Self(Double(self._rawValue).advanced(by: n)) + } + + public func distance(to other: Self) -> Double.Stride { + return Double(self._rawValue).distance(to: Double(other._rawValue)) + } + + // MARK: BinaryFloatingPoint + + public static var exponentBitCount: Int { Double.exponentBitCount } + public static var significandBitCount: Int { Double.significandBitCount } + + public var binade: Self { Self(Double(self._rawValue).binade) } + public var exponentBitPattern: Double.RawExponent { Double(self._rawValue).exponentBitPattern } + public var significandBitPattern: Double.RawSignificand { + Double(self._rawValue).significandBitPattern + } + public var significandWidth: Int { Double(self._rawValue).significandWidth } + + public init( + sign: FloatingPointSign, + exponentBitPattern: Double.RawExponent, + significandBitPattern: Double.RawSignificand + ) { + self.init(Double( + sign: sign, + exponentBitPattern: exponentBitPattern, + significandBitPattern: significandBitPattern + )) + } + + public init(_ value: Source) where Source: BinaryFloatingPoint { + self.init(rawValue: .init(rawValue: .init(value))) + } + + public init(_ value: Source) where Source: BinaryInteger { + self.init(rawValue: .init(rawValue: .init(value))) + } + + // MARK: CustomStringConvertible + + public var description: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter.string(for: self._rawValue) ?? "\(self._rawValue)" + } + + // MARK: CustomDebugStringConvertible + + public var debugDescription: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 99 + formatter.locale = .en + return formatter.string(for: self._rawValue) ?? "\(self._rawValue)" + } + +} + +// MARK: - ValidatableCoordinateComponent + +public protocol ValidatableCoordinateComponent: CoordinateComponent { + + static var min: Self { get } + static var max: Self { get } + static var validRange: ClosedRange { get } + + /// Tells if this value is in valid bounds or not. + var isValid: Bool { get } + + /// A copy of this value, in valid bounds. + /// + /// In angular coordinate system, it means rotating the value + /// to put it in the valid range. + /// + /// In linear coordinate system, it can mean making the value positive. + var valid: Self { get } + + static func random() -> Self + +} + +extension ValidatableCoordinateComponent { + public static var validRange: ClosedRange { min...max } + public var isValid: Bool { Self.validRange.contains(self) } +} + +extension ValidatableCoordinateComponent where Self.RawSignificand: FixedWidthInteger { + public static func random() -> Self { + return Self.random(in: validRange) + } +} + +// MARK: - AngularCoordinateComponent + +/// - Todo: Create custom struct that handles cycling in an interval. +public protocol AngularCoordinateComponent: ValidatableCoordinateComponent { + + static var fullRotation: Self { get } + static var halfRotation: Self { get } + + /// "N" or "E" depending on axis + static var positiveDirectionChar: Character { get } + + /// "S" or "W" depending on axis + static var negativeDirectionChar: Character { get } + + var decimalDegrees: Double { get set } + var radians: Double { get set } + + init(decimalDegrees: Double) + init(radians: Double) + +} + +public extension AngularCoordinateComponent { + + static var min: Self { -halfRotation } + static var max: Self { halfRotation } + + var valid: Self { + if isValid { + return self + } else { + let remainder = self.truncatingRemainder(dividingBy: Self.fullRotation) + + if abs(remainder) > .halfRotation { + let base = self < .zero ? Self.fullRotation : -Self.fullRotation + return base + remainder + } else { + return remainder + } + } + } + var radians: Double { + get { self.decimalDegrees * .pi / 180.0 } + set { self.decimalDegrees = newValue * 180.0 / .pi } + } + + init(radians: Double) { + self.init(decimalDegrees: radians * 180.0 / .pi) + } + +} diff --git a/Sources/Geodesy/Geodesy.swift b/Sources/Geodesy/Geodesy.swift index 599e5d0..2c77e74 100644 --- a/Sources/Geodesy/Geodesy.swift +++ b/Sources/Geodesy/Geodesy.swift @@ -7,11 +7,18 @@ // import Foundation -import Tagged +import ValueWithUnit + +// MARK: - General protocols + +public protocol EPSGItem { + static var epsgName: String { get } + static var epsgCode: Int { get } +} // MARK: - Coordinates -public protocol Coordinate: Hashable, CustomStringConvertible { +public protocol Coordinates: Hashable, AdditiveArithmetic, Zeroable, CustomStringConvertible, CustomDebugStringConvertible { associatedtype CRS: CoordinateReferenceSystem typealias Components = CRS.CoordinateSystem.Values @@ -20,13 +27,14 @@ public protocol Coordinate: Hashable, CustomStringConvertible { init(components: Components) } -public extension Coordinate { - var description: String { "[\(CRS.name)]\(String(describing: self.components))" } +public extension Coordinates { + var description: String { String(describing: self.components) } + var debugDescription: String { "[\(CRS.epsgName)]\(String(reflecting: self.components))" } } // MARK: 2D Coordinates -public extension Coordinate where CRS.CoordinateSystem: TwoDimensionsCS { +public extension Coordinates where CRS.CoordinateSystem: TwoDimensionsCS { var x: CRS.CoordinateSystem.Axis1.Value { self.components.0 } var y: CRS.CoordinateSystem.Axis2.Value { self.components.1 } @@ -38,7 +46,7 @@ public extension Coordinate where CRS.CoordinateSystem: TwoDimensionsCS { } } -public protocol TwoDimensionsCoordinate: Coordinate where CRS: TwoDimensionsCRS { +public protocol TwoDimensionsCoordinate: Coordinates where CRS: TwoDimensionsCRS { typealias X = CRS.CoordinateSystem.Axis1.Value typealias Y = CRS.CoordinateSystem.Axis2.Value @@ -54,9 +62,27 @@ public extension TwoDimensionsCoordinate { init(components: Components) { self.init(x: components.0, y: components.1) } + + static func + (lhs: Self, rhs: Self) -> Self { + Self(x: lhs.x + rhs.x, y: lhs.y + rhs.y) + } + static func - (lhs: Self, rhs: Self) -> Self { + Self(x: lhs.x - rhs.x, y: lhs.y - rhs.y) + } + + func offsetBy(dx: X, dy: Y) -> Self { + Self(x: self.x + dx, y: self.y + dy) + } +} + +public extension TwoDimensionsCoordinate where CRS: GeographicCRS { + var latitude: X { self.x } + var longitude: Y { self.y } } public struct Coordinate2DOf: TwoDimensionsCoordinate where CRS: TwoDimensionsCRS { + public static var zero: Self { Self(x: .zero, y: .zero) } + public let x: X public let y: Y @@ -73,7 +99,7 @@ public struct Coordinate2DOf: TwoDimensionsCoordinate where CRS: TwoDimensi // MARK: 3D Coordinates -public extension Coordinate where CRS.CoordinateSystem: ThreeDimensionsCS { +public extension Coordinates where CRS.CoordinateSystem: ThreeDimensionsCS { var x: CRS.CoordinateSystem.Axis1.Value { self.components.0 } var y: CRS.CoordinateSystem.Axis2.Value { self.components.1 } var z: CRS.CoordinateSystem.Axis3.Value { self.components.2 } @@ -87,7 +113,7 @@ public extension Coordinate where CRS.CoordinateSystem: ThreeDimensionsCS { } } -public protocol ThreeDimensionsCoordinate: Coordinate where CRS: ThreeDimensionsCRS { +public protocol ThreeDimensionsCoordinate: Coordinates where CRS: ThreeDimensionsCRS { typealias X = CRS.CoordinateSystem.Axis1.Value typealias Y = CRS.CoordinateSystem.Axis2.Value typealias Z = CRS.CoordinateSystem.Axis3.Value @@ -105,9 +131,28 @@ public extension ThreeDimensionsCoordinate { init(components: Components) { self.init(x: components.0, y: components.1, z: components.2) } + + static func + (lhs: Self, rhs: Self) -> Self { + Self(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z) + } + static func - (lhs: Self, rhs: Self) -> Self { + Self(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z) + } + + func offsetBy(dx: X, dy: Y, dz: Z) -> Self { + Self(x: self.x + dx, y: self.y + dy, z: self.z + dz) + } +} + +public extension ThreeDimensionsCoordinate where CRS: GeographicCRS { + var latitude: X { self.x } + var longitude: Y { self.y } + var altitude: Z { self.z } } public struct Coordinate3DOf: ThreeDimensionsCoordinate where CRS: ThreeDimensionsCRS { + public static var zero: Self { Self(x: .zero, y: .zero, z: .zero) } + public let x: X public let y: Y public let z: Z @@ -126,11 +171,9 @@ public struct Coordinate3DOf: ThreeDimensionsCoordinate where CRS: ThreeDim // MARK: - Coordinate Reference System (CRS/SRS) -public protocol CoordinateReferenceSystem { +public protocol CoordinateReferenceSystem: EPSGItem { associatedtype Datum: DatumProtocol associatedtype CoordinateSystem: Geodesy.CoordinateSystem - static var name: String { get } - static var code: Int { get } } public protocol TwoDimensionsCRS: CoordinateReferenceSystem @@ -148,24 +191,17 @@ public protocol DatumProtocol { associatedtype PrimeMeridian: MeridianProtocol } -public protocol DatumEnsemble: DatumProtocol { +public protocol DatumEnsemble: DatumProtocol, EPSGItem { associatedtype PrimaryMember: DatumEnsembleMember where PrimaryMember.Ellipsoid == Self.Ellipsoid, PrimaryMember.PrimeMeridian == Self.PrimeMeridian - static var name: String { get } - static var code: Int { get } -} -public protocol DatumEnsembleMember: DatumProtocol { - static var name: String { get } - static var code: Int { get } } +public protocol DatumEnsembleMember: DatumProtocol, EPSGItem {} public protocol DynamicGeodeticDatum: DatumEnsembleMember {} // MARK: Reference Ellipsoid -public protocol ReferenceEllipsoid { - static var name: String { get } - static var code: Int { get } +public protocol ReferenceEllipsoid: EPSGItem { static var semiMajorAxis: Double { get } static var semiMinorAxis: Double { get } static var inverseFlattening: Double { get } @@ -190,25 +226,21 @@ public extension ReferenceEllipsoid { // MARK: Meridian -public protocol MeridianProtocol { - static var name: String { get } - static var code: Int { get } +public protocol MeridianProtocol: EPSGItem { static var greenwichLongitude: Double { get } } public enum Greenwich: MeridianProtocol { - public static let name: String = "Greenwich" - public static let code: Int = 8901 + public static let epsgName: String = "Greenwich" + public static let epsgCode: Int = 8901 public static let greenwichLongitude: Double = 0 } // MARK: - Coordinate System (CS) -public protocol CoordinateSystem { +public protocol CoordinateSystem: EPSGItem { associatedtype Axes associatedtype Values - static var name: String { get } - static var code: Int { get } } public protocol TwoDimensionsCS: CoordinateSystem @@ -217,8 +249,6 @@ where Axes == (Axis1, Axis2), { associatedtype Axis1: Axis associatedtype Axis2: Axis - // associatedtype Axes = (Axis1, Axis2) - // associatedtype Values = (Axis1.Value, Axis2.Value) } public protocol ThreeDimensionsCS: CoordinateSystem @@ -228,99 +258,191 @@ where Axes == (Axis1, Axis2, Axis3), associatedtype Axis1: Axis associatedtype Axis2: Axis associatedtype Axis3: Axis - // associatedtype Axes = (Axis1, Axis2, Axis3) - // associatedtype Values = (Axis1.Value, Axis2.Value, Axis3.Value) } public enum GeocentricCartesian3DCS: ThreeDimensionsCS { public typealias Axis1 = GeocentricX public typealias Axis2 = GeocentricY public typealias Axis3 = GeocentricZ - public static let name: String = "Cartesian 3D CS (geocentric)" - public static let code: Int = 6500 + public static let epsgName: String = "Cartesian 3D CS (geocentric)" + public static let epsgCode: Int = 6500 } public enum Ellipsoidal3DCS: ThreeDimensionsCS { public typealias Axis1 = GeodeticLatitude public typealias Axis2 = GeodeticLongitude public typealias Axis3 = EllipsoidalHeight - public static let name: String = "Ellipsoidal 3D CS" - public static let code: Int = 6423 + public static let epsgName: String = "Ellipsoidal 3D CS" + public static let epsgCode: Int = 6423 } public enum Ellipsoidal2DCS: TwoDimensionsCS { public typealias Axis1 = GeodeticLatitude public typealias Axis2 = GeodeticLongitude - public static let name: String = "Ellipsoidal 2D CS" - public static let code: Int = 6422 + public static let epsgName: String = "Ellipsoidal 2D CS" + public static let epsgCode: Int = 6422 } // MARK: Axes public protocol Axis { - associatedtype UnitOfMeasurement: UnitOfMeasurementProtocol - associatedtype Value: BinaryFloatingPoint + associatedtype UnitOfMeasurement: Geodesy.UnitOfMeasurement + associatedtype Value: CoordinateComponent static var name: String { get } static var abbreviation: String { get } } -public extension Axis { - typealias Value = Tagged -} - public enum GeocentricX: Axis { - public typealias UnitOfMeasurement = Metre + public typealias UnitOfMeasurement = Meter + public struct Value: CoordinateComponent { + public var rawValue: DoubleOf + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + } public static let name: String = "Geocentric X" public static let abbreviation: String = "X" } public enum GeocentricY: Axis { - public typealias UnitOfMeasurement = Metre + public typealias UnitOfMeasurement = Meter + public struct Value: CoordinateComponent { + public var rawValue: DoubleOf + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + } public static let name: String = "Geocentric Y" public static let abbreviation: String = "Y" } public enum GeocentricZ: Axis { - public typealias UnitOfMeasurement = Metre + public typealias UnitOfMeasurement = Meter + public struct Value: CoordinateComponent { + public var rawValue: DoubleOf + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + } public static let name: String = "Geocentric Z" public static let abbreviation: String = "Z" } +/// The latitude component of a geographical coordinate. public enum GeodeticLatitude: Axis { public typealias UnitOfMeasurement = Degree + + public struct Value: AngularCoordinateComponent, RawRepresentable { + public typealias RawValue = DoubleOf + + public static let positiveDirectionChar: Character = "N" + public static let negativeDirectionChar: Character = "S" + + public static var fullRotation: Self = 180 + public static var halfRotation: Self = 90 + + public var rawValue: RawValue + + public var decimalDegrees: Double { + get { self.rawValue.rawValue } + set { self.rawValue.rawValue = newValue } + } + public var positive: Self { + if decimalDegrees < .zero { + // `degrees` is negative, so we end up with `180 - |degrees|` + return self + Self.fullRotation + } else { + return self + } + } + + public init(decimalDegrees: Double) { + self.init(rawValue: .init(rawValue: decimalDegrees)) + } + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + } + public static let name: String = "Geodetic latitude" public static let abbreviation: String = "Lat" } +public typealias Latitude = GeodeticLatitude.Value + +/// The longitude component of a geographical coordinate. public enum GeodeticLongitude: Axis { public typealias UnitOfMeasurement = Degree + + public struct Value: AngularCoordinateComponent, RawRepresentable { + public typealias RawValue = DoubleOf + + public static let positiveDirectionChar: Character = "E" + public static let negativeDirectionChar: Character = "W" + + public static var fullRotation: Self = 360 + public static var halfRotation: Self = 180 + + public var rawValue: DoubleOf + + public var decimalDegrees: Double { + get { self.rawValue.rawValue } + set { self.rawValue.rawValue = newValue } + } + public var positive: Self { + if decimalDegrees < .zero { + // `degrees` is negative, so we end up with `360 - |degrees|` + return self + Self.fullRotation + } else { + return self + } + } + + public init(decimalDegrees: Double) { + self.init(rawValue: .init(rawValue: decimalDegrees)) + } + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + } + public static let name: String = "Geodetic longitude" public static let abbreviation: String = "Lon" } +public typealias Longitude = GeodeticLongitude.Value + public enum EllipsoidalHeight: Axis { - public typealias UnitOfMeasurement = Metre + public typealias UnitOfMeasurement = Meter + + public struct Value: CoordinateComponent { + public var rawValue: DoubleOf + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + public static func random() -> Self { + return Self.random(in: -100...10_000) + } + } + public static let name: String = "Ellipsoidal height" public static let abbreviation: String = "h" } -// MARK: - Units of Measurement +public typealias Altitude = EllipsoidalHeight.Value -public protocol UnitOfMeasurementProtocol: Hashable, Sendable { - static var name: String { get } - static var code: Int { get } -} - -public protocol Length: UnitOfMeasurementProtocol {} +// MARK: - Units of Measurement -public protocol Angle: UnitOfMeasurementProtocol {} +public protocol UnitOfMeasurement: EPSGItem, Hashable, Sendable {} -public enum Metre: Length { - public static let name: String = "Metre" - public static let code: Int = 9001 +extension Meter: Geodesy.UnitOfMeasurement { + public static let epsgName: String = "Metre" + public static let epsgCode: Int = 9001 } -public enum Degree: Angle { - public static let name: String = "Degree" - public static let code: Int = 9122 +extension Degree: Geodesy.UnitOfMeasurement { + public static let epsgName: String = "Degree" + public static let epsgCode: Int = 9122 } diff --git a/Sources/GeoCoordinates/Helpers/Locale+Common.swift b/Sources/Geodesy/Toolbox/Locale+Common.swift similarity index 87% rename from Sources/GeoCoordinates/Helpers/Locale+Common.swift rename to Sources/Geodesy/Toolbox/Locale+Common.swift index a515b27..59622ca 100644 --- a/Sources/GeoCoordinates/Helpers/Locale+Common.swift +++ b/Sources/Geodesy/Toolbox/Locale+Common.swift @@ -6,10 +6,8 @@ // Copyright © 2022 Rémi Bardon. All rights reserved. // -import Foundation +import struct Foundation.Locale extension Locale { - static var en: Locale { Locale(identifier: "en_US") } - } diff --git a/Sources/GeoCoordinates/Protocols/Zeroable.swift b/Sources/Geodesy/Toolbox/Zeroable.swift similarity index 98% rename from Sources/GeoCoordinates/Protocols/Zeroable.swift rename to Sources/Geodesy/Toolbox/Zeroable.swift index ae1fb15..8e4a77b 100644 --- a/Sources/GeoCoordinates/Protocols/Zeroable.swift +++ b/Sources/Geodesy/Toolbox/Zeroable.swift @@ -7,7 +7,5 @@ // public protocol Zeroable { - static var zero: Self { get } - } diff --git a/Sources/GeoCoordinates/Display/Coordinate+Display.swift b/Sources/GeodeticDisplay/Coordinate+Display.swift similarity index 63% rename from Sources/GeoCoordinates/Display/Coordinate+Display.swift rename to Sources/GeodeticDisplay/Coordinate+Display.swift index 2bfb92b..384367b 100644 --- a/Sources/GeoCoordinates/Display/Coordinate+Display.swift +++ b/Sources/GeodeticDisplay/Coordinate+Display.swift @@ -6,9 +6,11 @@ // Copyright © 2021 Rémi Bardon. All rights reserved. // -import Foundation +import class Foundation.NumberFormatter +import class Foundation.NSNumber +import Geodesy -extension AngularCoordinate { +extension AngularCoordinateComponent { public var directionChar: Character { return self.decimalDegrees < .zero ? Self.negativeDirectionChar : Self.positiveDirectionChar @@ -36,14 +38,14 @@ extension AngularCoordinate { // MARK: - GeographicNotation -extension AngularCoordinate { +extension AngularCoordinateComponent { public func ddNotation(maxDigits: UInt8 = 6) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = Int(maxDigits) formatter.locale = .en - return formatter.string(from: NSNumber(value: decimalDegrees)) ?? String(decimalDegrees) + return formatter.string(for: decimalDegrees) ?? String(decimalDegrees) } public func dmNotation(full: Bool = false, maxDigits: UInt8 = 3) -> String { @@ -55,7 +57,7 @@ extension AngularCoordinate { formatter.numberStyle = .decimal formatter.maximumFractionDigits = Int(maxDigits) formatter.locale = .en - if full || minutes > 0, let string = formatter.string(from: NSNumber(value: minutes)) { + if full || minutes > 0, let string = formatter.string(for: minutes) { parts.append("\(string)'") } @@ -76,7 +78,7 @@ extension AngularCoordinate { formatter.numberStyle = .decimal formatter.maximumFractionDigits = Int(maxDigits) formatter.locale = .en - if full || seconds > 0, let string = formatter.string(from: NSNumber(value: seconds)) { + if full || seconds > 0, let string = formatter.string(for: seconds) { parts.append("\(string)\"") } @@ -86,25 +88,3 @@ extension AngularCoordinate { } } - -extension WGS84Coordinate2D: GeographicNotation { - - public func ddNotation(maxDigits: UInt8) -> String { - let lat = self.latitude.ddNotation(maxDigits: maxDigits) - let long = self.longitude.ddNotation(maxDigits: maxDigits) - return "\(lat), \(long)" - } - - public func dmNotation(full: Bool = false, maxDigits: UInt8) -> String { - let lat = self.latitude.dmNotation(full: full, maxDigits: maxDigits) - let long = self.longitude.dmNotation(full: full, maxDigits: maxDigits) - return "\(lat), \(long)" - } - - public func dmsNotation(full: Bool = false, maxDigits: UInt8) -> String { - let lat = self.latitude.dmsNotation(full: full, maxDigits: maxDigits) - let long = self.longitude.dmsNotation(full: full, maxDigits: maxDigits) - return "\(lat), \(long)" - } - -} diff --git a/Sources/GeoCoordinates/Display/GeographicNotation.swift b/Sources/GeodeticDisplay/GeographicNotation.swift similarity index 100% rename from Sources/GeoCoordinates/Display/GeographicNotation.swift rename to Sources/GeodeticDisplay/GeographicNotation.swift diff --git a/Sources/GeoModels/Helpers/Exports.swift b/Sources/GeodeticDisplay/Toolbox/Exports.swift similarity index 81% rename from Sources/GeoModels/Helpers/Exports.swift rename to Sources/GeodeticDisplay/Toolbox/Exports.swift index 0802286..a36aaca 100644 --- a/Sources/GeoModels/Helpers/Exports.swift +++ b/Sources/GeodeticDisplay/Toolbox/Exports.swift @@ -6,4 +6,4 @@ // Copyright © 2022 Rémi Bardon. All rights reserved. // -@_exported import GeoCoordinates +@_exported import Geodesy diff --git a/Sources/GeoCoordinates/Protocols/FloatingPoint+Fraction.swift b/Sources/GeodeticDisplay/Toolbox/FloatingPoint+Fraction.swift similarity index 100% rename from Sources/GeoCoordinates/Protocols/FloatingPoint+Fraction.swift rename to Sources/GeodeticDisplay/Toolbox/FloatingPoint+Fraction.swift diff --git a/Sources/GeodeticDisplay/Toolbox/Locale+Common.swift b/Sources/GeodeticDisplay/Toolbox/Locale+Common.swift new file mode 100644 index 0000000..59622ca --- /dev/null +++ b/Sources/GeodeticDisplay/Toolbox/Locale+Common.swift @@ -0,0 +1,13 @@ +// +// Locale+Common.swift +// SwiftGeo +// +// Created by Rémi Bardon on 03/02/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import struct Foundation.Locale + +extension Locale { + static var en: Locale { Locale(identifier: "en_US") } +} diff --git a/Sources/GeoModels/Protocols/Boundable.swift b/Sources/GeodeticGeometry/Protocols/Boundable.swift similarity index 96% rename from Sources/GeoModels/Protocols/Boundable.swift rename to Sources/GeodeticGeometry/Protocols/Boundable.swift index 548ed4c..d22031b 100644 --- a/Sources/GeoModels/Protocols/Boundable.swift +++ b/Sources/GeodeticGeometry/Protocols/Boundable.swift @@ -10,7 +10,7 @@ public protocol Boundable { - associatedtype BoundingBox: GeoModels.BoundingBox + associatedtype BoundingBox: GeodeticGeometry.BoundingBox var bbox: BoundingBox { get } diff --git a/Sources/GeoModels/Protocols/BoundingBox.swift b/Sources/GeodeticGeometry/Protocols/BoundingBox.swift similarity index 61% rename from Sources/GeoModels/Protocols/BoundingBox.swift rename to Sources/GeodeticGeometry/Protocols/BoundingBox.swift index 38445f7..d0ad981 100644 --- a/Sources/GeoModels/Protocols/BoundingBox.swift +++ b/Sources/GeodeticGeometry/Protocols/BoundingBox.swift @@ -6,13 +6,12 @@ // Copyright © 2022 Rémi Bardon. All rights reserved. // -import GeoCoordinates +import Geodesy -public protocol BoundingBox: Hashable, Zeroable { +public protocol BoundingBox: Hashable, Zeroable { - associatedtype CoordinateSystem: GeoModels.CoordinateSystem - typealias Point = Self.CoordinateSystem.Point - typealias Size = Self.CoordinateSystem.Size + associatedtype Point: GeodeticGeometry.Point + associatedtype Size: GeodeticGeometry.Size var origin: Point { get } var size: Size { get } diff --git a/Sources/GeodeticGeometry/Protocols/GeometricSystem.swift b/Sources/GeodeticGeometry/Protocols/GeometricSystem.swift new file mode 100644 index 0000000..df57e37 --- /dev/null +++ b/Sources/GeodeticGeometry/Protocols/GeometricSystem.swift @@ -0,0 +1,25 @@ +// +// GeometricSystem.swift +// SwiftGeo +// +// Created by Rémi Bardon on 20/03/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import Geodesy + +public protocol GeometricSystem { + + associatedtype CRS: CoordinateReferenceSystem + + associatedtype Point: GeodeticGeometry.Point + associatedtype Size: GeodeticGeometry.Size +// associatedtype MultiPoint: GeodeticGeometry.MultiPoint + associatedtype Line: GeodeticGeometry.Line +// associatedtype MultiLine: GeodeticGeometry.MultiLine + associatedtype LineString: GeodeticGeometry.LineString +// associatedtype LinearRing: GeodeticGeometry.LinearRing + + associatedtype BoundingBox: GeodeticGeometry.BoundingBox + +} diff --git a/Sources/GeoModels/Protocols/Line.swift b/Sources/GeodeticGeometry/Protocols/Line.swift similarity index 94% rename from Sources/GeoModels/Protocols/Line.swift rename to Sources/GeodeticGeometry/Protocols/Line.swift index 410ddfb..1c4625c 100644 --- a/Sources/GeoModels/Protocols/Line.swift +++ b/Sources/GeodeticGeometry/Protocols/Line.swift @@ -8,7 +8,7 @@ import NonEmpty -public protocol Line: GeoModels.MultiPoint +public protocol Line: GeodeticGeometry.MultiPoint where Self.Points == AtLeast2<[Self.Point]> { diff --git a/Sources/GeoModels/Protocols/LineString.swift b/Sources/GeodeticGeometry/Protocols/LineString.swift similarity index 95% rename from Sources/GeoModels/Protocols/LineString.swift rename to Sources/GeodeticGeometry/Protocols/LineString.swift index 35b1c4e..4f9e84a 100644 --- a/Sources/GeoModels/Protocols/LineString.swift +++ b/Sources/GeodeticGeometry/Protocols/LineString.swift @@ -7,10 +7,11 @@ // import Algorithms +import GeodeticDisplay import NonEmpty -public protocol LineString: - GeoModels.MultiLine, +public protocol LineString: + GeodeticGeometry.MultiLine, CustomDebugStringConvertible { diff --git a/Sources/GeoModels/Protocols/LinearRing.swift b/Sources/GeodeticGeometry/Protocols/LinearRing.swift similarity index 89% rename from Sources/GeoModels/Protocols/LinearRing.swift rename to Sources/GeodeticGeometry/Protocols/LinearRing.swift index e34f99d..14ea0bc 100644 --- a/Sources/GeoModels/Protocols/LinearRing.swift +++ b/Sources/GeodeticGeometry/Protocols/LinearRing.swift @@ -9,7 +9,7 @@ import Algorithms import NonEmpty -public protocol LinearRing: GeoModels.LineString {} +public protocol LinearRing: GeodeticGeometry.LineString {} extension LinearRing where Self.Lines.Collection: RangeReplaceableCollection { diff --git a/Sources/GeoModels/Protocols/MultiLine.swift b/Sources/GeodeticGeometry/Protocols/MultiLine.swift similarity index 82% rename from Sources/GeoModels/Protocols/MultiLine.swift rename to Sources/GeodeticGeometry/Protocols/MultiLine.swift index 375dad2..3d167a9 100644 --- a/Sources/GeoModels/Protocols/MultiLine.swift +++ b/Sources/GeodeticGeometry/Protocols/MultiLine.swift @@ -8,11 +8,11 @@ import NonEmpty -public protocol MultiLine: GeoModels.MultiPoint +public protocol MultiLine: GeodeticGeometry.MultiPoint where Points == AtLeast2<[Self.Point]> { - typealias Line = Self.CoordinateSystem.Line + associatedtype Line: GeodeticGeometry.Line associatedtype Lines: NonEmptyProtocol where Self.Lines.Element == Self.Line diff --git a/Sources/GeoModels/Protocols/MultiPoint.swift b/Sources/GeodeticGeometry/Protocols/MultiPoint.swift similarity index 65% rename from Sources/GeoModels/Protocols/MultiPoint.swift rename to Sources/GeodeticGeometry/Protocols/MultiPoint.swift index b190b8a..05ba4df 100644 --- a/Sources/GeoModels/Protocols/MultiPoint.swift +++ b/Sources/GeodeticGeometry/Protocols/MultiPoint.swift @@ -6,12 +6,12 @@ // Copyright © 2022 Rémi Bardon. All rights reserved. // +import Geodesy import NonEmpty -public protocol MultiPoint: Hashable { +public protocol MultiPoint: Hashable { - associatedtype CoordinateSystem: GeoModels.CoordinateSystem - typealias Point = Self.CoordinateSystem.Point + associatedtype Point: GeodeticGeometry.Point associatedtype Points: NonEmptyProtocol where Self.Points.Element == Self.Point diff --git a/Sources/GeodeticGeometry/Protocols/Point.swift b/Sources/GeodeticGeometry/Protocols/Point.swift new file mode 100644 index 0000000..82db1ce --- /dev/null +++ b/Sources/GeodeticGeometry/Protocols/Point.swift @@ -0,0 +1,84 @@ +// +// Point.swift +// SwiftGeo +// +// Created by Rémi Bardon on 06/03/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import Geodesy + +@dynamicMemberLookup +public protocol Point: Hashable, Zeroable, AdditiveArithmetic { + + associatedtype CRS: CoordinateReferenceSystem + associatedtype Coordinates: Geodesy.Coordinates + + var coordinates: Coordinates { get set } + + init(_ coordinates: Coordinates) + +} + +public extension Point { + + static var zero: Self { Self(.zero) } + + subscript(dynamicMember keyPath: KeyPath) -> T { + self.coordinates[keyPath: keyPath] + } + + // MARK: AdditiveArithmetic + + static func + (lhs: Self, rhs: Self) -> Self { + Self(lhs.coordinates + rhs.coordinates) + } + static func - (lhs: Self, rhs: Self) -> Self { + Self(lhs.coordinates - rhs.coordinates) + } + +} + +public extension Point where Coordinates: TwoDimensionsCoordinate { + func offsetBy( + dx: Coordinates.X = .zero, + dy: Coordinates.Y = .zero + ) -> Self { + Self(self.coordinates.offsetBy(dx: dx, dy: dy)) + } + func offsetBy( + dLat: Coordinates.X = .zero, + dLong: Coordinates.Y = .zero + ) -> Self where Coordinates.CRS: GeographicCRS { + self.offsetBy(dx: dLat, dy: dLong) + } +} + +public extension Point where Coordinates: ThreeDimensionsCoordinate { + func offsetBy( + dx: Coordinates.X = .zero, + dy: Coordinates.Y = .zero, + dz: Coordinates.Z = .zero + ) -> Self { + Self(self.coordinates.offsetBy(dx: dx, dy: dy, dz: dz)) + } + func offsetBy( + dLat: Coordinates.X = .zero, + dLong: Coordinates.Y = .zero, + dAlt: Coordinates.Z = .zero + ) -> Self where Coordinates.CRS: GeographicCRS { + self.offsetBy(dx: dLat, dy: dLong, dz: dAlt) + } +} + +public struct PointOf: Point { + + public typealias CRS = Coordinates.CRS + + public var coordinates: Coordinates + + public init(_ coordinates: Coordinates) { + self.coordinates = coordinates + } + +} diff --git a/Sources/GeoModels/Protocols/Size.swift b/Sources/GeodeticGeometry/Protocols/Size.swift similarity index 63% rename from Sources/GeoModels/Protocols/Size.swift rename to Sources/GeodeticGeometry/Protocols/Size.swift index 8365aa0..7de919d 100644 --- a/Sources/GeoModels/Protocols/Size.swift +++ b/Sources/GeodeticGeometry/Protocols/Size.swift @@ -6,11 +6,11 @@ // Copyright © 2022 Rémi Bardon. All rights reserved. // -import GeoCoordinates +import Geodesy -public protocol Size: Hashable, RawRepresentable, Zeroable { +public protocol Size: Hashable, RawRepresentable, Zeroable { - associatedtype CoordinateSystem: GeoModels.CoordinateSystem + associatedtype CRS: Geodesy.CoordinateReferenceSystem init(rawValue: RawValue) diff --git a/Sources/GeoModels/Helpers/CoreLocation+Equatable.swift b/Sources/GeodeticGeometry/Toolbox/CoreLocation+Equatable.swift similarity index 100% rename from Sources/GeoModels/Helpers/CoreLocation+Equatable.swift rename to Sources/GeodeticGeometry/Toolbox/CoreLocation+Equatable.swift diff --git a/Sources/GeodeticGeometry/Toolbox/Exports.swift b/Sources/GeodeticGeometry/Toolbox/Exports.swift new file mode 100644 index 0000000..a36aaca --- /dev/null +++ b/Sources/GeodeticGeometry/Toolbox/Exports.swift @@ -0,0 +1,9 @@ +// +// Exports.swift +// GeoSwift +// +// Created by Rémi Bardon on 03/07/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +@_exported import Geodesy diff --git a/Sources/GeoModels/Helpers/MapKit+Equatable.swift b/Sources/GeodeticGeometry/Toolbox/MapKit+Equatable.swift similarity index 100% rename from Sources/GeoModels/Helpers/MapKit+Equatable.swift rename to Sources/GeodeticGeometry/Toolbox/MapKit+Equatable.swift diff --git a/Sources/Turf/GeoModels+Turf.swift b/Sources/Turf/GeoModels+Turf.swift index eb5dd75..ef19945 100644 --- a/Sources/Turf/GeoModels+Turf.swift +++ b/Sources/Turf/GeoModels+Turf.swift @@ -172,10 +172,10 @@ public extension GeoModels.LineString where CoordinateSystem: CoordinateSystemAl 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.CoordinateSystem.LineString' to return type 'Self' + // 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.CoordinateSystem.LineString' - // Insert ' as! Self.CoordinateSystem.LineString' + // Cannot convert value of type 'Self' to expected argument type 'Self.GeometricSystem.LineString' + // Insert ' as! Self.GeometricSystem.LineString' Self.CoordinateSystem.bezier( forLineString: self as! Self.CoordinateSystem.LineString, sharpness: sharpness, diff --git a/Sources/Turf/Turf+GeoModels.swift b/Sources/Turf/Turf+GeoModels.swift index 29e8506..fee8811 100644 --- a/Sources/Turf/Turf+GeoModels.swift +++ b/Sources/Turf/Turf+GeoModels.swift @@ -18,7 +18,7 @@ import NonEmpty //public func naiveBBox( // forMultiPoint multiPoint: MultiPoint -//) -> MultiPoint.Point.CoordinateSystem.BoundingBox { +//) -> MultiPoint.Point.GeometricSystem.BoundingBox { // return naiveBBox(forNonEmptyCollection: multiPoint.points) //} diff --git a/Sources/ValueWithUnit/Conversions.swift b/Sources/ValueWithUnit/Conversions.swift new file mode 100644 index 0000000..86536fa --- /dev/null +++ b/Sources/ValueWithUnit/Conversions.swift @@ -0,0 +1,20 @@ +// +// Conversions.swift +// SwiftGeo +// +// Created by Rémi Bardon on 13/10/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +public extension Value where Unit: AngleUnit { + func to< + OtherUnit: AngleUnit, + Target: Value + >(_ other: OtherUnit) -> Target { + if Unit.radians == OtherUnit.radians { + return Target(rawValue: self.rawValue) + } else { + return Target(rawValue: self.rawValue / RawValue(Unit.radians * OtherUnit.radians)) + } + } +} diff --git a/Sources/ValueWithUnit/ValueWithUnit.swift b/Sources/ValueWithUnit/ValueWithUnit.swift new file mode 100644 index 0000000..f1b33be --- /dev/null +++ b/Sources/ValueWithUnit/ValueWithUnit.swift @@ -0,0 +1,44 @@ +// +// ValueWithUnit.swift +// SwiftGeo +// +// Created by Rémi Bardon on 13/10/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import Foundation + +public protocol Unit: Hashable, Sendable { + #warning("TODO: Add name") +} +public protocol AngleUnit: Unit { + static var radians: Double { get } +} +public protocol LengthUnit: Unit { + static var meters: Double { get } +} + +public protocol Value: RawRepresentable +where RawValue: BinaryFloatingPoint +{ + associatedtype Unit: ValueWithUnit.Unit + init(rawValue: RawValue) +} + +public struct Radian: AngleUnit { + public static let radians: Double = 1 +} +public struct Degree: AngleUnit { + public static let radians: Double = 180.0 / .pi +} +public struct Meter: LengthUnit { + public static let meters: Double = 1 +} + +public struct DoubleOf: Value, Hashable { + public typealias Unit = U + public var rawValue: Double + public init(rawValue: RawValue) { + self.rawValue = rawValue + } +} diff --git a/Sources/WGS84/Display.swift b/Sources/WGS84/Display.swift new file mode 100644 index 0000000..9efa037 --- /dev/null +++ b/Sources/WGS84/Display.swift @@ -0,0 +1,31 @@ +// +// Display.swift +// SwiftGeo +// +// Created by Rémi Bardon on 24/08/2021. +// Copyright © 2021 Rémi Bardon. All rights reserved. +// + +import GeodeticDisplay + +extension Coordinate2D: GeographicNotation { + + public func ddNotation(maxDigits: UInt8) -> String { + let lat = self.latitude.ddNotation(maxDigits: maxDigits) + let long = self.longitude.ddNotation(maxDigits: maxDigits) + return "\(lat), \(long)" + } + + public func dmNotation(full: Bool = false, maxDigits: UInt8) -> String { + let lat = self.latitude.dmNotation(full: full, maxDigits: maxDigits) + let long = self.longitude.dmNotation(full: full, maxDigits: maxDigits) + return "\(lat), \(long)" + } + + public func dmsNotation(full: Bool = false, maxDigits: UInt8) -> String { + let lat = self.latitude.dmsNotation(full: full, maxDigits: maxDigits) + let long = self.longitude.dmsNotation(full: full, maxDigits: maxDigits) + return "\(lat), \(long)" + } + +} diff --git a/Sources/WGS84/Interoperability/GeoModels+MapKit.swift b/Sources/WGS84/Interoperability/GeoModels+MapKit.swift new file mode 100644 index 0000000..2f09d06 --- /dev/null +++ b/Sources/WGS84/Interoperability/GeoModels+MapKit.swift @@ -0,0 +1,74 @@ +// +// WGS84+MapKit.swift +// SwiftGeo +// +// Created by Rémi Bardon on 02/02/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +#if canImport(MapKit) +import MapKit + +extension Coordinate2D { + + public var mkMapPoint: MKMapPoint { + MKMapPoint(clLocationCoordinate2D) + } + +} + +//extension WGS842D.BoundingBox { +// +// public var mkCoordinateSpan: MKCoordinateSpan { +// MKCoordinateSpan(latitudeDelta: height.decimalDegrees, longitudeDelta: width.decimalDegrees) +// } +// public var mkCoordinateRegion: MKCoordinateRegion { +// MKCoordinateRegion(center: center.clLocationCoordinate2D, span: mkCoordinateSpan) +// } +// +// public var mkMapWidth: Double { +// mkMapWidthAtLatitude(center.latitude) +// } +// public var mkMapHeight: Double { +// mkMapHeightAtLongitude(center.longitude) +// } +// +// public var mkMapRect: MKMapRect { +// // Avoid center recalculation +// let center = self.center +// +// return MKMapRect( +// origin: northWest.mkMapPoint, +// size: MKMapSize( +// width: mkMapWidthAtLatitude(center.latitude), +// height: mkMapHeightAtLongitude(center.longitude) +// ) +// ) +// } +// +// public func mkMapWidthAtLatitude(_ latitude: Latitude) -> Double { +// let east = eastAtLatitude(latitude).mkMapPoint +// let west = westAtLatitude(latitude).mkMapPoint +// +// // `east.x > west.x` +// return east.x - west.x +// } +// public func mkMapHeightAtLongitude(_ longitude: Longitude) -> Double { +// let south = southAtLongitude(longitude).mkMapPoint +// let north = northAtLongitude(longitude).mkMapPoint +// +// // `south.y > north.y` +// return south.y - north.y +// } +// +//} +// +//extension WGS842D.LineString { +// +// public var mkPolyline: MKPolyline { +// var points: [CLLocationCoordinate2D] = self.points.map(\.clLocationCoordinate2D) +// return MKPolyline(coordinates: &points, count: points.count) +// } +// +//} +#endif diff --git a/Sources/GeoCoordinates/Interoperability/GeoCoordinates+CoreLocation.swift b/Sources/WGS84/Interoperability/WGS84+CoreLocation.swift similarity index 75% rename from Sources/GeoCoordinates/Interoperability/GeoCoordinates+CoreLocation.swift rename to Sources/WGS84/Interoperability/WGS84+CoreLocation.swift index 953f9dd..e53175d 100644 --- a/Sources/GeoCoordinates/Interoperability/GeoCoordinates+CoreLocation.swift +++ b/Sources/WGS84/Interoperability/WGS84+CoreLocation.swift @@ -1,5 +1,5 @@ // -// GeoCoordinates+CoreLocation.swift +// WGS84+CoreLocation.swift // SwiftGeo // // Created by Rémi Bardon on 02/02/2022. @@ -9,7 +9,7 @@ #if canImport(CoreLocation) import CoreLocation -extension WGS84Coordinate2D { +extension Coordinate2D { public var clLocation: CLLocation { CLLocation(latitude: latitude.decimalDegrees, longitude: longitude.decimalDegrees) @@ -24,7 +24,10 @@ extension WGS84Coordinate2D { } public init(_ coordinate: CLLocationCoordinate2D) { - self.init(latitude: Latitude(decimalDegrees: coordinate.latitude), longitude: Longitude(decimalDegrees: coordinate.longitude)) + self.init( + latitude: .init(decimalDegrees: coordinate.latitude), + longitude: .init(decimalDegrees: coordinate.longitude) + ) } } diff --git a/Sources/WGS84/WGS84.swift b/Sources/WGS84/WGS84.swift index ca86977..da2485f 100644 --- a/Sources/WGS84/WGS84.swift +++ b/Sources/WGS84/WGS84.swift @@ -20,8 +20,8 @@ public enum WGS84Geographic2DCRS: GeographicCRS, TwoDimensionsCRS { public typealias Datum = WGS84Ensemble public typealias CoordinateSystem = Ellipsoidal2DCS - public static let name: String = "WGS 84 (geographic 2D)" - public static let code: Int = 4326 + public static let epsgName: String = "WGS 84 (geographic 2D)" + public static let epsgCode: Int = 4326 } public typealias EPSG4326 = WGS84Geographic2DCRS @@ -31,8 +31,8 @@ public enum WGS84Geographic3DCRS: GeographicCRS, ThreeDimensionsCRS { public typealias Datum = WGS84Ensemble public typealias CoordinateSystem = Ellipsoidal3DCS - public static let name: String = "WGS 84 (geographic 3D)" - public static let code: Int = 4979 + public static let epsgName: String = "WGS 84 (geographic 3D)" + public static let epsgCode: Int = 4979 } public typealias EPSG4979 = WGS84Geographic3DCRS @@ -42,8 +42,8 @@ public enum WGS84GeocentricCRS: GeocentricCRS, ThreeDimensionsCRS { public typealias Datum = WGS84Ensemble public typealias CoordinateSystem = GeocentricCartesian3DCS - public static let name: String = "WGS 84 (geocentric)" - public static let code: Int = 4978 + public static let epsgName: String = "WGS 84 (geocentric)" + public static let epsgCode: Int = 4978 } public typealias EPSG4978 = WGS84GeocentricCRS @@ -54,14 +54,14 @@ public enum WGS84Ensemble: DatumEnsemble { public typealias Ellipsoid = EPSG7030 public typealias PrimeMeridian = Greenwich public typealias PrimaryMember = Member7 - public static let name: String = "World Geodetic System 1984 ensemble" - public static let code: Int = 6326 + public static let epsgName: String = "World Geodetic System 1984 ensemble" + public static let epsgCode: Int = 6326 public enum Member7: DatumEnsembleMember { public typealias Ellipsoid = EPSG7030 public typealias PrimeMeridian = Greenwich - public static let name: String = "World Geodetic System 1984 (G2139)" - public static let code: Int = 1309 + public static let epsgName: String = "World Geodetic System 1984 (G2139)" + public static let epsgCode: Int = 1309 } } @@ -76,8 +76,8 @@ public typealias EPSG1309 = WGS84Ensemble.Member7 /// World Geodetic System 1984 ensemble member 7 ellipsoid /// public enum EPSG7030: ReferenceEllipsoid { - public static let name: String = "WGS 84" - public static let code: Int = 7030 + public static let epsgName: String = "WGS 84" + public static let epsgCode: Int = 7030 public static let semiMajorAxis: Double = 6378137 public static let inverseFlattening: Double = 298.257_223_563 } @@ -88,8 +88,8 @@ public enum GeocentricCartesian3DCS: ThreeDimensionsCS { public typealias Axis1 = GeocentricX public typealias Axis2 = GeocentricY public typealias Axis3 = GeocentricZ - public static let name: String = "Cartesian 3D CS (geocentric)" - public static let code: Int = 6500 + public static let epsgName: String = "Cartesian 3D CS (geocentric)" + public static let epsgCode: Int = 6500 } public typealias EPSG6500 = GeocentricCartesian3DCS @@ -98,8 +98,8 @@ public enum Ellipsoidal3DCS: ThreeDimensionsCS { public typealias Axis1 = GeodeticLatitude public typealias Axis2 = GeodeticLongitude public typealias Axis3 = EllipsoidalHeight - public static let name: String = "Ellipsoidal 3D CS" - public static let code: Int = 6423 + public static let epsgName: String = "Ellipsoidal 3D CS" + public static let epsgCode: Int = 6423 } public typealias EPSG6423 = Ellipsoidal3DCS @@ -107,8 +107,8 @@ public typealias EPSG6423 = Ellipsoidal3DCS public enum Ellipsoidal2DCS: TwoDimensionsCS { public typealias Axis1 = GeodeticLatitude public typealias Axis2 = GeodeticLongitude - public static let name: String = "Ellipsoidal 2D CS" - public static let code: Int = 6422 + public static let epsgName: String = "Ellipsoidal 2D CS" + public static let epsgCode: Int = 6422 } public typealias EPSG6422 = Ellipsoidal2DCS diff --git a/Sources/GeoModels/WGS842D.swift b/Sources/WGS84/WGS842D.swift similarity index 58% rename from Sources/GeoModels/WGS842D.swift rename to Sources/WGS84/WGS842D.swift index 943693e..a519091 100644 --- a/Sources/GeoModels/WGS842D.swift +++ b/Sources/WGS84/WGS842D.swift @@ -6,38 +6,56 @@ // Copyright © 2022 Rémi Bardon. All rights reserved. // -import GeoCoordinates +import GeodeticGeometry import NonEmpty -public enum WGS842D: GeoModels.CoordinateSystem { +public enum WGS842D: GeometricSystem { + + public typealias CRS = WGS84Geographic2DCRS + + public struct Point: GeodeticGeometry.Point { + + public typealias CRS = WGS84Geographic2DCRS + public typealias Coordinates = Coordinate2D + public typealias X = Coordinates.X + public typealias Y = Coordinates.Y + + public var coordinates: Coordinate2D - public typealias Point = WGS84Coordinate2D + public init(_ coordinates: Coordinate2D) { + self.coordinates = coordinates + } + + } - public struct Size: GeoModels.Size { + public struct Size: GeodeticGeometry.Size { - public typealias CoordinateSystem = WGS842D - public typealias RawValue = WGS842D.Point + public typealias CRS = WGS84Geographic2DCRS + public typealias GeometricSystem = WGS842D + public typealias RawValue = GeometricSystem.Point.Coordinates public let rawValue: Self.RawValue - public var width: Self.RawValue.X { self.rawValue.x } - public var height: Self.RawValue.Y { self.rawValue.y } + public var dx: Self.RawValue.X { self.rawValue.x } + public var dy: Self.RawValue.Y { self.rawValue.y } public init(rawValue: Self.RawValue) { self.rawValue = rawValue } - public init(width: Self.RawValue.X, height: Self.RawValue.Y) { - self.init(rawValue: Self.RawValue.init(x: width, y: height)) + public init(dx: Self.RawValue.X, dy: Self.RawValue.Y) { + self.init(rawValue: RawValue(x: dx, y: dy)) } } // public struct MultiPoint - public struct Line: GeoModels.Line, Hashable { + public struct Line: GeodeticGeometry.Line, Hashable { - public typealias CoordinateSystem = WGS842D + public typealias CRS = WGS84Geographic2DCRS + public typealias GeometricSystem = WGS842D + public typealias Point = GeometricSystem.Point public let start: Self.Point public let end: Self.Point @@ -73,11 +91,14 @@ public enum WGS842D: GeoModels.CoordinateSystem { // public typealias MultiLine = WGS84MultiLine2D - public struct LineString: GeoModels.LineString, Hashable { + public struct LineString: GeodeticGeometry.LineString, Hashable { - public typealias CoordinateSystem = WGS842D - public typealias Points = AtLeast2<[CoordinateSystem.Point]> - public typealias Lines = NonEmpty<[CoordinateSystem.Line]> + public typealias CRS = WGS84Geographic2DCRS + public typealias GeometricSystem = WGS842D + public typealias Point = GeometricSystem.Point + public typealias Points = AtLeast2<[Point]> + public typealias Line = GeometricSystem.Line + public typealias Lines = NonEmpty<[Line]> public internal(set) var points: Points @@ -95,26 +116,30 @@ public enum WGS842D: GeoModels.CoordinateSystem { public struct BoundingBox: Hashable { + public typealias GeometricSystem = WGS842D + public typealias Point = GeometricSystem.Point + public typealias Size = GeometricSystem.Size + public var southWest: Self.Point public var size: Self.Size - public var width: Longitude { self.size.width } - public var height: Latitude { self.size.height } + public var dLat: Latitude { self.size.dx } + public var dLong: Longitude { self.size.dy } public var southLatitude: Latitude { southWest.latitude } public var northLatitude: Latitude { - southLatitude + height + southLatitude + dLat } public var centerLatitude: Latitude { - southLatitude + (height / 2.0) + southLatitude + (dLat / 2.0) } public var westLongitude: Longitude { southWest.longitude } public var eastLongitude: Longitude { - let longitude = westLongitude + width + let longitude = westLongitude + dLong if longitude > .halfRotation { return longitude - .fullRotation @@ -123,7 +148,7 @@ public enum WGS842D: GeoModels.CoordinateSystem { } } public var centerLongitude: Longitude { - let longitude = westLongitude + (width / 2.0) + let longitude = westLongitude + (dLong / 2.0) if longitude > .halfRotation { return longitude - .fullRotation @@ -133,16 +158,16 @@ public enum WGS842D: GeoModels.CoordinateSystem { } public var northEast: Self.Point { - Self.Point.init(latitude: northLatitude, longitude: eastLongitude) + Self.Point.init(.init(latitude: northLatitude, longitude: eastLongitude)) } public var northWest: Self.Point { - Self.Point.init(latitude: northLatitude, longitude: westLongitude) + Self.Point.init(.init(latitude: northLatitude, longitude: westLongitude)) } public var southEast: Self.Point { - Self.Point.init(latitude: southLatitude, longitude: westLongitude) + Self.Point.init(.init(latitude: southLatitude, longitude: westLongitude)) } public var center: Self.Point { - Self.Point.init(latitude: centerLatitude, longitude: centerLongitude) + Self.Point.init(.init(latitude: centerLatitude, longitude: centerLongitude)) } public var south: Self.Point { @@ -169,10 +194,13 @@ public enum WGS842D: GeoModels.CoordinateSystem { public init( southWest: Self.Point, - width: Longitude, - height: Latitude + dLat: Latitude, + dLong: Longitude ) { - self.init(southWest: southWest, size: Self.Size.init(width: width, height: height)) + self.init( + southWest: southWest, + size: Self.Size(dx: dLat, dy: dLong) + ) } public init( @@ -181,36 +209,36 @@ public enum WGS842D: GeoModels.CoordinateSystem { ) { self.init( southWest: southWest, - width: northEast.longitude - southWest.longitude, - height: northEast.latitude - southWest.latitude + dLat: northEast.latitude - southWest.latitude, + dLong: northEast.longitude - southWest.longitude ) } public func southAtLongitude(_ longitude: Longitude) -> Self.Point { - Self.Point.init(latitude: northEast.latitude, longitude: longitude) + Self.Point.init(.init(latitude: northEast.latitude, longitude: longitude)) } public func northAtLongitude(_ longitude: Longitude) -> Self.Point { - Self.Point.init(latitude: southWest.latitude, longitude: longitude) + Self.Point.init(.init(latitude: southWest.latitude, longitude: longitude)) } public func westAtLatitude(_ latitude: Latitude) -> Self.Point { - Self.Point.init(latitude: latitude, longitude: southWest.longitude) + Self.Point.init(.init(latitude: latitude, longitude: southWest.longitude)) } public func eastAtLatitude(_ latitude: Latitude) -> Self.Point { - Self.Point.init(latitude: latitude, longitude: northEast.longitude) + Self.Point.init(.init(latitude: latitude, longitude: northEast.longitude)) } public func offsetBy(dLat: Latitude = .zero, dLong: Longitude = .zero) -> Self { Self.init( southWest: southWest.offsetBy(dLat: dLat, dLong: dLong), - width: width, - height: height + dLat: self.dLat, + dLong: self.dLong ) } - public func offsetBy(dx: Self.Point.X = .zero, dy: Self.Point.Y = .zero) -> Self { + public func offsetBy(dx: Point.X = .zero, dy: Point.Y = .zero) -> Self { Self.init( southWest: southWest.offsetBy(dx: dx, dy: dy), - width: self.width, - height: self.height + dLat: self.dLat, + dLong: self.dLong ) } @@ -218,9 +246,7 @@ public enum WGS842D: GeoModels.CoordinateSystem { } -extension WGS842D.BoundingBox: GeoModels.BoundingBox { - - public typealias CoordinateSystem = WGS842D +extension WGS842D.BoundingBox: GeodeticGeometry.BoundingBox { public var origin: Self.Point { self.southWest } @@ -232,14 +258,14 @@ extension WGS842D.BoundingBox: GeoModels.BoundingBox { public func union(_ other: Self) -> Self { // FIXME: Use width and height, because `eastLongitude` can cross the antimeridian Self.init( - southWest: Self.Point( + southWest: Self.Point(.init( latitude: min(self.southLatitude, other.southLatitude), longitude: min(self.westLongitude, other.westLongitude) - ), - northEast: Self.Point( + )), + northEast: Self.Point(.init( latitude: max(self.northLatitude, other.northLatitude), longitude: max(self.eastLongitude, other.eastLongitude) - ) + )) ) } diff --git a/Sources/WGS84/WGS843D.swift b/Sources/WGS84/WGS843D.swift new file mode 100644 index 0000000..8c09cc9 --- /dev/null +++ b/Sources/WGS84/WGS843D.swift @@ -0,0 +1,246 @@ +//// +//// WGS843D.swift +//// SwiftGeo +//// +//// Created by Rémi Bardon on 20/03/2022. +//// Copyright © 2022 Rémi Bardon. All rights reserved. +//// +// +//import GeodeticGeometry +//import NonEmpty +// +//public enum WGS843D: GeometricSystem { +// +// public typealias Point = Coordinate3D +// +// public struct Size: GeodeticGeometry.Size { +// +// public typealias CoordinateSystem = WGS843D +// public typealias RawValue = WGS843D.Point +// +// public let rawValue: Self.RawValue +// +// public var width: Self.RawValue.X { self.rawValue.x } +// public var height: Self.RawValue.Y { self.rawValue.y } +// public var zHeight: Self.RawValue.Z { self.rawValue.z } +// +// public init(rawValue: Self.RawValue) { +// self.rawValue = rawValue +// } +// +// public init(width: Self.RawValue.X, height: Self.RawValue.Y, zHeight: Self.RawValue.Z) { +// self.init(rawValue: Self.RawValue.init(x: width, y: height, z: zHeight)) +// } +// +// } +// +//// public struct MultiPoint +// +// public struct Line: GeodeticGeometry.Line, Hashable { +// +// public typealias CoordinateSystem = WGS843D +// public typealias Point = WGS843D.Point +// +// public let start: Point +// public let end: Point +// +// public var altitudeDelta: Altitude { +// end.altitude - start.altitude +// } +// +// public init(start: Point, end: Point) { +// self.start = start +// self.end = end +// } +// +// } +// +//// public struct MultiLine +// +// public struct LineString: GeodeticGeometry.LineString, Hashable { +// +// public typealias CoordinateSystem = WGS843D +// public typealias Points = AtLeast2<[CoordinateSystem.Point]> +// public typealias Lines = NonEmpty<[CoordinateSystem.Line]> +// +// public internal(set) var points: Points +// +// public init(points: Points) { +// self.points = points +// } +// +// public mutating func append(_ point: Self.Point) { +// self.points.append(point) +// } +// +// } +// +//// public struct LinearRing +// +// public struct BoundingBox: Hashable { +// +// public var twoDimensions: LowerDimension +// public var lowAltitude: Altitude +// public var zHeight: Altitude +// +// public var highAltitude: Altitude { +// lowAltitude + zHeight +// } +// public var centerAltitude: Altitude { +// lowAltitude + (zHeight / 2.0) +// } +// +// public var southWestLow: Self.Point { +// Self.Point.init(twoDimensions.southWest, altitude: lowAltitude) +// } +// public var northEastHigh: Self.Point { +// Self.Point.init(twoDimensions.northEast, altitude: highAltitude) +// } +// public var center: Self.Point { +// Self.Point.init(twoDimensions.center, altitude: centerAltitude) +// } +// +// public var crosses180thMeridian: Bool { +// twoDimensions.crosses180thMeridian +// } +// +// public init(_ boundingBox2d: LowerDimension, lowAltitude: Altitude, zHeight: Altitude) { +// self.twoDimensions = boundingBox2d +// self.lowAltitude = lowAltitude +// self.zHeight = zHeight +// } +// +// public init( +// southWestLow: Self.Point, +// width: Longitude, +// height: Latitude, +// zHeight: Altitude +// ) { +// self.init( +// Self.LowerDimension( +// southWest: southWestLow.lowerDimension, +// width: width, +// height: height +// ), +// lowAltitude: southWestLow.altitude, +// zHeight: zHeight +// ) +// } +// +// public init( +// southWestLow: Self.Point, +// northEastHigh: Self.Point +// ) { +// self.init( +// southWestLow: southWestLow, +// width: northEastHigh.longitude - southWestLow.longitude, +// height: northEastHigh.latitude - southWestLow.latitude, +// zHeight: northEastHigh.altitude - southWestLow.altitude +// ) +// } +// +// public func offsetBy( +// dLat: Latitude = .zero, +// dLong: Longitude = .zero, +// dAlt: Altitude = .zero +// ) -> Self { +// Self.init( +// twoDimensions.offsetBy(dLat: dLat, dLong: dLong), +// lowAltitude: lowAltitude + dAlt, +// zHeight: zHeight +// ) +// } +// public func offsetBy( +// dx: Self.Point.X = .zero, +// dy: Self.Point.Y = .zero, +// dz: Self.Point.Z = .zero +// ) -> Self { +// Self.init( +// twoDimensions.offsetBy(dx: dx, dy: dy), +// lowAltitude: lowAltitude + dz, +// zHeight: zHeight +// ) +// } +// +// } +// +//} +// +//extension WGS843D.Size: GeodeticGeometry.CompoundDimension { +// +// public typealias LowerDimension = WGS842D.Size +// +// public var lowerDimension: Self.LowerDimension { +// LowerDimension(rawValue: self.rawValue.lowerDimension) +// } +// +// public init(_ twoDimensions: Self.LowerDimension, zHeight: Self.RawValue.Z) { +// self.init(rawValue: Self.RawValue.init( +// x: twoDimensions.width, +// y: twoDimensions.height, +// z: zHeight +// )) +// } +// +//} +// +//extension WGS843D.Line: GeoCoordinates.CompoundDimension { +// +// public typealias LowerDimension = WGS842D.Line +// +// public var lowerDimension: LowerDimension { +// Self.LowerDimension(start: self.start.lowerDimension, end: self.end.lowerDimension) +// } +// +//} +// +//extension WGS843D.BoundingBox: GeoModels.BoundingBox { +// +// public typealias CoordinateSystem = WGS843D +// +// public var origin: Self.Point { self.southWestLow } +// public var size: Self.Size { Self.Size(self.twoDimensions.size, zHeight: self.zHeight) } +// +// public init(origin: Self.Point, size: Self.Size) { +// self.init( +// southWestLow: origin, +// width: size.width, +// height: size.height, +// zHeight: size.zHeight +// ) +// } +// +// /// The union of bounding boxes gives a new bounding box that encloses the given two. +// public func union(_ other: Self) -> Self { +// // FIXME: Use width, height and zHeight, because `eastLongitude` can cross the antimeridian +// Self.init( +// southWestLow: Self.Point( +// self.twoDimensions.union(other.twoDimensions).southWest, +// altitude: min(self.lowAltitude, other.lowAltitude) +// ), +// northEastHigh: Self.Point( +// self.twoDimensions.union(other.twoDimensions).northEast, +// altitude: max(self.highAltitude, other.highAltitude) +// ) +// ) +// } +// +//} +// +//extension WGS843D.BoundingBox: GeoCoordinates.CompoundDimension { +// +// public typealias LowerDimension = WGS842D.BoundingBox +// +// public var lowerDimension: LowerDimension { +// self.twoDimensions +// } +// +//} +// +//extension WGS843D.BoundingBox: CustomDebugStringConvertible { +// +// public var debugDescription: String { +// "BBox3D(southWestLow: \(String(reflecting: self.southWestLow)), northEastHigh: \(String(reflecting: self.northEastHigh)))" +// } +// +//} diff --git a/Tests/GeoModelsTests/LineTests.swift b/Tests/GeoModelsTests/LineTests.swift deleted file mode 100644 index c7f8d16..0000000 --- a/Tests/GeoModelsTests/LineTests.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// LineTests.swift -// SwiftGeo -// -// Created by Rémi Bardon on 01/02/2022. -// Copyright © 2022 Rémi Bardon. All rights reserved. -// - -import GeoModels -import XCTest - -final class LineTests: XCTestCase { - - typealias Coordinate2D = WGS842D.Point - typealias Line2D = WGS842D.Line - - func testLine2DCrosses180thMeridian() throws { - self.continueAfterFailure = true - - func test(from longitude1: Longitude, to longitude2: Longitude, crosses: Bool) { - let start = Coordinate2D(latitude: 0, longitude: longitude1) - let end = Coordinate2D(latitude: 0, longitude: longitude2) - let line = Line2D(start: start, end: end) - - XCTAssertEqual( - line.crosses180thMeridian, - crosses, - "From \(longitude1) to \(longitude2): longitudeDelta=\(line.longitudeDelta), minimalLongitudeDelta=\(line.minimalLongitudeDelta)" - ) - } - - // Exactly 0° length - test(from: -180, to: -180, crosses: false) - test(from: -90, to: -90, crosses: false) - test(from: 0, to: 0, crosses: false) - test(from: 90, to: 90, crosses: false) - test(from: 180, to: 180, crosses: false) - - // Exactly 180° length - test(from: 0, to: 180, crosses: false) - test(from: -90, to: 90, crosses: false) - test(from: -180, to: 0, crosses: false) - - throw XCTSkip("FIXME") - - // A bit more than 180° length - test(from: -1, to: 180, crosses: true) - test(from: -91, to: 90, crosses: true) - test(from: -180, to: 1, crosses: true) - - // Almost 360° length - test(from: -179, to: 180, crosses: false) - test(from: -180, to: 179, crosses: false) - test(from: 179, to: -180, crosses: true) - test(from: 180, to: -179, crosses: true) - - // Exactly 360° length - test(from: -180, to: 180, crosses: false) - test(from: 180, to: -180, crosses: true) - } - - func testLine2DMinimalLongitudeDelta() { - self.continueAfterFailure = true - - func test(from longitude1: Longitude, to longitude2: Longitude, equals minimalDelta: Longitude) { - let start = Coordinate2D(latitude: 0, longitude: longitude1) - let end = Coordinate2D(latitude: 0, longitude: longitude2) - let line = Line2D(start: start, end: end) - - XCTAssertEqual( - line.minimalLongitudeDelta, - minimalDelta, - "From \(longitude1) to \(longitude2): longitudeDelta=\(line.longitudeDelta), minimalLongitudeDelta=\(line.minimalLongitudeDelta)" - ) - } - - func symetricTest(between longitude1: Longitude, and longitude2: Longitude, equals minimalDelta: Longitude) { - test(from: longitude1, to: longitude2, equals: minimalDelta) - test(from: longitude2, to: longitude1, equals: minimalDelta) - } - - // Exactly 0° length - symetricTest(between: -180, and: -180, equals: 0) - symetricTest(between: -90, and: -90, equals: 0) - symetricTest(between: 0, and: 0, equals: 0) - - // Exactly 180° length - symetricTest(between: 0, and: 180, equals: 180) - symetricTest(between: -90, and: 90, equals: 180) - symetricTest(between: -180, and: 0, equals: 180) - - // A bit more than 180° length - test(from: -1, to: 180, equals: -179) - test(from: -91, to: 90, equals: -179) - test(from: -180, to: 1, equals: -179) - test(from: 1, to: -180, equals: 179) - test(from: 91, to: -90, equals: 179) - test(from: 180, to: -1, equals: 179) - - // Almost 360° length - test(from: -179, to: 180, equals: -1) - test(from: -180, to: 179, equals: -1) - test(from: 179, to: -180, equals: 1) - test(from: 180, to: -179, equals: 1) - - // Exactly 360° length - symetricTest(between: -180, and: 180, equals: 0) - } - - func testLine2DNonEmpty() { - XCTAssertEqual(Line2D(start: .zero, end: .zero).points.count, 2) - } - -} diff --git a/Tests/GeodeticDisplayTests/GeodeticDisplayTests.swift b/Tests/GeodeticDisplayTests/GeodeticDisplayTests.swift new file mode 100644 index 0000000..a3ce941 --- /dev/null +++ b/Tests/GeodeticDisplayTests/GeodeticDisplayTests.swift @@ -0,0 +1,26 @@ +// +// GeodeticDisplayTests.swift +// SwiftGeo +// +// Created by Rémi Bardon on 17/10/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import WGS84 +import XCTest + +final class GeodeticDisplayTests: XCTestCase { + + func testDescriptions() { + let coord = Coordinate2D(x: 1.1232456789, y: -1.1232456789) + XCTAssertEqual(String(describing: coord), "(1.1232456789, -1.1232456789)") + XCTAssertEqual( + String(reflecting: coord), + "[WGS 84 (geographic 2D)](1.1232456789, -1.1232456789)" + ) + XCTAssertEqual(coord.ddNotation, "1.123246, -1.123246") + XCTAssertEqual(coord.dmNotation, "1° 7.395' N, 1° 7.395' W") + XCTAssertEqual(coord.dmsNotation, #"1° 7' 23.6844" N, 1° 7' 23.6844" W"#) + } + +} diff --git a/Tests/GeodeticGeometryTests/LineTests.swift b/Tests/GeodeticGeometryTests/LineTests.swift new file mode 100644 index 0000000..2b7bb18 --- /dev/null +++ b/Tests/GeodeticGeometryTests/LineTests.swift @@ -0,0 +1,115 @@ +// +// LineTests.swift +// SwiftGeo +// +// Created by Rémi Bardon on 01/02/2022. +// Copyright © 2022 Rémi Bardon. All rights reserved. +// + +import GeodeticGeometry +import WGS84 +import XCTest + +//final class LineTests: XCTestCase { +// +// typealias Coordinate2D = WGS842D.Point +// typealias Line2D = WGS842D.Line +// +// func testLine2DCrosses180thMeridian() throws { +// self.continueAfterFailure = true +// +// func test(from longitude1: Longitude, to longitude2: Longitude, crosses: Bool) { +// let start = Coordinate2D(latitude: 0, longitude: longitude1) +// let end = Coordinate2D(latitude: 0, longitude: longitude2) +// let line = Line2D(start: start, end: end) +// +// XCTAssertEqual( +// line.crosses180thMeridian, +// crosses, +// "From \(longitude1) to \(longitude2): longitudeDelta=\(line.longitudeDelta), minimalLongitudeDelta=\(line.minimalLongitudeDelta)" +// ) +// } +// +// // Exactly 0° length +// test(from: -180, to: -180, crosses: false) +// test(from: -90, to: -90, crosses: false) +// test(from: 0, to: 0, crosses: false) +// test(from: 90, to: 90, crosses: false) +// test(from: 180, to: 180, crosses: false) +// +// // Exactly 180° length +// test(from: 0, to: 180, crosses: false) +// test(from: -90, to: 90, crosses: false) +// test(from: -180, to: 0, crosses: false) +// +// throw XCTSkip("FIXME") +// +// // A bit more than 180° length +// test(from: -1, to: 180, crosses: true) +// test(from: -91, to: 90, crosses: true) +// test(from: -180, to: 1, crosses: true) +// +// // Almost 360° length +// test(from: -179, to: 180, crosses: false) +// test(from: -180, to: 179, crosses: false) +// test(from: 179, to: -180, crosses: true) +// test(from: 180, to: -179, crosses: true) +// +// // Exactly 360° length +// test(from: -180, to: 180, crosses: false) +// test(from: 180, to: -180, crosses: true) +// } +// +// func testLine2DMinimalLongitudeDelta() { +// self.continueAfterFailure = true +// +// func test(from longitude1: Longitude, to longitude2: Longitude, equals minimalDelta: Longitude) { +// let start = Coordinate2D(latitude: 0, longitude: longitude1) +// let end = Coordinate2D(latitude: 0, longitude: longitude2) +// let line = Line2D(start: start, end: end) +// +// XCTAssertEqual( +// line.minimalLongitudeDelta, +// minimalDelta, +// "From \(longitude1) to \(longitude2): longitudeDelta=\(line.longitudeDelta), minimalLongitudeDelta=\(line.minimalLongitudeDelta)" +// ) +// } +// +// func symetricTest(between longitude1: Longitude, and longitude2: Longitude, equals minimalDelta: Longitude) { +// test(from: longitude1, to: longitude2, equals: minimalDelta) +// test(from: longitude2, to: longitude1, equals: minimalDelta) +// } +// +// // Exactly 0° length +// symetricTest(between: -180, and: -180, equals: 0) +// symetricTest(between: -90, and: -90, equals: 0) +// symetricTest(between: 0, and: 0, equals: 0) +// +// // Exactly 180° length +// symetricTest(between: 0, and: 180, equals: 180) +// symetricTest(between: -90, and: 90, equals: 180) +// symetricTest(between: -180, and: 0, equals: 180) +// +// // A bit more than 180° length +// test(from: -1, to: 180, equals: -179) +// test(from: -91, to: 90, equals: -179) +// test(from: -180, to: 1, equals: -179) +// test(from: 1, to: -180, equals: 179) +// test(from: 91, to: -90, equals: 179) +// test(from: 180, to: -1, equals: 179) +// +// // Almost 360° length +// test(from: -179, to: 180, equals: -1) +// test(from: -180, to: 179, equals: -1) +// test(from: 179, to: -180, equals: 1) +// test(from: 180, to: -179, equals: 1) +// +// // Exactly 360° length +// symetricTest(between: -180, and: 180, equals: 0) +// } +// +// func testLine2DNonEmpty() { +// XCTAssertEqual(Line2D(start: .zero, end: .zero).points.count, 2) +// } +// +//}