react-native-nitro-location-tracking 0.1.11 → 0.1.13

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 (36) hide show
  1. package/README.md +30 -8
  2. package/android/CMakeLists.txt +4 -1
  3. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/AirplaneModeMonitor.kt +72 -0
  4. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/MockLocationMonitor.kt +145 -0
  5. package/android/src/main/java/com/margelo/nitro/nitrolocationtracking/NitroLocationTracking.kt +75 -0
  6. package/cpp/HybridNitroLocationComplexLogicsCalculation.cpp +204 -0
  7. package/cpp/HybridNitroLocationComplexLogicsCalculation.hpp +29 -0
  8. package/ios/MockLocationMonitor.swift +120 -0
  9. package/ios/NitroLocationTracking.swift +29 -0
  10. package/lib/module/NitroLocationComplexLogicsCalculation.nitro.js +4 -0
  11. package/lib/module/NitroLocationComplexLogicsCalculation.nitro.js.map +1 -0
  12. package/lib/module/index.js +1 -2
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/typescript/src/NitroLocationComplexLogicsCalculation.nitro.d.ts +25 -0
  15. package/lib/typescript/src/NitroLocationComplexLogicsCalculation.nitro.d.ts.map +1 -0
  16. package/lib/typescript/src/NitroLocationTracking.nitro.d.ts +5 -0
  17. package/lib/typescript/src/NitroLocationTracking.nitro.d.ts.map +1 -1
  18. package/lib/typescript/src/index.d.ts +3 -1
  19. package/lib/typescript/src/index.d.ts.map +1 -1
  20. package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.cpp +17 -0
  21. package/nitrogen/generated/android/c++/JHybridNitroLocationTrackingSpec.hpp +4 -0
  22. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrolocationtracking/HybridNitroLocationTrackingSpec.kt +26 -0
  23. package/nitrogen/generated/android/nitrolocationtracking+autolinking.cmake +1 -0
  24. package/nitrogen/generated/ios/c++/HybridNitroLocationTrackingSpecSwift.hpp +26 -0
  25. package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec.swift +4 -0
  26. package/nitrogen/generated/ios/swift/HybridNitroLocationTrackingSpec_cxx.swift +55 -0
  27. package/nitrogen/generated/shared/c++/HybridNitroLocationComplexLogicsCalculationSpec.cpp +25 -0
  28. package/nitrogen/generated/shared/c++/HybridNitroLocationComplexLogicsCalculationSpec.hpp +72 -0
  29. package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.cpp +4 -0
  30. package/nitrogen/generated/shared/c++/HybridNitroLocationTrackingSpec.hpp +7 -0
  31. package/nitrogen/generated/shared/c++/LocationPoint.hpp +99 -0
  32. package/nitrogen/generated/shared/c++/TripMathStats.hpp +95 -0
  33. package/package.json +1 -1
  34. package/src/NitroLocationComplexLogicsCalculation.nitro.ts +37 -0
  35. package/src/NitroLocationTracking.nitro.ts +7 -0
  36. package/src/index.tsx +11 -1
package/README.md CHANGED
@@ -703,14 +703,17 @@ type PermissionStatus =
703
703
  | `stopTripCalculation()` | `TripStats` | Stop recording and get final stats |
704
704
  | `getTripStats()` | `TripStats` | Get current trip stats without stopping |
705
705
  | `resetTripCalculation()` | `void` | Reset trip calculator |
