@maydon_tech/react-native-nitro-maps 0.1.4 → 0.2.1

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 (222) 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/build.gradle +6 -2
  6. package/android/gradle.properties +4 -4
  7. package/android/src/main/cpp/ClusterEngineJNI.cpp +198 -0
  8. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMap.kt +397 -0
  9. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapConfig.kt +53 -0
  10. package/android/src/main/{java → kotlin}/com/margelo/nitro/nitromap/NitroMapPackage.kt +4 -4
  11. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapView.kt +73 -0
  12. package/android/src/main/kotlin/com/margelo/nitro/nitromap/UserLocationManager.kt +295 -0
  13. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusterIconRenderer.kt +111 -0
  14. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusteringManager.kt +104 -0
  15. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +166 -0
  16. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerIconFactory.kt +303 -0
  17. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerSelectionHandler.kt +72 -0
  18. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/PriceMarkerRenderer.kt +159 -0
  19. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderFactory.kt +24 -0
  20. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +128 -0
  21. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapDelegate.kt +317 -0
  22. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Clustering.kt +524 -0
  23. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Markers.kt +358 -0
  24. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Overlays.kt +272 -0
  25. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+UserLocation.kt +296 -0
  26. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt +815 -0
  27. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/MarkerTagData.kt +19 -0
  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 +137 -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 +84 -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++/JCoordinateRing.hpp +77 -0
  125. package/nitrogen/generated/android/c++/JFunc_void_UserLocationChangeEvent.hpp +79 -0
  126. package/nitrogen/generated/android/c++/JFunc_void_UserTrackingMode.hpp +77 -0
  127. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  128. package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +332 -21
  129. package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +53 -2
  130. package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +3 -6
  131. package/nitrogen/generated/android/c++/JMarkerData.hpp +15 -3
  132. package/nitrogen/generated/android/c++/JPolygonData.hpp +133 -0
  133. package/nitrogen/generated/android/c++/JPolylineData.hpp +113 -0
  134. package/nitrogen/generated/android/c++/JUserLocationChangeEvent.hpp +70 -0
  135. package/nitrogen/generated/android/c++/JUserTrackingMode.hpp +62 -0
  136. package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +72 -4
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CircleData.kt +62 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +4 -4
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CoordinateRing.kt +38 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserLocationChangeEvent.kt +80 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserTrackingMode.kt +80 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_std__string.kt +80 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +228 -2
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +1 -2
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +12 -3
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolygonData.kt +62 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolylineData.kt +62 -0
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/UserLocationChangeEvent.kt +47 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/{ClusterAnimationStyle.kt → UserTrackingMode.kt} +6 -8
  150. package/nitrogen/generated/android/nitromapOnLoad.cpp +6 -0
  151. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +24 -0
  152. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +178 -17
  153. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +18 -3
  154. package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +252 -16
  155. package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +90 -5
  156. package/nitrogen/generated/ios/swift/CircleData.swift +143 -0
  157. package/nitrogen/generated/ios/swift/ClusterConfig.swift +22 -15
  158. package/nitrogen/generated/ios/swift/CoordinateRing.swift +48 -0
  159. package/nitrogen/generated/ios/swift/Func_void_UserLocationChangeEvent.swift +47 -0
  160. package/nitrogen/generated/ios/swift/Func_void_UserTrackingMode.swift +47 -0
  161. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  162. package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +35 -1
  163. package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +582 -8
  164. package/nitrogen/generated/ios/swift/MarkerAnimation.swift +4 -8
  165. package/nitrogen/generated/ios/swift/MarkerData.swift +54 -2
  166. package/nitrogen/generated/ios/swift/PolygonData.swift +167 -0
  167. package/nitrogen/generated/ios/swift/PolylineData.swift +155 -0
  168. package/nitrogen/generated/ios/swift/UserLocationChangeEvent.swift +69 -0
  169. package/nitrogen/generated/ios/swift/UserTrackingMode.swift +44 -0
  170. package/nitrogen/generated/shared/c++/CircleData.hpp +113 -0
  171. package/nitrogen/generated/shared/c++/ClusterConfig.hpp +5 -8
  172. package/nitrogen/generated/shared/c++/CoordinateRing.hpp +77 -0
  173. package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +53 -2
  174. package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +75 -6
  175. package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +4 -8
  176. package/nitrogen/generated/shared/c++/MarkerData.hpp +14 -2
  177. package/nitrogen/generated/shared/c++/PolygonData.hpp +117 -0
  178. package/nitrogen/generated/shared/c++/PolylineData.hpp +114 -0
  179. package/nitrogen/generated/shared/c++/UserLocationChangeEvent.hpp +88 -0
  180. package/nitrogen/generated/shared/c++/UserTrackingMode.hpp +80 -0
  181. package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +216 -12
  182. package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +23 -1
  183. package/nitrogen/generated/shared/json/NitroMapConfig.json +18 -1
  184. package/package.json +36 -5
  185. package/src/components/ImageMarker.tsx +58 -42
  186. package/src/components/Marker.tsx +161 -0
  187. package/src/components/NitroCircle.tsx +183 -0
  188. package/src/components/NitroMap.tsx +328 -78
  189. package/src/components/NitroPolygon.tsx +229 -0
  190. package/src/components/NitroPolyline.tsx +208 -0
  191. package/src/components/PriceMarker.tsx +23 -48
  192. package/src/context/NitroMapContext.tsx +4 -0
  193. package/src/hooks/useNitroCircle.ts +25 -0
  194. package/src/hooks/useNitroMarker.ts +49 -10
  195. package/src/hooks/useNitroOverlay.ts +68 -0
  196. package/src/hooks/useNitroPolygon.ts +25 -0
  197. package/src/hooks/useNitroPolyline.ts +25 -0
  198. package/src/index.tsx +28 -2
  199. package/src/specs/NitroMap.nitro.ts +294 -5
  200. package/src/types/map.ts +36 -4
  201. package/src/types/marker.ts +24 -44
  202. package/src/types/overlay.ts +87 -0
  203. package/src/types/theme.ts +101 -0
  204. package/src/utils/colors.ts +48 -16
  205. package/src/utils/validation.ts +69 -0
  206. package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +0 -108
  207. package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +0 -63
  208. package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +0 -408
  209. package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +0 -68
  210. package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +0 -176
  211. package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +0 -252
  212. package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +0 -252
  213. package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +0 -195
  214. package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +0 -912
  215. package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +0 -70
  216. package/cpp/QuadTree.hpp +0 -246
  217. package/ios/NitroMapConfig/HybridNitroMapConfig.swift +0 -33
  218. package/ios/Providers/GoogleMapProvider+Camera.swift +0 -164
  219. package/ios/Providers/GoogleMapProvider.swift +0 -924
  220. package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +0 -68
  221. package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +0 -52
  222. package/nitrogen/generated/shared/c++/ClusterAnimationStyle.hpp +0 -88
