@bricks-soft/capacitor-location-tracking 0.0.0-stage → 8.0.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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2759 -2
  3. package/android/build.gradle +106 -0
  4. package/android/consumer-rules.pro +42 -0
  5. package/android/src/main/AndroidManifest.xml +52 -0
  6. package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
  7. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
  8. package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
  9. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
  10. package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
  11. package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
  12. package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
  13. package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
  14. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
  15. package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
  16. package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
  17. package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
  18. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
  19. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
  20. package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
  21. package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
  22. package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
  23. package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
  24. package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
  25. package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
  26. package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
  27. package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
  28. package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
  29. package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
  30. package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
  31. package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
  32. package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
  33. package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
  34. package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
  35. package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
  36. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
  37. package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
  38. package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
  39. package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
  40. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
  41. package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
  42. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
  43. package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
  44. package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
  45. package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
  46. package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
  47. package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
  48. package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
  49. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
  50. package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
  51. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
  52. package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
  53. package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
  54. package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
  55. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
  56. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
  57. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
  58. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
  59. package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
  60. package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
  61. package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
  62. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
  63. package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
  64. package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
  65. package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
  66. package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
  67. package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
  68. package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
  69. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
  70. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
  71. package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
  72. package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
  73. package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
  74. package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
  75. package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
  76. package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
  77. package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
  78. package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
  79. package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
  80. package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
  81. package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
  82. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
  83. package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
  84. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
  85. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
  86. package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
  87. package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
  88. package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
  89. package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
  90. package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
  91. package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
  92. package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
  93. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
  94. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
  95. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
  96. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
  97. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
  98. package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
  99. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
  100. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
  101. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
  102. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
  103. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
  104. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
  105. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
  106. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
  107. package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
  108. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
  109. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
  110. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
  111. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
  112. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
  113. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
  114. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
  115. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
  116. package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
  117. package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
  118. package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
  119. package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
  120. package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
  121. package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
  122. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
  123. package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
  124. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
  125. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
  126. package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
  127. package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
  128. package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
  129. package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
  130. package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
  131. package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
  132. package/android/src/main/res/values/lt_strings.xml +11 -0
  133. package/android/src/main/res/xml/lt_file_paths.xml +5 -0
  134. package/dist/esm/definitions.d.ts +719 -0
  135. package/dist/esm/definitions.js +2 -0
  136. package/dist/esm/definitions.js.map +1 -0
  137. package/dist/esm/events.d.ts +50 -0
  138. package/dist/esm/events.js +52 -0
  139. package/dist/esm/events.js.map +1 -0
  140. package/dist/esm/index.d.ts +3 -0
  141. package/dist/esm/index.js +5 -0
  142. package/dist/esm/index.js.map +1 -0
  143. package/dist/esm/plugin.d.ts +3 -0
  144. package/dist/esm/plugin.js +10 -0
  145. package/dist/esm/plugin.js.map +1 -0
  146. package/dist/esm/web/config.d.ts +19 -0
  147. package/dist/esm/web/config.js +243 -0
  148. package/dist/esm/web/config.js.map +1 -0
  149. package/dist/esm/web/device.d.ts +9 -0
  150. package/dist/esm/web/device.js +90 -0
  151. package/dist/esm/web/device.js.map +1 -0
  152. package/dist/esm/web/errors.d.ts +14 -0
  153. package/dist/esm/web/errors.js +19 -0
  154. package/dist/esm/web/errors.js.map +1 -0
  155. package/dist/esm/web/geolocation.d.ts +26 -0
  156. package/dist/esm/web/geolocation.js +117 -0
  157. package/dist/esm/web/geolocation.js.map +1 -0
  158. package/dist/esm/web/location.d.ts +7 -0
  159. package/dist/esm/web/location.js +70 -0
  160. package/dist/esm/web/location.js.map +1 -0
  161. package/dist/esm/web.d.ts +164 -0
  162. package/dist/esm/web.js +328 -0
  163. package/dist/esm/web.js.map +1 -0
  164. package/dist/plugin.cjs.js +945 -0
  165. package/dist/plugin.cjs.js.map +1 -0
  166. package/dist/plugin.js +948 -0
  167. package/dist/plugin.js.map +1 -0
  168. package/package.json +59 -4
