@maydon_tech/react-native-nitro-maps 0.1.3 → 0.2.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 (218) hide show
  1. package/LICENSE +1 -1
  2. package/NitroMap.podspec +1 -1
  3. package/README.md +82 -9
  4. package/android/CMakeLists.txt +4 -1
  5. package/android/gradle.properties +4 -4
  6. package/android/src/main/cpp/ClusterEngineJNI.cpp +198 -0
  7. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMap.kt +397 -0
  8. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapConfig.kt +53 -0
  9. package/android/src/main/{java → kotlin}/com/margelo/nitro/nitromap/NitroMapPackage.kt +4 -4
  10. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapView.kt +73 -0
  11. package/android/src/main/kotlin/com/margelo/nitro/nitromap/UserLocationManager.kt +295 -0
  12. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusterIconRenderer.kt +111 -0
  13. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusteringManager.kt +104 -0
  14. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +166 -0
  15. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerIconFactory.kt +303 -0
  16. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerSelectionHandler.kt +72 -0
  17. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/PriceMarkerRenderer.kt +159 -0
  18. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderFactory.kt +24 -0
  19. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +128 -0
  20. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapDelegate.kt +317 -0
  21. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Clustering.kt +524 -0
  22. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Markers.kt +358 -0
  23. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Overlays.kt +272 -0
  24. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+UserLocation.kt +296 -0
  25. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt +815 -0
  26. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/MarkerTagData.kt +19 -0
  27. package/ios/Clustering/ClusterIconRenderer.swift +3 -3
  28. package/ios/Location/NitroLocationManager.swift +116 -0
  29. package/ios/MarkerRenderer/MarkerIconFactory.swift +1 -3
  30. package/ios/MarkerRenderer/PriceMarkerRenderer.swift +10 -6
  31. package/ios/NitroMap.swift +279 -13
  32. package/ios/NitroMapConfig/NitroMapConfig.swift +45 -0
  33. package/ios/Providers/{GoogleMapDelegate.swift → Google/GoogleMapDelegate.swift} +48 -23
  34. package/ios/Providers/Google/GoogleMapProvider+Camera.swift +180 -0
  35. package/ios/Providers/Google/GoogleMapProvider+Clustering.swift +541 -0
  36. package/ios/Providers/Google/GoogleMapProvider+Markers.swift +270 -0
  37. package/ios/Providers/Google/GoogleMapProvider+Overlays.swift +245 -0
  38. package/ios/Providers/Google/GoogleMapProvider+UserLocation.swift +180 -0
  39. package/ios/Providers/Google/GoogleMapProvider.swift +342 -0
  40. package/ios/Providers/MapProviderFactory.swift +17 -0
  41. package/ios/Providers/MapProviderProtocol.swift +48 -1
  42. package/ios/Shared/ClusterConfig+Factory.swift +2 -2
  43. package/ios/Shared/MapStyleProvider.swift +6 -4
  44. package/ios/Shared/MarkerSelectionHandler.swift +4 -1
  45. package/ios/Utils/ColorValueExtension.swift +46 -67
  46. package/lib/module/components/ImageMarker.js +39 -29
  47. package/lib/module/components/ImageMarker.js.map +1 -1
  48. package/lib/module/components/Marker.js +118 -0
  49. package/lib/module/components/Marker.js.map +1 -0
  50. package/lib/module/components/NitroCircle.js +92 -0
  51. package/lib/module/components/NitroCircle.js.map +1 -0
  52. package/lib/module/components/NitroMap.js +216 -76
  53. package/lib/module/components/NitroMap.js.map +1 -1
  54. package/lib/module/components/NitroPolygon.js +135 -0
  55. package/lib/module/components/NitroPolygon.js.map +1 -0
  56. package/lib/module/components/NitroPolyline.js +115 -0
  57. package/lib/module/components/NitroPolyline.js.map +1 -0
  58. package/lib/module/components/PriceMarker.js +16 -29
  59. package/lib/module/components/PriceMarker.js.map +1 -1
  60. package/lib/module/context/NitroMapContext.js.map +1 -1
  61. package/lib/module/hooks/useNitroCircle.js +18 -0
  62. package/lib/module/hooks/useNitroCircle.js.map +1 -0
  63. package/lib/module/hooks/useNitroMarker.js +26 -9
  64. package/lib/module/hooks/useNitroMarker.js.map +1 -1
  65. package/lib/module/hooks/useNitroOverlay.js +59 -0
  66. package/lib/module/hooks/useNitroOverlay.js.map +1 -0
  67. package/lib/module/hooks/useNitroPolygon.js +18 -0
  68. package/lib/module/hooks/useNitroPolygon.js.map +1 -0
  69. package/lib/module/hooks/useNitroPolyline.js +18 -0
  70. package/lib/module/hooks/useNitroPolyline.js.map +1 -0
  71. package/lib/module/index.js +5 -0
  72. package/lib/module/index.js.map +1 -1
  73. package/lib/module/types/overlay.js +4 -0
  74. package/lib/module/types/overlay.js.map +1 -0
  75. package/lib/module/types/theme.js +4 -0
  76. package/lib/module/types/theme.js.map +1 -0
  77. package/lib/module/utils/colors.js +41 -13
  78. package/lib/module/utils/colors.js.map +1 -1
  79. package/lib/module/utils/validation.js +45 -0
  80. package/lib/module/utils/validation.js.map +1 -0
  81. package/lib/typescript/src/components/ImageMarker.d.ts.map +1 -1
  82. package/lib/typescript/src/components/Marker.d.ts +34 -0
  83. package/lib/typescript/src/components/Marker.d.ts.map +1 -0
  84. package/lib/typescript/src/components/NitroCircle.d.ts +70 -0
  85. package/lib/typescript/src/components/NitroCircle.d.ts.map +1 -0
  86. package/lib/typescript/src/components/NitroMap.d.ts +60 -3
  87. package/lib/typescript/src/components/NitroMap.d.ts.map +1 -1
  88. package/lib/typescript/src/components/NitroPolygon.d.ts +86 -0
  89. package/lib/typescript/src/components/NitroPolygon.d.ts.map +1 -0
  90. package/lib/typescript/src/components/NitroPolyline.d.ts +84 -0
  91. package/lib/typescript/src/components/NitroPolyline.d.ts.map +1 -0
  92. package/lib/typescript/src/components/PriceMarker.d.ts +0 -5
  93. package/lib/typescript/src/components/PriceMarker.d.ts.map +1 -1
  94. package/lib/typescript/src/context/NitroMapContext.d.ts +2 -0
  95. package/lib/typescript/src/context/NitroMapContext.d.ts.map +1 -1
  96. package/lib/typescript/src/hooks/useNitroCircle.d.ts +7 -0
  97. package/lib/typescript/src/hooks/useNitroCircle.d.ts.map +1 -0
  98. package/lib/typescript/src/hooks/useNitroMarker.d.ts +20 -0
  99. package/lib/typescript/src/hooks/useNitroMarker.d.ts.map +1 -1
  100. package/lib/typescript/src/hooks/useNitroOverlay.d.ts +26 -0
  101. package/lib/typescript/src/hooks/useNitroOverlay.d.ts.map +1 -0
  102. package/lib/typescript/src/hooks/useNitroPolygon.d.ts +7 -0
  103. package/lib/typescript/src/hooks/useNitroPolygon.d.ts.map +1 -0
  104. package/lib/typescript/src/hooks/useNitroPolyline.d.ts +7 -0
  105. package/lib/typescript/src/hooks/useNitroPolyline.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +15 -2
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/specs/NitroMap.nitro.d.ts +248 -6
  109. package/lib/typescript/src/specs/NitroMap.nitro.d.ts.map +1 -1
  110. package/lib/typescript/src/types/map.d.ts +34 -4
  111. package/lib/typescript/src/types/map.d.ts.map +1 -1
  112. package/lib/typescript/src/types/marker.d.ts +24 -36
  113. package/lib/typescript/src/types/marker.d.ts.map +1 -1
  114. package/lib/typescript/src/types/overlay.d.ts +75 -0
  115. package/lib/typescript/src/types/overlay.d.ts.map +1 -0
  116. package/lib/typescript/src/types/theme.d.ts +93 -0
  117. package/lib/typescript/src/types/theme.d.ts.map +1 -0
  118. package/lib/typescript/src/utils/colors.d.ts +6 -8
  119. package/lib/typescript/src/utils/colors.d.ts.map +1 -1
  120. package/lib/typescript/src/utils/validation.d.ts +12 -0
  121. package/lib/typescript/src/utils/validation.d.ts.map +1 -0
  122. package/nitrogen/generated/android/c++/JCircleData.hpp +94 -0
  123. package/nitrogen/generated/android/c++/JClusterConfig.hpp +5 -7
  124. package/nitrogen/generated/android/c++/JFunc_void_UserLocationChangeEvent.hpp +79 -0
  125. package/nitrogen/generated/android/c++/JFunc_void_UserTrackingMode.hpp +77 -0
  126. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  127. package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +328 -21
  128. package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +53 -2
  129. package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +3 -6
  130. package/nitrogen/generated/android/c++/JMarkerData.hpp +15 -3
  131. package/nitrogen/generated/android/c++/JPolygonData.hpp +149 -0
  132. package/nitrogen/generated/android/c++/JPolylineData.hpp +113 -0
  133. package/nitrogen/generated/android/c++/JUserLocationChangeEvent.hpp +70 -0
  134. package/nitrogen/generated/android/c++/JUserTrackingMode.hpp +62 -0
  135. package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +72 -4
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CircleData.kt +62 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +4 -4
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserLocationChangeEvent.kt +80 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserTrackingMode.kt +80 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_std__string.kt +80 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +228 -2
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +1 -2
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +12 -3
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolygonData.kt +62 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolylineData.kt +62 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/UserLocationChangeEvent.kt +47 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/{ClusterAnimationStyle.kt → UserTrackingMode.kt} +6 -8
  148. package/nitrogen/generated/android/nitromapOnLoad.cpp +6 -0
  149. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +24 -0
  150. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +175 -17
  151. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +15 -3
  152. package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +249 -16
  153. package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +90 -5
  154. package/nitrogen/generated/ios/swift/CircleData.swift +143 -0
  155. package/nitrogen/generated/ios/swift/ClusterConfig.swift +22 -15
  156. package/nitrogen/generated/ios/swift/Func_void_UserLocationChangeEvent.swift +47 -0
  157. package/nitrogen/generated/ios/swift/Func_void_UserTrackingMode.swift +47 -0
  158. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  159. package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +35 -1
  160. package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +582 -8
  161. package/nitrogen/generated/ios/swift/MarkerAnimation.swift +4 -8
  162. package/nitrogen/generated/ios/swift/MarkerData.swift +54 -2
  163. package/nitrogen/generated/ios/swift/PolygonData.swift +179 -0
  164. package/nitrogen/generated/ios/swift/PolylineData.swift +155 -0
  165. package/nitrogen/generated/ios/swift/UserLocationChangeEvent.swift +69 -0
  166. package/nitrogen/generated/ios/swift/UserTrackingMode.swift +44 -0
  167. package/nitrogen/generated/shared/c++/CircleData.hpp +113 -0
  168. package/nitrogen/generated/shared/c++/ClusterConfig.hpp +5 -8
  169. package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +53 -2
  170. package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +75 -6
  171. package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +4 -8
  172. package/nitrogen/generated/shared/c++/MarkerData.hpp +14 -2
  173. package/nitrogen/generated/shared/c++/PolygonData.hpp +114 -0
  174. package/nitrogen/generated/shared/c++/PolylineData.hpp +114 -0
  175. package/nitrogen/generated/shared/c++/UserLocationChangeEvent.hpp +88 -0
  176. package/nitrogen/generated/shared/c++/UserTrackingMode.hpp +80 -0
  177. package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +216 -12
  178. package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +23 -1
  179. package/nitrogen/generated/shared/json/NitroMapConfig.json +18 -1
  180. package/package.json +36 -5
  181. package/src/components/ImageMarker.tsx +58 -42
  182. package/src/components/Marker.tsx +161 -0
  183. package/src/components/NitroCircle.tsx +183 -0
  184. package/src/components/NitroMap.tsx +328 -78
  185. package/src/components/NitroPolygon.tsx +229 -0
  186. package/src/components/NitroPolyline.tsx +208 -0
  187. package/src/components/PriceMarker.tsx +23 -48
  188. package/src/context/NitroMapContext.tsx +4 -0
  189. package/src/hooks/useNitroCircle.ts +25 -0
  190. package/src/hooks/useNitroMarker.ts +49 -10
  191. package/src/hooks/useNitroOverlay.ts +68 -0
  192. package/src/hooks/useNitroPolygon.ts +25 -0
  193. package/src/hooks/useNitroPolyline.ts +25 -0
  194. package/src/index.tsx +23 -2
  195. package/src/specs/NitroMap.nitro.ts +294 -5
  196. package/src/types/map.ts +36 -4
  197. package/src/types/marker.ts +24 -44
  198. package/src/types/overlay.ts +77 -0
  199. package/src/types/theme.ts +101 -0
  200. package/src/utils/colors.ts +48 -16
  201. package/src/utils/validation.ts +69 -0
  202. package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +0 -108
  203. package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +0 -63
  204. package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +0 -408
  205. package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +0 -68
  206. package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +0 -176
  207. package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +0 -252
  208. package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +0 -252
  209. package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +0 -195
  210. package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +0 -912
  211. package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +0 -70
  212. package/cpp/QuadTree.hpp +0 -246
  213. package/ios/NitroMapConfig/HybridNitroMapConfig.swift +0 -33
  214. package/ios/Providers/GoogleMapProvider+Camera.swift +0 -164
  215. package/ios/Providers/GoogleMapProvider.swift +0 -924
  216. package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +0 -68
  217. package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +0 -52
  218. package/nitrogen/generated/shared/c++/ClusterAnimationStyle.hpp +0 -88
