UNPKG

@vis.gl/react-google-maps

Version:

React components and hooks for the Google Maps JavaScript API

1 lines 168 kB
{"version":3,"file":"index.modern.mjs","sources":["../src/libraries/api-loading-status.ts","../src/libraries/google-maps-api-loader.ts","../src/components/api-provider.tsx","../src/components/map/use-map-events.ts","../src/hooks/use-memoized.ts","../src/hooks/use-custom-compare-efffect.ts","../src/hooks/use-deep-compare-effect.ts","../src/components/map/use-map-options.ts","../src/hooks/use-api-loading-status.ts","../src/components/map/use-deckgl-camera-update.ts","../src/libraries/lat-lng-utils.ts","../src/components/map/use-map-camera-params.ts","../src/components/map/auth-failure-message.tsx","../src/hooks/use-callback-ref.ts","../src/hooks/use-api-is-loaded.ts","../src/hooks/use-force-update.ts","../src/components/map/use-tracked-camera-state-ref.ts","../src/components/map/use-map-instance.ts","../src/components/map/index.tsx","../src/libraries/errors.ts","../src/hooks/use-map.ts","../src/hooks/use-maps-library.ts","../src/hooks/use-maps-event-listener.ts","../src/hooks/use-prop-binding.ts","../src/hooks/use-dom-event-listener.ts","../src/components/advanced-marker.tsx","../src/libraries/set-value-for-styles.ts","../src/components/info-window.tsx","../src/libraries/create-static-maps-url/helpers.ts","../src/libraries/create-static-maps-url/assemble-marker-params.ts","../src/libraries/create-static-maps-url/assemble-path-params.ts","../src/libraries/create-static-maps-url/assemble-map-type-styles.ts","../src/libraries/create-static-maps-url/index.ts","../src/components/static-map.tsx","../src/components/map-control.tsx","../src/components/marker.tsx","../src/components/pin.tsx","../src/libraries/limit-tilt-range.ts"],"sourcesContent":["export const APILoadingStatus = {\n NOT_LOADED: 'NOT_LOADED',\n LOADING: 'LOADING',\n LOADED: 'LOADED',\n FAILED: 'FAILED',\n AUTH_FAILURE: 'AUTH_FAILURE'\n} as const;\nexport type APILoadingStatus =\n (typeof APILoadingStatus)[keyof typeof APILoadingStatus];\n","import {APILoadingStatus} from './api-loading-status';\n\nexport type ApiParams = {\n key: string;\n v?: string;\n language?: string;\n region?: string;\n libraries?: string;\n channel?: number;\n solutionChannel?: string;\n authReferrerPolicy?: string;\n};\n\ntype LoadingStatusCallback = (status: APILoadingStatus) => void;\n\nconst MAPS_API_BASE_URL = 'https://maps.googleapis.com/maps/api/js';\n\n/**\n * A GoogleMapsApiLoader to reliably load and unload the Google Maps JavaScript API.\n *\n * The actual loading and unloading is delayed into the microtask queue, to\n * allow using the API in an useEffect hook, without worrying about multiple API loads.\n */\nexport class GoogleMapsApiLoader {\n /**\n * The current loadingStatus of the API.\n */\n public static loadingStatus: APILoadingStatus = APILoadingStatus.NOT_LOADED;\n\n /**\n * The parameters used for first loading the API.\n */\n public static serializedApiParams?: string;\n\n /**\n * A list of functions to be notified when the loading status changes.\n */\n private static listeners: LoadingStatusCallback[] = [];\n\n /**\n * Loads the Maps JavaScript API with the specified parameters.\n * Since the Maps library can only be loaded once per page, this will\n * produce a warning when called multiple times with different\n * parameters.\n *\n * The returned promise resolves when loading completes\n * and rejects in case of an error or when the loading was aborted.\n */\n static async load(\n params: ApiParams,\n onLoadingStatusChange: (status: APILoadingStatus) => void\n ): Promise<void> {\n const libraries = params.libraries ? params.libraries.split(',') : [];\n const serializedParams = this.serializeParams(params);\n\n this.listeners.push(onLoadingStatusChange);\n\n // Note: if `google.maps.importLibrary` has been defined externally, we\n // assume that loading is complete and successful.\n // If it was defined by a previous call to this method, a warning\n // message is logged if there are differences in api-parameters used\n // for both calls.\n\n if (window.google?.maps?.importLibrary as unknown) {\n // no serialized parameters means it was loaded externally\n if (!this.serializedApiParams) {\n this.loadingStatus = APILoadingStatus.LOADED;\n }\n this.notifyLoadingStatusListeners();\n } else {\n this.serializedApiParams = serializedParams;\n this.initImportLibrary(params);\n }\n\n if (\n this.serializedApiParams &&\n this.serializedApiParams !== serializedParams\n ) {\n console.warn(\n `[google-maps-api-loader] The maps API has already been loaded ` +\n `with different parameters and will not be loaded again. Refresh the ` +\n `page for new values to have effect.`\n );\n }\n\n const librariesToLoad = ['maps', ...libraries];\n await Promise.all(\n librariesToLoad.map(name => google.maps.importLibrary(name))\n );\n }\n\n /**\n * Serialize the parameters used to load the library for easier comparison.\n */\n private static serializeParams(params: ApiParams): string {\n return [\n params.v,\n params.key,\n params.language,\n params.region,\n params.authReferrerPolicy,\n params.solutionChannel\n ].join('/');\n }\n\n /**\n * Creates the global `google.maps.importLibrary` function for bootstrapping.\n * This is essentially a formatted version of the dynamic loading script\n * from the official documentation with some minor adjustments.\n *\n * The created importLibrary function will load the Google Maps JavaScript API,\n * which will then replace the `google.maps.importLibrary` function with the full\n * implementation.\n *\n * @see https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import\n */\n private static initImportLibrary(params: ApiParams) {\n if (!window.google) window.google = {} as never;\n if (!window.google.maps) window.google.maps = {} as never;\n\n if (window.google.maps['importLibrary']) {\n console.error(\n '[google-maps-api-loader-internal]: initImportLibrary must only be called once'\n );\n\n return;\n }\n\n let apiPromise: Promise<void> | null = null;\n\n const loadApi = () => {\n if (apiPromise) return apiPromise;\n\n apiPromise = new Promise((resolve, reject) => {\n const scriptElement = document.createElement('script');\n const urlParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n const urlParamName = key.replace(\n /[A-Z]/g,\n t => '_' + t[0].toLowerCase()\n );\n urlParams.set(urlParamName, String(value));\n }\n urlParams.set('loading', 'async');\n urlParams.set('callback', '__googleMapsCallback__');\n\n scriptElement.async = true;\n scriptElement.src = MAPS_API_BASE_URL + `?` + urlParams.toString();\n scriptElement.nonce =\n (document.querySelector('script[nonce]') as HTMLScriptElement)\n ?.nonce || '';\n\n scriptElement.onerror = () => {\n this.loadingStatus = APILoadingStatus.FAILED;\n this.notifyLoadingStatusListeners();\n reject(new Error('The Google Maps JavaScript API could not load.'));\n };\n\n window.__googleMapsCallback__ = () => {\n this.loadingStatus = APILoadingStatus.LOADED;\n this.notifyLoadingStatusListeners();\n resolve();\n };\n\n window.gm_authFailure = () => {\n this.loadingStatus = APILoadingStatus.AUTH_FAILURE;\n this.notifyLoadingStatusListeners();\n };\n\n this.loadingStatus = APILoadingStatus.LOADING;\n this.notifyLoadingStatusListeners();\n\n document.head.append(scriptElement);\n });\n\n return apiPromise;\n };\n\n // for the first load, we declare an importLibrary function that will\n // be overwritten once the api is loaded.\n google.maps.importLibrary = libraryName =>\n loadApi().then(() => google.maps.importLibrary(libraryName));\n }\n\n /**\n * Calls all registered loadingStatusListeners after a status update.\n */\n private static notifyLoadingStatusListeners() {\n for (const fn of this.listeners) {\n fn(this.loadingStatus);\n }\n }\n}\n\n// Declare global maps callback functions\ndeclare global {\n interface Window {\n __googleMapsCallback__?: () => void;\n gm_authFailure?: () => void;\n }\n}\n","import React, {\n FunctionComponent,\n PropsWithChildren,\n useCallback,\n useEffect,\n useMemo,\n useReducer,\n useState\n} from 'react';\n\nimport {\n ApiParams,\n GoogleMapsApiLoader\n} from '../libraries/google-maps-api-loader';\nimport {APILoadingStatus} from '../libraries/api-loading-status';\n\ntype ImportLibraryFunction = typeof google.maps.importLibrary;\ntype GoogleMapsLibrary = Awaited<ReturnType<ImportLibraryFunction>>;\ntype LoadedLibraries = {[name: string]: GoogleMapsLibrary};\n\nexport interface APIProviderContextValue {\n status: APILoadingStatus;\n loadedLibraries: LoadedLibraries;\n importLibrary: typeof google.maps.importLibrary;\n mapInstances: Record<string, google.maps.Map>;\n addMapInstance: (map: google.maps.Map, id?: string) => void;\n removeMapInstance: (id?: string) => void;\n clearMapInstances: () => void;\n}\n\nconst DEFAULT_SOLUTION_CHANNEL = 'GMP_visgl_rgmlibrary_v1_default';\n\nexport const APIProviderContext =\n React.createContext<APIProviderContextValue | null>(null);\n\nexport type APIProviderProps = PropsWithChildren<{\n /**\n * apiKey must be provided to load the Google Maps JavaScript API. To create an API key, see: https://developers.google.com/maps/documentation/javascript/get-api-key\n * Part of:\n */\n apiKey: string;\n /**\n * A custom id to reference the script tag can be provided. The default is set to 'google-maps-api'\n * @default 'google-maps-api'\n */\n libraries?: Array<string>;\n /**\n * A specific version of the Google Maps JavaScript API can be used.\n * Read more about versioning: https://developers.google.com/maps/documentation/javascript/versions\n * Part of: https://developers.google.com/maps/documentation/javascript/url-params\n */\n version?: string;\n /**\n * Sets the map to a specific region.\n * Read more about localizing the Map: https://developers.google.com/maps/documentation/javascript/localization\n * Part of: https://developers.google.com/maps/documentation/javascript/url-params\n */\n region?: string;\n /**\n * Use a specific language for the map.\n * Read more about localizing the Map: https://developers.google.com/maps/documentation/javascript/localization\n * Part of: https://developers.google.com/maps/documentation/javascript/url-params\n */\n language?: string;\n /**\n * auth_referrer_policy can be set to 'origin'.\n * Part of: https://developers.google.com/maps/documentation/javascript/url-params\n */\n authReferrerPolicy?: string;\n /**\n * To understand usage and ways to improve our solutions, Google includes the\n * `solution_channel` query parameter in API calls to gather information about\n * code usage. You may opt out at any time by setting this attribute to an\n * empty string. Read more in the\n * [documentation](https://developers.google.com/maps/reporting-and-monitoring/reporting#solutions-usage).\n */\n channel?: number;\n /**\n * To track usage of Google Maps JavaScript API via numeric channels. The only acceptable channel values are numbers from 0-999.\n * Read more in the\n * [documentation](https://developers.google.com/maps/reporting-and-monitoring/reporting#usage-tracking-per-channel)\n */\n solutionChannel?: string;\n /**\n * A function that can be used to execute code after the Google Maps JavaScript API has been loaded.\n */\n onLoad?: () => void;\n /**\n * A function that will be called if there was an error when loading the Google Maps JavaScript API.\n */\n onError?: (error: unknown) => void;\n}>;\n\n/**\n * local hook to set up the map-instance management context.\n */\nfunction useMapInstances() {\n const [mapInstances, setMapInstances] = useState<\n Record<string, google.maps.Map>\n >({});\n\n const addMapInstance = (mapInstance: google.maps.Map, id = 'default') => {\n setMapInstances(instances => ({...instances, [id]: mapInstance}));\n };\n\n const removeMapInstance = (id = 'default') => {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n setMapInstances(({[id]: _, ...remaining}) => remaining);\n };\n\n const clearMapInstances = () => {\n setMapInstances({});\n };\n\n return {mapInstances, addMapInstance, removeMapInstance, clearMapInstances};\n}\n\n/**\n * local hook to handle the loading of the maps API, returns the current loading status\n * @param props\n */\nfunction useGoogleMapsApiLoader(props: APIProviderProps) {\n const {\n onLoad,\n onError,\n apiKey,\n version,\n libraries = [],\n ...otherApiParams\n } = props;\n\n const [status, setStatus] = useState<APILoadingStatus>(\n GoogleMapsApiLoader.loadingStatus\n );\n const [loadedLibraries, addLoadedLibrary] = useReducer(\n (\n loadedLibraries: LoadedLibraries,\n action: {name: keyof LoadedLibraries; value: LoadedLibraries[string]}\n ) => {\n return loadedLibraries[action.name]\n ? loadedLibraries\n : {...loadedLibraries, [action.name]: action.value};\n },\n {}\n );\n\n const librariesString = useMemo(() => libraries?.join(','), [libraries]);\n const serializedParams = useMemo(\n () => JSON.stringify({apiKey, version, ...otherApiParams}),\n [apiKey, version, otherApiParams]\n );\n\n const importLibrary: typeof google.maps.importLibrary = useCallback(\n async (name: string) => {\n if (loadedLibraries[name]) {\n return loadedLibraries[name];\n }\n\n if (!google?.maps?.importLibrary) {\n throw new Error(\n '[api-provider-internal] importLibrary was called before ' +\n 'google.maps.importLibrary was defined.'\n );\n }\n\n const res = await window.google.maps.importLibrary(name);\n addLoadedLibrary({name, value: res});\n\n return res;\n },\n [loadedLibraries]\n );\n\n useEffect(\n () => {\n (async () => {\n try {\n const params: ApiParams = {key: apiKey, ...otherApiParams};\n if (version) params.v = version;\n if (librariesString?.length > 0) params.libraries = librariesString;\n\n if (\n params.channel === undefined ||\n params.channel < 0 ||\n params.channel > 999\n )\n delete params.channel;\n\n if (params.solutionChannel === undefined)\n params.solutionChannel = DEFAULT_SOLUTION_CHANNEL;\n else if (params.solutionChannel === '') delete params.solutionChannel;\n\n await GoogleMapsApiLoader.load(params, status => setStatus(status));\n\n for (const name of ['core', 'maps', ...libraries]) {\n await importLibrary(name);\n }\n\n if (onLoad) {\n onLoad();\n }\n } catch (error) {\n if (onError) {\n onError(error);\n } else {\n console.error(\n '<ApiProvider> failed to load the Google Maps JavaScript API',\n error\n );\n }\n }\n })();\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [apiKey, librariesString, serializedParams]\n );\n\n return {\n status,\n loadedLibraries,\n importLibrary\n };\n}\n\n/**\n * Component to wrap the components from this library and load the Google Maps JavaScript API\n */\nexport const APIProvider: FunctionComponent<APIProviderProps> = props => {\n const {children, ...loaderProps} = props;\n const {mapInstances, addMapInstance, removeMapInstance, clearMapInstances} =\n useMapInstances();\n\n const {status, loadedLibraries, importLibrary} =\n useGoogleMapsApiLoader(loaderProps);\n\n const contextValue: APIProviderContextValue = useMemo(\n () => ({\n mapInstances,\n addMapInstance,\n removeMapInstance,\n clearMapInstances,\n status,\n loadedLibraries,\n importLibrary\n }),\n [\n mapInstances,\n addMapInstance,\n removeMapInstance,\n clearMapInstances,\n status,\n loadedLibraries,\n importLibrary\n ]\n );\n\n return (\n <APIProviderContext.Provider value={contextValue}>\n {children}\n </APIProviderContext.Provider>\n );\n};\n","import {useEffect} from 'react';\n\n/**\n * Handlers for all events that could be emitted by map-instances.\n */\nexport type MapEventProps = Partial<{\n // map view state events\n onBoundsChanged: (event: MapCameraChangedEvent) => void;\n onCenterChanged: (event: MapCameraChangedEvent) => void;\n onHeadingChanged: (event: MapCameraChangedEvent) => void;\n onTiltChanged: (event: MapCameraChangedEvent) => void;\n onZoomChanged: (event: MapCameraChangedEvent) => void;\n onCameraChanged: (event: MapCameraChangedEvent) => void;\n\n // mouse / touch / pointer events\n onClick: (event: MapMouseEvent) => void;\n onDblclick: (event: MapMouseEvent) => void;\n onContextmenu: (event: MapMouseEvent) => void;\n onMousemove: (event: MapMouseEvent) => void;\n onMouseover: (event: MapMouseEvent) => void;\n onMouseout: (event: MapMouseEvent) => void;\n onDrag: (event: MapEvent) => void;\n onDragend: (event: MapEvent) => void;\n onDragstart: (event: MapEvent) => void;\n\n // loading events\n onTilesLoaded: (event: MapEvent) => void;\n onIdle: (event: MapEvent) => void;\n\n // configuration events\n onProjectionChanged: (event: MapEvent) => void;\n onIsFractionalZoomEnabledChanged: (event: MapEvent) => void;\n onMapCapabilitiesChanged: (event: MapEvent) => void;\n onMapTypeIdChanged: (event: MapEvent) => void;\n onRenderingTypeChanged: (event: MapEvent) => void;\n}>;\n\n/**\n * Sets up effects to bind event-handlers for all event-props in MapEventProps.\n * @internal\n */\nexport function useMapEvents(\n map: google.maps.Map | null,\n props: MapEventProps\n) {\n // note: calling a useEffect hook from within a loop is prohibited by the\n // rules of hooks, but it's ok here since it's unconditional and the number\n // and order of iterations is always strictly the same.\n // (see https://legacy.reactjs.org/docs/hooks-rules.html)\n\n for (const propName of eventPropNames) {\n // fixme: this cast is essentially a 'trust me, bro' for typescript, but\n // a proper solution seems way too complicated right now\n const handler = props[propName] as (ev: MapEvent) => void;\n const eventType = propNameToEventType[propName];\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useEffect(() => {\n if (!map) return;\n if (!handler) return;\n\n const listener = google.maps.event.addListener(\n map,\n eventType,\n (ev?: google.maps.MapMouseEvent | google.maps.IconMouseEvent) => {\n handler(createMapEvent(eventType, map, ev));\n }\n );\n\n return () => listener.remove();\n }, [map, eventType, handler]);\n }\n}\n\n/**\n * Create the wrapped map-events used for the event-props.\n * @param type the event type as it is specified to the maps api\n * @param map the map instance the event originates from\n * @param srcEvent the source-event if there is one.\n */\nfunction createMapEvent(\n type: string,\n map: google.maps.Map,\n srcEvent?: google.maps.MapMouseEvent | google.maps.IconMouseEvent\n): MapEvent {\n const ev: MapEvent = {\n type,\n map,\n detail: {},\n stoppable: false,\n stop: () => {}\n };\n\n if (cameraEventTypes.includes(type)) {\n const camEvent = ev as MapCameraChangedEvent;\n\n const center = map.getCenter();\n const zoom = map.getZoom();\n const heading = map.getHeading() || 0;\n const tilt = map.getTilt() || 0;\n const bounds = map.getBounds();\n\n if (!center || !bounds || !Number.isFinite(zoom)) {\n console.warn(\n '[createEvent] at least one of the values from the map ' +\n 'returned undefined. This is not expected to happen. Please ' +\n 'report an issue at https://github.com/visgl/react-google-maps/issues/new'\n );\n }\n\n camEvent.detail = {\n center: center?.toJSON() || {lat: 0, lng: 0},\n zoom: (zoom as number) || 0,\n heading: heading as number,\n tilt: tilt as number,\n bounds: bounds?.toJSON() || {\n north: 90,\n east: 180,\n south: -90,\n west: -180\n }\n };\n\n return camEvent;\n } else if (mouseEventTypes.includes(type)) {\n if (!srcEvent)\n throw new Error('[createEvent] mouse events must provide a srcEvent');\n const mouseEvent = ev as MapMouseEvent;\n\n mouseEvent.domEvent = srcEvent.domEvent;\n mouseEvent.stoppable = true;\n mouseEvent.stop = () => srcEvent.stop();\n\n mouseEvent.detail = {\n latLng: srcEvent.latLng?.toJSON() || null,\n placeId: (srcEvent as google.maps.IconMouseEvent).placeId\n };\n\n return mouseEvent;\n }\n\n return ev;\n}\n\n/**\n * maps the camelCased names of event-props to the corresponding event-types\n * used in the maps API.\n */\nconst propNameToEventType: {[prop in keyof Required<MapEventProps>]: string} = {\n onBoundsChanged: 'bounds_changed',\n onCenterChanged: 'center_changed',\n onClick: 'click',\n onContextmenu: 'contextmenu',\n onDblclick: 'dblclick',\n onDrag: 'drag',\n onDragend: 'dragend',\n onDragstart: 'dragstart',\n onHeadingChanged: 'heading_changed',\n onIdle: 'idle',\n onIsFractionalZoomEnabledChanged: 'isfractionalzoomenabled_changed',\n onMapCapabilitiesChanged: 'mapcapabilities_changed',\n onMapTypeIdChanged: 'maptypeid_changed',\n onMousemove: 'mousemove',\n onMouseout: 'mouseout',\n onMouseover: 'mouseover',\n onProjectionChanged: 'projection_changed',\n onRenderingTypeChanged: 'renderingtype_changed',\n onTilesLoaded: 'tilesloaded',\n onTiltChanged: 'tilt_changed',\n onZoomChanged: 'zoom_changed',\n\n // note: onCameraChanged is an alias for the bounds_changed event,\n // since that is going to be fired in every situation where the camera is\n // updated.\n onCameraChanged: 'bounds_changed'\n} as const;\n\nconst cameraEventTypes = [\n 'bounds_changed',\n 'center_changed',\n 'heading_changed',\n 'tilt_changed',\n 'zoom_changed'\n];\n\nconst mouseEventTypes = [\n 'click',\n 'contextmenu',\n 'dblclick',\n 'mousemove',\n 'mouseout',\n 'mouseover'\n];\n\ntype MapEventPropName = keyof MapEventProps;\nconst eventPropNames = Object.keys(propNameToEventType) as MapEventPropName[];\n\nexport type MapEvent<T = unknown> = {\n type: string;\n map: google.maps.Map;\n detail: T;\n\n stoppable: boolean;\n stop: () => void;\n domEvent?: MouseEvent | TouchEvent | PointerEvent | KeyboardEvent | Event;\n};\n\nexport type MapMouseEvent = MapEvent<{\n latLng: google.maps.LatLngLiteral | null;\n placeId: string | null;\n}>;\n\nexport type MapCameraChangedEvent = MapEvent<{\n center: google.maps.LatLngLiteral;\n bounds: google.maps.LatLngBoundsLiteral;\n zoom: number;\n heading: number;\n tilt: number;\n}>;\n","import {useRef} from 'react';\n\nexport function useMemoized<T>(value: T, isEqual: (a: T, b: T) => boolean): T {\n const ref = useRef<T>(value);\n\n if (!isEqual(value, ref.current)) {\n ref.current = value;\n }\n\n return ref.current;\n}\n","import {DependencyList, EffectCallback, useEffect} from 'react';\nimport {useMemoized} from './use-memoized';\n\nexport function useCustomCompareEffect<T extends DependencyList>(\n effect: EffectCallback,\n dependencies: T,\n isEqual: (a: T, b: T) => boolean\n) {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(effect, [useMemoized(dependencies, isEqual)]);\n}\n","import {DependencyList, EffectCallback} from 'react';\nimport {useCustomCompareEffect} from './use-custom-compare-efffect';\nimport isDeepEqual from 'fast-deep-equal';\n\nexport function useDeepCompareEffect(\n effect: EffectCallback,\n dependencies: DependencyList\n) {\n useCustomCompareEffect(effect, dependencies, isDeepEqual);\n}\n","import {MapProps} from '../map';\nimport {useDeepCompareEffect} from '../../hooks/use-deep-compare-effect';\n\nconst mapOptionKeys: Set<keyof google.maps.MapOptions> = new Set([\n 'backgroundColor',\n 'clickableIcons',\n 'controlSize',\n 'disableDefaultUI',\n 'disableDoubleClickZoom',\n 'draggable',\n 'draggableCursor',\n 'draggingCursor',\n 'fullscreenControl',\n 'fullscreenControlOptions',\n 'gestureHandling',\n 'headingInteractionEnabled',\n 'isFractionalZoomEnabled',\n 'keyboardShortcuts',\n 'mapTypeControl',\n 'mapTypeControlOptions',\n 'mapTypeId',\n 'maxZoom',\n 'minZoom',\n 'noClear',\n 'panControl',\n 'panControlOptions',\n 'restriction',\n 'rotateControl',\n 'rotateControlOptions',\n 'scaleControl',\n 'scaleControlOptions',\n 'scrollwheel',\n 'streetView',\n 'streetViewControl',\n 'streetViewControlOptions',\n 'styles',\n 'tiltInteractionEnabled',\n 'zoomControl',\n 'zoomControlOptions'\n]);\n\n/**\n * Internal hook to update the map-options when props are changed.\n *\n * @param map the map instance\n * @param mapProps the props to update the map-instance with\n * @internal\n */\nexport function useMapOptions(map: google.maps.Map | null, mapProps: MapProps) {\n /* eslint-disable react-hooks/exhaustive-deps --\n *\n * The following effects aren't triggered when the map is changed.\n * In that case, the values will be or have been passed to the map\n * constructor via mapOptions.\n */\n\n const mapOptions: google.maps.MapOptions = {};\n const keys = Object.keys(mapProps) as (keyof google.maps.MapOptions)[];\n for (const key of keys) {\n if (!mapOptionKeys.has(key)) continue;\n\n mapOptions[key] = mapProps[key] as never;\n }\n\n // update the map options when mapOptions is changed\n // Note: due to the destructuring above, mapOptions will be seen as changed\n // with every re-render, so we're assuming the maps-api will properly\n // deal with unchanged option-values passed into setOptions.\n useDeepCompareEffect(() => {\n if (!map) return;\n\n map.setOptions(mapOptions);\n }, [mapOptions]);\n /* eslint-enable react-hooks/exhaustive-deps */\n}\n","import {useContext} from 'react';\nimport {APIProviderContext} from '../components/api-provider';\nimport {APILoadingStatus} from '../libraries/api-loading-status';\n\nexport function useApiLoadingStatus(): APILoadingStatus {\n return useContext(APIProviderContext)?.status || APILoadingStatus.NOT_LOADED;\n}\n","import {useLayoutEffect} from 'react';\n\nexport type DeckGlCompatProps = {\n /**\n * Viewport from deck.gl\n */\n viewport?: unknown;\n /**\n * View state from deck.gl\n */\n viewState?: Record<string, unknown>;\n /**\n * Initial View State from deck.gl\n */\n initialViewState?: Record<string, unknown>;\n};\n\n/**\n * Internal hook that updates the camera when deck.gl viewState changes.\n * @internal\n */\nexport function useDeckGLCameraUpdate(\n map: google.maps.Map | null,\n props: DeckGlCompatProps\n) {\n const {viewport, viewState} = props;\n const isDeckGlControlled = !!viewport;\n\n useLayoutEffect(() => {\n if (!map || !viewState) return;\n\n const {\n latitude,\n longitude,\n bearing: heading,\n pitch: tilt,\n zoom\n } = viewState as Record<string, number>;\n\n map.moveCamera({\n center: {lat: latitude, lng: longitude},\n heading,\n tilt,\n zoom: zoom + 1\n });\n }, [map, viewState]);\n\n return isDeckGlControlled;\n}\n","export function isLatLngLiteral(\n obj: unknown\n): obj is google.maps.LatLngLiteral {\n if (!obj || typeof obj !== 'object') return false;\n if (!('lat' in obj && 'lng' in obj)) return false;\n\n return Number.isFinite(obj.lat) && Number.isFinite(obj.lng);\n}\n\nexport function latLngEquals(\n a: google.maps.LatLngLiteral | google.maps.LatLng | undefined | null,\n b: google.maps.LatLngLiteral | google.maps.LatLng | undefined | null\n): boolean {\n if (!a || !b) return false;\n const A = toLatLngLiteral(a);\n const B = toLatLngLiteral(b);\n if (A.lat !== B.lat || A.lng !== B.lng) return false;\n return true;\n}\n\nexport function toLatLngLiteral(\n obj: google.maps.LatLngLiteral | google.maps.LatLng\n): google.maps.LatLngLiteral {\n if (isLatLngLiteral(obj)) return obj;\n\n return obj.toJSON();\n}\n","import {useLayoutEffect} from 'react';\nimport {CameraStateRef} from './use-tracked-camera-state-ref';\nimport {toLatLngLiteral} from '../../libraries/lat-lng-utils';\nimport {MapProps} from '../map';\n\nexport function useMapCameraParams(\n map: google.maps.Map | null,\n cameraStateRef: CameraStateRef,\n mapProps: MapProps\n) {\n const center = mapProps.center ? toLatLngLiteral(mapProps.center) : null;\n\n let lat: number | null = null;\n let lng: number | null = null;\n\n if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) {\n lat = center.lat as number;\n lng = center.lng as number;\n }\n\n const zoom: number | null = Number.isFinite(mapProps.zoom)\n ? (mapProps.zoom as number)\n : null;\n const heading: number | null = Number.isFinite(mapProps.heading)\n ? (mapProps.heading as number)\n : null;\n const tilt: number | null = Number.isFinite(mapProps.tilt)\n ? (mapProps.tilt as number)\n : null;\n\n // the following effect runs for every render of the map component and checks\n // if there are differences between the known state of the map instance\n // (cameraStateRef, which is updated by all bounds_changed events) and the\n // desired state in the props.\n\n useLayoutEffect(() => {\n if (!map) return;\n\n const nextCamera: google.maps.CameraOptions = {};\n let needsUpdate = false;\n\n if (\n lat !== null &&\n lng !== null &&\n (cameraStateRef.current.center.lat !== lat ||\n cameraStateRef.current.center.lng !== lng)\n ) {\n nextCamera.center = {lat, lng};\n needsUpdate = true;\n }\n\n if (zoom !== null && cameraStateRef.current.zoom !== zoom) {\n nextCamera.zoom = zoom as number;\n needsUpdate = true;\n }\n\n if (heading !== null && cameraStateRef.current.heading !== heading) {\n nextCamera.heading = heading as number;\n needsUpdate = true;\n }\n\n if (tilt !== null && cameraStateRef.current.tilt !== tilt) {\n nextCamera.tilt = tilt as number;\n needsUpdate = true;\n }\n\n if (needsUpdate) {\n map.moveCamera(nextCamera);\n }\n });\n}\n","import React, {CSSProperties, FunctionComponent} from 'react';\n\nexport const AuthFailureMessage: FunctionComponent = () => {\n const style: CSSProperties = {\n position: 'absolute',\n top: 0,\n left: 0,\n bottom: 0,\n right: 0,\n zIndex: 999,\n display: 'flex',\n flexFlow: 'column nowrap',\n textAlign: 'center',\n justifyContent: 'center',\n fontSize: '.8rem',\n color: 'rgba(0,0,0,0.6)',\n background: '#dddddd',\n padding: '1rem 1.5rem'\n };\n\n return (\n <div style={style}>\n <h2>Error: AuthFailure</h2>\n <p>\n A problem with your API key prevents the map from rendering correctly.\n Please make sure the value of the <code>APIProvider.apiKey</code> prop\n is correct. Check the error-message in the console for further details.\n </p>\n </div>\n );\n};\n","import {Ref, useCallback, useState} from 'react';\n\nexport function useCallbackRef<T>() {\n const [el, setEl] = useState<T | null>(null);\n const ref = useCallback((value: T) => setEl(value), [setEl]);\n\n return [el, ref as Ref<T>] as const;\n}\n","import {useApiLoadingStatus} from './use-api-loading-status';\nimport {APILoadingStatus} from '../libraries/api-loading-status';\n/**\n * Hook to check if the Maps JavaScript API is loaded\n */\nexport function useApiIsLoaded(): boolean {\n const status = useApiLoadingStatus();\n\n return status === APILoadingStatus.LOADED;\n}\n","import {useReducer} from 'react';\n\nexport function useForceUpdate(): () => void {\n const [, forceUpdate] = useReducer(x => x + 1, 0);\n\n return forceUpdate;\n}\n","import {MutableRefObject, useEffect, useRef} from 'react';\nimport {useForceUpdate} from '../../hooks/use-force-update';\n\nexport type CameraState = {\n center: google.maps.LatLngLiteral;\n heading: number;\n tilt: number;\n zoom: number;\n};\n\nexport type CameraStateRef = MutableRefObject<CameraState>;\n\nfunction handleBoundsChange(map: google.maps.Map, ref: CameraStateRef) {\n const center = map.getCenter();\n const zoom = map.getZoom();\n const heading = map.getHeading() || 0;\n const tilt = map.getTilt() || 0;\n const bounds = map.getBounds();\n\n if (!center || !bounds || !Number.isFinite(zoom)) {\n console.warn(\n '[useTrackedCameraState] at least one of the values from the map ' +\n 'returned undefined. This is not expected to happen. Please ' +\n 'report an issue at https://github.com/visgl/react-google-maps/issues/new'\n );\n }\n\n // fixme: do we need the `undefined` cases for the camera-params? When are they used in the maps API?\n Object.assign(ref.current, {\n center: center?.toJSON() || {lat: 0, lng: 0},\n zoom: (zoom as number) || 0,\n heading: heading as number,\n tilt: tilt as number\n });\n}\n\n/**\n * Creates a mutable ref object to track the last known state of the map camera.\n * This is used in `useMapCameraParams` to reduce stuttering in normal operation\n * by avoiding updates of the map camera with values that have already been processed.\n */\nexport function useTrackedCameraStateRef(\n map: google.maps.Map | null\n): CameraStateRef {\n const forceUpdate = useForceUpdate();\n const ref = useRef<CameraState>({\n center: {lat: 0, lng: 0},\n heading: 0,\n tilt: 0,\n zoom: 0\n });\n\n // Record camera state with every bounds_changed event dispatched by the map.\n // This data is used to prevent feeding these values back to the\n // map-instance when a typical \"controlled component\" setup (state variable is\n // fed into and updated by the map).\n useEffect(() => {\n if (!map) return;\n\n const listener = google.maps.event.addListener(\n map,\n 'bounds_changed',\n () => {\n handleBoundsChange(map, ref);\n\n // When an event is occured, we have to update during the next cycle.\n // The application could decide to ignore the event and not update any\n // camera props of the map, meaning that in that case we will have to\n // 'undo' the change to the camera.\n forceUpdate();\n }\n );\n\n return () => listener.remove();\n }, [map, forceUpdate]);\n\n return ref;\n}\n","import {Ref, useEffect, useRef, useState} from 'react';\n\nimport {MapProps} from '../map';\nimport {APIProviderContextValue} from '../api-provider';\n\nimport {useCallbackRef} from '../../hooks/use-callback-ref';\nimport {useApiIsLoaded} from '../../hooks/use-api-is-loaded';\nimport {\n CameraState,\n CameraStateRef,\n useTrackedCameraStateRef\n} from './use-tracked-camera-state-ref';\n\n/**\n * Stores a stack of map-instances for each mapId. Whenever an\n * instance is used, it is removed from the stack while in use,\n * and returned to the stack when the component unmounts.\n * This allows us to correctly implement caching for multiple\n * maps om the same page, while reusing as much as possible.\n *\n * FIXME: while it should in theory be possible to reuse maps solely\n * based on the mapId (as all other parameters can be changed at\n * runtime), we don't yet have good enough tracking of options to\n * reliably unset all the options that have been set.\n */\nclass CachedMapStack {\n static entries: {[key: string]: google.maps.Map[]} = {};\n\n static has(key: string) {\n return this.entries[key] && this.entries[key].length > 0;\n }\n\n static pop(key: string) {\n if (!this.entries[key]) return null;\n\n return this.entries[key].pop() || null;\n }\n\n static push(key: string, value: google.maps.Map) {\n if (!this.entries[key]) this.entries[key] = [];\n\n this.entries[key].push(value);\n }\n}\n\n/**\n * The main hook takes care of creating map-instances and registering them in\n * the api-provider context.\n * @return a tuple of the map-instance created (or null) and the callback\n * ref that will be used to pass the map-container into this hook.\n * @internal\n */\nexport function useMapInstance(\n props: MapProps,\n context: APIProviderContextValue\n): readonly [\n map: google.maps.Map | null,\n containerRef: Ref<HTMLDivElement>,\n cameraStateRef: CameraStateRef\n] {\n const apiIsLoaded = useApiIsLoaded();\n const [map, setMap] = useState<google.maps.Map | null>(null);\n const [container, containerRef] = useCallbackRef<HTMLDivElement>();\n\n const cameraStateRef = useTrackedCameraStateRef(map);\n\n const {\n id,\n defaultBounds,\n defaultCenter,\n defaultZoom,\n defaultHeading,\n defaultTilt,\n reuseMaps,\n renderingType,\n colorScheme,\n\n ...mapOptions\n } = props;\n\n const hasZoom = props.zoom !== undefined || props.defaultZoom !== undefined;\n const hasCenter =\n props.center !== undefined || props.defaultCenter !== undefined;\n\n if (!defaultBounds && (!hasZoom || !hasCenter)) {\n console.warn(\n '<Map> component is missing configuration. ' +\n 'You have to provide zoom and center (via the `zoom`/`defaultZoom` and ' +\n '`center`/`defaultCenter` props) or specify the region to show using ' +\n '`defaultBounds`. See ' +\n 'https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required'\n );\n }\n\n // apply default camera props if available and not overwritten by controlled props\n if (!mapOptions.center && defaultCenter) mapOptions.center = defaultCenter;\n if (!mapOptions.zoom && Number.isFinite(defaultZoom))\n mapOptions.zoom = defaultZoom;\n if (!mapOptions.heading && Number.isFinite(defaultHeading))\n mapOptions.heading = defaultHeading;\n if (!mapOptions.tilt && Number.isFinite(defaultTilt))\n mapOptions.tilt = defaultTilt;\n\n for (const key of Object.keys(mapOptions) as (keyof typeof mapOptions)[])\n if (mapOptions[key] === undefined) delete mapOptions[key];\n\n const savedMapStateRef = useRef<{\n mapId?: string | null;\n cameraState: CameraState;\n }>(undefined);\n\n // create the map instance and register it in the context\n useEffect(\n () => {\n if (!container || !apiIsLoaded) return;\n\n const {addMapInstance, removeMapInstance} = context;\n\n // note: colorScheme (upcoming feature) isn't yet in the typings, remove once that is fixed:\n const {mapId} = props;\n const cacheKey = `${mapId || 'default'}:${renderingType || 'default'}:${colorScheme || 'LIGHT'}`;\n\n let mapDiv: HTMLElement;\n let map: google.maps.Map;\n\n if (reuseMaps && CachedMapStack.has(cacheKey)) {\n map = CachedMapStack.pop(cacheKey) as google.maps.Map;\n mapDiv = map.getDiv();\n\n container.appendChild(mapDiv);\n map.setOptions(mapOptions);\n\n // detaching the element from the DOM lets the map fall back to its default\n // size, setting the center will trigger reloading the map.\n setTimeout(() => map.setCenter(map.getCenter()!), 0);\n } else {\n mapDiv = document.createElement('div');\n mapDiv.style.height = '100%';\n container.appendChild(mapDiv);\n\n map = new google.maps.Map(mapDiv, {\n ...mapOptions,\n ...(renderingType\n ? {renderingType: renderingType as google.maps.RenderingType}\n : {}),\n ...(colorScheme\n ? {colorScheme: colorScheme as google.maps.ColorScheme}\n : {})\n });\n }\n\n setMap(map);\n addMapInstance(map, id);\n\n if (defaultBounds) {\n const {padding, ...defBounds} = defaultBounds;\n map.fitBounds(defBounds, padding);\n }\n\n // prevent map not rendering due to missing configuration\n else if (!hasZoom || !hasCenter) {\n map.fitBounds({east: 180, west: -180, south: -90, north: 90});\n }\n\n // the savedMapState is used to restore the camera parameters when the mapId is changed\n if (savedMapStateRef.current) {\n const {mapId: savedMapId, cameraState: savedCameraState} =\n savedMapStateRef.current;\n if (savedMapId !== mapId) {\n map.setOptions(savedCameraState);\n }\n }\n\n return () => {\n savedMapStateRef.current = {\n mapId,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n cameraState: cameraStateRef.current\n };\n\n // detach the map-div from the dom\n mapDiv.remove();\n\n if (reuseMaps) {\n // push back on the stack\n CachedMapStack.push(cacheKey, map);\n } else {\n // remove all event-listeners to minimize the possibility of memory-leaks\n google.maps.event.clearInstanceListeners(map);\n }\n\n setMap(null);\n removeMapInstance(id);\n };\n },\n\n // some dependencies are ignored in the list below:\n // - defaultBounds and the default* camera props will only be used once, and\n // changes should be ignored\n // - mapOptions has special hooks that take care of updating the options\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n container,\n apiIsLoaded,\n id,\n\n // these props can't be changed after initialization and require a new\n // instance to be created\n props.mapId,\n props.renderingType,\n props.colorScheme\n ]\n );\n\n return [map, containerRef, cameraStateRef] as const;\n}\n","/* eslint-disable complexity */\nimport React, {\n CSSProperties,\n FunctionComponent,\n PropsWithChildren,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo\n} from 'react';\n\nimport {APIProviderContext} from '../api-provider';\n\nimport {MapEventProps, useMapEvents} from './use-map-events';\nimport {useMapOptions} from './use-map-options';\nimport {useApiLoadingStatus} from '../../hooks/use-api-loading-status';\nimport {APILoadingStatus} from '../../libraries/api-loading-status';\nimport {\n DeckGlCompatProps,\n useDeckGLCameraUpdate\n} from './use-deckgl-camera-update';\nimport {toLatLngLiteral} from '../../libraries/lat-lng-utils';\nimport {useMapCameraParams} from './use-map-camera-params';\nimport {AuthFailureMessage} from './auth-failure-message';\nimport {useMapInstance} from './use-map-instance';\n\nexport interface GoogleMapsContextValue {\n map: google.maps.Map | null;\n}\nexport const GoogleMapsContext =\n React.createContext<GoogleMapsContextValue | null>(null);\n\nexport type {\n MapCameraChangedEvent,\n MapEvent,\n MapEventProps,\n MapMouseEvent\n} from './use-map-events';\n\nexport type MapCameraProps = {\n center: google.maps.LatLngLiteral;\n zoom: number;\n heading?: number;\n tilt?: number;\n};\n\n// ColorScheme and RenderingType are redefined here to make them usable before the\n// maps API has been fully loaded.\n\nexport const ColorScheme = {\n DARK: 'DARK',\n LIGHT: 'LIGHT',\n FOLLOW_SYSTEM: 'FOLLOW_SYSTEM'\n} as const;\nexport type ColorScheme = (typeof ColorScheme)[keyof typeof ColorScheme];\n\nexport const RenderingType = {\n VECTOR: 'VECTOR',\n RASTER: 'RASTER',\n UNINITIALIZED: 'UNINITIALIZED'\n} as const;\nexport type RenderingType = (typeof RenderingType)[keyof typeof RenderingType];\n\n/**\n * Props for the Map Component\n */\nexport type MapProps = PropsWithChildren<\n Omit<google.maps.MapOptions, 'renderingType' | 'colorScheme'> &\n MapEventProps &\n DeckGlCompatProps & {\n /**\n * An id for the map, this is required when multiple maps are present\n * in the same APIProvider context.\n */\n id?: string;\n\n /**\n * Additional style rules to apply to the map dom-element.\n */\n style?: CSSProperties;\n\n /**\n * Additional css class-name to apply to the element containing the map.\n */\n className?: string;\n\n /**\n * The color-scheme to use for the map.\n */\n colorScheme?: ColorScheme;\n\n /**\n * The rendering-type to be used.\n */\n renderingType?: RenderingType;\n\n /**\n * Indicates that the map will be controlled externally. Disables all controls provided by the map itself.\n */\n controlled?: boolean;\n\n /**\n * Enable caching of map-instances created by this component.\n */\n reuseMaps?: boolean;\n\n defaultCenter?: google.maps.LatLngLiteral;\n defaultZoom?: number;\n defaultHeading?: number;\n defaultTilt?: number;\n /**\n * Alternative way to specify the default camera props as a geographic region that should be fully visible\n */\n defaultBounds?: google.maps.LatLngBoundsLiteral & {\n padding?: number | google.maps.Padding;\n };\n }\n>;\n\nexport const Map: FunctionComponent<MapProps> = (props: MapProps) => {\n const {children, id, className, style} = props;\n const context = useContext(APIProviderContext);\n const loadingStatus = useApiLoadingStatus();\n\n if (!context) {\n throw new Error(\n '<Map> can only be used inside an <ApiProvider> component.'\n );\n }\n\n const [map, mapRef, cameraStateRef] = useMapInstance(props, context);\n\n useMapCameraParams(map, cameraStateRef, props);\n useMapEvents(map, props);\n useMapOptions(map, props);\n\n const isDeckGlControlled = useDeckGLCameraUpdate(map, props);\n const isControlledExternally = !!props.controlled;\n\n // disable interactions with the map for externally controlled maps\n useEffect(() => {\n if (!map) return;\n\n // fixme: this doesn't seem to belong here (and it's mostly there for convenience anyway).\n // The reasoning is that a deck.gl canvas will be put on top of the map, rendering\n // any default map controls pretty much useless\n if (isDeckGlControlled) {\n map.setOptions({disableDefaultUI: true});\n }\n\n // disable all control-inputs when the map is controlled externally\n if (isDeckGlControlled || isControlledExternally) {\n map.setOptions({\n gestureHandling: 'none',\n keyboardShortcuts: false\n });\n }\n\n return () => {\n map.setOptions({\n gestureHandling: props.gestureHandling,\n keyboardShortcuts: props.keyboardShortcuts\n });\n };\n }, [\n map,\n isDeckGlControlled,\n isControlledExternally,\n props.gestureHandling,\n props.keyboardShortcuts\n ]);\n\n // setup a stable cameraOptions object that can be used as dependency\n const center = props.center ? toLatLngLiteral(props.center) : null;\n let lat: number | null = null;\n let lng: number | null = null;\n if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) {\n lat = center.lat as number;\n lng = center.lng as number;\n }\n\n const cameraOptions: google.maps.CameraOptions = useMemo(() => {\n return {\n center: {lat: lat ?? 0, lng: lng ?? 0},\n zoom: props.zoom ?? 0,\n heading: props.heading ?? 0,\n tilt: props.tilt ?? 0\n };\n }, [lat, lng, props.zoom, props.heading, props.tilt]);\n\n // externally controlled mode: reject all camera changes that don't correspond to changes in props\n useLayoutEffect(() => {\n if (!map || !isControlledExternally) return;\n\n map.moveCamera(cameraOptions);\n const listener = map.addListener('bounds_changed', () => {\n map.moveCamera(cameraOptions);\n });\n\n return () => listener.remove();\n }, [map, isControlledExternally, cameraOptions]);\n\n const combinedStyle: CSSProperties = useMemo(\n () => ({\n width: '100%',\n height: '100%',\n position: 'relative',\n // when using deckgl, the map should be sent to the back\n zIndex: isDeckGlControlled ? -1 : 0,\n\n ...style\n }),\n [style, isDeckGlControlled]\n );\n\n const contextValue: GoogleMapsContextValue = useMemo(() => ({map}), [map]);\n\n if (loadingStatus === APILoadingStatus.AUTH_FAILURE) {\n return (\n <div\n style={{position: 'relative', ...(className ? {} : combinedStyle)}}\n className={className}>\n <AuthFailureMessage />\n </div>\n );\n }\n\n return (\n <div\n ref={mapRef}\n data-testid={'map'}\n style={className ? undefined : combinedStyle}\n className={className}\n {...(id ? {id} : {})}>\n {map ? (\n <GoogleMapsContext.Provider value={contextValue}>\n {children}\n </GoogleMapsContext.Provider>\n ) : null}\n </div>\n );\n};\n\n// The deckGLViewProps flag here indicates to deck.gl that the Map component is\n// able to handle viewProps from deck.gl when deck.gl is used to control the map.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\n(Map as any).deckGLViewProps = true;\n","const shownMessages = new Set();\n\nexport function logErrorOnce(...args: Parameters<typeof console.error>) {\n const key = JSON.stringify(args);\n\n if (!shownMessages.has(key)) {\n shownMessages.add(key);\n\n console.error(...args);\n }\n}\n","import {useContext} from 'react';\n\nimport {APIProviderContext} from '../components/api-provider';\nimport {GoogleMapsContext} from '../components/m