1- import * as React from 'react' ;
2- import { GoogleMap , LoadScript , Marker } from '@react-google-maps/api' ;
3- import { Box } from '@mui/material' ;
1+ import React from 'react' ;
2+ import { GoogleMap , LoadScript , InfoWindow } from '@react-google-maps/api' ;
3+ import { Box , Stack } from '@mui/material' ;
4+ import {
5+ MarkerClusterer ,
6+ Cluster ,
7+ ClusterStats ,
8+ DefaultRenderer ,
9+ } from '@googlemaps/markerclusterer' ;
410
511export interface UIMarker {
612 id : string | number ;
@@ -62,7 +68,7 @@ const getMapBounds = (markers: UIMarker[]) => {
6268 return bounds ;
6369} ;
6470
65- // Fit map to its bounds after the api is loaded
71+ // Fit map to its bounds after the API is loaded
6672const onGoogleMapsApiLoad = ( map : google . maps . Map , markers : UIMarker [ ] ) => {
6773 if ( ! markers . length ) {
6874 map . setCenter ( DEFAULT_LATLNG ) ;
@@ -81,7 +87,7 @@ const onGoogleMapsApiLoad = (map: google.maps.Map, markers: UIMarker[]) => {
8187
8288 // If we run `setZoom` right after `fitBounds` the map won't refresh. With this we first wait for the map to be idle (from fitBounds), and then set the zoom level.
8389 const listener = google . maps . event . addListenerOnce ( map , 'idle' , ( ) => {
84- // Don't allow to zoom closer than the defailt detail zoom level on initial load.
90+ // Don't allow to zoom closer than the default detail zoom level on initial load.
8591 const currentZoom = map . getZoom ( ) ;
8692 if ( currentZoom != null && currentZoom > DETAIL_ZOOM_LEVEL ) {
8793 map . setZoom ( DETAIL_ZOOM_LEVEL ) ;
@@ -118,7 +124,7 @@ const BaseMap = <T extends any>({
118124 apiKey,
119125 mapClick,
120126} : MapProps < T > ) => {
121- const markers = React . useMemo (
127+ const markersData = React . useMemo (
122128 ( ) =>
123129 data
124130 . map ( ( entry ) => {
@@ -139,12 +145,128 @@ const BaseMap = <T extends any>({
139145 return null ;
140146 }
141147
142- return marker ;
148+ return marker as UIMarker ;
143149 } )
144- . filter ( ( x ) => ! ! x ) as UIMarker [ ] ,
150+ . filter ( ( x ) : x is UIMarker => ! ! x ) ,
145151 [ data , dataMap , getIcon , onItemClick ] ,
146152 ) ;
147153
154+ const mapRef = React . useRef < google . maps . Map | null > ( null ) ;
155+ const markerClustererRef = React . useRef < MarkerClusterer | null > ( null ) ;
156+ const markersRef = React . useRef < google . maps . Marker [ ] > ( [ ] ) ;
157+
158+ const [ infoWindowData , setInfoWindowData ] = React . useState < {
159+ position : google . maps . LatLng | google . maps . LatLngLiteral ;
160+ content : React . ReactNode ;
161+ } | null > ( null ) ;
162+
163+ const onMapLoad = React . useCallback (
164+ ( map : google . maps . Map ) => {
165+ onGoogleMapsApiLoad ( map , markersData ) ;
166+ mapRef . current = map ;
167+
168+ const googleMarkers = markersData . map ( ( marker ) => {
169+ const googleMarker = new google . maps . Marker ( {
170+ position : { lat : marker . lat , lng : marker . lng } ,
171+ title : marker . title ,
172+ icon : marker . icon ,
173+ } ) ;
174+
175+ if ( marker . click ) {
176+ googleMarker . addListener ( 'click' , marker . click ) ;
177+ }
178+
179+ return googleMarker ;
180+ } ) ;
181+
182+ markersRef . current = googleMarkers ;
183+
184+ markerClustererRef . current = new MarkerClusterer ( {
185+ markers : googleMarkers ,
186+ map,
187+ renderer : {
188+ render (
189+ cluster : Cluster ,
190+ stats : ClusterStats ,
191+ map : google . maps . Map ,
192+ ) : google . maps . Marker {
193+ const defaultRenderer = new DefaultRenderer ( ) ;
194+ const marker = defaultRenderer . render (
195+ cluster ,
196+ stats ,
197+ map ,
198+ ) as google . maps . Marker ;
199+ marker . addListener ( 'mouseover' , ( ) => {
200+ setInfoWindowData ( {
201+ position : cluster . position ,
202+ content : (
203+ < Box p = "2" >
204+ { cluster . markers ?. map ( ( marker : any ) => {
205+ const onClick = markersData . find (
206+ ( m ) => m . title === marker . title ,
207+ ) ?. click ;
208+ return (
209+ < Stack
210+ direction = "row"
211+ alignItems = "center"
212+ sx = { { cursor : onClick ? 'pointer' : 'inherit' } }
213+ onClick = { onClick }
214+ >
215+ < img src = { marker . icon } />
216+ { marker . title }
217+ </ Stack >
218+ ) ;
219+ } ) }
220+ </ Box >
221+ ) ,
222+ } ) ;
223+ } ) ;
224+
225+ return marker ;
226+ } ,
227+ } ,
228+ } ) ;
229+ } ,
230+ [ markersData , onItemClick ] ,
231+ ) ;
232+
233+ React . useEffect ( ( ) => {
234+ if ( mapRef . current && markerClustererRef . current ) {
235+ markerClustererRef . current . clearMarkers ( ) ;
236+
237+ const newGoogleMarkers = markersData . map ( ( marker ) => {
238+ const googleMarker = new google . maps . Marker ( {
239+ position : { lat : marker . lat , lng : marker . lng } ,
240+ title : marker . title ,
241+ icon : marker . icon ,
242+ } ) ;
243+
244+ if ( marker . click ) {
245+ googleMarker . addListener ( 'click' , marker . click ) ;
246+ }
247+
248+ return googleMarker ;
249+ } ) ;
250+
251+ markersRef . current = newGoogleMarkers ;
252+
253+ markerClustererRef . current . addMarkers ( newGoogleMarkers ) ;
254+ }
255+ } , [ markersData ] ) ;
256+
257+ React . useEffect ( ( ) => {
258+ return ( ) => {
259+ if ( markerClustererRef . current ) {
260+ markerClustererRef . current . clearMarkers ( ) ;
261+ }
262+ markersRef . current . forEach ( ( marker ) => marker . setMap ( null ) ) ;
263+ } ;
264+ } , [ ] ) ;
265+
266+ if ( ! data . length || ! markersData . length ) {
267+ return null ;
268+ }
269+
148270 return (
149271 < Box height = "100%" className = { className } >
150272 { apiKey && (
@@ -156,22 +278,17 @@ const BaseMap = <T extends any>({
156278 opacity : 1 ,
157279 } }
158280 options = { defaultMapOptions }
159- onLoad = { ( map ) => onGoogleMapsApiLoad ( map , markers ) }
281+ onLoad = { onMapLoad }
160282 onClick = { mapClick }
161283 >
162- { markers . map ( ( marker ) => (
163- < Marker
164- key = { marker . id }
165- position = { {
166- lat : isNaN ( marker . lat ) ? 0 : marker . lat ,
167- lng : isNaN ( marker . lng ) ? 0 : marker . lng ,
168- } }
169- clickable = { markers . length > 1 }
170- onClick = { marker . click }
171- title = { marker . title }
172- icon = { marker . icon }
173- />
174- ) ) }
284+ { infoWindowData && (
285+ < InfoWindow
286+ position = { infoWindowData . position }
287+ onCloseClick = { ( ) => setInfoWindowData ( null ) }
288+ >
289+ { infoWindowData . content }
290+ </ InfoWindow >
291+ ) }
175292 </ GoogleMap >
176293 </ LoadScript >
177294 ) }
0 commit comments