@@ -0,0 +1,295 @@
1
+ package com.margelo.nitro.nitromap
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.hardware.Sensor
7
+ import android.hardware.SensorEvent
8
+ import android.hardware.SensorEventListener
9
+ import android.hardware.SensorManager
10
+ import android.location.Location
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.util.Log
14
+ import androidx.core.content.ContextCompat
15
+ import com.google.android.gms.location.FusedLocationProviderClient
16
+ import com.google.android.gms.location.LocationCallback
17
+ import com.google.android.gms.location.LocationRequest
18
+ import com.google.android.gms.location.LocationResult
19
+ import com.google.android.gms.location.LocationServices
20
+ import com.google.android.gms.location.Priority
21
+ import kotlin.math.abs
22
+ import kotlin.math.atan2
23
+ import kotlin.math.cos
24
+ import kotlin.math.sin
25
+
26
+ /**
27
+ * Android equivalent of iOS NitroLocationManager.
28
+ *
29
+ * Wraps FusedLocationProviderClient for position updates and
30
+ * SensorManager for compass heading (Android has no CLHeading equivalent).
31
+ *
32
+ * Heading is smoothed with a low-pass filter on raw sensor data and
33
+ * circular exponential smoothing on the computed azimuth, matching
34
+ * the stability of iOS CLHeading.trueHeading.
35
+ *
36
+ * Thread safety: All callbacks fire on the main thread.
37
+ */
38
+ class UserLocationManager(private val context: Context) {
39
+
40
+ companion object {
41
+ private const val TAG = "NitroMap"
42
+ private const val UPDATE_INTERVAL_MS = 1000L
43
+ private const val FASTEST_INTERVAL_MS = 500L
44
+
45
+ /** Low-pass filter alpha for raw sensor arrays (0 = ignore new, 1 = no filter) */
46
+ private const val SENSOR_ALPHA = 0.15f
47
+
48
+ /** Exponential smoothing alpha for the final heading angle */
49
+ private const val HEADING_ALPHA = 0.25f
50
+
51
+ /**
52
+ * Minimum heading change (degrees) before firing an independent heading
53
+ * update — mirrors iOS CLLocationManager.headingFilter = 1.
54
+ */
55
+ private const val HEADING_FILTER_DEGREES = 2f
56
+
57
+ /**
58
+ * Minimum interval (ms) between independent heading-only updates.
59
+ * Prevents overwhelming the main thread when combined with 3000+ markers
60
+ * and clustering. 200ms = max 5 heading updates/second.
61
+ */
62
+ private const val MIN_HEADING_INTERVAL_MS = 200L
63
+ }
64
+
65
+ // MARK: - Callbacks
66
+
67
+ /** Called with (location, headingDegrees) — heading may be null if sensors unavailable */
68
+ var onLocationUpdate: ((Location, Float?) -> Unit)? = null
69
+
70
+ /** Called with (errorCode, errorMessage) */
71
+ var onLocationError: ((String, String) -> Unit)? = null
72
+
73
+ // MARK: - Location Provider
74
+
75
+ private var fusedClient: FusedLocationProviderClient? = null
76
+ private var locationCallback: LocationCallback? = null
77
+ private var isUpdating = false
78
+
79
+ /** Latest location — kept so heading-only updates can re-fire with position */
80
+ private var latestLocation: Location? = null
81
+
82
+ // MARK: - Heading (Compass) via Sensors
83
+
84
+ private var sensorManager: SensorManager? = null
85
+ private var accelerometerSensor: Sensor? = null
86
+ private var magneticSensor: Sensor? = null
87
+ private var sensorListener: SensorEventListener? = null
88
+
89
+ /** Low-pass filtered gravity values */
90
+ private var filteredGravity: FloatArray? = null
91
+ /** Low-pass filtered magnetic field values */
92
+ private var filteredGeomagnetic: FloatArray? = null
93
+
94
+ /** Smoothed heading (degrees, 0-360) — null until first valid reading */
95
+ private var smoothedHeading: Float? = null
96
+
97
+ /** Last heading value that was delivered to the callback */
98
+ private var lastDeliveredHeading: Float? = null
99
+
100
+ /** Timestamp of the last independent heading-only update delivery */
101
+ private var lastHeadingDeliveryTime: Long = 0L
102
+
103
+ // MARK: - Public API
104
+
105
+ fun startUpdating() {
106
+ if (isUpdating) return
107
+
108
+ // Check permission
109
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
110
+ != PackageManager.PERMISSION_GRANTED &&
111
+ ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
112
+ != PackageManager.PERMISSION_GRANTED
113
+ ) {
114
+ Log.w(TAG, "Location permission not granted")
115
+ onLocationError?.invoke("PERMISSION_DENIED", "Location permission not granted")
116
+ return
117
+ }
118
+
119
+ isUpdating = true
120
+
121
+ // Start location updates
122
+ val client = LocationServices.getFusedLocationProviderClient(context)
123
+ fusedClient = client
124
+
125
+ val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, UPDATE_INTERVAL_MS)
126
+ .setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
127
+ .build()
128
+
129
+ val callback = object : LocationCallback() {
130
+ override fun onLocationResult(result: LocationResult) {
131
+ val location = result.lastLocation ?: return
132
+ latestLocation = location
133
+ onLocationUpdate?.invoke(location, smoothedHeading)
134
+ }
135
+ }
136
+ locationCallback = callback
137
+
138
+ try {
139
+ client.requestLocationUpdates(request, callback, Looper.getMainLooper())
140
+ Log.d(TAG, "Location updates started")
141
+ } catch (e: SecurityException) {
142
+ Log.e(TAG, "Location security exception: ${e.message}")
143
+ onLocationError?.invoke("PERMISSION_DENIED", "Location permission revoked: ${e.message}")
144
+ isUpdating = false
145
+ return
146
+ }
147
+
148
+ // Start heading sensor
149
+ startHeadingSensor()
150
+ }
151
+
152
+ fun stopUpdating() {
153
+ if (!isUpdating) return
154
+ isUpdating = false
155
+
156
+ locationCallback?.let { callback ->
157
+ fusedClient?.removeLocationUpdates(callback)
158
+ }
159
+ locationCallback = null
160
+ fusedClient = null
161
+
162
+ stopHeadingSensor()
163
+ latestLocation = null
164
+ Log.d(TAG, "Location updates stopped")
165
+ }
166
+
167
+ // MARK: - Heading Sensor
168
+
169
+ private fun startHeadingSensor() {
170
+ val sm = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager ?: return
171
+ sensorManager = sm
172
+
173
+ accelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
174
+ magneticSensor = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
175
+
176
+ if (accelerometerSensor == null || magneticSensor == null) {
177
+ Log.w(TAG, "Compass sensors not available")
178
+ return
179
+ }
180
+
181
+ val listener = object : SensorEventListener {
182
+ override fun onSensorChanged(event: SensorEvent) {
183
+ when (event.sensor.type) {
184
+ Sensor.TYPE_ACCELEROMETER ->
185
+ filteredGravity = lowPassFilter(event.values, filteredGravity)
186
+ Sensor.TYPE_MAGNETIC_FIELD ->
187
+ filteredGeomagnetic = lowPassFilter(event.values, filteredGeomagnetic)
188
+ }
189
+
190
+ val g = filteredGravity ?: return
191
+ val m = filteredGeomagnetic ?: return
192
+
193
+ val R = FloatArray(9)
194
+ val I = FloatArray(9)
195
+ if (!SensorManager.getRotationMatrix(R, I, g, m)) return
196
+
197
+ val orientation = FloatArray(3)
198
+ SensorManager.getOrientation(R, orientation)
199
+
200
+ // orientation[0] = azimuth in radians
201
+ var rawHeading = Math.toDegrees(orientation[0].toDouble()).toFloat()
202
+ if (rawHeading < 0) rawHeading += 360f
203
+
204
+ // Apply circular exponential smoothing
205
+ val prev = smoothedHeading
206
+ smoothedHeading = if (prev == null) {
207
+ rawHeading
208
+ } else {
209
+ circularSmooth(prev, rawHeading, HEADING_ALPHA)
210
+ }
211
+
212
+ // Fire heading update independently (mirrors iOS didUpdateHeading)
213
+ // only when heading changed by more than the filter threshold
214
+ // AND enough time has passed (throttle to avoid flooding main thread)
215
+ val delivered = lastDeliveredHeading
216
+ val current = smoothedHeading ?: return
217
+ val now = System.currentTimeMillis()
218
+ if ((delivered == null || circularDistance(delivered, current) >= HEADING_FILTER_DEGREES)
219
+ && (now - lastHeadingDeliveryTime >= MIN_HEADING_INTERVAL_MS)
220
+ ) {
221
+ lastDeliveredHeading = current
222
+ lastHeadingDeliveryTime = now
223
+ val loc = latestLocation ?: return
224
+ onLocationUpdate?.invoke(loc, current)
225
+ }
226
+ }
227
+
228
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
229
+ }
230
+ sensorListener = listener
231
+
232
+ val mainHandler = Handler(Looper.getMainLooper())
233
+ sm.registerListener(listener, accelerometerSensor, SensorManager.SENSOR_DELAY_UI, mainHandler)
234
+ sm.registerListener(listener, magneticSensor, SensorManager.SENSOR_DELAY_UI, mainHandler)
235
+ }
236
+
237
+ private fun stopHeadingSensor() {
238
+ sensorListener?.let { listener ->
239
+ sensorManager?.unregisterListener(listener)
240
+ }
241
+ sensorListener = null
242
+ sensorManager = null
243
+ accelerometerSensor = null
244
+ magneticSensor = null
245
+ filteredGravity = null
246
+ filteredGeomagnetic = null
247
+ smoothedHeading = null
248
+ lastDeliveredHeading = null
249
+ lastHeadingDeliveryTime = 0L
250
+ }
251
+
252
+ // MARK: - Signal Processing Helpers
253
+
254
+ /**
255
+ * Low-pass filter for raw sensor arrays.
256
+ * Smooths out high-frequency jitter in accelerometer/magnetometer readings.
257
+ */
258
+ private fun lowPassFilter(input: FloatArray, output: FloatArray?): FloatArray {
259
+ if (output == null) return input.clone()
260
+ for (i in input.indices) {
261
+ output[i] = output[i] + SENSOR_ALPHA * (input[i] - output[i])
262
+ }
263
+ return output
264
+ }
265
+
266
+ /**
267
+ * Circular exponential smoothing that handles the 0°/360° wraparound correctly.
268
+ * Uses sin/cos decomposition to avoid the discontinuity at north.
269
+ */
270
+ private fun circularSmooth(prev: Float, next: Float, alpha: Float): Float {
271
+ val prevRad = Math.toRadians(prev.toDouble())
272
+ val nextRad = Math.toRadians(next.toDouble())
273
+
274
+ val sinSmooth = sin(prevRad) + alpha * (sin(nextRad) - sin(prevRad))
275
+ val cosSmooth = cos(prevRad) + alpha * (cos(nextRad) - cos(prevRad))
276
+
277
+ var result = Math.toDegrees(atan2(sinSmooth, cosSmooth)).toFloat()
278
+ if (result < 0) result += 360f
279
+ return result
280
+ }
281
+
282
+ /**
283
+ * Shortest angular distance between two headings (0-180 range).
284
+ */
285
+ private fun circularDistance(a: Float, b: Float): Float {
286
+ val diff = abs(a - b) % 360f
287
+ return if (diff > 180f) 360f - diff else diff
288
+ }
289
+
290
+ fun destroy() {
291
+ stopUpdating()
292
+ onLocationUpdate = null
293
+ onLocationError = null
294
+ }
295
+ }
@@ -0,0 +1,111 @@
1
+ package com.margelo.nitro.nitromap.clustering
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.Typeface
8
+ import android.util.LruCache
9
+ import com.margelo.nitro.nitromap.ClusterConfig
10
+ import com.margelo.nitro.nitromap.markers.PriceMarkerRenderer
11
+
12
+ /**
13
+ * Renders cluster icons as circle + count text.
14
+ * Mirrors iOS ClusterIconRenderer.swift.
15
+ *
16
+ * Uses LruCache to avoid re-rendering the same cluster icon at each zoom level.
17
+ */
18
+ object ClusterIconRenderer {
19
+
20
+ private const val MAX_CACHE_SIZE = 100
21
+ // L-2: Override entryRemoved to recycle evicted bitmaps and prevent memory leaks
22
+ private val cache = object : LruCache<String, Bitmap>(MAX_CACHE_SIZE) {
23
+ override fun entryRemoved(evicted: Boolean, key: String?, oldValue: Bitmap?, newValue: Bitmap?) {
24
+ if (evicted && oldValue != null && !oldValue.isRecycled) {
25
+ oldValue.recycle()
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Render a cluster icon bitmap.
32
+ *
33
+ * @param count Number of markers in the cluster
34
+ * @param config Optional cluster configuration for colors/border
35
+ * @param density Screen density for sizing
36
+ * @return Rendered cluster icon bitmap
37
+ */
38
+ fun renderIcon(count: Int, config: ClusterConfig?, density: Float): Bitmap {
39
+ // Check cache
40
+ val cacheKey = buildCacheKey(count, config)
41
+ cache.get(cacheKey)?.let { return it }
42
+
43
+ val diameter = clusterDiameter(count) * density
44
+ val diameterInt = diameter.toInt()
45
+
46
+ val bitmap = Bitmap.createBitmap(diameterInt, diameterInt, Bitmap.Config.ARGB_8888)
47
+ val canvas = Canvas(bitmap)
48
+
49
+ // M-4: Use shared resolveColor from PriceMarkerRenderer
50
+ val bgColor = PriceMarkerRenderer.resolveColor(config?.backgroundColor, Color.argb(255, 0, 122, 255))
51
+ val textColor = PriceMarkerRenderer.resolveColor(config?.textColor, Color.WHITE)
52
+ val borderColor = PriceMarkerRenderer.resolveColor(config?.borderColor, Color.WHITE)
53
+ val borderWidth = ((config?.borderWidth ?: 2.0) * density).toFloat()
54
+
55
+ // Draw border circle
56
+ val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
57
+ color = borderColor
58
+ style = Paint.Style.FILL
59
+ }
60
+ canvas.drawCircle(diameter / 2f, diameter / 2f, diameter / 2f, borderPaint)
61
+
62
+ // Draw background circle (inset by border width)
63
+ val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
64
+ color = bgColor
65
+ style = Paint.Style.FILL
66
+ }
67
+ canvas.drawCircle(diameter / 2f, diameter / 2f, diameter / 2f - borderWidth, bgPaint)
68
+
69
+ // Draw count text
70
+ val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
71
+ color = textColor
72
+ textSize = diameter * 0.35f
73
+ typeface = Typeface.DEFAULT_BOLD
74
+ textAlign = Paint.Align.CENTER
75
+ }
76
+ val textBounds = android.graphics.Rect()
77
+ val text = "$count"
78
+ textPaint.getTextBounds(text, 0, text.length, textBounds)
79
+ val textY = diameter / 2f + textBounds.height() / 2f
80
+ canvas.drawText(text, diameter / 2f, textY, textPaint)
81
+
82
+ cache.put(cacheKey, bitmap)
83
+ return bitmap
84
+ }
85
+
86
+ /**
87
+ * Standard cluster diameter in dp based on count.
88
+ * Matches iOS: 44pt for <10, 52pt for <100, 60pt for ≥100.
89
+ */
90
+ fun clusterDiameter(count: Int): Float {
91
+ return when {
92
+ count < 10 -> 44f
93
+ count < 100 -> 52f
94
+ else -> 60f
95
+ }
96
+ }
97
+
98
+ fun clearCache() {
99
+ cache.evictAll()
100
+ }
101
+
102
+ private fun buildCacheKey(count: Int, config: ClusterConfig?): String {
103
+ val bgHash = config?.backgroundColor?.hashCode() ?: 0
104
+ val textHash = config?.textColor?.hashCode() ?: 0
105
+ val borderHash = config?.borderColor?.hashCode() ?: 0
106
+ val borderW = config?.borderWidth ?: 2.0
107
+ return "cluster_${count}_${bgHash}_${textHash}_${borderHash}_${borderW}"
108
+ }
109
+
110
+ // M-4: resolveColor is now shared via PriceMarkerRenderer.resolveColor()
111
+ }
@@ -0,0 +1,104 @@
1
+ package com.margelo.nitro.nitromap.clustering
2
+
3
+ import android.graphics.Bitmap
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import com.margelo.nitro.nitromap.ClusterConfig
7
+ import com.margelo.nitro.nitromap.MarkerData
8
+
9
+ /**
10
+ * Clustering manager — wraps NitroClusterEngine and ClusterIconRenderer.
11
+ * Mirrors iOS ClusteringManager.swift.
12
+ *
13
+ * Provides:
14
+ * - Marker management → feeds to C++ engine
15
+ * - Configuration → radius, minSize, maxZoom
16
+ * - Clustering query → bounds-based spatial grouping
17
+ * - Debounce → timer-based to prevent rapid re-clustering
18
+ * - Icon generation → delegates to ClusterIconRenderer
19
+ */
20
+ class ClusteringManager(
21
+ clusterRadius: Double = 80.0,
22
+ minClusterSize: Int = 2,
23
+ maxZoom: Double = 20.0,
24
+ private val debounceIntervalMs: Long = 100L
25
+ ) {
26
+
27
+ private val engine = NitroClusterEngine()
28
+ private val mainHandler = Handler(Looper.getMainLooper())
29
+ private var debounceRunnable: Runnable? = null
30
+ private var density: Float = 1f
31
+
32
+ var clusterConfig: ClusterConfig? = null
33
+ set(value) {
34
+ field = value
35
+ value?.let {
36
+ engine.setClusterRadius(it.radius ?: 80.0)
37
+ engine.setMinClusterSize((it.minimumClusterSize ?: 2.0).toInt())
38
+ engine.setMaxZoom(it.maxZoom ?: 20.0)
39
+ }
40
+ }
41
+
42
+ init {
43
+ engine.setClusterRadius(clusterRadius)
44
+ engine.setMinClusterSize(minClusterSize)
45
+ engine.setMaxZoom(maxZoom)
46
+ }
47
+
48
+ fun setDensity(density: Float) {
49
+ this.density = density
50
+ }
51
+
52
+ // MARK: - Marker Management
53
+
54
+ fun addMarker(marker: MarkerData) {
55
+ engine.addMarker(
56
+ marker.id,
57
+ marker.coordinate.latitude,
58
+ marker.coordinate.longitude
59
+ )
60
+ }
61
+
62
+ fun removeMarker(id: String) {
63
+ engine.removeMarker(id)
64
+ }
65
+
66
+ fun clearMarkers() {
67
+ engine.clearMarkers()
68
+ }
69
+
70
+ // MARK: - Clustering
71
+
72
+ fun cluster(
73
+ minLat: Double, maxLat: Double,
74
+ minLon: Double, maxLon: Double,
75
+ zoom: Double,
76
+ mapWidth: Double, mapHeight: Double
77
+ ): NitroClusterEngine.ClusteringResult {
78
+ return engine.clusterWithBounds(minLat, maxLat, minLon, maxLon, zoom, mapWidth, mapHeight)
79
+ }
80
+
81
+ // MARK: - Debounce
82
+
83
+ fun debounce(action: () -> Unit) {
84
+ debounceRunnable?.let { mainHandler.removeCallbacks(it) }
85
+ val runnable = Runnable { action() }
86
+ debounceRunnable = runnable
87
+ mainHandler.postDelayed(runnable, debounceIntervalMs)
88
+ }
89
+
90
+ // MARK: - Icon Generation
91
+
92
+ fun clusterIcon(forCount: Int): Bitmap? {
93
+ return ClusterIconRenderer.renderIcon(forCount, clusterConfig, density)
94
+ }
95
+
96
+ // MARK: - Cleanup
97
+
98
+ fun destroy() {
99
+ debounceRunnable?.let { mainHandler.removeCallbacks(it) }
100
+ debounceRunnable = null
101
+ engine.destroy()
102
+ ClusterIconRenderer.clearCache()
103
+ }
104
+ }
@@ -0,0 +1,166 @@
1
+ package com.margelo.nitro.nitromap.clustering
2
+
3
+ /**
4
+ * Kotlin wrapper for C++ ClusterEngine via JNI.
5
+ * Mirrors iOS NitroClusterEngine.swift.
6
+ *
7
+ * Pure spatial grouping — no icon dimensions.
8
+ * Caller (GoogleMapProvider+Clustering) handles visual collision with real rendered sizes.
9
+ */
10
+ // H-5: Implement AutoCloseable instead of relying on unreliable finalize()
11
+ class NitroClusterEngine : AutoCloseable {
12
+
13
+ // Native pointer to C++ ClusterEngine
14
+ private var nativePtr: Long = nativeCreate()
15
+
16
+ // MARK: - JNI Data Classes
17
+
18
+ /** JNI-friendly cluster data — created in C++ JNI bridge */
19
+ class ClusterDataJNI(
20
+ val latitude: Double,
21
+ val longitude: Double,
22
+ val count: Int,
23
+ val markerIds: Array<String>,
24
+ val iconSize: Double
25
+ )
26
+
27
+ /** JNI-friendly single marker point */
28
+ class SingleMarkerJNI(
29
+ val markerId: String,
30
+ val latitude: Double,
31
+ val longitude: Double
32
+ )
33
+
34
+ /** JNI-friendly clustering result */
35
+ class ClusteringResultJNI(
36
+ val clusters: Array<ClusterDataJNI>,
37
+ val singles: Array<SingleMarkerJNI>
38
+ )
39
+
40
+ // MARK: - Public Result Types (clean Kotlin API)
41
+
42
+ data class ClusterDataResult(
43
+ val latitude: Double,
44
+ val longitude: Double,
45
+ val markerIds: List<String>,
46
+ val count: Int,
47
+ val iconSize: Double
48
+ )
49
+
50
+ data class SingleMarkerResult(
51
+ val markerId: String,
52
+ val latitude: Double,
53
+ val longitude: Double
54
+ )
55
+
56
+ data class ClusteringResult(
57
+ val clusters: List<ClusterDataResult>,
58
+ val singleMarkers: List<SingleMarkerResult>
59
+ )
60
+
61
+ // MARK: - Configuration
62
+ // M-6: All public methods guard against use-after-destroy (nativePtr == 0L)
63
+
64
+ fun setClusterRadius(radius: Double) {
65
+ if (nativePtr == 0L) return
66
+ nativeSetClusterRadius(nativePtr, radius)
67
+ }
68
+ fun setMinClusterSize(size: Int) {
69
+ if (nativePtr == 0L) return
70
+ nativeSetMinClusterSize(nativePtr, size)
71
+ }
72
+ fun setMaxZoom(zoom: Double) {
73
+ if (nativePtr == 0L) return
74
+ nativeSetMaxZoom(nativePtr, zoom)
75
+ }
76
+
77
+ // MARK: - Marker Management
78
+
79
+ fun addMarker(id: String, latitude: Double, longitude: Double) {
80
+ if (nativePtr == 0L) return
81
+ nativeAddMarker(nativePtr, id, latitude, longitude)
82
+ }
83
+
84
+ fun removeMarker(id: String) {
85
+ if (nativePtr == 0L) return
86
+ nativeRemoveMarker(nativePtr, id)
87
+ }
88
+ fun clearMarkers() {
89
+ if (nativePtr == 0L) return
90
+ nativeClearMarkers(nativePtr)
91
+ }
92
+ fun markerCount(): Int {
93
+ if (nativePtr == 0L) return 0
94
+ return nativeMarkerCount(nativePtr)
95
+ }
96
+
97
+ // MARK: - Clustering
98
+
99
+ fun clusterWithBounds(
100
+ minLat: Double, maxLat: Double,
101
+ minLon: Double, maxLon: Double,
102
+ zoom: Double,
103
+ mapWidth: Double, mapHeight: Double
104
+ ): ClusteringResult {
105
+ if (nativePtr == 0L) return ClusteringResult(emptyList(), emptyList())
106
+ val jniResults = nativeCluster(nativePtr, minLat, maxLat, minLon, maxLon, zoom, mapWidth, mapHeight)
107
+
108
+ if (jniResults.isEmpty()) {
109
+ return ClusteringResult(emptyList(), emptyList())
110
+ }
111
+
112
+ val jniResult = jniResults[0]
113
+
114
+ val clusters = jniResult.clusters.map { c ->
115
+ ClusterDataResult(
116
+ latitude = c.latitude,
117
+ longitude = c.longitude,
118
+ markerIds = c.markerIds.toList(),
119
+ count = c.count,
120
+ iconSize = c.iconSize
121
+ )
122
+ }
123
+
124
+ val singles = jniResult.singles.map { s ->
125
+ SingleMarkerResult(
126
+ markerId = s.markerId,
127
+ latitude = s.latitude,
128
+ longitude = s.longitude
129
+ )
130
+ }
131
+
132
+ return ClusteringResult(clusters, singles)
133
+ }
134
+
135
+ // MARK: - Cleanup
136
+
137
+ fun destroy() {
138
+ if (nativePtr != 0L) {
139
+ nativeDestroy(nativePtr)
140
+ nativePtr = 0
141
+ }
142
+ }
143
+
144
+ // H-5: AutoCloseable.close() — deterministic cleanup via try-with-resources or explicit close
145
+ override fun close() {
146
+ destroy()
147
+ }
148
+
149
+ // MARK: - JNI External Functions
150
+
151
+ private external fun nativeCreate(): Long
152
+ private external fun nativeDestroy(ptr: Long)
153
+ private external fun nativeSetClusterRadius(ptr: Long, radius: Double)
154
+ private external fun nativeSetMinClusterSize(ptr: Long, size: Int)
155
+ private external fun nativeSetMaxZoom(ptr: Long, zoom: Double)
156
+ private external fun nativeAddMarker(ptr: Long, id: String, lat: Double, lon: Double)
157
+ private external fun nativeRemoveMarker(ptr: Long, id: String)
158
+ private external fun nativeClearMarkers(ptr: Long)
159
+ private external fun nativeMarkerCount(ptr: Long): Int
160
+ private external fun nativeCluster(
161
+ ptr: Long,
162
+ minLat: Double, maxLat: Double,
163
+ minLon: Double, maxLon: Double,
164
+ zoom: Double, mapW: Double, mapH: Double
165
+ ): Array<ClusteringResultJNI>
166
+ }