1 package expo.modules.maps.googleMaps 2 3 import android.content.Context 4 import android.widget.LinearLayout 5 import com.google.android.gms.maps.GoogleMap 6 import com.google.android.gms.maps.MapView 7 import com.google.android.gms.maps.OnMapReadyCallback 8 import com.google.android.gms.maps.model.MapStyleOptions 9 import com.google.maps.android.collections.MarkerManager 10 import expo.modules.core.interfaces.services.UIManager 11 import expo.modules.kotlin.AppContext 12 import expo.modules.kotlin.Promise 13 import expo.modules.kotlin.viewevent.EventDispatcher 14 import expo.modules.maps.* 15 import expo.modules.maps.interfaces.ExpoMapView 16 import expo.modules.maps.records.* 17 import kotlinx.coroutines.* 18 import kotlinx.coroutines.flow.MutableStateFlow 19 import kotlinx.coroutines.flow.collectLatest 20 21 class GoogleMapsView(context: Context, appContext: AppContext) : LinearLayout(context), OnMapReadyCallback, ExpoMapView { 22 23 private val mapView: MapView = MapView(context) 24 private lateinit var googleMap: GoogleMap 25 private lateinit var controls: GoogleMapsControls 26 private lateinit var gestures: GoogleMapsGestures 27 private lateinit var markers: GoogleMapsMarkers 28 private lateinit var clusters: GoogleMapsClusters 29 private lateinit var polygons: GoogleMapsPolygons 30 private lateinit var polylines: GoogleMapsPolylines 31 private lateinit var circles: GoogleMapsCircles 32 private lateinit var kmls: GoogleMapsKMLs 33 private lateinit var geojsons: GoogleMapsGeoJsons 34 private lateinit var markerManager: MarkerManager 35 private lateinit var overlays: GoogleMapsOverlays 36 private lateinit var heatmaps: GoogleMapsHeatmaps 37 private lateinit var places: GoogleMapsPlaces 38 private lateinit var callbacks: GoogleMapsCallbacks 39 private lateinit var cameraAnimations: GoogleMapsCameraAnimations 40 41 private val mapReady = MutableStateFlow(false) 42 private var wasInitialCameraPositionSet = false 43 44 private val onMapLoaded by EventDispatcher<Unit>() 45 private val onMapPress by EventDispatcher<LatLngRecord>() 46 private val onLongPress by EventDispatcher<LatLngRecord>() 47 private val onRegionChange by EventDispatcher<CameraPositionRecord>() 48 private val onRegionChangeStarted by EventDispatcher<CameraPositionRecord>() 49 private val onRegionChangeComplete by EventDispatcher<CameraPositionRecord>() 50 private val onPoiClick by EventDispatcher<PointOfInterestRecord>() 51 private val onMarkerPress by EventDispatcher<MarkerRecord>() 52 private val onMarkerDrag by EventDispatcher<MarkerRecord>() 53 private val onMarkerDragStarted by EventDispatcher<MarkerRecord>() 54 private val onMarkerDragComplete by EventDispatcher<MarkerRecord>() 55 private val onClusterPress by EventDispatcher<ClusterRecord>() 56 private val onLocationButtonPress by EventDispatcher<UserLocationRecord>() 57 private val onLocationDotPress by EventDispatcher<UserLocationRecord>() 58 private val onLocationChange by EventDispatcher<UserLocationRecord>() 59 60 val lifecycleEventListener = MapViewLifecycleEventListener(mapView) 61 62 init { 63 mapView.onCreate(null) 64 mapView.getMapAsync(this) 65 mapView.onStart() 66 mapView.onResume() 67 addView(mapView) 68 69 appContext.legacyModule<UIManager>() 70 ?.registerLifecycleEventListener(lifecycleEventListener) 71 } 72 onMapReadynull73 override fun onMapReady(googleMap: GoogleMap) { 74 this.googleMap = googleMap 75 markerManager = MarkerManager(googleMap) 76 controls = GoogleMapsControls(googleMap) 77 gestures = GoogleMapsGestures(googleMap) 78 markers = GoogleMapsMarkers(googleMap, markerManager) 79 clusters = GoogleMapsClusters(context, googleMap, markerManager, onClusterPress, onMarkerPress) 80 polygons = GoogleMapsPolygons(googleMap) 81 polylines = GoogleMapsPolylines(googleMap) 82 circles = GoogleMapsCircles(googleMap) 83 kmls = GoogleMapsKMLs(context, googleMap) 84 geojsons = GoogleMapsGeoJsons(googleMap) 85 overlays = GoogleMapsOverlays(googleMap) 86 heatmaps = GoogleMapsHeatmaps(googleMap) 87 places = GoogleMapsPlaces(context, googleMap, markers) 88 callbacks = GoogleMapsCallbacks(googleMap, context) 89 cameraAnimations = GoogleMapsCameraAnimations(googleMap) 90 91 CoroutineScope(Dispatchers.Default).launch { 92 mapReady.emit(true) 93 } 94 95 setupCallbacks() 96 } 97 onViewDestroyednull98 fun onViewDestroyed() { 99 callbacks.removeLocationRequests() 100 } 101 setShowZoomControlnull102 fun setShowZoomControl(enable: Boolean) { 103 updateMap { 104 controls.setShowZoomControl(enable) 105 } 106 } 107 setShowCompassnull108 fun setShowCompass(enable: Boolean) { 109 updateMap { 110 controls.setShowCompass(enable) 111 } 112 } 113 setShowMapToolbarnull114 fun setShowMapToolbar(enable: Boolean) { 115 updateMap { 116 controls.setShowMapToolbar(enable) 117 } 118 } 119 setShowMyLocationButtonnull120 fun setShowMyLocationButton(enable: Boolean) { 121 updateMap { 122 controls.setShowMyLocationButton(enable) 123 } 124 } 125 setShowLevelPickernull126 fun setShowLevelPicker(enable: Boolean) { 127 updateMap { 128 controls.setShowLevelPicker(enable) 129 } 130 } 131 setEnabledRotateGesturesnull132 fun setEnabledRotateGestures(enabled: Boolean) { 133 updateMap { 134 gestures.setEnabledRotateGesture(enabled) 135 } 136 } 137 setEnabledScrollGesturesnull138 fun setEnabledScrollGestures(enabled: Boolean) { 139 updateMap { 140 gestures.setEnabledScrollGesture(enabled) 141 } 142 } 143 setEnabledTiltGesturesnull144 fun setEnabledTiltGestures(enabled: Boolean) { 145 updateMap { 146 gestures.setEnabledTiltGesture(enabled) 147 } 148 } 149 setEnabledZoomGesturesnull150 fun setEnabledZoomGestures(enabled: Boolean) { 151 updateMap { 152 gestures.setEnabledZoomGesture(enabled) 153 } 154 } 155 fetchPlacesSearchCompletionsnull156 fun fetchPlacesSearchCompletions(searchQueryFragment: String, promise: Promise) { 157 updateMap { 158 places.fetchSearchCompletions(searchQueryFragment, promise) 159 } 160 } 161 createPlaceSearchRequestnull162 fun createPlaceSearchRequest(place: String) { 163 updateMap { 164 places.createSearchRequest(place) 165 } 166 } 167 setClickablePOIsnull168 fun setClickablePOIs(arePOIClickable: Boolean) { 169 updateMap { 170 places.setClickablePOIs(arePOIClickable) 171 } 172 } 173 setLocationCallbackPrioritynull174 fun setLocationCallbackPriority(priority: Int) { 175 updateMap { 176 callbacks.setLocationCallbackPriority(priority) 177 } 178 } 179 setLocationCallbackIntervalnull180 fun setLocationCallbackInterval(interval: Long) { 181 updateMap { 182 callbacks.setLocationCallbackInterval(interval) 183 } 184 } 185 moveCameranull186 override fun moveCamera(cameraMove: CameraMoveRecord, promise: Promise?) { 187 updateMap { 188 cameraAnimations.moveCamera(cameraMove, promise) 189 } 190 } 191 setMapTypenull192 override fun setMapType(mapType: MapType) { 193 val googleMapType = when (mapType) { 194 MapType.Normal -> GoogleMap.MAP_TYPE_NORMAL 195 MapType.Terrain -> GoogleMap.MAP_TYPE_TERRAIN 196 MapType.Satellite -> GoogleMap.MAP_TYPE_SATELLITE 197 MapType.Hybrid -> GoogleMap.MAP_TYPE_HYBRID 198 } 199 200 updateMap { 201 googleMap.mapType = googleMapType 202 } 203 } 204 setMapStylenull205 fun setMapStyle(jsonStyleString: String) { 206 if (jsonStyleString.isNotEmpty()) { 207 updateMap { 208 googleMap.setMapStyle(MapStyleOptions(jsonStyleString)) 209 } 210 } else { 211 updateMap { 212 googleMap.setMapStyle(null) 213 } 214 } 215 } 216 setMarkersnull217 override fun setMarkers(markerObjects: Array<MarkerObject>) { 218 updateMap { 219 markers.setMarkers(markerObjects) 220 } 221 } 222 setPolygonsnull223 override fun setPolygons(polygonObjects: Array<PolygonObject>) { 224 updateMap { 225 polygons.setPolygons(polygonObjects) 226 } 227 } 228 setPolylinesnull229 override fun setPolylines(polylineObjects: Array<PolylineObject>) { 230 updateMap { 231 polylines.setPolylines(polylineObjects) 232 } 233 } 234 setCirclesnull235 override fun setCircles(circleObjects: Array<CircleObject>) { 236 updateMap { 237 circles.setCircles(circleObjects) 238 } 239 } 240 setInitialCameraPositionnull241 override fun setInitialCameraPosition(initialCameraPosition: CameraMoveRecord) { 242 if (!wasInitialCameraPositionSet) { 243 updateMap { 244 moveCamera(initialCameraPosition, null) 245 } 246 wasInitialCameraPositionSet = true 247 } 248 } 249 setClustersnull250 override fun setClusters(clusterObjects: Array<ClusterObject>) { 251 updateMap { 252 clusters.setClusters(clusterObjects) 253 } 254 } 255 setEnabledTrafficnull256 override fun setEnabledTraffic(enableTraffic: Boolean) { 257 updateMap { 258 googleMap.isTrafficEnabled = enableTraffic 259 } 260 } 261 setKMLsnull262 override fun setKMLs(kmlObjects: Array<KMLObject>) { 263 updateMap { 264 kmls.setKMLs(kmlObjects) 265 } 266 } 267 setGeoJsonsnull268 override fun setGeoJsons(geoJsonObjects: Array<GeoJsonObject>) { 269 updateMap { 270 geojsons.setGeoJsons(geoJsonObjects) 271 } 272 } 273 setHeatmapsnull274 override fun setHeatmaps(heatmapObjects: Array<HeatmapObject>) { 275 updateMap { 276 heatmaps.setHeatmaps(heatmapObjects) 277 } 278 } 279 setOverlaysnull280 override fun setOverlays(overlayObjects: Array<OverlayObject>) { 281 updateMap { 282 overlays.setOverlays(overlayObjects) 283 } 284 } 285 setupCallbacksnull286 private fun setupCallbacks() { 287 callbacks.setupOnMapPress(onMapPress) 288 callbacks.setupOnMapLoaded(onMapLoaded) 289 callbacks.setupOnRegionChange(onRegionChange) 290 callbacks.setupOnRegionChangeStarted(onRegionChangeStarted) 291 callbacks.setupOnRegionChangeComplete(onRegionChangeComplete, clusters) 292 callbacks.setupOnPoiClick(onPoiClick) 293 callbacks.setupOnLongPress(onLongPress) 294 callbacks.setupOnLocationButtonButtonPress(onLocationButtonPress) 295 callbacks.setupOnLocationDotPress(onLocationDotPress) 296 callbacks.setupOnLocationChange(onLocationChange) 297 298 markers.setOnMarkerPressListener(onMarkerPress) 299 markers.setOnMarkerDragListeners(onMarkerDrag, onMarkerDragStarted, onMarkerDragComplete) 300 } 301 302 /* 303 Calls function provided as an argument when OnMapReadyCallback fires, 304 subscribes to StateFlow in a background but calls lambda on a main thread. 305 After calling lambda the subscription is canceled. 306 StateFlow holds the latest value so even if updateMap is called after 307 OnMapReadyCallback, StateFlow emits the latest value letting provided lambda to be executed. 308 */ updateMapnull309 private fun updateMap(update: () -> Unit) { 310 CoroutineScope(Dispatchers.IO).launch { 311 mapReady.collectLatest { 312 if (it) { 313 withContext(Dispatchers.Main) { 314 update() 315 } 316 cancel() 317 } 318 } 319 } 320 } 321 } 322