@@ -0,0 +1,80 @@
1
+ package com.brickssoft.locationtracking.geofence
2
+
3
+ import com.brickssoft.locationtracking.core.Clock
4
+ import kotlinx.coroutines.CoroutineScope
5
+ import kotlinx.coroutines.CoroutineStart
6
+ import kotlinx.coroutines.Job
7
+ import kotlinx.coroutines.delay
8
+ import kotlinx.coroutines.launch
9
+
10
+ /**
11
+ * Synthesized dwell for geofences whose backend has no dwell support, and for polygons.
12
+ *
13
+ * [arm] starts a one-shot timer that fires [onDue] once the geofence has been inside for its loitering delay;
14
+ * [cancel] stops it (EXIT). Timers are coroutines on [scope], measured with [clock] (wall time, so a deadline
15
+ * survives a process restart). Because a coroutine `delay` can be stretched by deep sleep, callers also poll
16
+ * [takeDue] whenever they get a fix. Every armed timer is reported at most once, either by [onDue] or by [takeDue].
17
+ *
18
+ * Thread-safe. [onDue] runs on [scope] without any lock held; the token passed to it is the one given to [arm].
19
+ */
20
+ internal class DwellTracker(
21
+ private val scope: CoroutineScope,
22
+ private val clock: Clock,
23
+ private val onDue: suspend (id: String, token: Long) -> Unit,
24
+ ) {
25
+ private class Pending(val deadline: Long, val token: Long, val seq: Long, val job: Job)
26
+
27
+ private val pending = HashMap<String, Pending>()
28
+ private var nextSeq = 0L
29
+
30
+ /**
31
+ * Arms (or re-arms) the dwell timer of [id] to fire at [deadline] (epoch ms, [Clock.now] base). A deadline in
32
+ * the past fires as soon as [scope] runs the timer.
33
+ */
34
+ fun arm(id: String, deadline: Long, token: Long) {
35
+ val seq = synchronized(pending) { ++nextSeq }
36
+ val job = scope.launch(start = CoroutineStart.LAZY) {
37
+ val wait = deadline - clock.now()
38
+ if (wait > 0) delay(wait)
39
+ if (release(id, seq)) onDue(id, token)
40
+ }
41
+ val previous = synchronized(pending) { pending.put(id, Pending(deadline, token, seq, job)) }
42
+ previous?.job?.cancel()
43
+ job.start()
44
+ }
45
+
46
+ fun cancel(id: String) {
47
+ synchronized(pending) { pending.remove(id) }?.job?.cancel()
48
+ }
49
+
50
+ fun cancelAll() {
51
+ val all = synchronized(pending) { pending.values.toList().also { pending.clear() } }
52
+ all.forEach { it.job.cancel() }
53
+ }
54
+
55
+ fun isArmed(id: String): Boolean = synchronized(pending) { id in pending }
56
+
57
+ fun hasArmed(): Boolean = synchronized(pending) { pending.isNotEmpty() }
58
+
59
+ /** Removes every timer whose deadline is at or before [now] and returns their (id, token) pairs. */
60
+ fun takeDue(now: Long = clock.now()): List<Pair<String, Long>> {
61
+ val due = synchronized(pending) {
62
+ val entries = pending.entries.filter { it.value.deadline <= now }.map { it.key to it.value }
63
+ entries.forEach { (id, _) -> pending.remove(id) }
64
+ entries
65
+ }
66
+ due.forEach { (_, p) -> p.job.cancel() }
67
+ return due.map { (id, p) -> id to p.token }
68
+ }
69
+
70
+ /** True if timer [seq] of [id] was still armed; it is then disarmed. */
71
+ private fun release(id: String, seq: Long): Boolean = synchronized(pending) {
72
+ val current = pending[id]
73
+ if (current != null && current.seq == seq) {
74
+ pending.remove(id)
75
+ true
76
+ } else {
77
+ false
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,33 @@
1
+ package com.brickssoft.locationtracking.geofence
2
+
3
+ import com.brickssoft.locationtracking.config.TrackingMode
4
+ import com.brickssoft.locationtracking.model.GeofenceSpec
5
+ import com.brickssoft.locationtracking.model.TrackedLocation
6
+ import com.brickssoft.locationtracking.provider.GeofenceTransitionSink
7
+ import kotlinx.coroutines.flow.StateFlow
8
+
9
+ /** Geofence registry, OS registration, polygon hit-testing and dwell synthesis. */
10
+ interface GeofenceManager : GeofenceTransitionSink {
11
+ /** More than 100 in total -> TOO_MANY_GEOFENCES; emits GeofencesChange. */
12
+ suspend fun add(geofences: List<GeofenceSpec>)
13
+
14
+ suspend fun remove(ids: List<String>)
15
+
16
+ suspend fun removeAll()
17
+
18
+ suspend fun list(): List<GeofenceSpec>
19
+
20
+ suspend fun get(id: String): GeofenceSpec?
21
+
22
+ /** Registers the geofences with the OS. */
23
+ suspend fun onTrackingStarted(mode: TrackingMode)
24
+
25
+ /** Unregisters the geofences from the OS. */
26
+ suspend fun onTrackingStopped()
27
+
28
+ /** Polygon hit-test and synthesized dwell. */
29
+ fun onLocation(location: TrackedLocation)
30
+
31
+ /** True while inside any polygon's enclosing circle. */
32
+ val needsContinuousLocation: StateFlow<Boolean>
33
+ }
@@ -0,0 +1,194 @@
1
+ package com.brickssoft.locationtracking.geofence
2
+
3
+ import com.brickssoft.locationtracking.model.LatLng
4
+ import kotlin.math.abs
5
+ import kotlin.math.asin
6
+ import kotlin.math.cos
7
+ import kotlin.math.hypot
8
+ import kotlin.math.max
9
+ import kotlin.math.min
10
+ import kotlin.math.sin
11
+ import kotlin.math.sqrt
12
+ import kotlin.random.Random
13
+
14
+ /**
15
+ * Geometry for polygon geofences: ray-cast point-in-polygon, distance to the boundary and the enclosing circle
16
+ * that is registered with the OS.
17
+ *
18
+ * Coordinates are in the normal range (latitude -90..90, longitude -180..180); polygons that cross the
19
+ * anti-meridian are not supported. Planar work uses a local equirectangular projection, which is accurate for
20
+ * geofence-sized shapes (up to tens of kilometres).
21
+ */
22
+ internal object PolygonMath {
23
+ /** Mean Earth radius (m). */
24
+ const val EARTH_RADIUS_M = 6_371_008.8
25
+
26
+ /** Padding factor applied to the minimal enclosing circle. */
27
+ const val PADDING = 1.1
28
+
29
+ /** Smallest radius (m) of a padded enclosing circle. */
30
+ const val MIN_ENCLOSING_RADIUS_M = 50.0
31
+
32
+ private const val METERS_PER_RADIAN = EARTH_RADIUS_M
33
+ private const val EPSILON_M = 1e-6
34
+
35
+ /** A circle; [radius] in meters. */
36
+ data class Circle(val latitude: Double, val longitude: Double, val radius: Double)
37
+
38
+ fun isValidLatLng(latitude: Double, longitude: Double): Boolean =
39
+ latitude.isFinite() && longitude.isFinite() && latitude in -90.0..90.0 && longitude in -180.0..180.0
40
+
41
+ /** Great-circle (haversine) distance in meters. */
42
+ fun distanceMeters(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
43
+ val phi1 = Math.toRadians(lat1)
44
+ val phi2 = Math.toRadians(lat2)
45
+ val dPhi = phi2 - phi1
46
+ val dLambda = Math.toRadians(lng2 - lng1)
47
+ val a = sin(dPhi / 2) * sin(dPhi / 2) + cos(phi1) * cos(phi2) * sin(dLambda / 2) * sin(dLambda / 2)
48
+ return 2 * EARTH_RADIUS_M * asin(min(1.0, sqrt(a)))
49
+ }
50
+
51
+ /**
52
+ * Even-odd ray casting in the latitude/longitude plane. A repeated closing vertex is harmless. Points exactly
53
+ * on an edge may be classified either way; callers apply hysteresis around the boundary.
54
+ */
55
+ fun contains(vertices: List<LatLng>, latitude: Double, longitude: Double): Boolean {
56
+ var inside = false
57
+ var j = vertices.size - 1
58
+ for (i in vertices.indices) {
59
+ val yi = vertices[i].latitude
60
+ val xi = vertices[i].longitude
61
+ val yj = vertices[j].latitude
62
+ val xj = vertices[j].longitude
63
+ if ((yi > latitude) != (yj > latitude) && longitude < (xj - xi) * (latitude - yi) / (yj - yi) + xi) {
64
+ inside = !inside
65
+ }
66
+ j = i
67
+ }
68
+ return inside
69
+ }
70
+
71
+ /** Shortest distance (m) from the point to any edge of the polygon (including the closing edge). */
72
+ fun distanceToBoundary(vertices: List<LatLng>, latitude: Double, longitude: Double): Double {
73
+ if (vertices.isEmpty()) return Double.POSITIVE_INFINITY
74
+ val projection = Projection(latitude, longitude)
75
+ val points = vertices.map { projection.toPoint(it.latitude, it.longitude) }
76
+ if (points.size == 1) return hypot(points[0].x, points[0].y)
77
+ var best = Double.POSITIVE_INFINITY
78
+ var j = points.size - 1
79
+ for (i in points.indices) {
80
+ best = min(best, distanceToSegment(points[j], points[i]))
81
+ j = i
82
+ }
83
+ return best
84
+ }
85
+
86
+ /** Unsigned planar area (m²) of the polygon (shoelace formula in a local projection). */
87
+ fun areaSquareMeters(vertices: List<LatLng>): Double {
88
+ if (vertices.size < 3) return 0.0
89
+ val projection = Projection.around(vertices)
90
+ val points = vertices.map { projection.toPoint(it.latitude, it.longitude) }
91
+ var sum = 0.0
92
+ var j = points.size - 1
93
+ for (i in points.indices) {
94
+ sum += points[j].x * points[i].y - points[i].x * points[j].y
95
+ j = i
96
+ }
97
+ return abs(sum) / 2
98
+ }
99
+
100
+ /**
101
+ * Minimal enclosing circle of [vertices] (randomized incremental Welzl algorithm, deterministic seed). The
102
+ * radius is the largest great-circle distance from the centre to a vertex, so every vertex is inside.
103
+ */
104
+ fun minimalEnclosingCircle(vertices: List<LatLng>): Circle {
105
+ require(vertices.isNotEmpty()) { "no vertices" }
106
+ val projection = Projection.around(vertices)
107
+ val points = vertices.map { projection.toPoint(it.latitude, it.longitude) }.shuffled(Random(vertices.size))
108
+ var c = PlaneCircle(points[0].x, points[0].y, 0.0)
109
+ for (i in points.indices) {
110
+ if (c.contains(points[i])) continue
111
+ c = PlaneCircle(points[i].x, points[i].y, 0.0)
112
+ for (j in 0 until i) {
113
+ if (c.contains(points[j])) continue
114
+ c = PlaneCircle.diameter(points[i], points[j])
115
+ for (k in 0 until j) {
116
+ if (!c.contains(points[k])) c = PlaneCircle.through(points[i], points[j], points[k])
117
+ }
118
+ }
119
+ }
120
+ val center = projection.toLatLng(c.x, c.y)
121
+ val radius = vertices.maxOf { distanceMeters(center.latitude, center.longitude, it.latitude, it.longitude) }
122
+ return Circle(center.latitude, center.longitude, radius)
123
+ }
124
+
125
+ /** The circle registered with the OS for a polygon: the minimal enclosing circle padded by 10 %, at least 50 m. */
126
+ fun enclosingCircle(vertices: List<LatLng>): Circle {
127
+ val mec = minimalEnclosingCircle(vertices)
128
+ return mec.copy(radius = max(mec.radius * PADDING, MIN_ENCLOSING_RADIUS_M))
129
+ }
130
+
131
+ private fun distanceToSegment(a: Point, b: Point): Double {
132
+ // Distance from the origin (the query point) to segment ab.
133
+ val dx = b.x - a.x
134
+ val dy = b.y - a.y
135
+ val lengthSquared = dx * dx + dy * dy
136
+ if (lengthSquared == 0.0) return hypot(a.x, a.y)
137
+ val t = ((-a.x) * dx + (-a.y) * dy) / lengthSquared
138
+ val clamped = t.coerceIn(0.0, 1.0)
139
+ return hypot(a.x + clamped * dx, a.y + clamped * dy)
140
+ }
141
+
142
+ private data class Point(val x: Double, val y: Double)
143
+
144
+ /** Local equirectangular projection around (lat0, lng0); x east and y north, in meters. */
145
+ private class Projection(private val lat0: Double, private val lng0: Double) {
146
+ private val cosLat0 = cos(Math.toRadians(lat0)).coerceAtLeast(1e-9)
147
+
148
+ fun toPoint(latitude: Double, longitude: Double) = Point(
149
+ x = Math.toRadians(longitude - lng0) * cosLat0 * METERS_PER_RADIAN,
150
+ y = Math.toRadians(latitude - lat0) * METERS_PER_RADIAN,
151
+ )
152
+
153
+ fun toLatLng(x: Double, y: Double) = LatLng(
154
+ latitude = lat0 + Math.toDegrees(y / METERS_PER_RADIAN),
155
+ longitude = lng0 + Math.toDegrees(x / (METERS_PER_RADIAN * cosLat0)),
156
+ )
157
+
158
+ companion object {
159
+ /** Centred on the bounding box of [vertices]. */
160
+ fun around(vertices: List<LatLng>): Projection {
161
+ val lat = (vertices.minOf { it.latitude } + vertices.maxOf { it.latitude }) / 2
162
+ val lng = (vertices.minOf { it.longitude } + vertices.maxOf { it.longitude }) / 2
163
+ return Projection(lat, lng)
164
+ }
165
+ }
166
+ }
167
+
168
+ private data class PlaneCircle(val x: Double, val y: Double, val r: Double) {
169
+ fun contains(p: Point): Boolean = hypot(p.x - x, p.y - y) <= r + EPSILON_M + r * 1e-12
170
+
171
+ companion object {
172
+ fun diameter(a: Point, b: Point) =
173
+ PlaneCircle((a.x + b.x) / 2, (a.y + b.y) / 2, hypot(a.x - b.x, a.y - b.y) / 2)
174
+
175
+ /** Circumcircle of three points; for (nearly) collinear points, the circle over the farthest pair. */
176
+ fun through(a: Point, b: Point, c: Point): PlaneCircle {
177
+ val bx = b.x - a.x
178
+ val by = b.y - a.y
179
+ val cx = c.x - a.x
180
+ val cy = c.y - a.y
181
+ val d = 2 * (bx * cy - by * cx)
182
+ val scale = max(bx * bx + by * by, cx * cx + cy * cy)
183
+ if (abs(d) <= 1e-12 * max(scale, 1e-12)) {
184
+ return listOf(diameter(a, b), diameter(a, c), diameter(b, c)).maxBy { it.r }
185
+ }
186
+ val b2 = bx * bx + by * by
187
+ val c2 = cx * cx + cy * cy
188
+ val ux = (cy * b2 - by * c2) / d
189
+ val uy = (bx * c2 - cx * b2) / d
190
+ return PlaneCircle(a.x + ux, a.y + uy, hypot(ux, uy))
191
+ }
192
+ }
193
+ }
194
+ }