@capgo/background-geolocation 7.0.10 → 7.0.11

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,54 @@ 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
+ // Get the sound file path
163
+ guard let soundFile = call.getString("soundFile") else {
164
+ call.reject("Sound file is required")
165
+ return
166
+ }
167
+
168
+ // Get the route array
169
+ let routeArray = call.getArray("route", JSObject.self) ?? []
170
+ var route: [[Double]] = []
171
+
172
+ for routePoint in routeArray {
173
+ if let pointArray = routePoint as? [Double], pointArray.count == 2 {
174
+ route.append(pointArray)
175
+ }
176
+ }
177
+
178
+ // Get distance threshold
179
+ let distance = call.getDouble("distance") ?? 50.0
180
+
181
+ // Set up audio player
182
+ let assetPath = "public/" + soundFile
183
+ let assetPathSplit = assetPath.components(separatedBy: ".")
184
+ guard let url = Bundle.main.url(forResource: assetPathSplit[0], withExtension: assetPathSplit[1]) else {
185
+ call.reject("Sound file not found: \(assetPath)")
186
+ return
187
+ }
188
+
189
+ do {
190
+ self.audioPlayer?.stop()
191
+ self.audioPlayer = nil
192
+ self.audioPlayer = try AVAudioPlayer(contentsOf: url)
193
+
194
+ // Store route configuration
195
+ self.plannedRoute = route
196
+ self.distanceThreshold = distance
197
+ self.isOffRoute = true
198
+
199
+ call.resolve()
200
+ } catch {
201
+ call.reject("Could not load the sound file: \(error.localizedDescription)")
202
+ }
203
+ }
204
+ }
205
+
152
206
  private func startUpdatingLocation() {
153
207
  // Avoid unnecessary calls to startUpdatingLocation, which can
154
208
  // result in extraneous invocations of didFailWithError.
@@ -173,28 +227,104 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate {
173
227
  )
174
228
  }
175
229
 
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 }
230
+ private func toRadians(_ degrees: Double) -> Double {
231
+ return degrees * Double.pi / 180.0
232
+ }
180
233
 
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
234
+ private func haversine(_ point1: [Double], _ point2: [Double]) -> Double {
235
+ let lon1 = point1[0]
236
+ let lat1 = point1[1]
237
+ let lon2 = point2[0]
238
+ let lat2 = point2[1]
239
+
240
+ let dLat = toRadians(lat2 - lat1)
241
+ let dLon = toRadians(lon2 - lon1)
242
+
243
+ let aaa = sin(dLat / 2) * sin(dLat / 2) +
244
+ cos(toRadians(lat1)) * cos(toRadians(lat2)) *
245
+ sin(dLon / 2) * sin(dLon / 2)
246
+
247
+ let ccc = 2 * atan2(sqrt(aaa), sqrt(1 - aaa))
248
+
249
+ return BackgroundGeolocation.EARTH_RADIUS_M * ccc
250
+ }
251
+
252
+ private func distancePointToLineSegment(_ point: [Double], _ lineStart: [Double], _ lineEnd: [Double]) -> Double {
253
+ // Calculate the distances between the three points using Haversine
254
+ let distAB = haversine(point, lineStart)
255
+ let distAC = haversine(point, lineEnd)
256
+ let distBC = haversine(lineStart, lineEnd)
257
+
258
+ // Handle the edge case where the line segment is a single point
259
+ if distBC == 0 {
260
+ return distAB
261
+ }
262
+
263
+ // Check if the angles at the line segment's endpoints are obtuse.
264
+ // We use the Law of Cosines (c^2 = a^2 + b^2 - 2ab*cos(C))
265
+ // If cos(C) < 0, the angle is obtuse.
266
+
267
+ // Angle at B (lineStart)
268
+ let epsilon = Double.ulpOfOne
269
+ let cosB = (pow(distAB, 2) + pow(distBC, 2) - pow(distAC, 2)) / (2 * distAB * distBC + epsilon)
270
+ if cosB < 0 {
271
+ return distAB
272
+ }
273
+
274
+ // Angle at C (lineEnd)
275
+ let cosC = (pow(distAC, 2) + pow(distBC, 2) - pow(distAB, 2)) / (2 * distAC * distBC + epsilon)
276
+ if cosC < 0 {
277
+ return distAC
278
+ }
279
+
280
+ // If both angles are acute, the closest point is on the line segment itself.
281
+ // We can calculate the distance (height of the triangle) using its area.
282
+
283
+ // 1. Calculate the semi-perimeter of the triangle ABC
284
+ let semi = (distAB + distAC + distBC) / 2
285
+
286
+ // 2. Calculate the area using Heron's formula
287
+ let area = sqrt(max(0, semi * (semi - distAB) * (semi - distAC) * (semi - distBC)))
288
+
289
+ // 3. The distance is the height of the triangle from point A to the base BC
290
+ // Area = 0.5 * base * height => height = 2 * Area / base
291
+ return (2 * area) / (distBC + epsilon)
292
+ }
293
+
294
+ private func distancePointToRoute(_ point: [Double]) -> Double {
295
+ // If the route has less than 2 points, we can't form a segment.
296
+ if plannedRoute.count < 2 {
297
+ if plannedRoute.count == 1 {
298
+ return haversine(point, plannedRoute[0])
186
299
  }
300
+ return Double.infinity // No line segments to measure against
301
+ }
187
302
 
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)")
303
+ var minDistance = Double.infinity
304
+
305
+ for pointIndex in 0..<(plannedRoute.count - 1) {
306
+ let lineStart = plannedRoute[pointIndex]
307
+ let lineEnd = plannedRoute[pointIndex + 1]
308
+ let distance = distancePointToLineSegment(point, lineStart, lineEnd)
309
+ if distance < minDistance {
310
+ minDistance = distance
196
311
  }
197
312
  }
313
+
314
+ return minDistance
315
+ }
316
+
317
+ private func checkRouteDeviation(_ location: CLLocation) {
318
+ guard audioPlayer != nil && plannedRoute.count > 0 else { return }
319
+
320
+ let currentPoint = [location.coordinate.longitude, location.coordinate.latitude]
321
+ let offRoute = distancePointToRoute(currentPoint) > distanceThreshold
322
+
323
+ if offRoute && !isOffRoute {
324
+ audioPlayer?.play()
325
+ }
326
+
327
+ isOffRoute = offRoute
198
328
  }
199
329
 
200
330
  public func locationManager(
@@ -233,6 +363,7 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate {
233
363
  }
234
364
 
235
365
  if isLocationValid(location) {
366
+ checkRouteDeviation(location)
236
367
  return call.resolve(formatLocation(location))
237
368
  }
238
369
  }
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.11",
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",