-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGEOCoordinateFormatter.swift
299 lines (245 loc) · 12.8 KB
/
GEOCoordinateFormatter.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
//
// GEOCoordinateFormatter.swift
// GEOCoordinateFormatter
//
// Created by Martin Kiss on 3 Apr 2018.
// https://github.com/Tricertops/GEOCoordinateFormatter
//
// The MIT License (MIT)
// Copyright © Martin Kiss
//
import Foundation
import CoreLocation
/// An NSFormatter that creates human-readable strings of geographic coordinates.
/// Default format for new instance is: 12° 34′ N, 12° 34′ W
@objc public class GEOCoordinateFormatter: Formatter {
//MARK: Precision & Locale
/// Locale used to format numbers, passed to the underlaying NSNumberFormatter.
@objc public var locale: Locale = Locale(identifier: "en_US_POSIX")
/// Specify which units will be used. This formatter always prints all larger units, for example 0° 0′ 3″ (smallestUnit = seconds).
@objc public var smallestUnit: GEOCoordinateFormatterUnit = .minutes
/// Specify how many digits beyond integer degrees should be printed, degrees are always printer with up to 3 integer digits.
/// After using all integer digits allowed by smallestUnit, the smallest unit will get fractional digits.
/// For example: 12° 34.567′ (smallestUnit = minutes, precisionDigits = 5).
@objc public var precisionDigits: UInt = 2
/// To get a sense of distances, here is a convenient table for equator.
/// Keep in mind that longitudal resolution decreases toward poles (70% at ±45° latitude, 50% at ±60° latitude).
/// 1° = 111 km (smallestUnit = degrees, precisionDigits = 0)
/// 0° 10′ = 18.6 km (smallestUnit = minutes, precisionDigits = 1)
/// 0.1° = 11.1 km (smallestUnit = degrees, precisionDigits = 1)
/// 0° 1′ = 1.86 km (smallestUnit = minutes, precisionDigits = 2)
/// 0° 0′ 10″ = 310 m (smallestUnit = seconds, precisionDigits = 3)
/// 0° 0.1′ = 186 m (smallestUnit = minutes, precisionDigits = 3)
/// 0° 0′ 1″ = 31 m (smallestUnit = seconds, precisionDigits = 4)
/// 0° 0′ 0.1″ = 3.1 m (smallestUnit = seconds, precisionDigits = 5)
/// 0° 0′ 0.01″ = 31 cm (smallestUnit = seconds, precisionDigits = 6)
/// 0° 0′ 0.001″ = 3.1 cm (smallestUnit = seconds, precisionDigits = 7)
//MARK: Units & Separators
/// Customize string to be used as degrees unit string. This string will be adjacent to the number, so include leading space as needed.
@objc public var degreeString: String = "°" // U+00B0 DEGREE SIGN
/// Customize string to be used as minues unit string. This string will be adjacent to the number, so include leading space as needed.
@objc public var minuteString: String = "′" // U+2032 PRIME
/// Customize string to be used as seconds unit string. This string will be adjacent to the number, so include leading space as needed.
@objc public var secondString: String = "″" // U+2033 DOUBLE PRIME
/// Customize string that will be inserted between degrees, minutes, and seconds.
@objc public var componentSeparator: String = " " // U+00A0 NO-BREAK SPACE
/// Customize decimal separator for numbers. If nil, default of locale will be used.
@objc public var decimalSeparator: String? = nil
//MARK: Hemispheres
/// Specify whether the formatter appends hemisphere suffix after the coordinate. If false, the formatter will use minusSign to indicate Southern or Western hemisphere. Examples 12° 34′ W and −12° 34′.
@objc public var usesHemisphereSuffixes: Bool = true
/// Customize minus sign when printing without hemisphere suffixes. You may prefer hyphen (U+002D HYPHEN-MINUS).
@objc public var minusSign: String = "−" // U+2212 MINUS SIGN
/// Customize string to be used for North hemisphere. This is not localized automatically.
@objc public var northString: String = "N"
/// Customize string to be used for South hemisphere. This is not localized automatically.
@objc public var southString: String = "S"
/// Customize string to be used for East hemisphere. This is not localized automatically.
@objc public var eastString: String = "E"
/// Customize string to be used for West hemisphere. This is not localized automatically.
@objc public var westString: String = "W"
//MARK: Formatting
/// Builds coordinate format for no specific axis. Appends no hemishpere suffix, but respects the configuration.
@objc public func stringFor(number: Double) -> String {
return self.buildFormat(number: number, axis: .undefined)
}
/// Builds coordinate format for latitude. Appends North/South suffix, if configured so.
@objc public func stringFor(latitude: Double) -> String {
return self.buildFormat(number: latitude, axis: .latitudal)
}
/// Builds coordinate format for longitude. Appends East/West suffix, if configured so.
@objc public func stringFor(longitude: Double) -> String {
return self.buildFormat(number: longitude, axis: .longitudal)
}
/// Builds coordinate format for both latitude and longitude. Joins them using comma.
@objc public func stringFor(coordinate: CLLocationCoordinate2D, joiner: String = ", ") -> String {
return self.stringFor(latitude: coordinate.latitude, longitude: coordinate.longitude, joiner: joiner)
}
/// Builds coordinate format for both latitude and longitude. Joins them using comma.
@objc public func stringFor(latitude: Double, longitude: Double, joiner: String = ", ") -> String {
let latitudeString = self.buildFormat(number: latitude, axis: .latitudal)
let longitudeString = self.buildFormat(number: longitude, axis: .longitudal)
return latitudeString + joiner + longitudeString
}
}
/// List of all possible units, that can be used with GEOCoordinateFormatter.
@objc public enum GEOCoordinateFormatterUnit: Int {
case degrees = 0
case minutes = 1
case seconds = 2
}
//MARK: - Implementation Details
extension GEOCoordinateFormatter {
@objc public override func string(for object: Any?) -> String? {
switch object {
// Simple numbers are formatted with undefined axis. Pass NSNumber from Objective-C.
case let number as Double:
return self.stringFor(number: number)
// Compound coordinates are formatted properly. Pass NSValue from Objective-C.
case let coordinate as CLLocationCoordinate2D:
return self.stringFor(coordinate: coordinate)
// Accepts arrays of number with length of 2.
case let array as [Double] where array.count == 2:
return self.stringFor(latitude: array[0], longitude: array[1])
default:
return nil
}
}
private enum Axis {
case undefined
case latitudal
case longitudal
}
private func buildFormat(number: Double, axis: Axis) -> String {
if number.isNaN || number.isInfinite {
return ""
}
let decomposed = self.decompose(number: number)
var components: [String]
let formatter = self.makeNumberFormatter()
// Number of digits beyond integer degrees.
var precision = Int(self.precisionDigits)
switch self.smallestUnit {
case .degrees:
// 12.34567° S
// -12.34567°
let degrees = self.format(number: number, precision: precision, formatter: formatter)
components = [ degrees + self.degreeString ]
case .minutes:
// 12° 34.567′ S
// -12° 34.567′
// Two digits are consumed by integer minutes.
precision -= 2
let degreesString = self.format(number: decomposed.degrees, precision: 0, formatter: formatter)
let minutesString = self.format(number: decomposed.minutes, precision: precision, formatter: formatter)
components = [
degreesString + self.degreeString,
minutesString + self.minuteString,
]
case .seconds:
// 12° 34′ 56.7″ S
// -12° 34′ 56.7″
// Two digits are consumed by integer minutes.
// Two digits are consumed by integer seconds.
precision -= 4
let degreesString = self.format(number: decomposed.degrees, precision: 0, formatter: formatter)
let minutesString = self.format(number: decomposed.minutes, precision: 0, formatter: formatter)
let secondsString = self.format(number: decomposed.seconds, precision: precision, formatter: formatter)
components = [
degreesString + self.degreeString,
minutesString + self.minuteString,
secondsString + self.secondString,
]
}
if self.usesHemisphereSuffixes {
let hemisphere = self.hemisphereSufffix(for: axis, negative: (number < 0))
if !hemisphere.isEmpty {
components.append(hemisphere)
}
}
return components.joined(separator: self.componentSeparator)
}
private func makeNumberFormatter() -> NumberFormatter {
let formatter = NumberFormatter()
formatter.locale = self.locale
formatter.decimalSeparator = self.decimalSeparator ?? self.locale.decimalSeparator;
formatter.numberStyle = .decimal
formatter.minusSign = self.minusSign
formatter.usesGroupingSeparator = false
formatter.minimumIntegerDigits = 1
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 0
return formatter
}
private func decompose(number: Double) -> (degrees: Double, minutes: Double, seconds: Double) {
// Number of digits beyond integer degrees.
var precision = Int(self.precisionDigits)
switch self.smallestUnit {
case .degrees:
// The number is degrees.
return (number, 0, 0)
case .minutes:
precision -= 2
// Degrees are simply rounded.
var degrees = abs(number)
// Minutes are 60× the fractional part.
var minutes = degrees.truncatingRemainder(dividingBy: 1) * 60
degrees.round(.towardZero)
minutes = self.rounded(number: minutes, precision: precision)
// If rounding overflowed valid range, increment higher unit.
if minutes > 60 {
minutes -= 60
degrees += 1
}
// If we need to use minus sign, negate degrees.
if !self.usesHemisphereSuffixes && number < 0 {
degrees *= -1
}
return (degrees, minutes, 0)
case .seconds:
// Two digits are consumed by integer minutes.
// Two digits are consumed by integer seconds.
precision -= 4
// Degrees are simply rounded.
var degrees = abs(number)
// Minutes and seconds are 60× the fractional part.
var minutes = degrees.truncatingRemainder(dividingBy: 1) * 60
var seconds = minutes.truncatingRemainder(dividingBy: 1) * 60
degrees.round(.towardZero)
minutes.round(.towardZero)
seconds = self.rounded(number: seconds, precision: precision)
// If rounding overflowed valid range, increment higher unit.
if seconds > 60 {
seconds -= 60
minutes += 1
}
if minutes > 60 {
minutes -= 60
degrees += 1
}
// If we need to use minus sign, negate degrees.
if !self.usesHemisphereSuffixes && number < 0 {
degrees *= -1
}
return (degrees, minutes, seconds)
}
}
private func rounded(number: Double, precision: Int) -> Double {
// Works also for negative precision: precision -1 will round to 10.
let roundingScale = pow(10.0, Double(precision))
return (number * roundingScale).rounded() / roundingScale
}
private func format(number: Double, precision: Int, formatter: NumberFormatter) -> String {
let precision = max(precision, 0)
formatter.minimumFractionDigits = precision
formatter.maximumFractionDigits = precision
return formatter.string(from: number as NSNumber)!
}
private func hemisphereSufffix(for axis: Axis, negative: Bool) -> String {
switch axis {
case .undefined: return ""
case .latitudal: return (negative ? self.southString : self.northString)
case .longitudal: return (negative ? self.westString : self.eastString)
}
}
}