@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.
- package/LICENSE +21 -0
- package/README.md +2759 -2
- package/android/build.gradle +106 -0
- package/android/consumer-rules.pro +42 -0
- package/android/src/main/AndroidManifest.xml +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/LocationTrackingPlugin.kt +431 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingListener.kt +52 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/LocationTrackingNative.kt +267 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeListeners.kt +270 -0
- package/android/src/main/java/com/brickssoft/locationtracking/api/NativeThread.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/BridgeServices.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/CallResponder.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +541 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginPermissionHost.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/WatchRegistry.kt +84 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +231 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +574 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +170 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/AppDispatchers.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Clock.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Constants.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Errors.kt +27 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/EventBus.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Iso8601.kt +63 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Logger.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/RecordHooks.kt +47 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +64 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqlSupport.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteGeofenceStore.kt +144 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/SqliteLocationStore.kt +215 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/Stores.kt +45 -0
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +111 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +104 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceMonitor.kt +335 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceMonitor.kt +36 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceStateMapping.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +1281 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/ElasticDistance.kt +68 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/LocationRequests.kt +74 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/MotionStateMachine.kt +291 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/StationaryRegion.kt +41 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +90 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DefaultGeofenceManager.kt +899 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/DwellTracker.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/GeofenceManager.kt +33 -0
- package/android/src/main/java/com/brickssoft/locationtracking/geofence/PolygonMath.kt +194 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +713 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +69 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarms.kt +100 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatScheduler.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatWindow.kt +179 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +213 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/BodyBuilder.kt +73 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +118 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +18 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +551 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/StrictJson.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +109 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +143 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +461 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFileProvider.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogFormatter.kt +148 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogStore.kt +30 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/GeofenceSpec.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +550 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +106 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +53 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/BackgroundRationaleDialog.kt +85 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/CurrentActivityTracker.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/DefaultPermissionManager.kt +333 -0
- package/android/src/main/java/com/brickssoft/locationtracking/permission/PermissionManager.kt +54 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +277 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionService.kt +39 -0
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultLocationProcessor.kt +93 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultOdometer.kt +99 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +166 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Geo.kt +56 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/KalmanFilter.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/processing/Processing.kt +48 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +187 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +169 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/ProviderPackaging.kt +135 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidActivityBackend.kt +25 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceBackend.kt +181 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidGeofenceReceiver.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +265 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidProviderBundle.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +98 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityBackend.kt +80 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsActivityReceiver.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsErrors.kt +72 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceBackend.kt +152 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsGeofenceReceiver.kt +78 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +176 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +62 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsReceiverSupport.kt +88 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsTasks.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityBackend.kt +71 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsActivityReceiver.kt +57 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsErrors.kt +76 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceBackend.kt +116 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsGeofenceReceiver.kt +67 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsIntents.kt +83 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +197 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +55 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsTasks.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +95 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +44 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootReceiver.kt +175 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/DefaultServiceController.kt +196 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/LocationTrackingService.kt +319 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationActionReceiver.kt +42 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/NotificationFactory.kt +338 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceCommands.kt +204 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceController.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/ServiceDeps.kt +61 -0
- package/android/src/main/java/com/brickssoft/locationtracking/service/WhileInUseStartCheck.kt +115 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DefaultDeviceSettings.kt +79 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/DeviceSettings.kt +21 -0
- package/android/src/main/java/com/brickssoft/locationtracking/settings/OemPowerManagers.kt +198 -0
- package/android/src/main/res/drawable/lt_ic_notification.xml +11 -0
- package/android/src/main/res/values/lt_strings.xml +11 -0
- package/android/src/main/res/xml/lt_file_paths.xml +5 -0
- package/dist/esm/definitions.d.ts +719 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/events.d.ts +50 -0
- package/dist/esm/events.js +52 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +10 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/web/config.d.ts +19 -0
- package/dist/esm/web/config.js +243 -0
- package/dist/esm/web/config.js.map +1 -0
- package/dist/esm/web/device.d.ts +9 -0
- package/dist/esm/web/device.js +90 -0
- package/dist/esm/web/device.js.map +1 -0
- package/dist/esm/web/errors.d.ts +14 -0
- package/dist/esm/web/errors.js +19 -0
- package/dist/esm/web/errors.js.map +1 -0
- package/dist/esm/web/geolocation.d.ts +26 -0
- package/dist/esm/web/geolocation.js +117 -0
- package/dist/esm/web/geolocation.js.map +1 -0
- package/dist/esm/web/location.d.ts +7 -0
- package/dist/esm/web/location.js +70 -0
- package/dist/esm/web/location.js.map +1 -0
- package/dist/esm/web.d.ts +164 -0
- package/dist/esm/web.js +328 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +945 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +948 -0
- package/dist/plugin.js.map +1 -0
- 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
|
+
}
|