@capgo/background-geolocation 7.0.10 → 7.0.12

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.
@@ -42,6 +42,12 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate {
42
42
  private var isUpdatingLocation: Bool = false
43
43
  private var activeCallbackId: String?
44
44
  private var audioPlayer: AVAudioPlayer?
45
+ private var plannedRoute: [[Double]] = []
46
+ private var isOffRoute: Bool = true
47
+ private var distanceThreshold: Double = 50.0 // Default distance threshold in meters
48
+
49
+ // Earth radius in meters for distance calculations
50
+ private static let EARTH_RADIUS_M: Double = 6371000.0
45
51
 
46
52
  @objc override public func load() {
47
53
  UIDevice.current.isBatteryMonitoringEnabled = true
@@ -149,6 +155,50 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate {
149
155
  }
150
156
  }
151
157
 
158
+ @objc func setPlannedRoute(_ call: CAPPluginCall) {
159
+ DispatchQueue.global(qos: .background).async { [weak self] in
160
+ guard let self = self else { return }
161
+
162
+ guard let soundFile = call.getString("soundFile") else {
163
+ call.reject("Sound file is required")
164
+ return
165
+ }
166
+
167
+ let routeArray = call.getArray("route", Any.self) ?? []
168
+ var route: [[Double]] = []
169
+
170
+ for routePoint in routeArray {
171
+ if let pointArray = routePoint as? [Double], pointArray.count == 2 {
172
+ route.append(pointArray)
173
+ }
174
+ }
175
+
176
+ let distance = call.getDouble("distance") ?? 50.0
177
+
178
+ let assetPath = "public/" + soundFile
179
+ let assetPathSplit = assetPath.components(separatedBy: ".")
180
+ guard let url = Bundle.main.url(forResource: assetPathSplit[0], withExtension: assetPathSplit[1]) else {
181
+ call.reject("Sound file not found: \(assetPath)")
182
+ return
183
+ }
184
+
185
+ do {
186
+ self.audioPlayer?.stop()
187
+ self.audioPlayer = nil
188
+ self.audioPlayer = try AVAudioPlayer(contentsOf: url)
189
+
190
+ // Store route configuration
191
+ self.plannedRoute = route
192
+ self.distanceThreshold = distance
193
+ self.isOffRoute = true
194
+
195
+ call.resolve()
196
+ } catch {
197
+ call.reject("Could not load the sound file: \(error.localizedDescription)")
198
+ }
199
+ }
200
+ }
201
+
152
202
  private func startUpdatingLocation() {
153
203
  // Avoid unnecessary calls to startUpdatingLocation, which can
154
204
  // result in extraneous invocations of didFailWithError.
@@ -173,28 +223,104 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate {
173
223
  )
174
224
  }
175
225
 
176
- @objc func playSound(_ call: CAPPluginCall) {
177
- // Use a background queue for audio loading to avoid blocking the main thread
178
- DispatchQueue.global(qos: .background).async { [weak self] in
179
- guard let self = self else { return }
226
+ private func toRadians(_ degrees: Double) -> Double {
227
+ return degrees * Double.pi / 180.0
228
+ }
180
229
 
181
- let assetPath = "public/" + (call.getString("soundFile") ?? "")
182
- let assetPathSplit = assetPath.components(separatedBy: ".")
183
- guard let url = Bundle.main.url(forResource: assetPathSplit[0], withExtension: assetPathSplit[1]) else {
184
- call.reject("Sound file not found: \(assetPath)")
185
- return
230
+ private func haversine(_ point1: [Double], _ point2: [Double]) -> Double {
231
+ let lon1 = point1[0]
232
+ let lat1 = point1[1]
233
+ let lon2 = point2[0]
234
+ let lat2 = point2[1]
235
+
236
+ let dLat = toRadians(lat2 - lat1)
237
+ let dLon = toRadians(lon2 - lon1)
238
+
239
+ let aaa = sin(dLat / 2) * sin(dLat / 2) +
240
+ cos(toRadians(lat1)) * cos(toRadians(lat2)) *
241
+ sin(dLon / 2) * sin(dLon / 2)
242
+
243
+ let ccc = 2 * atan2(sqrt(aaa), sqrt(1 - aaa))
244
+
245
+ return BackgroundGeolocation.EARTH_RADIUS_M * ccc
246
+ }
247
+
248
+ private func distancePointToLineSegment(_ point: [Double], _ lineStart: [Double], _ lineEnd: [Double]) -> Double {
249
+ // Calculate the distances between the three points using Haversine
250
+ let distAB = haversine(point, lineStart)
251
+ let distAC = haversine(point, lineEnd)
252
+ let distBC = haversine(lineStart, lineEnd)
253
+
254
+ // Handle the edge case where the line segment is a single point
255
+ if distBC == 0 {
256
+ return distAB
257
+ }
258
+
259
+ // Check if the angles at the line segment's endpoints are obtuse.
260
+ // We use the Law of Cosines (c^2 = a^2 + b^2 - 2ab*cos(C))
261
+ // If cos(C) < 0, the angle is obtuse.
262
+
263
+ // Angle at B (lineStart)
264
+ let epsilon = Double.ulpOfOne
265
+ let cosB = (pow(distAB, 2) + pow(distBC, 2) - pow(distAC, 2)) / (2 * distAB * distBC + epsilon)
266
+ if cosB < 0 {
267
+ return distAB
268
+ }
269
+
270
+ // Angle at C (lineEnd)
271
+ let cosC = (pow(distAC, 2) + pow(distBC, 2) - pow(distAB, 2)) / (2 * distAC * distBC + epsilon)
272
+ if cosC < 0 {
273
+ return distAC
274
+ }
275
+
276
+ // If both angles are acute, the closest point is on the line segment itself.
277
+ // We can calculate the distance (height of the triangle) using its area.
278
+
279
+ // 1. Calculate the semi-perimeter of the triangle ABC
280
+ let semi = (distAB + distAC + distBC) / 2
281
+
282
+ // 2. Calculate the area using Heron's formula
283
+ let area = sqrt(max(0, semi * (semi - distAB) * (semi - distAC) * (semi - distBC)))
284
+
285
+ // 3. The distance is the height of the triangle from point A to the base BC
286
+ // Area = 0.5 * base * height => height = 2 * Area / base
287
+ return (2 * area) / (distBC + epsilon)
288
+ }
289
+
290
+ private func distancePointToRoute(_ point: [Double]) -> Double {
291
+ // If the route has less than 2 points, we can't form a segment.
292
+ if plannedRoute.count < 2 {
293
+ if plannedRoute.count == 1 {
294
+ return haversine(point, plannedRoute[0])
186
295
  }
296
+ return Double.infinity // No line segments to measure against
297
+ }
187
298
 
188
- do {
189
- self.audioPlayer?.stop()
190
- self.audioPlayer = nil
191
- self.audioPlayer = try AVAudioPlayer(contentsOf: url)
192
- self.audioPlayer?.play()
193
- call.resolve()
194
- } catch {
195
- call.reject("Could not play the sound file: \(error.localizedDescription)")
299
+ var minDistance = Double.infinity
300
+
301
+ for pointIndex in 0..<(plannedRoute.count - 1) {
302
+ let lineStart = plannedRoute[pointIndex]
303
+ let lineEnd = plannedRoute[pointIndex + 1]
304
+ let distance = distancePointToLineSegment(point, lineStart, lineEnd)
305
+ if distance < minDistance {
306
+ minDistance = distance
196
307
  }
197
308
  }
309
+
310
+ return minDistance
311
+ }
312
+
313
+ private func checkRouteDeviation(_ location: CLLocation) {
314
+ guard audioPlayer != nil && plannedRoute.count > 0 else { return }
315
+
316
+ let currentPoint = [location.coordinate.longitude, location.coordinate.latitude]
317
+ let offRoute = distancePointToRoute(currentPoint) > distanceThreshold
318
+
319
+ if offRoute && !isOffRoute {
320
+ audioPlayer?.play()
321
+ }
322
+
323
+ isOffRoute = offRoute
198
324
  }
199
325
 
200
326
  public func locationManager(
@@ -233,6 +359,7 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate {
233
359
  }
234
360
 
235
361
  if isLocationValid(location) {
362
+ checkRouteDeviation(location)
236
363
  return call.resolve(formatLocation(location))
237
364
  }
238
365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/background-geolocation",
3
- "version": "7.0.10",
3
+ "version": "7.0.12",
4
4
  "description": "Receive geolocation updates even while the app is in the background.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",