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