706
- | `isLocationServicesEnabled()` | `boolean` | Check if GPS/location is enabled on device |
707
- | `onProviderStatusChange(callback)` | `void` | Register GPS/network provider status callback |
708
- | `getLocationPermissionStatus()` | `PermissionStatus` | Check current location permission without prompting |
709
- | `requestLocationPermission()` | `Promise<PermissionStatus>` | Request location permission and return the resulting status |
710
- | `onPermissionStatusChange(callback)` | `void` | Register a callback that fires when location permission status changes |
711
- | `showLocalNotification(title, body)` | `void` | Show a local notification |
712
- | `updateForegroundNotification(title, body)` | `void` | Update the foreground service notification |
713
- | `destroy()` | `void` | Stop tracking and disconnect |
706
+ | `isLocationServicesEnabled()` | `boolean` | Check if GPS/location is enabled on device |
707
+ | `openLocationSettings(accuracy, interval)` | `void` | Native app dialog to enable GPS or redirect to system settings |
708
+ | `onProviderStatusChange(callback)` | `void` | Register GPS/network provider status callback |
709
+ | `isAirplaneModeEnabled()` | `boolean` | Check if Airplane mode is active on Android |
710
+ | `onAirplaneModeChange(callback)` | `void` | Register Airplane mode state-transition callback |
711
+ | `getLocationPermissionStatus()` | `PermissionStatus` | Check current location permission without prompting |
712
+ | `requestLocationPermission()` | `Promise<PermissionStatus>` | Request location permission and return the resulting status |
713
+ | `onPermissionStatusChange(callback)` | `void` | Register a callback that fires when location permission status changes |
714
+ | `showLocalNotification(title, body)` | `void` | Show a local notification |
715
+ | `updateForegroundNotification(title, body)` | `void` | Update the foreground service notification |
716
+ | `destroy()` | `void` | Stop tracking and disconnect |
714
717
 
715
718
  ### Utility Exports
716
719
 
@@ -721,6 +724,25 @@ type PermissionStatus =
721
724
  | `shortestRotation(from, to)` | Calculate shortest rotation path to avoid spinning |
722
725
  | `requestLocationPermission()` | Request location + notification permissions (Android) |
723
726
 
727
+ ### Pure C++ Math Engine
728
+
729
+ For computationally heavy tasks like array slicing and trip mapping, use the pure C++ Math Engine to bypass the JS thread entirely.
730
+
731
+ ```typescript
732
+ import { NitroLocationCalculations } from 'react-native-nitro-location-tracking';
733
+
734
+ // Instantly compute heavy math directly in C++
735
+ const stats = NitroLocationCalculations.calculateTotalTripStats(points);
736
+ ```
737
+
738
+ | Method | Returns | Description |
739
+ | :--- | :--- | :--- |
740
+ | `calculateTotalTripStats(points)` | `TripMathStats` | Instantly computes exact Haversine distance, time, and max/average speed over an array of thousands of points. |
741
+ | `filterAnomalousPoints(points, maxSpeedMps)` | `LocationPoint[]` | Cleans an array of points by mathematically stripping out teleportation jumps that exceed the given speed limit. |
742
+ | `smoothPath(points, toleranceMeters)` | `LocationPoint[]` | Simplifies a highly dense GPS path into perfect drawing lines using the Ramer-Douglas-Peucker algorithm. |
743
+ | `calculateBearing(lat1, lon1, lat2, lon2)` | `number` | Lightning fast C++ trigonometric bearing computation for raw coordinates. |
744
+ | `encodeGeohash(lat, lon, precision)` | `string` | Converts coordinates into a Geohash string instantly representing geological boundaries. |
745
+
724
746
  ## Publishing to npm
725
747
 
726
748
  ### Prerequisites
@@ -6,7 +6,10 @@ set(CMAKE_VERBOSE_MAKEFILE ON)
6
6
  set(CMAKE_CXX_STANDARD 20)
7
7
 
8
8
  # Define C++ library and add all sources
9
- add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
9
+ add_library(${PACKAGE_NAME} SHARED
10
+ src/main/cpp/cpp-adapter.cpp
11
+ ../cpp/HybridNitroLocationComplexLogicsCalculation.cpp
12
+ )
10
13
 
11
14
  # Add Nitrogen specs :)
12
15
  include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nitrolocationtracking+autolinking.cmake)
