|
1 | 1 | # 1 SwiftGeographicLib
|
2 |
| -Ready-to-use Swift wrapper for geodesic methods from the renowned library GeographicLib. The Swift wrapper is built on top of the C version of the library. |
3 |
| - |
| 2 | + |
| 3 | +Ready-to-use Swift wrapper for the geodesic routines from the renowned [GeographicLib](https://geographiclib.sourceforge.io/). |
| 4 | +Under the hood it calls the C library, but exposes a Swifty, type-safe API. |
| 5 | + |
4 | 6 | ## 1.1 Installation
|
5 |
| -You can install the library using Swift Package Manager. Add the following line to your `Package.swift` dependencies array: |
| 7 | + |
| 8 | +Use the Swift Package Manager. In your `Package.swift`: |
| 9 | + |
| 10 | +```swift |
| 11 | +dependencies: [ |
| 12 | + .package( |
| 13 | + url: "https://github.com/sindreoyen/SwiftGeographicLib.git", |
| 14 | + .upToNextMinor(from: "1.0.1") |
| 15 | + ) |
| 16 | +] |
| 17 | +``` |
| 18 | + |
| 19 | +## 1.2 Masks & Flags |
| 20 | + |
| 21 | +SwiftGeographicLib provides two `OptionSet` types to mirror the C bitmasks: |
6 | 22 |
|
7 | 23 | ```swift
|
8 |
| -.package(url: "https://github.com/sindreoyen/SwiftGeographicLib.git", .upToNextMinor(from: "1.0.1")) |
| 24 | +/// geod_mask values |
| 25 | +public struct GeodesicMask: OptionSet { |
| 26 | + public let rawValue: UInt32 |
| 27 | + public init(rawValue: UInt32) { self.rawValue = rawValue } |
| 28 | + |
| 29 | + public static let none = GeodesicMask([]) |
| 30 | + public static let latitude = GeodesicMask(rawValue: 1<<7) |
| 31 | + public static let longitude = GeodesicMask(rawValue: (1<<8)|(1<<3)) |
| 32 | + public static let azimuth = GeodesicMask(rawValue: 1<<9) |
| 33 | + public static let distance = GeodesicMask(rawValue: (1<<10)|(1<<0)) |
| 34 | + public static let distanceIn = GeodesicMask(rawValue: (1<<11)|(1<<0)|(1<<1)) |
| 35 | + public static let reducedLength = GeodesicMask(rawValue: (1<<12)|(1<<0)|(1<<2)) |
| 36 | + public static let scale = GeodesicMask(rawValue: (1<<13)|(1<<0)|(1<<2)) |
| 37 | + public static let area = GeodesicMask(rawValue: (1<<14)|(1<<4)) |
| 38 | + public static let all: GeodesicMask = [ |
| 39 | + .latitude, .longitude, .azimuth, |
| 40 | + .distance, .distanceIn, .reducedLength, |
| 41 | + .scale, .area |
| 42 | + ] |
| 43 | +} |
| 44 | + |
| 45 | +/// geod_flags values |
| 46 | +public struct GeodesicFlags: OptionSet { |
| 47 | + public let rawValue: UInt32 |
| 48 | + public init(rawValue: UInt32) { self.rawValue = rawValue } |
| 49 | + |
| 50 | + public static let none = GeodesicFlags([]) |
| 51 | + public static let arcMode = GeodesicFlags(rawValue: 1<<0) |
| 52 | + public static let unrollLong = GeodesicFlags(rawValue: 1<<15) |
| 53 | +} |
9 | 54 | ```
|
10 | 55 |
|
| 56 | +Place those in `Sources/SwiftGeographicLib/Masks.swift` (or similar). |
| 57 | + |
| 58 | +--- |
| 59 | + |
11 | 60 | # 2 SwiftGeographicLib Usage Guide
|
12 | 61 |
|
13 |
| -This library provides Swift-friendly wrappers around the C geodesic routines from GeographicLib. It lets you compute accurate distances, bearings, and areas on an ellipsoidal model of the Earth (default WGS-84) via three main APIs: |
| 62 | +This library gives you three main APIs to solve geodesic problems on an ellipsoid: |
14 | 63 |
|
15 |
| -1. **`Geodesic`** – one-off static functions for direct/inverse problems |
16 |
| -2. **`GeodesicLine`** – incremental “line” API for stepping along a geodesic |
17 |
| -3. **`GeodesicPolygon`** – accumulate points or edges to compute perimeter & area |
| 64 | +1. **`Geodesic`** – static direct/inverse calls |
| 65 | +2. **`GeodesicLine`** – incremental “walk a geodesic” API |
| 66 | +3. **`GeodesicPolygon`** – accumulate points/edges to get perimeter & area |
18 | 67 |
|
19 | 68 | ---
|
20 | 69 |
|
21 | 70 | ## 2.1 Geodesic
|
22 | 71 |
|
23 | 72 | ### `direct(from:distance:azimuth:geodesic:)`
|
24 | 73 |
|
25 |
| -Solve the **direct problem** (point + azimuth + distance ⇒ endpoint): |
26 |
| - |
27 | 74 | ```swift
|
28 |
| -let start = (lat: 40.64, lon: -73.78) // JFK Airport |
29 |
| -let d = 10_000_000.0 // 10 000 km |
30 |
| -let azi = 45.0 // north-east |
31 |
| -let dest = Geodesic.direct(from: start, |
32 |
| - distance: d, |
33 |
| - azimuth: azi) |
34 |
| -// dest.latitude, dest.longitude hold the endpoint coordinates |
| 75 | +let start = (lat: 40.64, lon: -73.78) // JFK |
| 76 | +let dest = Geodesic.direct( |
| 77 | + from: start, |
| 78 | + distance: 10_000_000, // 10 000 km |
| 79 | + azimuth: 45.0 // north-east |
| 80 | +) |
| 81 | +// dest.latitude, dest.longitude |
35 | 82 | ```
|
36 | 83 |
|
37 | 84 | ### `generalDirect(from:azimuth:flags:s12_a12:geodesic:)`
|
38 | 85 |
|
39 |
| -Solve the **general direct problem**, returning extra quantities: |
40 |
| - |
41 | 86 | ```swift
|
42 | 87 | let (lat2, lon2, azi2,
|
43 | 88 | s12, m12, M12, M21, S12,
|
44 | 89 | a12) = Geodesic.generalDirect(
|
45 |
| - from: start, |
46 |
| - azimuth: azi, |
47 |
| - flags: GEOD_ALL, // request all outputs |
48 |
| - s12_a12: d |
49 |
| - ) |
50 |
| - |
51 |
| -// • lat2, lon2, azi2: endpoint lat/lon/bearing |
52 |
| -// • s12 : distance (m) |
53 |
| -// • m12 : reduced length (m) |
54 |
| -// • M12, M21 : geodesic scales (dimensionless) |
55 |
| -// • S12 : area under the geodesic (m²) |
56 |
| -// • a12 : arc length (°) |
| 90 | + from: start, |
| 91 | + azimuth: 45, |
| 92 | + flags: .all, // all outputs |
| 93 | + s12_a12: 10e6 |
| 94 | +) |
57 | 95 | ```
|
58 | 96 |
|
59 |
| ---- |
60 |
| - |
61 | 97 | ### `inverse(between:and:geodesic:)`
|
62 | 98 |
|
63 |
| -Solve the **inverse problem** (two points ⇒ distance & azimuths): |
64 |
| - |
65 | 99 | ```swift
|
66 |
| -let a = (lat: 40.64, lon: -73.78) // JFK |
67 |
| -let b = (lat: 1.36, lon: 103.99) // Singapore Changi |
68 |
| -let (s, fwd, rev) = Geodesic.inverse(between: a, and: b) |
69 |
| -// s = distance in meters |
70 |
| -// fwd = forward azimuth at A (°) |
71 |
| -// rev = forward azimuth at B (°) |
| 100 | +let a = (lat: 40.64, lon: -73.78) |
| 101 | +let b = (lat: 1.36, lon: 103.99) |
| 102 | +let (distance, fwd, rev) = Geodesic.inverse(between: a, and: b) |
72 | 103 | ```
|
73 | 104 |
|
74 | 105 | ### `generalInverse(between:and:geodesic:)`
|
75 | 106 |
|
76 |
| -Get extended inverse outputs: |
77 |
| - |
78 | 107 | ```swift
|
79 | 108 | let (a12, s12, azi1, azi2, m12, M12, M21, S12) =
|
80 |
| - Geodesic.generalInverse(between: a, and: b) |
81 |
| -// same fields as generalDirect but for the inverse problem |
| 109 | + Geodesic.generalInverse(between: a, and: b) |
82 | 110 | ```
|
83 | 111 |
|
84 | 112 | ---
|
85 | 113 |
|
86 | 114 | ## 2.2 GeodesicLine
|
87 | 115 |
|
88 |
| -Use `GeodesicLine` when you want to **walk** along a geodesic: |
| 116 | +Use this when you want to step along a geodesic: |
89 | 117 |
|
90 | 118 | ### Initialize
|
91 | 119 |
|
92 |
| -- **Basic** (no endpoint known yet): |
93 |
| - |
94 |
| - ```swift |
95 |
| - let caps: UInt32 = GEOD_DISTANCE_IN // allow distance as input |
96 |
| - | GEOD_LONGITUDE // allow computing longitude |
97 |
| - let line = GeodesicLine( |
98 |
| - from: (lat: 40.64, lon: -73.78), |
99 |
| - azimuth: 45.0, |
100 |
| - caps: caps |
101 |
| - ) |
102 |
| - ``` |
103 |
| - |
104 |
| -- **Direct** (endpoint fixed at construction): |
105 |
| - |
106 |
| - ```swift |
107 |
| - let line2 = GeodesicLine( |
108 |
| - directFrom: (lat: 40.64, lon: -73.78), |
109 |
| - azimuth: 45.0, |
110 |
| - distance: 10_000_000, |
111 |
| - caps: GEOD_ALL |
112 |
| - ) |
113 |
| - ``` |
114 |
| - |
115 |
| -### Query Points |
116 |
| - |
117 |
| -- **Position by distance** |
118 |
| - (requires `GEOD_DISTANCE_IN` + `GEOD_LONGITUDE` in `caps`) |
119 |
| - |
120 |
| - ```swift |
121 |
| - let pt = line.position(distance: 1_000_000) |
122 |
| - // pt.latitude, pt.longitude |
123 |
| - ``` |
124 |
| - |
125 |
| -- **General position** |
126 |
| - to retrieve all requested quantities for a given distance or arc: |
127 |
| - |
128 |
| - ```swift |
129 |
| - let (lat, lon, azi2, s12, m12, M12, M21, S12, a12) = |
130 |
| - line.genPosition( |
131 |
| - flags: GEOD_ARCMODE, // or GEOD_NOFLAGS |
132 |
| - s12_a12: 100.0 // meters or degrees |
133 |
| - ) |
134 |
| - ``` |
135 |
| - |
136 |
| -### Change the “third point” |
137 |
| - |
138 |
| -After a basic init (without endpoint): |
139 |
| - |
140 |
| -- **Set by distance**: |
141 |
| - ```swift |
142 |
| - line.setDistance(500_000) |
143 |
| - ``` |
144 |
| -- **Set by arc or distance**: |
145 |
| - ```swift |
146 |
| - line.genSetDistance(flags: GEOD_ARCMODE, |
147 |
| - s13_a13: 4.5) // arc-length in degrees |
148 |
| - ``` |
149 |
| - |
150 |
| ---- |
151 |
| - |
152 |
| -## 2.3 GeodesicPolygon |
| 120 | +```swift |
| 121 | +import SwiftGeographicLib |
| 122 | + |
| 123 | +// 1) Basic init (no endpoint pinned) |
| 124 | +let caps: GeodesicMask = [.distanceIn, .longitude] |
| 125 | +let line = GeodesicLine( |
| 126 | + from: (lat: 40.64, lon: -73.78), |
| 127 | + azimuth: 45.0, |
| 128 | + caps: caps |
| 129 | +) |
153 | 130 |
|
154 |
| -Compute **perimeter** & **area** by streaming in vertices or edges. |
| 131 | +// 2) Direct init (endpoint fixed) |
| 132 | +let line2 = GeodesicLine( |
| 133 | + directFrom: (lat: 40.64, lon: -73.78), |
| 134 | + azimuth: 45.0, |
| 135 | + distance: 10_000_000, |
| 136 | + caps: .all |
| 137 | +) |
| 138 | +``` |
155 | 139 |
|
156 |
| -### Initialize |
| 140 | +### Query positions |
157 | 141 |
|
158 | 142 | ```swift
|
159 |
| -// polygon: area + perimeter |
160 |
| -let poly = GeodesicPolygon(polyline: false) |
| 143 | +// Simple: by distance |
| 144 | +let pt1 = line.position(distance: 1_000_000) |
161 | 145 |
|
162 |
| -// or a polyline: only perimeter |
163 |
| -let pl = GeodesicPolygon(polyline: true) |
| 146 | +// Full: all outputs |
| 147 | +let (lat, lon, azi2, s12, m12, M12, M21, S12, a12) = |
| 148 | + line.genPosition(flags: .arcMode, s12_a12: 100.0) |
164 | 149 | ```
|
165 | 150 |
|
166 |
| -### Add by point |
| 151 | +### Adjust “third point” |
167 | 152 |
|
168 | 153 | ```swift
|
169 |
| -poly.addPoint((lat: 0.0, lon: 0.0)) |
170 |
| -poly.addPoint((lat: 0.0, lon: 1.0)) |
171 |
| -poly.addPoint((lat: 1.0, lon: 1.0)) |
| 154 | +line.setDistance(500_000) |
| 155 | +line.genSetDistance(flags: .arcMode, s13_a13: 4.5) |
172 | 156 | ```
|
173 | 157 |
|
174 |
| -### Add by edge |
| 158 | +--- |
175 | 159 |
|
176 |
| -```swift |
177 |
| -// from the last point, go 90° for 111 km |
178 |
| -poly.addEdge(azimuth: 90.0, distance: 111_000) |
179 |
| -``` |
| 160 | +## 2.3 GeodesicPolygon |
180 | 161 |
|
181 |
| -### Compute results |
| 162 | +Accumulate vertices or edges to get perimeter & area: |
182 | 163 |
|
183 | 164 | ```swift
|
184 |
| -let (count, area, perimeter) = poly.compute( |
185 |
| - reverse: false, |
186 |
| - signed: true |
187 |
| -) |
188 |
| -// • count : #points/edges processed |
189 |
| -// • area : area in m² (0 for polyline) |
190 |
| -// • perimeter : length in m |
| 165 | +let poly = GeodesicPolygon(polyline: false) |
| 166 | +poly.addPoint((lat: 0.0, lon: 0.0)) |
| 167 | +poly.addEdge(azimuth: 90, distance: 111_000) |
| 168 | +let (count, area, perimeter) = poly.compute(reverse: false, signed: true) |
191 | 169 | ```
|
192 | 170 |
|
193 | 171 | ### “Test” methods
|
194 | 172 |
|
195 |
| -Compute what _would_ happen if you added one more point or edge, **without** modifying the internal state: |
196 |
| - |
197 | 173 | ```swift
|
198 |
| -let (n, testArea, testPerim) = |
199 |
| - poly.testPoint((lat: 1.5, lon: 0.5)) |
200 |
| - |
201 |
| -let (m, testArea2, testPerim2) = |
202 |
| - poly.testEdge(azimuth: 180.0, distance: 50_000) |
| 174 | +let (_, testArea, testPerim) = |
| 175 | + poly.testPoint((lat: 1.5, lon: 0.5)) |
| 176 | +let (_, testArea2, testPerim2) = |
| 177 | + poly.testEdge(azimuth: 180, distance: 50_000) |
| 178 | +// none of these mutate `poly`—a fresh compute() still returns the original. |
203 | 179 | ```
|
204 | 180 |
|
205 |
| -Internal state remains unchanged, so a subsequent `poly.compute()` returns the same original `(area, perimeter)`. |
206 |
| - |
207 |
| ---- |
208 |
| - |
209 |
| -### One-Line Polygon Area |
210 |
| - |
211 |
| -For quick closed‐polygon area/perimeter in one call: |
| 181 | +### Quick one-liner |
212 | 182 |
|
213 | 183 | ```swift
|
214 |
| -let coords = [ |
215 |
| - (lat: 0.0, lon: 0.0), |
216 |
| - (lat: 0.0, lon: 1.0), |
217 |
| - (lat: 1.0, lon: 1.0), |
218 |
| - (lat: 1.0, lon: 0.0) |
219 |
| -] |
220 |
| -let (area, perimeter) = GeodesicPolygon.area(of: coords) |
| 184 | +let coords = [(lat:0, lon:0), (lat:0, lon:1), (lat:1, lon:1), (lat:1, lon:0)] |
| 185 | +let (area, peri) = GeodesicPolygon.area(of: coords) |
221 | 186 | ```
|
222 | 187 |
|
| 188 | +> **Tip:** All APIs default to WGS-84 unless you supply another `GeodGeodesic` model. |
| 189 | +
|
223 | 190 | ---
|
224 | 191 |
|
225 |
| -> **Tip:** All routines default to WGS-84 unless you pass a custom `GeodGeoDesic` model when calling. |
226 |
| - |
227 | 192 | # 3 Contributing
|
228 |
| -If you encounter any bugs or want to suggest new features, please submit a pull request. You can also report any issues, and I will address them when I have the opportunity. All contributions are welcome! Please note that all implemented methods should be organized in the appropriate folder locations and tested using the latest testing framework, `Swift Testing`. |
| 193 | + |
| 194 | +Feel free to open issues, suggest features, or submit pull requests. |
| 195 | +All methods live in `Sources/SwiftGeographicLib` and tests in `Tests/SwiftGeographicLibTests`. |
| 196 | +Thank you for your contributions! |
0 commit comments