@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.
- package/LICENSE +1 -1
- package/NitroMap.podspec +1 -1
- package/README.md +82 -9
- package/android/CMakeLists.txt +4 -1
- package/android/build.gradle +6 -2
- package/android/gradle.properties +4 -4
- package/android/src/main/cpp/ClusterEngineJNI.cpp +198 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMap.kt +397 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapConfig.kt +53 -0
- package/android/src/main/{java → kotlin}/com/margelo/nitro/nitromap/NitroMapPackage.kt +4 -4
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapView.kt +73 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/UserLocationManager.kt +295 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusterIconRenderer.kt +111 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusteringManager.kt +104 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +166 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerIconFactory.kt +303 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerSelectionHandler.kt +72 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/PriceMarkerRenderer.kt +159 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderFactory.kt +24 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +128 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapDelegate.kt +317 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Clustering.kt +524 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Markers.kt +358 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Overlays.kt +272 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+UserLocation.kt +296 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt +815 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/MarkerTagData.kt +19 -0
- package/ios/Location/NitroLocationManager.swift +116 -0
- package/ios/MarkerRenderer/MarkerIconFactory.swift +1 -3
- package/ios/MarkerRenderer/PriceMarkerRenderer.swift +10 -6
- package/ios/NitroMap.swift +279 -13
- package/ios/NitroMapConfig/NitroMapConfig.swift +45 -0
- package/ios/Providers/{GoogleMapDelegate.swift → Google/GoogleMapDelegate.swift} +48 -23
- package/ios/Providers/Google/GoogleMapProvider+Camera.swift +180 -0
- package/ios/Providers/Google/GoogleMapProvider+Clustering.swift +541 -0
- package/ios/Providers/Google/GoogleMapProvider+Markers.swift +270 -0
- package/ios/Providers/Google/GoogleMapProvider+Overlays.swift +245 -0
- package/ios/Providers/Google/GoogleMapProvider+UserLocation.swift +180 -0
- package/ios/Providers/Google/GoogleMapProvider.swift +342 -0
- package/ios/Providers/MapProviderFactory.swift +17 -0
- package/ios/Providers/MapProviderProtocol.swift +48 -1
- package/ios/Shared/ClusterConfig+Factory.swift +2 -2
- package/ios/Shared/MapStyleProvider.swift +6 -4
- package/ios/Shared/MarkerSelectionHandler.swift +4 -1
- package/ios/Utils/ColorValueExtension.swift +46 -67
- package/lib/module/components/ImageMarker.js +39 -29
- package/lib/module/components/ImageMarker.js.map +1 -1
- package/lib/module/components/Marker.js +118 -0
- package/lib/module/components/Marker.js.map +1 -0
- package/lib/module/components/NitroCircle.js +92 -0
- package/lib/module/components/NitroCircle.js.map +1 -0
- package/lib/module/components/NitroMap.js +216 -76
- package/lib/module/components/NitroMap.js.map +1 -1
- package/lib/module/components/NitroPolygon.js +137 -0
- package/lib/module/components/NitroPolygon.js.map +1 -0
- package/lib/module/components/NitroPolyline.js +115 -0
- package/lib/module/components/NitroPolyline.js.map +1 -0
- package/lib/module/components/PriceMarker.js +16 -29
- package/lib/module/components/PriceMarker.js.map +1 -1
- package/lib/module/context/NitroMapContext.js.map +1 -1
- package/lib/module/hooks/useNitroCircle.js +18 -0
- package/lib/module/hooks/useNitroCircle.js.map +1 -0
- package/lib/module/hooks/useNitroMarker.js +26 -9
- package/lib/module/hooks/useNitroMarker.js.map +1 -1
- package/lib/module/hooks/useNitroOverlay.js +59 -0
- package/lib/module/hooks/useNitroOverlay.js.map +1 -0
- package/lib/module/hooks/useNitroPolygon.js +18 -0
- package/lib/module/hooks/useNitroPolygon.js.map +1 -0
- package/lib/module/hooks/useNitroPolyline.js +18 -0
- package/lib/module/hooks/useNitroPolyline.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types/overlay.js +4 -0
- package/lib/module/types/overlay.js.map +1 -0
- package/lib/module/types/theme.js +4 -0
- package/lib/module/types/theme.js.map +1 -0
- package/lib/module/utils/colors.js +41 -13
- package/lib/module/utils/colors.js.map +1 -1
- package/lib/module/utils/validation.js +45 -0
- package/lib/module/utils/validation.js.map +1 -0
- package/lib/typescript/src/components/ImageMarker.d.ts.map +1 -1
- package/lib/typescript/src/components/Marker.d.ts +34 -0
- package/lib/typescript/src/components/Marker.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroCircle.d.ts +70 -0
- package/lib/typescript/src/components/NitroCircle.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroMap.d.ts +60 -3
- package/lib/typescript/src/components/NitroMap.d.ts.map +1 -1
- package/lib/typescript/src/components/NitroPolygon.d.ts +86 -0
- package/lib/typescript/src/components/NitroPolygon.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroPolyline.d.ts +84 -0
- package/lib/typescript/src/components/NitroPolyline.d.ts.map +1 -0
- package/lib/typescript/src/components/PriceMarker.d.ts +0 -5
- package/lib/typescript/src/components/PriceMarker.d.ts.map +1 -1
- package/lib/typescript/src/context/NitroMapContext.d.ts +2 -0
- package/lib/typescript/src/context/NitroMapContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useNitroCircle.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroCircle.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts +20 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useNitroOverlay.d.ts +26 -0
- package/lib/typescript/src/hooks/useNitroOverlay.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroPolygon.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroPolygon.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroPolyline.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroPolyline.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts +248 -6
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types/map.d.ts +34 -4
- package/lib/typescript/src/types/map.d.ts.map +1 -1
- package/lib/typescript/src/types/marker.d.ts +24 -36
- package/lib/typescript/src/types/marker.d.ts.map +1 -1
- package/lib/typescript/src/types/overlay.d.ts +84 -0
- package/lib/typescript/src/types/overlay.d.ts.map +1 -0
- package/lib/typescript/src/types/theme.d.ts +93 -0
- package/lib/typescript/src/types/theme.d.ts.map +1 -0
- package/lib/typescript/src/utils/colors.d.ts +6 -8
- package/lib/typescript/src/utils/colors.d.ts.map +1 -1
- package/lib/typescript/src/utils/validation.d.ts +12 -0
- package/lib/typescript/src/utils/validation.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JCircleData.hpp +94 -0
- package/nitrogen/generated/android/c++/JClusterConfig.hpp +5 -7
- package/nitrogen/generated/android/c++/JCoordinateRing.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_UserLocationChangeEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JFunc_void_UserTrackingMode.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +332 -21
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +53 -2
- package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +3 -6
- package/nitrogen/generated/android/c++/JMarkerData.hpp +15 -3
- package/nitrogen/generated/android/c++/JPolygonData.hpp +133 -0
- package/nitrogen/generated/android/c++/JPolylineData.hpp +113 -0
- package/nitrogen/generated/android/c++/JUserLocationChangeEvent.hpp +70 -0
- package/nitrogen/generated/android/c++/JUserTrackingMode.hpp +62 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +72 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CircleData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CoordinateRing.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserLocationChangeEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserTrackingMode.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +228 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +1 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolygonData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolylineData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/UserLocationChangeEvent.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/{ClusterAnimationStyle.kt → UserTrackingMode.kt} +6 -8
- package/nitrogen/generated/android/nitromapOnLoad.cpp +6 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +24 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +178 -17
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +18 -3
- package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +252 -16
- package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +90 -5
- package/nitrogen/generated/ios/swift/CircleData.swift +143 -0
- package/nitrogen/generated/ios/swift/ClusterConfig.swift +22 -15
- package/nitrogen/generated/ios/swift/CoordinateRing.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void_UserLocationChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_UserTrackingMode.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +35 -1
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +582 -8
- package/nitrogen/generated/ios/swift/MarkerAnimation.swift +4 -8
- package/nitrogen/generated/ios/swift/MarkerData.swift +54 -2
- package/nitrogen/generated/ios/swift/PolygonData.swift +167 -0
- package/nitrogen/generated/ios/swift/PolylineData.swift +155 -0
- package/nitrogen/generated/ios/swift/UserLocationChangeEvent.swift +69 -0
- package/nitrogen/generated/ios/swift/UserTrackingMode.swift +44 -0
- package/nitrogen/generated/shared/c++/CircleData.hpp +113 -0
- package/nitrogen/generated/shared/c++/ClusterConfig.hpp +5 -8
- package/nitrogen/generated/shared/c++/CoordinateRing.hpp +77 -0
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +53 -2
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +75 -6
- package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +4 -8
- package/nitrogen/generated/shared/c++/MarkerData.hpp +14 -2
- package/nitrogen/generated/shared/c++/PolygonData.hpp +117 -0
- package/nitrogen/generated/shared/c++/PolylineData.hpp +114 -0
- package/nitrogen/generated/shared/c++/UserLocationChangeEvent.hpp +88 -0
- package/nitrogen/generated/shared/c++/UserTrackingMode.hpp +80 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +216 -12
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +23 -1
- package/nitrogen/generated/shared/json/NitroMapConfig.json +18 -1
- package/package.json +36 -5
- package/src/components/ImageMarker.tsx +58 -42
- package/src/components/Marker.tsx +161 -0
- package/src/components/NitroCircle.tsx +183 -0
- package/src/components/NitroMap.tsx +328 -78
- package/src/components/NitroPolygon.tsx +229 -0
- package/src/components/NitroPolyline.tsx +208 -0
- package/src/components/PriceMarker.tsx +23 -48
- package/src/context/NitroMapContext.tsx +4 -0
- package/src/hooks/useNitroCircle.ts +25 -0
- package/src/hooks/useNitroMarker.ts +49 -10
- package/src/hooks/useNitroOverlay.ts +68 -0
- package/src/hooks/useNitroPolygon.ts +25 -0
- package/src/hooks/useNitroPolyline.ts +25 -0
- package/src/index.tsx +28 -2
- package/src/specs/NitroMap.nitro.ts +294 -5
- package/src/types/map.ts +36 -4
- package/src/types/marker.ts +24 -44
- package/src/types/overlay.ts +87 -0
- package/src/types/theme.ts +101 -0
- package/src/utils/colors.ts +48 -16
- package/src/utils/validation.ts +69 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +0 -108
- package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +0 -63
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +0 -408
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +0 -68
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +0 -176
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +0 -252
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +0 -252
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +0 -195
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +0 -912
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +0 -70
- package/cpp/QuadTree.hpp +0 -246
- package/ios/NitroMapConfig/HybridNitroMapConfig.swift +0 -33
- package/ios/Providers/GoogleMapProvider+Camera.swift +0 -164
- package/ios/Providers/GoogleMapProvider.swift +0 -924
- package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +0 -68
- package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +0 -52
- package/nitrogen/generated/shared/c++/ClusterAnimationStyle.hpp +0 -88
package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromap.providers.google
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.content.res.Resources
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.View
|
|
9
|
+
import com.google.android.gms.maps.CameraUpdateFactory
|
|
10
|
+
import com.google.android.gms.maps.GoogleMap
|
|
11
|
+
import com.google.android.gms.maps.model.CameraPosition
|
|
12
|
+
import com.google.android.gms.maps.model.LatLng
|
|
13
|
+
import com.google.android.gms.maps.model.LatLngBounds
|
|
14
|
+
import com.google.android.gms.maps.model.Marker
|
|
15
|
+
import com.google.android.gms.maps.model.Polyline
|
|
16
|
+
import com.google.android.gms.maps.model.Polygon
|
|
17
|
+
import com.google.android.gms.maps.model.Circle
|
|
18
|
+
import com.google.android.gms.maps.model.MapColorScheme
|
|
19
|
+
import com.google.android.gms.maps.model.MapStyleOptions
|
|
20
|
+
import android.os.Bundle
|
|
21
|
+
import com.margelo.nitro.nitromap.*
|
|
22
|
+
import com.margelo.nitro.nitromap.markers.MarkerIconFactory
|
|
23
|
+
import com.margelo.nitro.nitromap.providers.MapProviderInterface
|
|
24
|
+
import kotlinx.coroutines.CoroutineScope
|
|
25
|
+
import kotlinx.coroutines.Dispatchers
|
|
26
|
+
import kotlinx.coroutines.SupervisorJob
|
|
27
|
+
import kotlinx.coroutines.cancel
|
|
28
|
+
import java.util.concurrent.ConcurrentLinkedQueue
|
|
29
|
+
import kotlin.math.abs
|
|
30
|
+
import kotlin.math.ln
|
|
31
|
+
import kotlin.math.pow
|
|
32
|
+
import kotlin.math.roundToInt
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Google Maps implementation of the map provider.
|
|
36
|
+
*
|
|
37
|
+
* The core challenge: GoogleMap is ASYNC — available only after getMapAsync().
|
|
38
|
+
* All operations use withMap {} to queue commands until the map is ready.
|
|
39
|
+
*
|
|
40
|
+
* This class holds all map data (markers, overlays, settings) in Kotlin data structures
|
|
41
|
+
* that survive MapView detach/reattach cycles. Only native GoogleMap objects are recreated.
|
|
42
|
+
*/
|
|
43
|
+
class GoogleMapProvider(internal val context: Context) : MapProviderInterface {
|
|
44
|
+
|
|
45
|
+
companion object {
|
|
46
|
+
private const val TAG = "NitroMap"
|
|
47
|
+
private const val DEFAULT_LATITUDE = 41.2995
|
|
48
|
+
private const val DEFAULT_LONGITUDE = 69.2401
|
|
49
|
+
private const val DEFAULT_ZOOM = 10f
|
|
50
|
+
private const val BASE_ALTITUDE = 40_000_000.0 // meters at zoom 0, matches iOS
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// MARK: - MapView Lifecycle (absorbed from NitroMap facade)
|
|
55
|
+
|
|
56
|
+
private var mapView: NitroMapView? = null
|
|
57
|
+
private var savedState: Bundle? = null
|
|
58
|
+
|
|
59
|
+
override fun createMapView(context: Context): View {
|
|
60
|
+
val mv = NitroMapView(context)
|
|
61
|
+
mv.onCreate(savedState)
|
|
62
|
+
mv.onStart()
|
|
63
|
+
mv.onResume()
|
|
64
|
+
mapView = mv
|
|
65
|
+
return mv
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun onViewAttached(view: View) {
|
|
69
|
+
val mv = view as NitroMapView
|
|
70
|
+
mv.getMapAsync { googleMap ->
|
|
71
|
+
onMapReady(googleMap, mv)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override fun onViewDetaching() {
|
|
76
|
+
mapView?.let { mv ->
|
|
77
|
+
val state = Bundle()
|
|
78
|
+
mv.onSaveInstanceState(state)
|
|
79
|
+
savedState = state
|
|
80
|
+
mv.onPause()
|
|
81
|
+
mv.onStop()
|
|
82
|
+
}
|
|
83
|
+
onMapDetaching()
|
|
84
|
+
mapView = null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun onViewDestroying() {
|
|
88
|
+
onMapDestroying()
|
|
89
|
+
mapView?.let { mv ->
|
|
90
|
+
mv.onPause()
|
|
91
|
+
mv.onStop()
|
|
92
|
+
mv.onDestroy()
|
|
93
|
+
}
|
|
94
|
+
mapView = null
|
|
95
|
+
savedState = null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - GoogleMap Reference + Command Queue
|
|
99
|
+
|
|
100
|
+
var googleMap: GoogleMap? = null
|
|
101
|
+
private set
|
|
102
|
+
@Volatile
|
|
103
|
+
private var isMapReady = false
|
|
104
|
+
// C-1: Thread-safe queue — accessed from JS thread (add) and main thread (flush)
|
|
105
|
+
private val pendingCommands = ConcurrentLinkedQueue<(GoogleMap) -> Unit>()
|
|
106
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
107
|
+
|
|
108
|
+
// C-3: Structured coroutine scope — cancelled on destroy to prevent leaks
|
|
109
|
+
val providerScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
110
|
+
|
|
111
|
+
// Icon factory — created lazily on first onMapReady, cleared on destroy
|
|
112
|
+
var iconFactory: MarkerIconFactory? = null
|
|
113
|
+
private set
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Execute a command on GoogleMap, always on the main thread.
|
|
117
|
+
* If map isn't ready, queue it. When onMapReady fires, all queued commands
|
|
118
|
+
* are flushed in order on the main thread.
|
|
119
|
+
*
|
|
120
|
+
* IMPORTANT: GoogleMap API calls MUST run on the main thread.
|
|
121
|
+
* Methods called from JS arrive on a background thread (NativeRunnable),
|
|
122
|
+
* so we always post to mainHandler.
|
|
123
|
+
*/
|
|
124
|
+
fun withMap(command: (GoogleMap) -> Unit) {
|
|
125
|
+
val map = googleMap
|
|
126
|
+
if (map != null && isMapReady) {
|
|
127
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
128
|
+
command(map)
|
|
129
|
+
} else {
|
|
130
|
+
mainHandler.post { command(map) }
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// C-1: ConcurrentLinkedQueue.add is thread-safe
|
|
134
|
+
pendingCommands.add(command)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MARK: - Delegate
|
|
139
|
+
|
|
140
|
+
private var delegate: GoogleMapDelegate? = null
|
|
141
|
+
|
|
142
|
+
// MARK: - State (survives detach/reattach)
|
|
143
|
+
|
|
144
|
+
// Stored prop values — applied to new GoogleMap on reattach.
|
|
145
|
+
// Props with withMap {} setters are live-applied when changed by React.
|
|
146
|
+
// applyAllSettings() bulk-applies all stored values on map ready / reattach.
|
|
147
|
+
|
|
148
|
+
// M-7: Track whether initialRegion has been applied to prevent reapply on reattach
|
|
149
|
+
private var initialRegionApplied = false
|
|
150
|
+
|
|
151
|
+
override var initialRegion: Region? = Region(DEFAULT_LATITUDE, DEFAULT_LONGITUDE, 0.15, 0.15)
|
|
152
|
+
|
|
153
|
+
override var region: Region? = null
|
|
154
|
+
set(value) {
|
|
155
|
+
field = value
|
|
156
|
+
if (value == null) return
|
|
157
|
+
withMap { map ->
|
|
158
|
+
val currentPos = map.cameraPosition.target
|
|
159
|
+
val latDiff = abs(currentPos.latitude - value.latitude)
|
|
160
|
+
val lonDiff = abs(currentPos.longitude - value.longitude)
|
|
161
|
+
if (latDiff < 0.0001 && lonDiff < 0.0001) return@withMap
|
|
162
|
+
val camera = CameraPosition.Builder()
|
|
163
|
+
.target(LatLng(value.latitude, value.longitude))
|
|
164
|
+
.zoom(calculateZoom(value))
|
|
165
|
+
.build()
|
|
166
|
+
map.animateCamera(CameraUpdateFactory.newCameraPosition(camera))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override var showsUserLocation: Boolean? = null
|
|
171
|
+
|
|
172
|
+
override var zoomEnabled: Boolean? = null
|
|
173
|
+
set(value) { field = value; withMap { it.uiSettings.isZoomGesturesEnabled = value ?: true } }
|
|
174
|
+
|
|
175
|
+
override var scrollEnabled: Boolean? = null
|
|
176
|
+
set(value) { field = value; withMap { it.uiSettings.isScrollGesturesEnabled = value ?: true } }
|
|
177
|
+
|
|
178
|
+
override var rotateEnabled: Boolean? = null
|
|
179
|
+
set(value) { field = value; withMap { it.uiSettings.isRotateGesturesEnabled = value ?: true } }
|
|
180
|
+
|
|
181
|
+
override var pitchEnabled: Boolean? = null
|
|
182
|
+
set(value) { field = value; withMap { it.uiSettings.isTiltGesturesEnabled = value ?: true } }
|
|
183
|
+
|
|
184
|
+
override var mapType: MapType? = null
|
|
185
|
+
set(value) { field = value; withMap { it.mapType = convertMapType(value) } }
|
|
186
|
+
|
|
187
|
+
override var customMapStyle: Array<MapStyleElement>? = null
|
|
188
|
+
set(value) { field = value; withMap { applyMapStyleInternal(it) } }
|
|
189
|
+
|
|
190
|
+
override var clusterConfig: ClusterConfig? = null
|
|
191
|
+
set(value) { field = value; updateClusterConfig() }
|
|
192
|
+
|
|
193
|
+
override var mapPadding: EdgePadding = EdgePadding(0.0, 0.0, 0.0, 0.0)
|
|
194
|
+
set(value) {
|
|
195
|
+
field = value
|
|
196
|
+
// M-6: Standardize on context.resources.displayMetrics.density
|
|
197
|
+
val density = context.resources.displayMetrics.density
|
|
198
|
+
withMap {
|
|
199
|
+
it.setPadding(
|
|
200
|
+
(value.left * density).roundToInt(),
|
|
201
|
+
(value.top * density).roundToInt(),
|
|
202
|
+
(value.right * density).roundToInt(),
|
|
203
|
+
(value.bottom * density).roundToInt()
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override var showsTraffic: Boolean? = null
|
|
209
|
+
set(value) { field = value; withMap { it.isTrafficEnabled = value ?: false } }
|
|
210
|
+
|
|
211
|
+
override var showsBuildings: Boolean? = null
|
|
212
|
+
set(value) { field = value; withMap { it.isBuildingsEnabled = value ?: true } }
|
|
213
|
+
|
|
214
|
+
override var showsCompass: Boolean? = null
|
|
215
|
+
set(value) { field = value; withMap { it.uiSettings.isCompassEnabled = value ?: true } }
|
|
216
|
+
|
|
217
|
+
// L-2: Align with iOS default (1.0, not 0.0)
|
|
218
|
+
override var minZoom: Double = 1.0
|
|
219
|
+
set(value) { field = value; withMap { it.setMinZoomPreference(value.toFloat()) } }
|
|
220
|
+
|
|
221
|
+
// L-1: Align with facade default (22.0, not 21.0)
|
|
222
|
+
override var maxZoom: Double = 22.0
|
|
223
|
+
set(value) { field = value; withMap { it.setMaxZoomPreference(value.toFloat()) } }
|
|
224
|
+
|
|
225
|
+
override var darkMode: Boolean? = null
|
|
226
|
+
set(value) { field = value; withMap { applyMapStyleInternal(it) } }
|
|
227
|
+
|
|
228
|
+
override var userTrackingMode: UserTrackingMode? = null
|
|
229
|
+
override var userLocationImage: String = ""
|
|
230
|
+
override var userLocationSize: Double = 40.0
|
|
231
|
+
override var userLocationAnchor: Point? = null
|
|
232
|
+
set(value) {
|
|
233
|
+
field = value
|
|
234
|
+
// Update anchor on existing custom marker (parity with iOS)
|
|
235
|
+
customUserLocationMarker?.setAnchor(
|
|
236
|
+
value?.x?.toFloat() ?: 0.5f,
|
|
237
|
+
value?.y?.toFloat() ?: 0.5f
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Marker/overlay data — survives detach, re-applied on reattach
|
|
242
|
+
val markerData = mutableMapOf<String, MarkerData>()
|
|
243
|
+
val polylineData = mutableMapOf<String, PolylineData>()
|
|
244
|
+
val renderedPolylines = mutableMapOf<String, Polyline>()
|
|
245
|
+
val polygonData = mutableMapOf<String, PolygonData>()
|
|
246
|
+
val renderedPolygons = mutableMapOf<String, Polygon>()
|
|
247
|
+
val circleData = mutableMapOf<String, CircleData>()
|
|
248
|
+
val renderedCircles = mutableMapOf<String, Circle>()
|
|
249
|
+
|
|
250
|
+
var selectedMarkerId: String? = null
|
|
251
|
+
|
|
252
|
+
// User location state (used by GoogleMapProvider+UserLocation.kt)
|
|
253
|
+
var lastKnownUserLocation: android.location.Location? = null
|
|
254
|
+
var lastKnownHeading: Float? = null
|
|
255
|
+
var customUserLocationMarker: Marker? = null
|
|
256
|
+
var cachedUserLocationImageUrl: String = ""
|
|
257
|
+
var userLocationImageBitmap: android.graphics.Bitmap? = null
|
|
258
|
+
var userLocationPositionAnimator: android.animation.ValueAnimator? = null
|
|
259
|
+
var userLocationRotationAnimator: android.animation.ValueAnimator? = null
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Set to true while updateUserLocation is animating the camera for follow mode.
|
|
263
|
+
* Cleared in onCameraIdle. When true, clustering is skipped because the
|
|
264
|
+
* zoom level hasn't changed — only the center shifted slightly to follow the user.
|
|
265
|
+
*/
|
|
266
|
+
@Volatile var isFollowingUserCamera: Boolean = false
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* M-3: Set to true when markers are added/removed. Cleared after clustering runs.
|
|
270
|
+
* When isFollowingUserCamera is true, clustering is only skipped if this is false
|
|
271
|
+
* (i.e., no marker changes since last cluster run).
|
|
272
|
+
*/
|
|
273
|
+
@Volatile var clusteringDirty: Boolean = false
|
|
274
|
+
var lastClusteredZoom: Float = -1f
|
|
275
|
+
|
|
276
|
+
// Clustering state (used by GoogleMapProvider+Clustering.kt)
|
|
277
|
+
var clusteringManager: com.margelo.nitro.nitromap.clustering.ClusteringManager? = null
|
|
278
|
+
val renderedClusterMarkers = mutableListOf<Marker>()
|
|
279
|
+
val renderedSingleMarkers = mutableMapOf<String, Marker>()
|
|
280
|
+
val nonClusteredMarkers = mutableMapOf<String, Marker>()
|
|
281
|
+
val hiddenNonClusteredIds = mutableSetOf<String>()
|
|
282
|
+
// H-3: Removed clusterableMarkerData — markerData is now the single source of truth.
|
|
283
|
+
// All markers are fed to the clustering engine; non-clusterable ones also get direct markers.
|
|
284
|
+
var throttleActive = false
|
|
285
|
+
var currentMapView: android.view.View? = null
|
|
286
|
+
|
|
287
|
+
// MARK: - Callbacks (set by NitroMap from JS props)
|
|
288
|
+
|
|
289
|
+
override var onPress: ((MapPressEvent) -> Unit)? = null
|
|
290
|
+
override var onLongPress: ((MapPressEvent) -> Unit)? = null
|
|
291
|
+
override var onMapReadyCallback: (() -> Unit)? = null
|
|
292
|
+
override var onRegionChange: ((RegionChangeEvent) -> Unit)? = null
|
|
293
|
+
override var onRegionChangeComplete: ((RegionChangeEvent) -> Unit)? = null
|
|
294
|
+
override var onMarkerPress: ((MarkerPressEvent) -> Unit)? = null
|
|
295
|
+
override var onMarkerDragStart: ((MarkerDragEvent) -> Unit)? = null
|
|
296
|
+
override var onMarkerDrag: ((MarkerDragEvent) -> Unit)? = null
|
|
297
|
+
override var onMarkerDragEnd: ((MarkerDragEvent) -> Unit)? = null
|
|
298
|
+
override var onClusterPress: ((ClusterPressEvent) -> Unit)? = null
|
|
299
|
+
override var onError: ((MapError) -> Unit)? = null
|
|
300
|
+
override var onMapIdle: (() -> Unit)? = null
|
|
301
|
+
override var onPolylinePress: ((String) -> Unit)? = null
|
|
302
|
+
override var onPolygonPress: ((String) -> Unit)? = null
|
|
303
|
+
override var onCirclePress: ((String) -> Unit)? = null
|
|
304
|
+
override var onUserLocationChange: ((UserLocationChangeEvent) -> Unit)? = null
|
|
305
|
+
override var onUserTrackingModeChange: ((UserTrackingMode) -> Unit)? = null
|
|
306
|
+
override var onUserLocationError: ((MapError) -> Unit)? = null
|
|
307
|
+
|
|
308
|
+
// MARK: - Map Lifecycle
|
|
309
|
+
|
|
310
|
+
fun onMapReady(map: GoogleMap, mapView: View? = null) {
|
|
311
|
+
googleMap = map
|
|
312
|
+
isMapReady = true
|
|
313
|
+
currentMapView = mapView
|
|
314
|
+
|
|
315
|
+
// Create icon factory (holds an LruCache — keep alive across onMapReady calls)
|
|
316
|
+
if (iconFactory == null) {
|
|
317
|
+
// C-3: Pass providerScope so async image loads are cancelled on destroy
|
|
318
|
+
iconFactory = MarkerIconFactory(context, providerScope).also { factory ->
|
|
319
|
+
factory.onIconLoaded = { markerId, icon ->
|
|
320
|
+
// When async URL image loads, update the rendered marker icon in-place
|
|
321
|
+
renderedSingleMarkers[markerId]?.setIcon(icon)
|
|
322
|
+
nonClusteredMarkers[markerId]?.setIcon(icon)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Create clustering manager (keep alive across onMapReady calls)
|
|
328
|
+
if (clusteringManager == null) {
|
|
329
|
+
clusteringManager = com.margelo.nitro.nitromap.clustering.ClusteringManager().also {
|
|
330
|
+
it.setDensity(context.resources.displayMetrics.density)
|
|
331
|
+
it.clusterConfig = clusterConfig
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Set up delegate for all listeners
|
|
336
|
+
delegate = GoogleMapDelegate(this)
|
|
337
|
+
delegate?.setupListeners(map)
|
|
338
|
+
|
|
339
|
+
// Apply all stored settings
|
|
340
|
+
applyAllSettings(map)
|
|
341
|
+
|
|
342
|
+
// C-1: Drain thread-safe queue (poll returns null when empty)
|
|
343
|
+
var commandCount = 0
|
|
344
|
+
while (true) {
|
|
345
|
+
val cmd = pendingCommands.poll() ?: break
|
|
346
|
+
cmd(map)
|
|
347
|
+
commandCount++
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Fire JS callback
|
|
351
|
+
onMapReadyCallback?.invoke()
|
|
352
|
+
|
|
353
|
+
Log.d(TAG, "GoogleMap ready, flushed $commandCount queued commands")
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Called on view detach — releases GoogleMap ref but keeps data.
|
|
358
|
+
*/
|
|
359
|
+
fun onMapDetaching() {
|
|
360
|
+
// Clear native object references (they're invalidated anyway)
|
|
361
|
+
renderedPolylines.clear()
|
|
362
|
+
renderedPolygons.clear()
|
|
363
|
+
renderedCircles.clear()
|
|
364
|
+
renderedClusterMarkers.clear()
|
|
365
|
+
renderedSingleMarkers.clear()
|
|
366
|
+
nonClusteredMarkers.clear()
|
|
367
|
+
hiddenNonClusteredIds.clear()
|
|
368
|
+
customUserLocationMarker = null
|
|
369
|
+
|
|
370
|
+
// Cancel user location animators to prevent updates after detach
|
|
371
|
+
userLocationPositionAnimator?.cancel()
|
|
372
|
+
userLocationRotationAnimator?.cancel()
|
|
373
|
+
|
|
374
|
+
// Drain stale commands — they reference the old GoogleMap instance
|
|
375
|
+
pendingCommands.clear()
|
|
376
|
+
|
|
377
|
+
// Release GoogleMap ref
|
|
378
|
+
googleMap = null
|
|
379
|
+
isMapReady = false
|
|
380
|
+
delegate = null
|
|
381
|
+
currentMapView = null
|
|
382
|
+
|
|
383
|
+
Log.d(TAG, "Map detaching — GoogleMap ref released, data preserved")
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Called on dispose() — full cleanup.
|
|
388
|
+
*/
|
|
389
|
+
fun onMapDestroying() {
|
|
390
|
+
onMapDetaching()
|
|
391
|
+
|
|
392
|
+
// M-4: Cancel user location animators to prevent leaks
|
|
393
|
+
userLocationPositionAnimator?.cancel()
|
|
394
|
+
userLocationPositionAnimator = null
|
|
395
|
+
userLocationRotationAnimator?.cancel()
|
|
396
|
+
userLocationRotationAnimator = null
|
|
397
|
+
|
|
398
|
+
// C-3: Cancel all coroutines to prevent leaks
|
|
399
|
+
providerScope.cancel()
|
|
400
|
+
|
|
401
|
+
// Clear all data
|
|
402
|
+
markerData.clear()
|
|
403
|
+
polylineData.clear()
|
|
404
|
+
polygonData.clear()
|
|
405
|
+
circleData.clear()
|
|
406
|
+
pendingCommands.clear()
|
|
407
|
+
iconFactory?.clearCache()
|
|
408
|
+
iconFactory = null
|
|
409
|
+
clusteringManager?.destroy()
|
|
410
|
+
clusteringManager = null
|
|
411
|
+
|
|
412
|
+
// Null all callbacks to prevent leaks
|
|
413
|
+
onPress = null
|
|
414
|
+
onLongPress = null
|
|
415
|
+
onMapReadyCallback = null
|
|
416
|
+
onRegionChange = null
|
|
417
|
+
onRegionChangeComplete = null
|
|
418
|
+
onMarkerPress = null
|
|
419
|
+
onMarkerDragStart = null
|
|
420
|
+
onMarkerDrag = null
|
|
421
|
+
onMarkerDragEnd = null
|
|
422
|
+
onClusterPress = null
|
|
423
|
+
onError = null
|
|
424
|
+
onMapIdle = null
|
|
425
|
+
onPolylinePress = null
|
|
426
|
+
onPolygonPress = null
|
|
427
|
+
onCirclePress = null
|
|
428
|
+
onUserLocationChange = null
|
|
429
|
+
onUserTrackingModeChange = null
|
|
430
|
+
onUserLocationError = null
|
|
431
|
+
|
|
432
|
+
Log.d(TAG, "Map destroying — full cleanup complete")
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// MARK: - Apply Settings
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Applies all currently stored prop values to a (possibly new) GoogleMap instance.
|
|
439
|
+
* Called after onMapReady and after view reattach.
|
|
440
|
+
*/
|
|
441
|
+
private fun applyAllSettings(map: GoogleMap) {
|
|
442
|
+
// UiSettings
|
|
443
|
+
map.uiSettings.isZoomGesturesEnabled = zoomEnabled ?: true
|
|
444
|
+
map.uiSettings.isScrollGesturesEnabled = scrollEnabled ?: true
|
|
445
|
+
map.uiSettings.isRotateGesturesEnabled = rotateEnabled ?: true
|
|
446
|
+
map.uiSettings.isTiltGesturesEnabled = pitchEnabled ?: true
|
|
447
|
+
map.uiSettings.isCompassEnabled = showsCompass ?: true
|
|
448
|
+
map.uiSettings.isMyLocationButtonEnabled = false // Always hidden
|
|
449
|
+
|
|
450
|
+
// Map type
|
|
451
|
+
map.mapType = convertMapType(mapType)
|
|
452
|
+
|
|
453
|
+
// Zoom limits
|
|
454
|
+
map.setMinZoomPreference(minZoom.toFloat())
|
|
455
|
+
map.setMaxZoomPreference(maxZoom.toFloat())
|
|
456
|
+
|
|
457
|
+
// Traffic + buildings
|
|
458
|
+
map.isTrafficEnabled = showsTraffic ?: false
|
|
459
|
+
map.isBuildingsEnabled = showsBuildings ?: true
|
|
460
|
+
|
|
461
|
+
// User location — show native blue dot when no custom image is set
|
|
462
|
+
if (userLocationImage.isEmpty()) {
|
|
463
|
+
try {
|
|
464
|
+
map.isMyLocationEnabled = showsUserLocation ?: false
|
|
465
|
+
} catch (e: SecurityException) {
|
|
466
|
+
Log.w(TAG, "Location permission not granted")
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// H-4: mapPadding is non-nullable EdgePadding — no null check needed
|
|
471
|
+
// M-6: Standardize on context.resources.displayMetrics.density
|
|
472
|
+
val density = context.resources.displayMetrics.density
|
|
473
|
+
map.setPadding(
|
|
474
|
+
(mapPadding.left * density).roundToInt(),
|
|
475
|
+
(mapPadding.top * density).roundToInt(),
|
|
476
|
+
(mapPadding.right * density).roundToInt(),
|
|
477
|
+
(mapPadding.bottom * density).roundToInt()
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
// Camera — use region if set, otherwise initialRegion (only on first apply)
|
|
481
|
+
val targetRegion = if (region != null) {
|
|
482
|
+
region
|
|
483
|
+
} else if (!initialRegionApplied) {
|
|
484
|
+
// M-7: Only apply initialRegion once (matches iOS guard oldValue == nil)
|
|
485
|
+
initialRegionApplied = true
|
|
486
|
+
initialRegion
|
|
487
|
+
} else {
|
|
488
|
+
null
|
|
489
|
+
}
|
|
490
|
+
if (targetRegion != null) {
|
|
491
|
+
val camera = CameraPosition.Builder()
|
|
492
|
+
.target(LatLng(targetRegion.latitude, targetRegion.longitude))
|
|
493
|
+
.zoom(calculateZoom(targetRegion))
|
|
494
|
+
.build()
|
|
495
|
+
map.moveCamera(CameraUpdateFactory.newCameraPosition(camera))
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Map style (custom style takes priority over dark mode)
|
|
499
|
+
applyMapStyleInternal(map)
|
|
500
|
+
|
|
501
|
+
// Re-apply markers (after detach/reattach, native Marker objects are gone)
|
|
502
|
+
reapplyMarkers()
|
|
503
|
+
|
|
504
|
+
// Re-apply overlays (polylines, polygons, circles)
|
|
505
|
+
reapplyOverlays()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// MARK: - Map Style Helpers
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Applies the current map style to the GoogleMap.
|
|
512
|
+
* Priority: customMapStyle > darkMode > default (null).
|
|
513
|
+
*/
|
|
514
|
+
private fun applyMapStyleInternal(map: GoogleMap) {
|
|
515
|
+
// Custom style takes priority
|
|
516
|
+
val styleElements = customMapStyle
|
|
517
|
+
if (styleElements != null && styleElements.isNotEmpty()) {
|
|
518
|
+
try {
|
|
519
|
+
val jsonString = mapStyleElementsToJson(styleElements)
|
|
520
|
+
val success = map.setMapStyle(MapStyleOptions(jsonString))
|
|
521
|
+
if (!success) {
|
|
522
|
+
Log.w(TAG, "Map style parsing failed")
|
|
523
|
+
onError?.invoke(MapError("STYLE_ERROR", "Map style parsing failed"))
|
|
524
|
+
}
|
|
525
|
+
return
|
|
526
|
+
} catch (e: Exception) {
|
|
527
|
+
Log.e(TAG, "Failed to apply custom map style", e)
|
|
528
|
+
onError?.invoke(MapError("STYLE_ERROR", "Failed to apply map style: ${e.message}"))
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Dark mode fallback — use native MapColorScheme (SDK 19.0.0+)
|
|
533
|
+
if (darkMode == true) {
|
|
534
|
+
map.setMapStyle(null) // clear any custom JSON style
|
|
535
|
+
map.setMapColorScheme(MapColorScheme.DARK)
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Clear style — reset to light
|
|
540
|
+
map.setMapStyle(null)
|
|
541
|
+
map.setMapColorScheme(MapColorScheme.LIGHT)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private fun mapStyleElementsToJson(elements: Array<MapStyleElement>): String {
|
|
545
|
+
val sb = StringBuilder("[")
|
|
546
|
+
elements.forEachIndexed { index, element ->
|
|
547
|
+
if (index > 0) sb.append(",")
|
|
548
|
+
sb.append("{")
|
|
549
|
+
val parts = mutableListOf<String>()
|
|
550
|
+
element.featureType?.let { parts.add("\"featureType\":\"$it\"") }
|
|
551
|
+
element.elementType?.let { parts.add("\"elementType\":\"$it\"") }
|
|
552
|
+
|
|
553
|
+
val stylerParts = element.stylers.map { styler ->
|
|
554
|
+
val s = mutableListOf<String>()
|
|
555
|
+
styler.color?.let { s.add("\"color\":\"$it\"") }
|
|
556
|
+
styler.visibility?.let { s.add("\"visibility\":\"$it\"") }
|
|
557
|
+
styler.weight?.let { s.add("\"weight\":$it") }
|
|
558
|
+
styler.saturation?.let { s.add("\"saturation\":$it") }
|
|
559
|
+
styler.lightness?.let { s.add("\"lightness\":$it") }
|
|
560
|
+
styler.gamma?.let { s.add("\"gamma\":$it") }
|
|
561
|
+
"{${s.joinToString(",")}}"
|
|
562
|
+
}
|
|
563
|
+
parts.add("\"stylers\":[${stylerParts.joinToString(",")}]")
|
|
564
|
+
|
|
565
|
+
sb.append(parts.joinToString(","))
|
|
566
|
+
sb.append("}")
|
|
567
|
+
}
|
|
568
|
+
sb.append("]")
|
|
569
|
+
return sb.toString()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// MARK: - Interface Method Delegations
|
|
573
|
+
// These delegate to internal extension functions defined in separate files,
|
|
574
|
+
// allowing the extension file organization while satisfying the interface contract.
|
|
575
|
+
|
|
576
|
+
// Markers (delegated to GoogleMapProvider+Markers.kt)
|
|
577
|
+
override fun addMarker(marker: MarkerData) = addMarkerInternal(marker)
|
|
578
|
+
override fun addMarkers(markers: Array<MarkerData>) = addMarkersInternal(markers)
|
|
579
|
+
override fun updateMarker(marker: MarkerData) = updateMarkerInternal(marker)
|
|
580
|
+
override fun removeMarker(id: String) = removeMarkerInternal(id)
|
|
581
|
+
override fun clearMarkers() = clearMarkersInternal()
|
|
582
|
+
override fun selectMarker(id: String) = selectMarkerInternal(id)
|
|
583
|
+
override fun deselectMarker() = deselectMarkerInternal()
|
|
584
|
+
|
|
585
|
+
// Overlays (delegated to GoogleMapProvider+Overlays.kt)
|
|
586
|
+
override fun addPolyline(polyline: PolylineData) = addPolylineInternal(polyline)
|
|
587
|
+
override fun updatePolyline(polyline: PolylineData) = updatePolylineInternal(polyline)
|
|
588
|
+
override fun removePolyline(id: String) = removePolylineInternal(id)
|
|
589
|
+
override fun clearPolylines() = clearPolylinesInternal()
|
|
590
|
+
override fun addPolygon(polygon: PolygonData) = addPolygonInternal(polygon)
|
|
591
|
+
override fun updatePolygon(polygon: PolygonData) = updatePolygonInternal(polygon)
|
|
592
|
+
override fun removePolygon(id: String) = removePolygonInternal(id)
|
|
593
|
+
override fun clearPolygons() = clearPolygonsInternal()
|
|
594
|
+
override fun addCircle(circle: CircleData) = addCircleInternal(circle)
|
|
595
|
+
override fun updateCircle(circle: CircleData) = updateCircleInternal(circle)
|
|
596
|
+
override fun removeCircle(id: String) = removeCircleInternal(id)
|
|
597
|
+
override fun clearCircles() = clearCirclesInternal()
|
|
598
|
+
|
|
599
|
+
// Clustering (delegated to GoogleMapProvider+Clustering.kt)
|
|
600
|
+
override fun setClusteringEnabled(enabled: Boolean) = setClusteringEnabledInternal(enabled)
|
|
601
|
+
override fun refreshClusters() = refreshClustersInternal()
|
|
602
|
+
|
|
603
|
+
// User Location (delegated to GoogleMapProvider+UserLocation.kt)
|
|
604
|
+
override fun updateUserLocation(location: android.location.Location, heading: Float?) = updateUserLocationInternal(location, heading)
|
|
605
|
+
override fun setupCustomUserLocationMarker(context: Context) = setupCustomUserLocationMarkerInternal(context)
|
|
606
|
+
override fun centerOnUserLocation() = centerOnUserLocationInternal()
|
|
607
|
+
|
|
608
|
+
// MARK: - User Location Disable
|
|
609
|
+
|
|
610
|
+
override fun disableUserLocation() {
|
|
611
|
+
customUserLocationMarker?.remove()
|
|
612
|
+
customUserLocationMarker = null
|
|
613
|
+
withMap { map ->
|
|
614
|
+
try { map.isMyLocationEnabled = false } catch (_: SecurityException) {}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// MARK: - Camera Methods (matches iOS GoogleMapProvider+Camera.swift)
|
|
619
|
+
|
|
620
|
+
override fun animateToRegion(region: Region, duration: Double?) {
|
|
621
|
+
withMap { map ->
|
|
622
|
+
val camera = CameraPosition.Builder()
|
|
623
|
+
.target(LatLng(region.latitude, region.longitude))
|
|
624
|
+
.zoom(calculateZoom(region))
|
|
625
|
+
.build()
|
|
626
|
+
// L-6: Default to 500ms to match spec (iOS uses 500ms)
|
|
627
|
+
map.animateCamera(
|
|
628
|
+
CameraUpdateFactory.newCameraPosition(camera),
|
|
629
|
+
(duration?.toInt() ?: 500),
|
|
630
|
+
null
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
override fun fitToCoordinates(coordinates: Array<Coordinate>, edgePadding: EdgePadding?, animated: Boolean?) {
|
|
636
|
+
if (coordinates.isEmpty()) return
|
|
637
|
+
|
|
638
|
+
if (coordinates.size == 1) {
|
|
639
|
+
val first = coordinates[0]
|
|
640
|
+
animateToRegion(
|
|
641
|
+
Region(first.latitude, first.longitude, 0.05, 0.05),
|
|
642
|
+
if (animated != false) 300.0 else 0.0
|
|
643
|
+
)
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
withMap { map ->
|
|
648
|
+
val builder = LatLngBounds.Builder()
|
|
649
|
+
coordinates.forEach {
|
|
650
|
+
builder.include(LatLng(it.latitude, it.longitude))
|
|
651
|
+
}
|
|
652
|
+
val bounds = builder.build()
|
|
653
|
+
|
|
654
|
+
// Per-edge padding via temporary setPadding + restore
|
|
655
|
+
// M-6: Standardize on context.resources.displayMetrics.density
|
|
656
|
+
val density = context.resources.displayMetrics.density
|
|
657
|
+
val topPx = ((edgePadding?.top ?: 50.0) * density).toInt()
|
|
658
|
+
val leftPx = ((edgePadding?.left ?: 50.0) * density).toInt()
|
|
659
|
+
val bottomPx = ((edgePadding?.bottom ?: 50.0) * density).toInt()
|
|
660
|
+
val rightPx = ((edgePadding?.right ?: 50.0) * density).toInt()
|
|
661
|
+
|
|
662
|
+
// Save current padding, apply fit padding, then restore
|
|
663
|
+
// H-4: mapPadding is non-nullable — no null check needed
|
|
664
|
+
map.setPadding(leftPx, topPx, rightPx, bottomPx)
|
|
665
|
+
val update = CameraUpdateFactory.newLatLngBounds(bounds, 0)
|
|
666
|
+
|
|
667
|
+
val restorePadding = {
|
|
668
|
+
map.setPadding(
|
|
669
|
+
(mapPadding.left * density).roundToInt(),
|
|
670
|
+
(mapPadding.top * density).roundToInt(),
|
|
671
|
+
(mapPadding.right * density).roundToInt(),
|
|
672
|
+
(mapPadding.bottom * density).roundToInt()
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (animated != false) {
|
|
677
|
+
map.animateCamera(update, object : GoogleMap.CancelableCallback {
|
|
678
|
+
override fun onFinish() { restorePadding() }
|
|
679
|
+
override fun onCancel() { restorePadding() }
|
|
680
|
+
})
|
|
681
|
+
} else {
|
|
682
|
+
map.moveCamera(update)
|
|
683
|
+
restorePadding()
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
override fun fitToSuppliedMarkers(markerIds: Array<String>, edgePadding: EdgePadding?, animated: Boolean?) {
|
|
689
|
+
val coords = markerIds.mapNotNull { id ->
|
|
690
|
+
markerData[id]?.coordinate
|
|
691
|
+
}.toTypedArray()
|
|
692
|
+
if (coords.isEmpty()) return
|
|
693
|
+
fitToCoordinates(coords, edgePadding, animated)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
override fun animateCamera(camera: Camera, duration: Double?) {
|
|
697
|
+
withMap { map ->
|
|
698
|
+
val cameraPos = CameraPosition.Builder()
|
|
699
|
+
.target(LatLng(camera.center.latitude, camera.center.longitude))
|
|
700
|
+
.zoom(camera.zoom.toFloat())
|
|
701
|
+
.bearing(camera.heading.toFloat())
|
|
702
|
+
.tilt(camera.pitch.toFloat())
|
|
703
|
+
.build()
|
|
704
|
+
map.animateCamera(
|
|
705
|
+
CameraUpdateFactory.newCameraPosition(cameraPos),
|
|
706
|
+
(duration?.toInt() ?: 300),
|
|
707
|
+
null
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
override fun setCamera(camera: Camera) {
|
|
713
|
+
withMap { map ->
|
|
714
|
+
val cameraPos = CameraPosition.Builder()
|
|
715
|
+
.target(LatLng(camera.center.latitude, camera.center.longitude))
|
|
716
|
+
.zoom(camera.zoom.toFloat())
|
|
717
|
+
.bearing(camera.heading.toFloat())
|
|
718
|
+
.tilt(camera.pitch.toFloat())
|
|
719
|
+
.build()
|
|
720
|
+
map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPos))
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
override fun getCamera(): Camera {
|
|
725
|
+
val map = googleMap
|
|
726
|
+
return if (map != null) {
|
|
727
|
+
val pos = map.cameraPosition
|
|
728
|
+
Camera(
|
|
729
|
+
center = Coordinate(pos.target.latitude, pos.target.longitude),
|
|
730
|
+
pitch = pos.tilt.toDouble(),
|
|
731
|
+
heading = pos.bearing.toDouble(),
|
|
732
|
+
altitude = altitudeFromZoom(pos.zoom.toDouble()),
|
|
733
|
+
zoom = pos.zoom.toDouble()
|
|
734
|
+
)
|
|
735
|
+
} else {
|
|
736
|
+
Camera(
|
|
737
|
+
center = Coordinate(0.0, 0.0),
|
|
738
|
+
pitch = 0.0,
|
|
739
|
+
heading = 0.0,
|
|
740
|
+
altitude = 0.0,
|
|
741
|
+
zoom = 0.0
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
override fun getMapBoundaries(): MapBoundaries? {
|
|
747
|
+
val map = googleMap ?: return null
|
|
748
|
+
|
|
749
|
+
val bounds = map.projection.visibleRegion.latLngBounds
|
|
750
|
+
return MapBoundaries(
|
|
751
|
+
northEast = Coordinate(bounds.northeast.latitude, bounds.northeast.longitude),
|
|
752
|
+
southWest = Coordinate(bounds.southwest.latitude, bounds.southwest.longitude)
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
override fun pointForCoordinate(coordinate: Coordinate): Point {
|
|
757
|
+
val map = googleMap ?: return Point(0.0, 0.0)
|
|
758
|
+
val screenPoint = map.projection.toScreenLocation(
|
|
759
|
+
LatLng(coordinate.latitude, coordinate.longitude)
|
|
760
|
+
)
|
|
761
|
+
return Point(screenPoint.x.toDouble(), screenPoint.y.toDouble())
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
override fun coordinateForPoint(point: Point): Coordinate {
|
|
765
|
+
val map = googleMap ?: return Coordinate(0.0, 0.0)
|
|
766
|
+
val latLng = map.projection.fromScreenLocation(
|
|
767
|
+
android.graphics.Point(point.x.toInt(), point.y.toInt())
|
|
768
|
+
)
|
|
769
|
+
return Coordinate(latLng.latitude, latLng.longitude)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// MARK: - Helper Methods
|
|
773
|
+
|
|
774
|
+
fun getCurrentRegion(): Region {
|
|
775
|
+
val map = googleMap ?: return Region(DEFAULT_LATITUDE, DEFAULT_LONGITUDE, 0.15, 0.15)
|
|
776
|
+
|
|
777
|
+
val bounds = map.projection.visibleRegion.latLngBounds
|
|
778
|
+
val center = map.cameraPosition.target
|
|
779
|
+
|
|
780
|
+
return Region(
|
|
781
|
+
latitude = center.latitude,
|
|
782
|
+
longitude = center.longitude,
|
|
783
|
+
latitudeDelta = bounds.northeast.latitude - bounds.southwest.latitude,
|
|
784
|
+
longitudeDelta = bounds.northeast.longitude - bounds.southwest.longitude
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
fun calculateZoom(region: Region): Float {
|
|
789
|
+
val latDelta = region.latitudeDelta
|
|
790
|
+
val lonDelta = region.longitudeDelta
|
|
791
|
+
val delta = maxOf(latDelta, lonDelta)
|
|
792
|
+
if (delta <= 0) return DEFAULT_ZOOM
|
|
793
|
+
|
|
794
|
+
// Same formula as iOS MapStyleProvider.zoomFromDeltas
|
|
795
|
+
val zoom = ln(360.0 / delta) / ln(2.0)
|
|
796
|
+
return zoom.coerceIn(minZoom, maxZoom).toFloat()
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Converts zoom level to approximate altitude in meters.
|
|
801
|
+
* Matches iOS MapStyleProvider.altitudeFromZoom.
|
|
802
|
+
*/
|
|
803
|
+
fun altitudeFromZoom(zoom: Double): Double {
|
|
804
|
+
val clamped = zoom.coerceIn(0.0, 21.0)
|
|
805
|
+
return BASE_ALTITUDE / 2.0.pow(clamped)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private fun convertMapType(type: MapType?): Int {
|
|
809
|
+
return when (type) {
|
|
810
|
+
MapType.SATELLITE -> GoogleMap.MAP_TYPE_SATELLITE
|
|
811
|
+
MapType.HYBRID -> GoogleMap.MAP_TYPE_HYBRID
|
|
812
|
+
MapType.STANDARD, null -> GoogleMap.MAP_TYPE_NORMAL
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|