@@ -0,0 +1,72 @@
1
+ package com.margelo.nitro.nitrolocationtracking
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.os.Build
8
+ import android.provider.Settings
9
+ import android.util.Log
10
+
11
+ class AirplaneModeMonitor(private val context: Context) {
12
+
13
+ companion object {
14
+ private const val TAG = "AirplaneModeMonitor"
15
+ }
16
+
17
+ private var callback: ((Boolean) -> Unit)? = null
18
+ private var lastState: Boolean? = null
19
+ private var receiver: BroadcastReceiver? = null
20
+
21
+ fun setCallback(callback: (Boolean) -> Unit) {
22
+ this.callback = callback
23
+
24
+ // Emit current state immediately
25
+ val current = isAirplaneModeEnabled()
26
+ lastState = current
27
+ callback.invoke(current)
28
+
29
+ registerReceiver()
30
+ }
31
+
32
+ fun isAirplaneModeEnabled(): Boolean {
33
+ return Settings.Global.getInt(
34
+ context.contentResolver,
35
+ Settings.Global.AIRPLANE_MODE_ON, 0
36
+ ) != 0
37
+ }
38
+
39
+ private fun registerReceiver() {
40
+ if (receiver != null) return
41
+
42
+ receiver = object : BroadcastReceiver() {
43
+ override fun onReceive(context: Context?, intent: Intent?) {
44
+ if (intent?.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
45
+ val isAirplaneModeOn = intent.getBooleanExtra("state", false)
46
+ if (isAirplaneModeOn != lastState) {
47
+ lastState = isAirplaneModeOn
48
+ Log.d(TAG, "Airplane mode changed: $isAirplaneModeOn")
49
+ callback?.invoke(isAirplaneModeOn)
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
56
+ context.registerReceiver(receiver, filter)
57
+ Log.d(TAG, "Airplane mode receiver registered")
58
+ }
59
+
60
+ fun destroy() {
61
+ receiver?.let {
62
+ try {
63
+ context.unregisterReceiver(it)
64
+ } catch (e: Exception) {
65
+ Log.w(TAG, "Error unregistering receiver: ${e.message}")
66
+ }
67
+ }
68
+ receiver = null
69
+ callback = null
70
+ lastState = null
71
+ }
72
+ }
@@ -0,0 +1,145 @@
1
+ package com.margelo.nitro.nitrolocationtracking
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.app.AppOpsManager
5
+ import android.content.Context
6
+ import android.os.Build
7
+ import android.os.Handler
8
+ import android.os.Looper
9
+ import android.provider.Settings
10
+ import android.util.Log
11
+
12
+ /**
13
+ * Periodically polls whether a mock/fake GPS provider is active on the device.
14
+ * This works independently of location tracking — it detects the presence of
15
+ * mock location apps (e.g. Fake GPS, Mock Locations) at the system level.
16
+ *
17
+ * On each poll it checks:
18
+ * - Pre-API 23: Settings.Secure.ALLOW_MOCK_LOCATION
19
+ * - API 23+: AppOpsManager OP_MOCK_LOCATION (op code 58)
20
+ * - Also checks all installed packages for mock location permission
21
+ *
22
+ * The callback fires only when the state changes (deduplicated).
23
+ */
24
+ class MockLocationMonitor(private val context: Context) {
25
+
26
+ companion object {
27
+ private const val TAG = "MockLocationMonitor"
28
+ /** Default poll interval in milliseconds */
29
+ private const val DEFAULT_POLL_INTERVAL_MS = 3000L
30
+ }
31
+
32
+ private val mainHandler = Handler(Looper.getMainLooper())
33
+ private var callback: ((Boolean) -> Unit)? = null
34
+ private var lastState: Boolean? = null
35
+ private var polling = false
36
+ private var pollIntervalMs = DEFAULT_POLL_INTERVAL_MS
37
+
38
+ private val pollRunnable = object : Runnable {
39
+ override fun run() {
40
+ if (!polling) return
41
+ checkAndNotify()
42
+ mainHandler.postDelayed(this, pollIntervalMs)
43
+ }
44
+ }
45
+
46
+ fun setCallback(callback: (Boolean) -> Unit) {
47
+ this.callback = callback
48
+ // Emit current state immediately
49
+ val current = isMockLocationActive()
50
+ lastState = current
51
+ callback.invoke(current)
52
+ // Start polling
53
+ startPolling()
54
+ }
55
+
56
+ fun startPolling() {
57
+ if (polling) return
58
+ polling = true
59
+ mainHandler.postDelayed(pollRunnable, pollIntervalMs)
60
+ Log.d(TAG, "Mock location polling started (interval=${pollIntervalMs}ms)")
61
+ }
62
+
63
+ fun stopPolling() {
64
+ polling = false
65
+ mainHandler.removeCallbacks(pollRunnable)
66
+ Log.d(TAG, "Mock location polling stopped")
67
+ }
68
+
69
+ private fun checkAndNotify() {
70
+ val current = isMockLocationActive()
71
+ if (current != lastState) {
72
+ lastState = current
73
+ Log.d(TAG, "Mock location state changed: isMockEnabled=$current")
74
+ callback?.invoke(current)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Comprehensive check for mock location activity on the device.
80
+ */
81
+ @SuppressLint("DiscouragedPrivateApi")
82
+ fun isMockLocationActive(): Boolean {
83
+ // Pre-API 23: check the global mock location setting
84
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
85
+ @Suppress("DEPRECATION")
86
+ val mockSetting = Settings.Secure.getString(
87
+ context.contentResolver,
88
+ Settings.Secure.ALLOW_MOCK_LOCATION
89
+ )
90
+ return mockSetting == "1"
91
+ }
92
+
93
+ // API 23+: check if any app holds MOCK_LOCATION permission via AppOpsManager
94
+ try {
95
+ val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
96
+ val opMockLocation = 58 // OP_MOCK_LOCATION
97
+ val method = AppOpsManager::class.java.getMethod(
98
+ "checkOp",
99
+ Int::class.javaPrimitiveType,
100
+ Int::class.javaPrimitiveType,
101
+ String::class.java
102
+ )
103
+ val result = method.invoke(
104
+ appOps, opMockLocation, android.os.Process.myUid(), context.packageName
105
+ ) as Int
106
+ if (result == AppOpsManager.MODE_ALLOWED) return true
107
+ } catch (e: Exception) {
108
+ Log.w(TAG, "Could not check mock location app ops: ${e.message}")
109
+ }
110
+
111
+ // Additional check: scan installed packages for known mock location apps
112
+ try {
113
+ val pm = context.packageManager
114
+ val knownMockApps = listOf(
115
+ "com.lexa.fakegps",
116
+ "com.incorporateapps.fakegps.fre",
117
+ "com.fakegps.mock",
118
+ "com.lkr.fakegps",
119
+ "com.fake.gps.go.location.spoofer.free",
120
+ "com.theappninjas.gpsjoystick",
121
+ "com.evezzon.fgl",
122
+ "com.mock.location"
123
+ )
124
+ for (pkg in knownMockApps) {
125
+ try {
126
+ pm.getApplicationInfo(pkg, 0)
127
+ Log.d(TAG, "Known mock location app detected: $pkg")
128
+ return true
129
+ } catch (_: Exception) {
130
+ // Not installed
131
+ }
132
+ }
133
+ } catch (e: Exception) {
134
+ Log.w(TAG, "Could not check for mock location apps: ${e.message}")
135
+ }
136
+
137
+ return false
138
+ }
139
+
140
+ fun destroy() {
141
+ stopPolling()
142
+ callback = null
143
+ lastState = null
144
+ }
145
+ }
@@ -27,6 +27,8 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
27
27
  private var geofenceManager: GeofenceManager? = null
28
28
  private var providerStatusMonitor: ProviderStatusMonitor? = null
29
29
  private var permissionStatusMonitor: PermissionStatusMonitor? = null
30
+ private var mockLocationMonitor: MockLocationMonitor? = null
31
+ private var airplaneModeMonitor: AirplaneModeMonitor? = null
30
32
 
31
33
  private var locationCallback: ((LocationData) -> Unit)? = null
32
34
  private var motionCallback: ((Boolean) -> Unit)? = null
@@ -36,6 +38,8 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
36
38
  private var speedAlertCallback: ((SpeedAlertType, Double) -> Unit)? = null
37
39
  private var providerStatusCallback: ((LocationProviderStatus, LocationProviderStatus) -> Unit)? = null
38
40
  private var permissionStatusCallback: ((PermissionStatus) -> Unit)? = null
41
+ private var mockLocationCallback: ((Boolean) -> Unit)? = null
42
+ private var airplaneModeCallback: ((Boolean) -> Unit)? = null
39
43
 
40
44
  private var locationConfig: LocationConfig? = null
41
45
 
@@ -53,6 +57,8 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
53
57
  geofenceManager = GeofenceManager(context)
54
58
  providerStatusMonitor = ProviderStatusMonitor(context)
55
59
  permissionStatusMonitor = PermissionStatusMonitor(context)
60
+ mockLocationMonitor = MockLocationMonitor(context)
61
+ airplaneModeMonitor = AirplaneModeMonitor(context)
56
62
  locationEngine?.dbWriter = dbWriter
57
63
  connectionManager.dbWriter = dbWriter
58
64
  Log.d(TAG, "Components initialized successfully")
@@ -186,6 +192,12 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
186
192
  locationEngine?.rejectMockLocations = reject
187
193
  }
188
194
 
195
+ override fun onMockLocationDetected(callback: (isMockEnabled: Boolean) -> Unit) {
196
+ mockLocationCallback = callback
197
+ ensureInitialized()
198
+ mockLocationMonitor?.setCallback(callback)
199
+ }
200
+
189
201
  // === Geofencing ===
190
202
 
191
203
  override fun addGeofence(region: GeofenceRegion) {
@@ -355,6 +367,67 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
355
367
  }
356
368
  }
357
369
 
370
+ override fun openLocationSettings(accuracy: AccuracyLevel, intervalMs: Double) {
371
+ val context = NitroModules.applicationContext ?: return
372
+ val activity = (context as? com.facebook.react.bridge.ReactContext)?.currentActivity
373
+
374
+ if (activity != null) {
375
+ val priority = when (accuracy) {
376
+ AccuracyLevel.BALANCED -> com.google.android.gms.location.Priority.PRIORITY_BALANCED_POWER_ACCURACY
377
+ AccuracyLevel.LOW -> com.google.android.gms.location.Priority.PRIORITY_LOW_POWER
378
+ else -> com.google.android.gms.location.Priority.PRIORITY_HIGH_ACCURACY
379
+ }
380
+ val interval = intervalMs.toLong()
381
+
382
+ val locationRequest = com.google.android.gms.location.LocationRequest.Builder(
383
+ priority, interval
384
+ ).build()
385
+ val builder = com.google.android.gms.location.LocationSettingsRequest.Builder()
386
+ .addLocationRequest(locationRequest)
387
+ .setAlwaysShow(true)
388
+
389
+ val client = com.google.android.gms.location.LocationServices.getSettingsClient(context)
390
+ val task = client.checkLocationSettings(builder.build())
391
+
392
+ task.addOnFailureListener { exception ->
393
+ if (exception is com.google.android.gms.common.api.ResolvableApiException) {
394
+ try {
395
+ exception.startResolutionForResult(activity, 9002) // arbitrary request code
396
+ } catch (e: Exception) {
397
+ openLocationSettingsFallback(context)
398
+ }
399
+ } else {
400
+ openLocationSettingsFallback(context)
401
+ }
402
+ }
403
+ } else {
404
+ openLocationSettingsFallback(context)
405
+ }
406
+ }
407
+
408
+ private fun openLocationSettingsFallback(context: android.content.Context) {
409
+ val intent = android.content.Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)
410
+ intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
411
+ try {
412
+ context.startActivity(intent)
413
+ } catch (e: Exception) {
414
+ Log.e(TAG, "Failed to open location settings: ${e.message}")
415
+ }
416
+ }
417
+
418
+ // === Device State Monitoring ===
419
+
420
+ override fun isAirplaneModeEnabled(): Boolean {
421
+ ensureInitialized()
422
+ return airplaneModeMonitor?.isAirplaneModeEnabled() ?: false
423
+ }
424
+
425
+ override fun onAirplaneModeChange(callback: (isEnabled: Boolean) -> Unit) {
426
+ airplaneModeCallback = callback
427
+ ensureInitialized()
428
+ airplaneModeMonitor?.setCallback(callback)
429
+ }
430
+
358
431
  // === Distance Utilities ===
359
432
 
360
433
  override fun getDistanceBetween(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
@@ -389,5 +462,7 @@ class NitroLocationTracking : HybridNitroLocationTrackingSpec() {
389
462
  geofenceManager?.destroy()
390
463
  providerStatusMonitor?.destroy()
391
464
  permissionStatusMonitor?.destroy()
465
+ mockLocationMonitor?.destroy()
466
+ airplaneModeMonitor?.destroy()
392
467
  }
393
468
  }
@@ -0,0 +1,204 @@
1
+ #include "HybridNitroLocationComplexLogicsCalculation.hpp"
2
+ #include <cmath>
3
+ #include <algorithm>
4
+
5
+ namespace margelo::nitro::nitrolocationtracking {
6
+
7
+ // Mathematical constants
8
+ constexpr double PI = 3.14159265358979323846;
9
+ constexpr double EARTH_RADIUS_M = 6371000.0;
10
+
11
+ double HybridNitroLocationComplexLogicsCalculation::haversineDistance(double lat1, double lon1, double lat2, double lon2) {
12
+ double p = PI / 180.0;
13
+ double a = 0.5 - std::cos((lat2 - lat1) * p)/2.0 +
14
+ std::cos(lat1 * p) * std::cos(lat2 * p) *
15
+ (1.0 - std::cos((lon2 - lon1) * p))/2.0;
16
+
17
+ return 2.0 * EARTH_RADIUS_M * std::asin(std::sqrt(a));
18
+ }
19
+
20
+ TripMathStats HybridNitroLocationComplexLogicsCalculation::calculateTotalTripStats(const std::vector<LocationPoint>& points) {
21
+ TripMathStats stats = {0.0, 0.0, 0.0, 0.0};
22
+
23
+ if (points.size() < 2) return stats;
24
+
25
+ double totalDist = 0.0;
26
+ double maxSpeed = 0.0;
27
+ double totalTimeMs = points.back().timestamp - points.front().timestamp;
28
+
29
+ for (size_t i = 1; i < points.size(); ++i) {
30
+ const auto& p1 = points[i-1];
31
+ const auto& p2 = points[i];
32
+
33
+ double dist = haversineDistance(p1.latitude, p1.longitude, p2.latitude, p2.longitude);
34
+ totalDist += dist;
35
+
36
+ if (p2.speed > maxSpeed) {
37
+ maxSpeed = p2.speed;
38
+ }
39
+ }
40
+
41
+ stats.totalDistanceMeters = totalDist;
42
+ stats.elapsedTimeMs = totalTimeMs;
43
+
44
+ // Convert max speed m/s to km/h if it comes in m/s, assume it's m/s
45
+ stats.maxSpeedKmh = maxSpeed * 3.6;
46
+
47
+ if (totalTimeMs > 0) {
48
+ double avgSpeedMps = totalDist / (totalTimeMs / 1000.0);
49
+ stats.averageSpeedKmh = avgSpeedMps * 3.6;
50
+ }
51
+
52
+ return stats;
53
+ }
54
+
55
+ std::vector<LocationPoint> HybridNitroLocationComplexLogicsCalculation::filterAnomalousPoints(const std::vector<LocationPoint>& points, double maxSpeedLimitMs) {
56
+ std::vector<LocationPoint> filtered;
57
+ if (points.empty()) return filtered;
58
+
59
+ filtered.push_back(points.front());
60
+
61
+ for (size_t i = 1; i < points.size(); ++i) {
62
+ const auto& curr = points[i];
63
+ const auto& last = filtered.back();
64
+
65
+ double dist = haversineDistance(last.latitude, last.longitude, curr.latitude, curr.longitude);
66
+ double timeDiffSec = (curr.timestamp - last.timestamp) / 1000.0;
67
+
68
+ if (timeDiffSec <= 0) continue; // Invalid time
69
+
70
+ double calcSpeed = dist / timeDiffSec;
71
+
72
+ // If the calculated speed is ridiculously high (teleportation), ignore point
73
+ if (calcSpeed <= maxSpeedLimitMs) {
74
+ filtered.push_back(curr);
75
+ }
76
+ }
77
+
78
+ return filtered;
79
+ }
80
+
81
+ double HybridNitroLocationComplexLogicsCalculation::perpendicularDistance(const LocationPoint& pt, const LocationPoint& lineStart, const LocationPoint& lineEnd) {
82
+ double dx = lineEnd.longitude - lineStart.longitude;
83
+ double dy = lineEnd.latitude - lineStart.latitude;
84
+
85
+ double mag = std::sqrt(dx*dx + dy*dy);
86
+ if (mag > 0.0) {
87
+ dx /= mag;
88
+ dy /= mag;
89
+ }
90
+
91
+ double pvx = pt.longitude - lineStart.longitude;
92
+ double pvy = pt.latitude - lineStart.latitude;
93
+
94
+ double pvdot = dx * pvx + dy * pvy;
95
+
96
+ double ax = pvx - pvdot * dx;
97
+ double ay = pvy - pvdot * dy;
98
+
99
+ // Calculate distance in coordinates and roughly convert to meters using Haversine
100
+ double projLat = pt.latitude - ay;
101
+ double projLon = pt.longitude - ax;
102
+ return haversineDistance(pt.latitude, pt.longitude, projLat, projLon);
103
+ }
104
+
105
+ void HybridNitroLocationComplexLogicsCalculation::douglasPeucker(const std::vector<LocationPoint>& points, double tolerance, int firstIdx, int lastIdx, std::vector<int>& keepIndexes) {
106
+ if (lastIdx <= firstIdx + 1) return;
107
+
108
+ double maxDist = 0.0;
109
+ int index = firstIdx;
110
+
111
+ for (int i = firstIdx + 1; i < lastIdx; ++i) {
112
+ double dist = perpendicularDistance(points[i], points[firstIdx], points[lastIdx]);
113
+ if (dist > maxDist) {
114
+ index = i;
115
+ maxDist = dist;
116
+ }
117
+ }
118
+
119
+ if (maxDist > tolerance) {
120
+ keepIndexes.push_back(index);
121
+ douglasPeucker(points, tolerance, firstIdx, index, keepIndexes);
122
+ douglasPeucker(points, tolerance, index, lastIdx, keepIndexes);
123
+ }
124
+ }
125
+
126
+ std::vector<LocationPoint> HybridNitroLocationComplexLogicsCalculation::smoothPath(const std::vector<LocationPoint>& points, double toleranceMeters) {
127
+ if (points.size() < 3) return points;
128
+
129
+ std::vector<int> keepIndexes;
130
+ keepIndexes.push_back(0);
131
+ keepIndexes.push_back(points.size() - 1);
132
+
133
+ douglasPeucker(points, toleranceMeters, 0, points.size() - 1, keepIndexes);
134
+
135
+ std::sort(keepIndexes.begin(), keepIndexes.end());
136
+
137
+ std::vector<LocationPoint> smoothed;
138
+ for (int idx : keepIndexes) {
139
+ smoothed.push_back(points[idx]);
140
+ }
141
+
142
+ return smoothed;
143
+ }
144
+
145
+ double HybridNitroLocationComplexLogicsCalculation::calculateBearing(double lat1, double lon1, double lat2, double lon2) {
146
+ double p = PI / 180.0;
147
+ double dLon = (lon2 - lon1) * p;
148
+ double rLat1 = lat1 * p;
149
+ double rLat2 = lat2 * p;
150
+
151
+ double y = std::sin(dLon) * std::cos(rLat2);
152
+ double x = std::cos(rLat1) * std::sin(rLat2) -
153
+ std::sin(rLat1) * std::cos(rLat2) * std::cos(dLon);
154
+
155
+ double bearing = std::atan2(y, x) * (180.0 / PI);
156
+ return std::fmod((bearing + 360.0), 360.0);
157
+ }
158
+
159
+ std::string HybridNitroLocationComplexLogicsCalculation::encodeGeohash(double latitude, double longitude, double precisionRaw) {
160
+ static const char BASE32[] = "0123456789bcdefghjkmnpqrstuvwxyz";
161
+
162
+ int precision = static_cast<int>(precisionRaw);
163
+ if (precision < 1) precision = 1;
164
+ if (precision > 12) precision = 12;
165
+
166
+ bool is_even = true;
167
+ double lat[2] = {-90.0, 90.0};
168
+ double lon[2] = {-180.0, 180.0};
169
+ int bit = 0;
170
+ int ch = 0;
171
+ std::string geohash = "";
172
+
173
+ while (geohash.length() < precision) {
174
+ if (is_even) {
175
+ double mid = (lon[0] + lon[1]) / 2;
176
+ if (longitude > mid) {
177
+ ch |= (1 << (4 - bit));
178
+ lon[0] = mid;
179
+ } else {
180
+ lon[1] = mid;
181
+ }
182
+ } else {
183
+ double mid = (lat[0] + lat[1]) / 2;
184
+ if (latitude > mid) {
185
+ ch |= (1 << (4 - bit));
186
+ lat[0] = mid;
187
+ } else {
188
+ lat[1] = mid;
189
+ }
190
+ }
191
+
192
+ is_even = !is_even;
193
+ if (bit < 4) {
194
+ bit++;
195
+ } else {
196
+ geohash += BASE32[ch];
197
+ bit = 0;
198
+ ch = 0;
199
+ }
200
+ }
201
+ return geohash;
202
+ }
203
+
204
+ } // namespace margelo::nitro::nitrolocationtracking
@@ -0,0 +1,29 @@
1
+ #pragma once
2
+
3
+ #include "HybridNitroLocationComplexLogicsCalculationSpec.hpp"
4
+ #include <vector>
5
+
6
+ namespace margelo::nitro::nitrolocationtracking {
7
+
8
+ class HybridNitroLocationComplexLogicsCalculation : public HybridNitroLocationComplexLogicsCalculationSpec {
9
+ public:
10
+ HybridNitroLocationComplexLogicsCalculation() : HybridObject(TAG) {}
11
+
12
+ // Methods
13
+ TripMathStats calculateTotalTripStats(const std::vector<LocationPoint>& points) override;
14
+
15
+ std::vector<LocationPoint> filterAnomalousPoints(const std::vector<LocationPoint>& points, double maxSpeedLimitMs) override;
16
+
17
+ std::vector<LocationPoint> smoothPath(const std::vector<LocationPoint>& points, double toleranceMeters) override;
18
+
19
+ double calculateBearing(double lat1, double lon1, double lat2, double lon2) override;
20
+
21
+ std::string encodeGeohash(double latitude, double longitude, double precision) override;
22
+
23
+ private:
24
+ double haversineDistance(double lat1, double lon1, double lat2, double lon2);
25
+ double perpendicularDistance(const LocationPoint& pt, const LocationPoint& lineStart, const LocationPoint& lineEnd);
26
+ void douglasPeucker(const std::vector<LocationPoint>& points, double tolerance, int firstIdx, int lastIdx, std::vector<int>& keepIndexes);
27
+ };
28
+
29
+ } // namespace margelo::nitro::nitrolocationtracking