@bricks-soft/capacitor-location-tracking 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BricksSoftCapacitorLocationTracking.podspec +18 -0
- package/Package.swift +13 -0
- package/README.md +107 -29
- package/android/build.gradle +3 -1
- package/dist/esm/definitions.d.ts +10 -4
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web/device.d.ts +1 -1
- package/dist/esm/web/device.js +1 -1
- package/dist/esm/web/device.js.map +1 -1
- package/dist/plugin.cjs.js +1 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
- package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
- package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
- package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
- package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
- package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
- package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
- package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
- package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
- package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
- package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
- package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
- package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
- package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
- package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
- package/package.json +9 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreFoundation
|
|
3
|
+
|
|
4
|
+
struct GeofenceSpec: Equatable {
|
|
5
|
+
var identifier: String
|
|
6
|
+
var latitude: Double
|
|
7
|
+
var longitude: Double
|
|
8
|
+
var radius: Double
|
|
9
|
+
var vertices: [LatLng]? = nil
|
|
10
|
+
var notifyOnEntry = true
|
|
11
|
+
var notifyOnExit = true
|
|
12
|
+
var notifyOnDwell = false
|
|
13
|
+
var loiteringDelay: Double = 30000
|
|
14
|
+
var extras: TrackingJSON? = nil
|
|
15
|
+
|
|
16
|
+
var isPolygon: Bool { vertices != nil }
|
|
17
|
+
var isActive: Bool { notifyOnEntry || notifyOnExit || notifyOnDwell }
|
|
18
|
+
|
|
19
|
+
static func == (a: GeofenceSpec, b: GeofenceSpec) -> Bool {
|
|
20
|
+
a.identifier == b.identifier && a.latitude == b.latitude && a.longitude == b.longitude && a.radius == b.radius && a.vertices == b.vertices && a.notifyOnEntry == b.notifyOnEntry && a.notifyOnExit == b.notifyOnExit && a.notifyOnDwell == b.notifyOnDwell && a.loiteringDelay == b.loiteringDelay && NSDictionary(dictionary: a.extras ?? [:]).isEqual(to: b.extras ?? [:]) && (a.extras == nil) == (b.extras == nil)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var json: TrackingJSON {
|
|
24
|
+
var value: TrackingJSON = ["identifier": identifier, "latitude": latitude, "longitude": longitude, "radius": radius, "notifyOnEntry": notifyOnEntry, "notifyOnExit": notifyOnExit, "notifyOnDwell": notifyOnDwell, "loiteringDelay": loiteringDelay]
|
|
25
|
+
if let vertices { value["vertices"] = vertices.map { [$0.latitude, $0.longitude] } }
|
|
26
|
+
if let extras { value["extras"] = extras }
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func stored(_ json: TrackingJSON) -> GeofenceSpec? {
|
|
31
|
+
guard let identifier = json["identifier"] as? String else { return nil }
|
|
32
|
+
let vertices = (json["vertices"] as? [[Double]]).map { $0.map { LatLng(latitude: $0[0], longitude: $0[1]) } }
|
|
33
|
+
return GeofenceSpec(identifier: identifier, latitude: json.number("latitude"), longitude: json.number("longitude"), radius: json.number("radius"), vertices: vertices, notifyOnEntry: json.flag("notifyOnEntry", true), notifyOnExit: json.flag("notifyOnExit", true), notifyOnDwell: json.flag("notifyOnDwell"), loiteringDelay: json.number("loiteringDelay", 30000), extras: json["extras"] as? TrackingJSON)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static func parse(_ json: TrackingJSON) throws -> GeofenceSpec {
|
|
37
|
+
guard let identifier = json["identifier"] as? String, !identifier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw invalid("geofence.identifier is required") }
|
|
38
|
+
for key in ["latitude", "longitude", "radius", "loiteringDelay"] where !isNull(json[key]) && number(json[key]) == nil { throw invalid("geofence '\(identifier)' \(key) must be a number") }
|
|
39
|
+
for key in ["notifyOnEntry", "notifyOnExit", "notifyOnDwell"] where !isNull(json[key]) && !isBoolean(json[key]) { throw invalid("geofence '\(identifier)' \(key) must be a boolean") }
|
|
40
|
+
if !isNull(json["extras"]) && !(json["extras"] is TrackingJSON) { throw invalid("geofence '\(identifier)' extras must be an object") }
|
|
41
|
+
var vertices: [LatLng]?
|
|
42
|
+
if !isNull(json["vertices"]) {
|
|
43
|
+
guard let array = json["vertices"] as? [Any] else { throw invalid("geofence '\(identifier)': vertices must be [[lat,lng],...]") }
|
|
44
|
+
vertices = try array.enumerated().map { index, item in
|
|
45
|
+
guard let pair = item as? [Any], pair.count == 2, let latitude = number(pair[0]), let longitude = number(pair[1]) else { throw invalid("geofence '\(identifier)': vertices[\(index)] must be [lat,lng]") }
|
|
46
|
+
return LatLng(latitude: latitude, longitude: longitude)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let latitude = number(json["latitude"]), longitude = number(json["longitude"]), radius = number(json["radius"])
|
|
50
|
+
if vertices == nil && (latitude == nil || longitude == nil || radius == nil) { throw invalid("geofence '\(identifier)' needs latitude, longitude and radius, or vertices") }
|
|
51
|
+
let delay = number(json["loiteringDelay"]) ?? 30000
|
|
52
|
+
if delay < 0 { throw invalid("geofence '\(identifier)' loiteringDelay must be >= 0") }
|
|
53
|
+
let spec = GeofenceSpec(identifier: identifier, latitude: latitude ?? 0, longitude: longitude ?? 0, radius: radius ?? 0, vertices: vertices, notifyOnEntry: (json["notifyOnEntry"] as? Bool) ?? true, notifyOnExit: (json["notifyOnExit"] as? Bool) ?? true, notifyOnDwell: (json["notifyOnDwell"] as? Bool) ?? false, loiteringDelay: delay.rounded(.towardZero), extras: json["extras"] as? TrackingJSON)
|
|
54
|
+
if let vertices {
|
|
55
|
+
if vertices.count < 3 { throw invalid("geofence '\(identifier)' needs at least 3 vertices") }
|
|
56
|
+
for (index, vertex) in vertices.enumerated() {
|
|
57
|
+
if !(-90...90).contains(vertex.latitude) || !vertex.latitude.isFinite { throw invalid("geofence '\(identifier)' vertices[\(index)] latitude is out of range") }
|
|
58
|
+
if !(-180...180).contains(vertex.longitude) || !vertex.longitude.isFinite { throw invalid("geofence '\(identifier)' vertices[\(index)] longitude is out of range") }
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
if !(-90...90).contains(spec.latitude) || !spec.latitude.isFinite { throw invalid("geofence '\(identifier)' latitude is out of range") }
|
|
62
|
+
if !(-180...180).contains(spec.longitude) || !spec.longitude.isFinite { throw invalid("geofence '\(identifier)' longitude is out of range") }
|
|
63
|
+
if !(spec.radius > 0) || spec.radius.isInfinite { throw invalid("geofence '\(identifier)' radius must be > 0") }
|
|
64
|
+
}
|
|
65
|
+
return spec
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static func isNull(_ value: Any?) -> Bool { value == nil || value is NSNull }
|
|
69
|
+
private static func isBoolean(_ value: Any?) -> Bool { (value as? NSNumber).map { CFGetTypeID($0) == CFBooleanGetTypeID() } ?? false }
|
|
70
|
+
private static func number(_ value: Any?) -> Double? {
|
|
71
|
+
guard let number = value as? NSNumber, CFGetTypeID(number) != CFBooleanGetTypeID(), number.doubleValue.isFinite else { return nil }
|
|
72
|
+
return number.doubleValue
|
|
73
|
+
}
|
|
74
|
+
private static func invalid(_ message: String) -> TrackingError { TrackingError("INVALID_ARGUMENT", message) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
struct GeofenceRuntime: Equatable {
|
|
78
|
+
var insideCircle = false
|
|
79
|
+
var insidePolygon = false
|
|
80
|
+
var enteredAt: Double? = nil
|
|
81
|
+
static let unknown = GeofenceRuntime()
|
|
82
|
+
var json: TrackingJSON { ["insideCircle": insideCircle, "insidePolygon": insidePolygon, "enteredAt": enteredAt as Any? ?? NSNull()] }
|
|
83
|
+
static func from(_ json: TrackingJSON) -> GeofenceRuntime { GeofenceRuntime(insideCircle: json.flag("insideCircle"), insidePolygon: json.flag("insidePolygon"), enteredAt: json["enteredAt"] as? Double) }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
enum GeofenceAction: String { case enter = "ENTER", exit = "EXIT", dwell = "DWELL" }
|
|
87
|
+
|
|
88
|
+
struct OsGeofence: Equatable {
|
|
89
|
+
var id: String
|
|
90
|
+
var latitude: Double
|
|
91
|
+
var longitude: Double
|
|
92
|
+
var radius: Double
|
|
93
|
+
var onEntry: Bool
|
|
94
|
+
var onExit: Bool
|
|
95
|
+
var initialTriggerEntry: Bool
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct OsGeofenceTransition: Equatable {
|
|
99
|
+
var id: String
|
|
100
|
+
var action: GeofenceAction
|
|
101
|
+
var location: TrackedLocation?
|
|
102
|
+
var initial = false
|
|
103
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
protocol GeofenceStore: AnyObject {
|
|
4
|
+
func upsert(_ specs: [GeofenceSpec]) throws
|
|
5
|
+
func remove(_ ids: [String]) throws
|
|
6
|
+
func removeAll() throws
|
|
7
|
+
func all() throws -> [GeofenceSpec]
|
|
8
|
+
func get(_ id: String) throws -> GeofenceSpec?
|
|
9
|
+
func setRuntime(_ id: String, _ runtime: GeofenceRuntime) throws
|
|
10
|
+
func runtimes() throws -> [String: GeofenceRuntime]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
final class SQLiteGeofenceStore: GeofenceStore {
|
|
14
|
+
private let store: TrackingStore
|
|
15
|
+
private let key = "geofences"
|
|
16
|
+
init(store: TrackingStore) { self.store = store }
|
|
17
|
+
private func load() throws -> TrackingJSON { try store.load(key) ?? [:] }
|
|
18
|
+
private func save(_ rows: TrackingJSON) throws { try store.save(key, rows) }
|
|
19
|
+
func upsert(_ specs: [GeofenceSpec]) throws {
|
|
20
|
+
guard !specs.isEmpty else { return }
|
|
21
|
+
var rows = try load()
|
|
22
|
+
for spec in specs {
|
|
23
|
+
var row = rows.object(spec.identifier)
|
|
24
|
+
row["spec"] = spec.json
|
|
25
|
+
rows[spec.identifier] = row
|
|
26
|
+
}
|
|
27
|
+
try save(rows)
|
|
28
|
+
}
|
|
29
|
+
func remove(_ ids: [String]) throws {
|
|
30
|
+
guard !ids.isEmpty else { return }
|
|
31
|
+
var rows = try load()
|
|
32
|
+
ids.forEach { rows.removeValue(forKey: $0) }
|
|
33
|
+
try save(rows)
|
|
34
|
+
}
|
|
35
|
+
func removeAll() throws { try save([:]) }
|
|
36
|
+
func all() throws -> [GeofenceSpec] {
|
|
37
|
+
try load().sorted { $0.key.utf8.lexicographicallyPrecedes($1.key.utf8) }.compactMap { GeofenceSpec.stored(($0.value as? TrackingJSON)?.object("spec") ?? [:]) }
|
|
38
|
+
}
|
|
39
|
+
func get(_ id: String) throws -> GeofenceSpec? { GeofenceSpec.stored(try load().object(id).object("spec")) }
|
|
40
|
+
func setRuntime(_ id: String, _ runtime: GeofenceRuntime) throws {
|
|
41
|
+
var rows = try load()
|
|
42
|
+
guard var row = rows[id] as? TrackingJSON else { return }
|
|
43
|
+
row["runtime"] = runtime.json
|
|
44
|
+
rows[id] = row
|
|
45
|
+
try save(rows)
|
|
46
|
+
}
|
|
47
|
+
func runtimes() throws -> [String: GeofenceRuntime] {
|
|
48
|
+
var result: [String: GeofenceRuntime] = [:]
|
|
49
|
+
for (id, value) in try load() { if let runtime = (value as? TrackingJSON)?["runtime"] as? TrackingJSON { result[id] = GeofenceRuntime.from(runtime) } }
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct LatLng: Equatable, Hashable {
|
|
4
|
+
var latitude: Double
|
|
5
|
+
var longitude: Double
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
struct XorWowRandom {
|
|
9
|
+
private var x: Int32, y: Int32, z: Int32, w: Int32, v: Int32, addend: Int32
|
|
10
|
+
init(seed: Int32) {
|
|
11
|
+
x = seed; y = seed >> 31; z = 0; w = 0; v = ~seed
|
|
12
|
+
addend = (seed << 10) ^ Int32(bitPattern: UInt32(bitPattern: seed >> 31) >> 4)
|
|
13
|
+
for _ in 0..<64 { _ = nextInt() }
|
|
14
|
+
}
|
|
15
|
+
mutating func nextInt() -> Int32 {
|
|
16
|
+
var t = x
|
|
17
|
+
t = t ^ Int32(bitPattern: UInt32(bitPattern: t) >> 2)
|
|
18
|
+
x = y; y = z; z = w
|
|
19
|
+
let v0 = v
|
|
20
|
+
w = v0
|
|
21
|
+
t = (t ^ (t << 1)) ^ v0 ^ (v0 << 4)
|
|
22
|
+
v = t
|
|
23
|
+
addend = addend &+ 362437
|
|
24
|
+
return t &+ addend
|
|
25
|
+
}
|
|
26
|
+
mutating func nextBits(_ count: Int32) -> Int32 {
|
|
27
|
+
Int32(bitPattern: UInt32(bitPattern: nextInt()) >> UInt32(32 - count)) & ((-count) >> 31)
|
|
28
|
+
}
|
|
29
|
+
mutating func nextInt(_ until: Int32) -> Int32 {
|
|
30
|
+
if until & -until == until { return nextBits(Int32(31 - until.leadingZeroBitCount)) }
|
|
31
|
+
while true {
|
|
32
|
+
let bits = Int32(bitPattern: UInt32(bitPattern: nextInt()) >> 1)
|
|
33
|
+
let value = bits % until
|
|
34
|
+
if bits &- value &+ (until - 1) >= 0 { return value }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
mutating func nextDouble() -> Double {
|
|
38
|
+
let high = Int64(nextBits(26)), low = Int64(nextBits(27))
|
|
39
|
+
return Double((high << 27) + low) / Double(Int64(1) << 53)
|
|
40
|
+
}
|
|
41
|
+
mutating func nextDouble(_ from: Double, _ until: Double) -> Double {
|
|
42
|
+
let value = from + nextDouble() * (until - from)
|
|
43
|
+
return value >= until ? until.nextDown : value
|
|
44
|
+
}
|
|
45
|
+
mutating func shuffled<T>(_ values: [T]) -> [T] {
|
|
46
|
+
var result = values
|
|
47
|
+
for index in stride(from: result.count - 1, through: 1, by: -1) { result.swapAt(index, Int(nextInt(Int32(index + 1)))) }
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
enum PolygonMath {
|
|
53
|
+
static let earthRadius = 6371008.8
|
|
54
|
+
static let padding = 1.1
|
|
55
|
+
static let minimumEnclosingRadius = 50.0
|
|
56
|
+
private static let epsilon = 1e-6
|
|
57
|
+
|
|
58
|
+
struct Circle: Equatable {
|
|
59
|
+
var latitude: Double
|
|
60
|
+
var longitude: Double
|
|
61
|
+
var radius: Double
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static func isValidLatLng(_ latitude: Double, _ longitude: Double) -> Bool {
|
|
65
|
+
latitude.isFinite && longitude.isFinite && (-90...90).contains(latitude) && (-180...180).contains(longitude)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static func distanceMeters(_ lat1: Double, _ lng1: Double, _ lat2: Double, _ lng2: Double) -> Double {
|
|
69
|
+
let phi1 = lat1 * .pi / 180, phi2 = lat2 * .pi / 180
|
|
70
|
+
let dPhi = phi2 - phi1, dLambda = (lng2 - lng1) * .pi / 180
|
|
71
|
+
let a = sin(dPhi / 2) * sin(dPhi / 2) + cos(phi1) * cos(phi2) * sin(dLambda / 2) * sin(dLambda / 2)
|
|
72
|
+
return 2 * earthRadius * asin(min(1, sqrt(a)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static func contains(_ vertices: [LatLng], _ latitude: Double, _ longitude: Double) -> Bool {
|
|
76
|
+
var inside = false
|
|
77
|
+
var j = vertices.count - 1
|
|
78
|
+
for i in vertices.indices {
|
|
79
|
+
let yi = vertices[i].latitude, xi = vertices[i].longitude, yj = vertices[j].latitude, xj = vertices[j].longitude
|
|
80
|
+
if (yi > latitude) != (yj > latitude) && longitude < (xj - xi) * (latitude - yi) / (yj - yi) + xi { inside.toggle() }
|
|
81
|
+
j = i
|
|
82
|
+
}
|
|
83
|
+
return inside
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static func distanceToBoundary(_ vertices: [LatLng], _ latitude: Double, _ longitude: Double) -> Double {
|
|
87
|
+
guard !vertices.isEmpty else { return .infinity }
|
|
88
|
+
let projection = Projection(latitude, longitude)
|
|
89
|
+
let points = vertices.map { projection.point($0.latitude, $0.longitude) }
|
|
90
|
+
if points.count == 1 { return hypot(points[0].x, points[0].y) }
|
|
91
|
+
var best = Double.infinity
|
|
92
|
+
var j = points.count - 1
|
|
93
|
+
for i in points.indices {
|
|
94
|
+
best = min(best, distanceToSegment(points[j], points[i]))
|
|
95
|
+
j = i
|
|
96
|
+
}
|
|
97
|
+
return best
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static func areaSquareMeters(_ vertices: [LatLng]) -> Double {
|
|
101
|
+
guard vertices.count >= 3 else { return 0 }
|
|
102
|
+
let projection = Projection.around(vertices)
|
|
103
|
+
let points = vertices.map { projection.point($0.latitude, $0.longitude) }
|
|
104
|
+
var sum = 0.0
|
|
105
|
+
var j = points.count - 1
|
|
106
|
+
for i in points.indices {
|
|
107
|
+
sum += points[j].x * points[i].y - points[i].x * points[j].y
|
|
108
|
+
j = i
|
|
109
|
+
}
|
|
110
|
+
return abs(sum) / 2
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static func minimalEnclosingCircle(_ vertices: [LatLng]) -> Circle {
|
|
114
|
+
precondition(!vertices.isEmpty)
|
|
115
|
+
let projection = Projection.around(vertices)
|
|
116
|
+
var random = XorWowRandom(seed: Int32(vertices.count))
|
|
117
|
+
let points = random.shuffled(vertices.map { projection.point($0.latitude, $0.longitude) })
|
|
118
|
+
var c = PlaneCircle(x: points[0].x, y: points[0].y, r: 0)
|
|
119
|
+
for i in points.indices where !c.contains(points[i]) {
|
|
120
|
+
c = PlaneCircle(x: points[i].x, y: points[i].y, r: 0)
|
|
121
|
+
for j in 0..<i where !c.contains(points[j]) {
|
|
122
|
+
c = PlaneCircle.diameter(points[i], points[j])
|
|
123
|
+
for k in 0..<j where !c.contains(points[k]) { c = PlaneCircle.through(points[i], points[j], points[k]) }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let center = projection.latLng(c.x, c.y)
|
|
127
|
+
let radius = vertices.map { distanceMeters(center.latitude, center.longitude, $0.latitude, $0.longitude) }.max() ?? 0
|
|
128
|
+
return Circle(latitude: center.latitude, longitude: center.longitude, radius: radius)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static func enclosingCircle(_ vertices: [LatLng]) -> Circle {
|
|
132
|
+
var circle = minimalEnclosingCircle(vertices)
|
|
133
|
+
circle.radius = max(circle.radius * padding, minimumEnclosingRadius)
|
|
134
|
+
return circle
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static func distanceToSegment(_ a: Point, _ b: Point) -> Double {
|
|
138
|
+
let dx = b.x - a.x, dy = b.y - a.y
|
|
139
|
+
let lengthSquared = dx * dx + dy * dy
|
|
140
|
+
if lengthSquared == 0 { return hypot(a.x, a.y) }
|
|
141
|
+
let t = min(1, max(0, (-a.x * dx - a.y * dy) / lengthSquared))
|
|
142
|
+
return hypot(a.x + t * dx, a.y + t * dy)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private struct Point { var x: Double; var y: Double }
|
|
146
|
+
|
|
147
|
+
private struct Projection {
|
|
148
|
+
let lat0: Double, lng0: Double, cosLat0: Double
|
|
149
|
+
init(_ lat0: Double, _ lng0: Double) { self.lat0 = lat0; self.lng0 = lng0; cosLat0 = max(cos(lat0 * .pi / 180), 1e-9) }
|
|
150
|
+
func point(_ latitude: Double, _ longitude: Double) -> Point {
|
|
151
|
+
Point(x: (longitude - lng0) * .pi / 180 * cosLat0 * PolygonMath.earthRadius, y: (latitude - lat0) * .pi / 180 * PolygonMath.earthRadius)
|
|
152
|
+
}
|
|
153
|
+
func latLng(_ x: Double, _ y: Double) -> LatLng {
|
|
154
|
+
LatLng(latitude: lat0 + y / PolygonMath.earthRadius * 180 / .pi, longitude: lng0 + x / (PolygonMath.earthRadius * cosLat0) * 180 / .pi)
|
|
155
|
+
}
|
|
156
|
+
static func around(_ vertices: [LatLng]) -> Projection {
|
|
157
|
+
let latitudes = vertices.map(\.latitude), longitudes = vertices.map(\.longitude)
|
|
158
|
+
return Projection((latitudes.min()! + latitudes.max()!) / 2, (longitudes.min()! + longitudes.max()!) / 2)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private struct PlaneCircle {
|
|
163
|
+
var x: Double, y: Double, r: Double
|
|
164
|
+
func contains(_ p: Point) -> Bool { hypot(p.x - x, p.y - y) <= r + PolygonMath.epsilon + r * 1e-12 }
|
|
165
|
+
static func diameter(_ a: Point, _ b: Point) -> PlaneCircle { PlaneCircle(x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, r: hypot(a.x - b.x, a.y - b.y) / 2) }
|
|
166
|
+
static func through(_ a: Point, _ b: Point, _ c: Point) -> PlaneCircle {
|
|
167
|
+
let bx = b.x - a.x, by = b.y - a.y, cx = c.x - a.x, cy = c.y - a.y
|
|
168
|
+
let d = 2 * (bx * cy - by * cx)
|
|
169
|
+
let scale = max(bx * bx + by * by, cx * cx + cy * cy)
|
|
170
|
+
if abs(d) <= 1e-12 * max(scale, 1e-12) { return [diameter(a, b), diameter(a, c), diameter(b, c)].max { $0.r < $1.r }! }
|
|
171
|
+
let b2 = bx * bx + by * by, c2 = cx * cx + cy * cy
|
|
172
|
+
let ux = (cy * b2 - by * c2) / d, uy = (bx * c2 - cx * b2) / d
|
|
173
|
+
return PlaneCircle(x: a.x + ux, y: a.y + uy, r: hypot(ux, uy))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
final class ExecutionAssertion {
|
|
5
|
+
private let lock = NSLock()
|
|
6
|
+
private let begin: (@escaping () -> Void) -> Int
|
|
7
|
+
private let end: (Int) -> Void
|
|
8
|
+
private var identifier: Int?
|
|
9
|
+
private var ended = false
|
|
10
|
+
private var timeout: DispatchWorkItem?
|
|
11
|
+
init(begin: @escaping (@escaping () -> Void) -> Int = { expiration in UIApplication.shared.beginBackgroundTask(withName: "Location tracking heartbeat", expirationHandler: expiration).rawValue }, end: @escaping (Int) -> Void = { UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier(rawValue: $0)) }) { self.begin = begin; self.end = end }
|
|
12
|
+
func start(seconds: Double, queue: DispatchQueue) {
|
|
13
|
+
guard seconds > 0 else { finish(); return }
|
|
14
|
+
let id = begin { [weak self] in self?.finish() }
|
|
15
|
+
lock.lock()
|
|
16
|
+
if ended { lock.unlock(); if id != UIBackgroundTaskIdentifier.invalid.rawValue { end(id) }; return }
|
|
17
|
+
identifier = id
|
|
18
|
+
let item = DispatchWorkItem { [weak self] in self?.finish() }
|
|
19
|
+
timeout = item
|
|
20
|
+
lock.unlock()
|
|
21
|
+
queue.asyncAfter(deadline: .now() + seconds, execute: item)
|
|
22
|
+
}
|
|
23
|
+
func finish() {
|
|
24
|
+
lock.lock()
|
|
25
|
+
guard !ended else { lock.unlock(); return }
|
|
26
|
+
ended = true
|
|
27
|
+
timeout?.cancel(); timeout = nil
|
|
28
|
+
let id = identifier; identifier = nil
|
|
29
|
+
lock.unlock()
|
|
30
|
+
if let id, id != UIBackgroundTaskIdentifier.invalid.rawValue { end(id) }
|
|
31
|
+
}
|
|
32
|
+
deinit { finish() }
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct HeartbeatWindow: Equatable {
|
|
4
|
+
var baseElapsed: Double
|
|
5
|
+
var dueElapsed: Double
|
|
6
|
+
var deadlineElapsed: Double
|
|
7
|
+
var nextHeartbeatAt: Double?
|
|
8
|
+
var minInterval: Double
|
|
9
|
+
var enabled: Bool
|
|
10
|
+
func shouldFire(_ elapsed: Double) -> Bool { elapsed >= dueElapsed - 1000 }
|
|
11
|
+
func needsRearm(_ armed: HeartbeatWindow?) -> Bool {
|
|
12
|
+
guard let armed, armed.enabled == enabled else { return true }
|
|
13
|
+
let move = dueElapsed - armed.dueElapsed
|
|
14
|
+
return move < 0 || move >= 30000
|
|
15
|
+
}
|
|
16
|
+
static func compute(lastRecordAt: Double?, lastRecordElapsed: Double?, sameBoot: Bool?, now: Double, nowElapsed: Double, minInterval: Double = 180, maxInterval: Double = 300, enabled: Bool = true, noRecordBaseElapsed: Double? = nil, lastAttemptElapsed: Double? = nil, trackingStartedAt: Double? = nil) -> HeartbeatWindow {
|
|
17
|
+
let minimum = max(60, minInterval) * 1000
|
|
18
|
+
let maximum = max(minimum, maxInterval * 1000)
|
|
19
|
+
var base: Double
|
|
20
|
+
if let at = lastRecordAt {
|
|
21
|
+
if let elapsed = lastRecordElapsed, sameBoot ?? (elapsed <= nowElapsed) { base = elapsed }
|
|
22
|
+
else { base = nowElapsed - (now - at) }
|
|
23
|
+
if let started = trackingStartedAt, at < started, started <= now { base = max(base, nowElapsed - (now - started)) }
|
|
24
|
+
} else { base = noRecordBaseElapsed ?? nowElapsed }
|
|
25
|
+
base = min(base, nowElapsed)
|
|
26
|
+
let due = max(base + minimum, lastAttemptElapsed.map { $0 + minimum } ?? -.infinity)
|
|
27
|
+
return HeartbeatWindow(baseElapsed: base, dueElapsed: due, deadlineElapsed: base + maximum, nextHeartbeatAt: enabled ? now + max(0, due - nowElapsed) : nil, minInterval: minimum, enabled: enabled)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct BodyBuilder {
|
|
4
|
+
private static let placeholder = try! NSRegularExpression(pattern: "(\"?)<%=\\s*(.*?)\\s*%>(\"?)", options: .dotMatchesLineSeparators)
|
|
5
|
+
private static let coordinateKeys: Set<String> = ["latitude", "longitude", "accuracy", "altitude", "altitude_accuracy", "speed", "speed_accuracy", "heading", "heading_accuracy"]
|
|
6
|
+
private static let supported: Set<String> = ["uuid", "event", "timestamp", "recorded_at", "sent_at", "latitude", "longitude", "accuracy", "altitude", "altitude_accuracy", "speed", "speed_accuracy", "heading", "heading_accuracy", "is_moving", "odometer", "mock", "activity.type", "activity.confidence", "battery.level", "battery.is_charging", "battery.temperature", "battery.health", "battery.plugged", "network", "device.device_id", "device.background_restricted", "device.standby_bucket", "device.disk_free_bytes", "device.thermal_status", "device.manufacturer", "device.model", "device.os_version", "device.sdk_int", "device.low_ram_device", "device.ram_total_bytes", "elapsed_realtime_ms", "boot_count", "backend", "reason", "stopped_at", "geofence.identifier", "geofence.action", "provider.enabled", "provider.gps", "provider.network", "provider.permission", "extras", "record", "provider", "device"]
|
|
7
|
+
static func build(_ records: [TrackingJSON], http: TrackingJSON, sentAt: String = iso()) throws -> Data {
|
|
8
|
+
let rendered = records.map { record -> Any in
|
|
9
|
+
var wire = record
|
|
10
|
+
wire["sent_at"] = sentAt
|
|
11
|
+
let template = record.text("event") == "geofence" ? http.text("geofenceTemplate", http.text("locationTemplate")) : http.text("locationTemplate")
|
|
12
|
+
return render(template, record: wire) ?? wire
|
|
13
|
+
}
|
|
14
|
+
let root = http.text("rootProperty", "location")
|
|
15
|
+
let merge = root == "." || root.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
16
|
+
let batch = http.flag("batchSync")
|
|
17
|
+
if merge && batch { return try jsonData(rendered) }
|
|
18
|
+
let value: Any = batch ? rendered : rendered[0]
|
|
19
|
+
var body: TrackingJSON
|
|
20
|
+
if merge {
|
|
21
|
+
guard let object = value as? TrackingJSON else { return try jsonData(value) }
|
|
22
|
+
body = object
|
|
23
|
+
} else { body = [root: value] }
|
|
24
|
+
for (key, value) in http.object("params") where body[key] == nil { body[key] = value }
|
|
25
|
+
return try jsonData(body)
|
|
26
|
+
}
|
|
27
|
+
static func render(_ template: String, record: TrackingJSON) -> Any? {
|
|
28
|
+
guard !template.isEmpty else { return nil }
|
|
29
|
+
let source = template as NSString
|
|
30
|
+
var output = template
|
|
31
|
+
for match in Self.placeholder.matches(in: template, range: NSRange(location: 0, length: source.length)).reversed() {
|
|
32
|
+
let key = source.substring(with: match.range(at: 2))
|
|
33
|
+
let open = source.substring(with: match.range(at: 1))
|
|
34
|
+
let close = source.substring(with: match.range(at: 3))
|
|
35
|
+
let path = Self.coordinateKeys.contains(key) ? "coords." + key : key
|
|
36
|
+
var value: Any? = record
|
|
37
|
+
if key != "record" {
|
|
38
|
+
for component in path.split(separator: ".") { value = (value as? TrackingJSON)?[String(component)] }
|
|
39
|
+
}
|
|
40
|
+
if key == "extras", value == nil { value = TrackingJSON() }
|
|
41
|
+
let replacement: String
|
|
42
|
+
if !Self.supported.contains(key) { replacement = open + close }
|
|
43
|
+
else if value == nil || value is NSNull { replacement = open.isEmpty || close.isEmpty ? open + "null" + close : "null" }
|
|
44
|
+
else if let data = try? jsonData(value!), let literal = String(data: data, encoding: .utf8) {
|
|
45
|
+
replacement = open + (value is String ? String(literal.dropFirst().dropLast()) : literal) + close
|
|
46
|
+
} else { replacement = "null" }
|
|
47
|
+
if let range = Range(match.range, in: output) { output.replaceSubrange(range, with: replacement) }
|
|
48
|
+
}
|
|
49
|
+
guard StrictJSON.isValid(output), let data = output.data(using: .utf8), let value = try? JSONSerialization.jsonObject(with: data), value is TrackingJSON || value is [Any] else { return nil }
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct HTTPResponse {
|
|
4
|
+
let status: Int
|
|
5
|
+
let body: Data
|
|
6
|
+
let retryAfter: String?
|
|
7
|
+
var success: Bool { (200..<300).contains(status) }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
final class HTTPTransport: NSObject, URLSessionDataDelegate {
|
|
11
|
+
private struct Pending {
|
|
12
|
+
var data = Data()
|
|
13
|
+
var response: HTTPURLResponse?
|
|
14
|
+
var timeout: DispatchWorkItem?
|
|
15
|
+
let responseLimit: Int?
|
|
16
|
+
let completion: (Result<HTTPResponse, Error>) -> Void
|
|
17
|
+
}
|
|
18
|
+
private var pending: [Int: Pending] = [:]
|
|
19
|
+
private let lock = NSLock()
|
|
20
|
+
private var closed = false
|
|
21
|
+
private lazy var session: URLSession = {
|
|
22
|
+
let config = URLSessionConfiguration.ephemeral
|
|
23
|
+
config.urlCache = nil
|
|
24
|
+
config.httpCookieStorage = nil
|
|
25
|
+
config.urlCredentialStorage = nil
|
|
26
|
+
config.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
27
|
+
config.waitsForConnectivity = false
|
|
28
|
+
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
|
29
|
+
}()
|
|
30
|
+
func send(url: URL, method: String, headers: [String: String], body: Data, timeout: Double, responseLimit: Int? = 16384, completion: @escaping (Result<HTTPResponse, Error>) -> Void) {
|
|
31
|
+
var request = URLRequest(url: url, timeoutInterval: timeout / 1000)
|
|
32
|
+
request.httpMethod = method
|
|
33
|
+
request.httpBody = body
|
|
34
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
35
|
+
for (name, value) in headers { request.setValue(value, forHTTPHeaderField: name) }
|
|
36
|
+
lock.lock()
|
|
37
|
+
guard !closed else { lock.unlock(); completion(.failure(URLError(.cancelled))); return }
|
|
38
|
+
let task = session.dataTask(with: request)
|
|
39
|
+
let deadline = DispatchWorkItem { [weak self, weak task] in
|
|
40
|
+
guard let self, let task else { return }
|
|
41
|
+
self.lock.lock(); let item = self.pending.removeValue(forKey: task.taskIdentifier); self.lock.unlock()
|
|
42
|
+
guard let item else { return }
|
|
43
|
+
task.cancel()
|
|
44
|
+
item.completion(.failure(URLError(.timedOut)))
|
|
45
|
+
}
|
|
46
|
+
pending[task.taskIdentifier] = Pending(timeout: deadline, responseLimit: responseLimit, completion: completion); lock.unlock()
|
|
47
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + timeout / 1000, execute: deadline)
|
|
48
|
+
task.resume()
|
|
49
|
+
}
|
|
50
|
+
func cancel() {
|
|
51
|
+
lock.lock()
|
|
52
|
+
guard !closed else { lock.unlock(); return }
|
|
53
|
+
let current = session
|
|
54
|
+
lock.unlock()
|
|
55
|
+
current.getAllTasks { $0.forEach { $0.cancel() } }
|
|
56
|
+
}
|
|
57
|
+
func close() {
|
|
58
|
+
lock.lock()
|
|
59
|
+
guard !closed else { lock.unlock(); return }
|
|
60
|
+
closed = true
|
|
61
|
+
let current = session
|
|
62
|
+
lock.unlock()
|
|
63
|
+
current.invalidateAndCancel()
|
|
64
|
+
}
|
|
65
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) }
|
|
66
|
+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
|
67
|
+
lock.lock(); pending[dataTask.taskIdentifier]?.response = response as? HTTPURLResponse; lock.unlock()
|
|
68
|
+
completionHandler(.allow)
|
|
69
|
+
}
|
|
70
|
+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
|
71
|
+
lock.lock()
|
|
72
|
+
if var item = pending[dataTask.taskIdentifier] {
|
|
73
|
+
if let limit = item.responseLimit { item.data.append(data.prefix(max(0, limit - item.data.count))) }
|
|
74
|
+
else { item.data.append(data) }
|
|
75
|
+
pending[dataTask.taskIdentifier] = item
|
|
76
|
+
}
|
|
77
|
+
lock.unlock()
|
|
78
|
+
}
|
|
79
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
80
|
+
lock.lock(); let item = pending.removeValue(forKey: task.taskIdentifier); lock.unlock()
|
|
81
|
+
guard let item else { return }
|
|
82
|
+
item.timeout?.cancel()
|
|
83
|
+
if let error { item.completion(.failure(error)) }
|
|
84
|
+
else if let response = item.response { item.completion(.success(HTTPResponse(status: response.statusCode, body: item.data, retryAfter: response.value(forHTTPHeaderField: "Retry-After")))) }
|
|
85
|
+
else { item.completion(.failure(TrackingError("NETWORK_ERROR", "No HTTP response"))) }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct StrictJSON {
|
|
4
|
+
private let bytes: [UInt8]
|
|
5
|
+
private var index = 0
|
|
6
|
+
init(_ text: String) { bytes = Array(text.utf8) }
|
|
7
|
+
static func isValid(_ text: String) -> Bool {
|
|
8
|
+
var parser = StrictJSON(text)
|
|
9
|
+
parser.whitespace()
|
|
10
|
+
guard parser.value(0) else { return false }
|
|
11
|
+
parser.whitespace()
|
|
12
|
+
return parser.index == parser.bytes.count
|
|
13
|
+
}
|
|
14
|
+
private mutating func whitespace() { while index < bytes.count && [9, 10, 13, 32].contains(bytes[index]) { index += 1 } }
|
|
15
|
+
private mutating func consume(_ byte: UInt8) -> Bool {
|
|
16
|
+
guard index < bytes.count && bytes[index] == byte else { return false }
|
|
17
|
+
index += 1; return true
|
|
18
|
+
}
|
|
19
|
+
private mutating func value(_ depth: Int) -> Bool {
|
|
20
|
+
guard depth <= 256 && index < bytes.count else { return false }
|
|
21
|
+
switch bytes[index] {
|
|
22
|
+
case 123: return object(depth + 1)
|
|
23
|
+
case 91: return array(depth + 1)
|
|
24
|
+
case 34: return string()
|
|
25
|
+
case 116: return literal("true")
|
|
26
|
+
case 102: return literal("false")
|
|
27
|
+
case 110: return literal("null")
|
|
28
|
+
default: return number()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
private mutating func object(_ depth: Int) -> Bool {
|
|
32
|
+
index += 1; whitespace()
|
|
33
|
+
if consume(125) { return true }
|
|
34
|
+
while true {
|
|
35
|
+
whitespace()
|
|
36
|
+
guard index < bytes.count && bytes[index] == 34 && string() else { return false }
|
|
37
|
+
whitespace()
|
|
38
|
+
guard consume(58) else { return false }
|
|
39
|
+
whitespace()
|
|
40
|
+
guard value(depth) else { return false }
|
|
41
|
+
whitespace()
|
|
42
|
+
if consume(125) { return true }
|
|
43
|
+
guard consume(44) else { return false }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
private mutating func array(_ depth: Int) -> Bool {
|
|
47
|
+
index += 1; whitespace()
|
|
48
|
+
if consume(93) { return true }
|
|
49
|
+
while true {
|
|
50
|
+
whitespace()
|
|
51
|
+
guard value(depth) else { return false }
|
|
52
|
+
whitespace()
|
|
53
|
+
if consume(93) { return true }
|
|
54
|
+
guard consume(44) else { return false }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
private mutating func string() -> Bool {
|
|
58
|
+
index += 1
|
|
59
|
+
while index < bytes.count {
|
|
60
|
+
let byte = bytes[index]; index += 1
|
|
61
|
+
if byte == 34 { return true }
|
|
62
|
+
if byte < 32 { return false }
|
|
63
|
+
if byte == 92 {
|
|
64
|
+
guard index < bytes.count else { return false }
|
|
65
|
+
let escape = bytes[index]; index += 1
|
|
66
|
+
if escape == 117 {
|
|
67
|
+
guard index + 4 <= bytes.count else { return false }
|
|
68
|
+
guard bytes[index..<index + 4].allSatisfy({ (48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0) }) else { return false }
|
|
69
|
+
index += 4
|
|
70
|
+
} else if ![34, 92, 47, 98, 102, 110, 114, 116].contains(escape) { return false }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
private mutating func literal(_ text: String) -> Bool {
|
|
76
|
+
let expected = Array(text.utf8)
|
|
77
|
+
guard index + expected.count <= bytes.count && Array(bytes[index..<index + expected.count]) == expected else { return false }
|
|
78
|
+
index += expected.count; return true
|
|
79
|
+
}
|
|
80
|
+
private mutating func number() -> Bool {
|
|
81
|
+
_ = consume(45)
|
|
82
|
+
guard index < bytes.count else { return false }
|
|
83
|
+
if bytes[index] == 48 { index += 1 }
|
|
84
|
+
else if (49...57).contains(bytes[index]) { _ = digits() }
|
|
85
|
+
else { return false }
|
|
86
|
+
if consume(46) && !digits() { return false }
|
|
87
|
+
if consume(101) || consume(69) {
|
|
88
|
+
if !consume(43) { _ = consume(45) }
|
|
89
|
+
if !digits() { return false }
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
private mutating func digits() -> Bool {
|
|
94
|
+
let start = index
|
|
95
|
+
while index < bytes.count && (48...57).contains(bytes[index]) { index += 1 }
|
|
96
|
+
return index > start
|
|
97
|
+
}
|
|
98
|
+
}
|