@@ -0,0 +1,296 @@
1
+ package com.margelo.nitro.nitromap.providers.google
2
+
3
+ import android.animation.ValueAnimator
4
+ import com.margelo.nitro.nitromap.*
5
+ import android.content.Context
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.Canvas
9
+ import android.graphics.Paint
10
+ import android.graphics.RectF
11
+ import android.location.Location
12
+ import android.util.Log
13
+ import android.view.animation.LinearInterpolator
14
+ import com.google.android.gms.maps.CameraUpdateFactory
15
+ import com.google.android.gms.maps.model.BitmapDescriptorFactory
16
+ import com.google.android.gms.maps.model.LatLng
17
+ import com.google.android.gms.maps.model.Marker
18
+ import com.google.android.gms.maps.model.MarkerOptions
19
+ import kotlinx.coroutines.launch
20
+ import kotlinx.coroutines.withContext
21
+ import kotlinx.coroutines.Dispatchers
22
+ import java.net.HttpURLConnection
23
+ import java.net.URL
24
+ import kotlin.math.abs
25
+
26
+ /**
27
+ * User location management for GoogleMapProvider.
28
+ * Mirrors iOS GoogleMapProvider+UserLocation.swift.
29
+ *
30
+ * Handles:
31
+ * - Custom user location marker (URL or local asset image)
32
+ * - Tracking modes (none, follow, followWithHeading)
33
+ * - Camera following
34
+ * - centerOnUserLocation() method
35
+ */
36
+
37
+ // MARK: - User Location State (stored in GoogleMapProvider)
38
+ // These fields are already declared in GoogleMapProvider:
39
+ // - var lastKnownUserLocation: Location? (add to GoogleMapProvider)
40
+ // - var customUserLocationMarker: Marker? (add to GoogleMapProvider)
41
+
42
+ // MARK: - Location Update Handler
43
+
44
+ /**
45
+ * Called when the UserLocationManager fires a location/heading update.
46
+ * Updates custom marker position + rotation and handles camera following.
47
+ */
48
+ /** Animation duration matching iOS CATransaction (0.3s) */
49
+ private const val LOCATION_ANIMATION_MS = 300L
50
+
51
+ internal fun GoogleMapProvider.updateUserLocationInternal(location: Location, heading: Float?) {
52
+ lastKnownUserLocation = location
53
+ if (heading != null && heading >= 0) lastKnownHeading = heading
54
+
55
+ val marker = customUserLocationMarker
56
+ val shouldFollow = userTrackingMode != null && userTrackingMode != UserTrackingMode.NONE
57
+ val targetLatLng = LatLng(location.latitude, location.longitude)
58
+
59
+ val targetRotation: Float? = when {
60
+ heading != null && heading >= 0 -> heading
61
+ location.hasBearing() -> location.bearing
62
+ else -> null
63
+ }
64
+
65
+ withMap { map ->
66
+ if (marker != null) {
67
+ animateMarkerPosition(marker, targetLatLng)
68
+ targetRotation?.let { animateMarkerRotation(marker, it) }
69
+ }
70
+
71
+ // Camera following — mark as programmatic so onCameraIdle skips clustering
72
+ if (shouldFollow) {
73
+ isFollowingUserCamera = true
74
+ when (userTrackingMode) {
75
+ UserTrackingMode.FOLLOW -> {
76
+ map.animateCamera(
77
+ CameraUpdateFactory.newLatLng(targetLatLng),
78
+ LOCATION_ANIMATION_MS.toInt(),
79
+ null
80
+ )
81
+ }
82
+ UserTrackingMode.FOLLOWWITHHEADING -> {
83
+ val bearing = targetRotation ?: map.cameraPosition.bearing
84
+ val cameraPos = com.google.android.gms.maps.model.CameraPosition.Builder()
85
+ .target(targetLatLng)
86
+ .zoom(map.cameraPosition.zoom)
87
+ .bearing(bearing)
88
+ .tilt(map.cameraPosition.tilt)
89
+ .build()
90
+ map.animateCamera(
91
+ CameraUpdateFactory.newCameraPosition(cameraPos),
92
+ LOCATION_ANIMATION_MS.toInt(),
93
+ null
94
+ )
95
+ }
96
+ else -> {}
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Smoothly animate marker position from current to target.
104
+ * Cancels any in-flight position animation to avoid conflicts.
105
+ */
106
+ private fun GoogleMapProvider.animateMarkerPosition(marker: Marker, target: LatLng) {
107
+ userLocationPositionAnimator?.cancel()
108
+
109
+ val startLat = marker.position.latitude
110
+ val startLng = marker.position.longitude
111
+
112
+ val animator = ValueAnimator.ofFloat(0f, 1f).apply {
113
+ duration = LOCATION_ANIMATION_MS
114
+ interpolator = LinearInterpolator()
115
+ addUpdateListener { animation ->
116
+ val t = animation.animatedFraction.toDouble()
117
+ val lat = startLat + (target.latitude - startLat) * t
118
+ val lng = startLng + (target.longitude - startLng) * t
119
+ marker.position = LatLng(lat, lng)
120
+ }
121
+ }
122
+ userLocationPositionAnimator = animator
123
+ animator.start()
124
+ }
125
+
126
+ /**
127
+ * Smoothly animate marker rotation from current to target.
128
+ * Uses shortest-path rotation (handles 0°/360° wraparound).
129
+ */
130
+ private fun GoogleMapProvider.animateMarkerRotation(marker: Marker, target: Float) {
131
+ userLocationRotationAnimator?.cancel()
132
+
133
+ val start = marker.rotation
134
+ // Shortest angular path
135
+ var delta = (target - start) % 360f
136
+ if (delta > 180f) delta -= 360f
137
+ if (delta < -180f) delta += 360f
138
+ val end = start + delta
139
+
140
+ val animator = ValueAnimator.ofFloat(start, end).apply {
141
+ duration = LOCATION_ANIMATION_MS
142
+ interpolator = LinearInterpolator()
143
+ addUpdateListener { animation ->
144
+ var rot = animation.animatedValue as Float
145
+ rot = ((rot % 360f) + 360f) % 360f
146
+ marker.rotation = rot
147
+ }
148
+ }
149
+ userLocationRotationAnimator = animator
150
+ animator.start()
151
+ }
152
+
153
+ // MARK: - Custom User Location Marker
154
+
155
+ /**
156
+ * Sets up a custom user location marker with the provided image.
157
+ * Called when userLocationImage or userLocationSize changes.
158
+ */
159
+ internal fun GoogleMapProvider.setupCustomUserLocationMarkerInternal(context: Context) {
160
+ // Remove old custom marker
161
+ customUserLocationMarker?.remove()
162
+ customUserLocationMarker = null
163
+
164
+ if (userLocationImage.isEmpty()) {
165
+ // No custom image — re-enable default blue dot
166
+ cachedUserLocationImageUrl = ""
167
+ // H-1: Recycle old bitmap before clearing reference
168
+ userLocationImageBitmap?.recycle()
169
+ userLocationImageBitmap = null
170
+ withMap { map ->
171
+ try {
172
+ map.isMyLocationEnabled = showsUserLocation ?: false
173
+ } catch (_: SecurityException) {}
174
+ }
175
+ return
176
+ }
177
+
178
+ // Disable native blue dot — custom marker replaces it
179
+ withMap { map ->
180
+ try {
181
+ map.isMyLocationEnabled = false
182
+ } catch (_: SecurityException) {}
183
+ }
184
+
185
+ // Create custom marker
186
+ withMap { map ->
187
+ val opts = MarkerOptions()
188
+ .position(
189
+ lastKnownUserLocation?.let {
190
+ LatLng(it.latitude, it.longitude)
191
+ } ?: LatLng(0.0, 0.0)
192
+ )
193
+ .anchor(
194
+ userLocationAnchor?.x?.toFloat() ?: 0.5f,
195
+ userLocationAnchor?.y?.toFloat() ?: 0.5f
196
+ )
197
+ .flat(true)
198
+ .zIndex(Float.MAX_VALUE)
199
+
200
+ // Reuse cached image if URL is the same (only size changed)
201
+ val cachedBitmap = userLocationImageBitmap
202
+ if (cachedBitmap != null && cachedUserLocationImageUrl == userLocationImage) {
203
+ val size = userLocationSize.toInt().coerceAtLeast(1)
204
+ val density = context.resources.displayMetrics.density
205
+ val sizePx = (size * density).toInt()
206
+ val resized = Bitmap.createScaledBitmap(cachedBitmap, sizePx, sizePx, true)
207
+ opts.icon(BitmapDescriptorFactory.fromBitmap(resized))
208
+ }
209
+
210
+ val marker = map.addMarker(opts)
211
+ if (marker != null) {
212
+ marker.tag = MarkerTagData.UserLocation
213
+ // Restore last known heading so rotation isn't lost on marker recreation
214
+ lastKnownHeading?.let { marker.rotation = it }
215
+ customUserLocationMarker = marker
216
+ }
217
+
218
+ // Skip download if same URL and we already have cached data
219
+ if (cachedUserLocationImageUrl == userLocationImage && userLocationImageBitmap != null) {
220
+ return@withMap
221
+ }
222
+
223
+ // Load image
224
+ val size = userLocationSize.toInt().coerceAtLeast(1)
225
+ val density = context.resources.displayMetrics.density
226
+ val sizePx = (size * density).toInt()
227
+
228
+ if (userLocationImage.startsWith("http://") || userLocationImage.startsWith("https://")) {
229
+ // Remote image — async load
230
+ val imageUrl = userLocationImage
231
+ cachedUserLocationImageUrl = imageUrl
232
+
233
+ // C-3: Use providerScope instead of unstructured CoroutineScope
234
+ providerScope.launch {
235
+ val bitmap = withContext(Dispatchers.IO) {
236
+ var connection: HttpURLConnection? = null
237
+ try {
238
+ val url = URL(imageUrl)
239
+ connection = url.openConnection() as HttpURLConnection
240
+ connection.connectTimeout = 10_000
241
+ connection.readTimeout = 10_000
242
+ connection.connect()
243
+ if (connection.responseCode in 200..299) {
244
+ BitmapFactory.decodeStream(connection.inputStream)
245
+ } else null
246
+ } catch (e: Exception) {
247
+ Log.w("NitroMap", "Failed to load user location image: ${e.message}")
248
+ null
249
+ } finally {
250
+ // H-6: Always disconnect HttpURLConnection
251
+ connection?.disconnect()
252
+ }
253
+ } ?: return@launch
254
+
255
+ // H-1/H-2: Recycle old bitmap before replacing
256
+ userLocationImageBitmap?.recycle()
257
+ userLocationImageBitmap = bitmap
258
+ val resized = Bitmap.createScaledBitmap(bitmap, sizePx, sizePx, true)
259
+ customUserLocationMarker?.setIcon(BitmapDescriptorFactory.fromBitmap(resized))
260
+ }
261
+ } else {
262
+ // Local asset (from resources)
263
+ cachedUserLocationImageUrl = userLocationImage
264
+ val resId = context.resources.getIdentifier(
265
+ userLocationImage, "drawable", context.packageName
266
+ )
267
+ if (resId != 0) {
268
+ val bitmap = BitmapFactory.decodeResource(context.resources, resId)
269
+ if (bitmap != null) {
270
+ // H-1/H-2: Recycle old bitmap before replacing
271
+ userLocationImageBitmap?.recycle()
272
+ userLocationImageBitmap = bitmap
273
+ val resized = Bitmap.createScaledBitmap(bitmap, sizePx, sizePx, true)
274
+ marker?.setIcon(BitmapDescriptorFactory.fromBitmap(resized))
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ // MARK: - Center on User Location
282
+
283
+ internal fun GoogleMapProvider.centerOnUserLocationInternal() {
284
+ val location = lastKnownUserLocation ?: return
285
+
286
+ withMap { map ->
287
+ map.animateCamera(
288
+ CameraUpdateFactory.newLatLng(
289
+ LatLng(location.latitude, location.longitude)
290
+ ),
291
+ 300,
292
+ null
293
+ )
294
+ }
295
+ Log.d("NitroMap", "centerOnUserLocation: lat=${location.latitude}, lng=${location.longitude}")
296
+ }