UNPKG

@vis.gl/react-google-maps

Version:

React components and hooks for the Google Maps JavaScript API

1,293 lines (1,267 loc) 93.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom'), require('fast-deep-equal')) : typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom', 'fast-deep-equal'], factory) : (global = global || self, factory(global.reactGoogleMaps = {}, global.React, global.ReactDOM, global.fastDeepEqual)); })(this, (function (exports, React, reactDom, isDeepEqual) { function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var isDeepEqual__default = /*#__PURE__*/_interopDefaultLegacy(isDeepEqual); function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _createForOfIteratorHelperLoose(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (t) return (t = t.call(r)).next.bind(t); if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var o = 0; return function () { return o >= r.length ? { done: !0 } : { done: !1, value: r[o++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } var APILoadingStatus = { NOT_LOADED: 'NOT_LOADED', LOADING: 'LOADING', LOADED: 'LOADED', FAILED: 'FAILED', AUTH_FAILURE: 'AUTH_FAILURE' }; var MAPS_API_BASE_URL = 'https://maps.googleapis.com/maps/api/js'; /** * A GoogleMapsApiLoader to reliably load and unload the Google Maps JavaScript API. * * The actual loading and unloading is delayed into the microtask queue, to * allow using the API in an useEffect hook, without worrying about multiple API loads. */ var GoogleMapsApiLoader = /*#__PURE__*/function () { function GoogleMapsApiLoader() {} /** * Loads the Maps JavaScript API with the specified parameters. * Since the Maps library can only be loaded once per page, this will * produce a warning when called multiple times with different * parameters. * * The returned promise resolves when loading completes * and rejects in case of an error or when the loading was aborted. */ GoogleMapsApiLoader.load = function load(params, onLoadingStatusChange) { try { var _window$google; var _this = this; var libraries = params.libraries ? params.libraries.split(',') : []; var serializedParams = _this.serializeParams(params); _this.listeners.push(onLoadingStatusChange); // Note: if `google.maps.importLibrary` has been defined externally, we // assume that loading is complete and successful. // If it was defined by a previous call to this method, a warning // message is logged if there are differences in api-parameters used // for both calls. if ((_window$google = window.google) != null && (_window$google = _window$google.maps) != null && _window$google.importLibrary) { // no serialized parameters means it was loaded externally if (!_this.serializedApiParams) { _this.loadingStatus = APILoadingStatus.LOADED; } _this.notifyLoadingStatusListeners(); } else { _this.serializedApiParams = serializedParams; _this.initImportLibrary(params); } if (_this.serializedApiParams && _this.serializedApiParams !== serializedParams) { console.warn("[google-maps-api-loader] The maps API has already been loaded " + "with different parameters and will not be loaded again. Refresh the " + "page for new values to have effect."); } var librariesToLoad = ['maps'].concat(libraries); return Promise.resolve(Promise.all(librariesToLoad.map(function (name) { return google.maps.importLibrary(name); }))).then(function () {}); } catch (e) { return Promise.reject(e); } } /** * Serialize the parameters used to load the library for easier comparison. */ ; GoogleMapsApiLoader.serializeParams = function serializeParams(params) { return [params.v, params.key, params.language, params.region, params.authReferrerPolicy, params.solutionChannel].join('/'); } /** * Creates the global `google.maps.importLibrary` function for bootstrapping. * This is essentially a formatted version of the dynamic loading script * from the official documentation with some minor adjustments. * * The created importLibrary function will load the Google Maps JavaScript API, * which will then replace the `google.maps.importLibrary` function with the full * implementation. * * @see https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import */; GoogleMapsApiLoader.initImportLibrary = function initImportLibrary(params) { var _this2 = this; if (!window.google) window.google = {}; if (!window.google.maps) window.google.maps = {}; if (window.google.maps['importLibrary']) { console.error('[google-maps-api-loader-internal]: initImportLibrary must only be called once'); return; } var apiPromise = null; var loadApi = function loadApi() { if (apiPromise) return apiPromise; apiPromise = new Promise(function (resolve, reject) { var _document$querySelect; var scriptElement = document.createElement('script'); var urlParams = new URLSearchParams(); for (var _i = 0, _Object$entries = Object.entries(params); _i < _Object$entries.length; _i++) { var _Object$entries$_i = _Object$entries[_i], key = _Object$entries$_i[0], value = _Object$entries$_i[1]; var urlParamName = key.replace(/[A-Z]/g, function (t) { return '_' + t[0].toLowerCase(); }); urlParams.set(urlParamName, String(value)); } urlParams.set('loading', 'async'); urlParams.set('callback', '__googleMapsCallback__'); scriptElement.async = true; scriptElement.src = MAPS_API_BASE_URL + "?" + urlParams.toString(); scriptElement.nonce = ((_document$querySelect = document.querySelector('script[nonce]')) == null ? void 0 : _document$querySelect.nonce) || ''; scriptElement.onerror = function () { _this2.loadingStatus = APILoadingStatus.FAILED; _this2.notifyLoadingStatusListeners(); reject(new Error('The Google Maps JavaScript API could not load.')); }; window.__googleMapsCallback__ = function () { _this2.loadingStatus = APILoadingStatus.LOADED; _this2.notifyLoadingStatusListeners(); resolve(); }; window.gm_authFailure = function () { _this2.loadingStatus = APILoadingStatus.AUTH_FAILURE; _this2.notifyLoadingStatusListeners(); }; _this2.loadingStatus = APILoadingStatus.LOADING; _this2.notifyLoadingStatusListeners(); document.head.append(scriptElement); }); return apiPromise; }; // for the first load, we declare an importLibrary function that will // be overwritten once the api is loaded. google.maps.importLibrary = function (libraryName) { return loadApi().then(function () { return google.maps.importLibrary(libraryName); }); }; } /** * Calls all registered loadingStatusListeners after a status update. */; GoogleMapsApiLoader.notifyLoadingStatusListeners = function notifyLoadingStatusListeners() { for (var _iterator = _createForOfIteratorHelperLoose(this.listeners), _step; !(_step = _iterator()).done;) { var fn = _step.value; fn(this.loadingStatus); } }; return GoogleMapsApiLoader; }(); /** * The current loadingStatus of the API. */ GoogleMapsApiLoader.loadingStatus = APILoadingStatus.NOT_LOADED; /** * The parameters used for first loading the API. */ GoogleMapsApiLoader.serializedApiParams = void 0; /** * A list of functions to be notified when the loading status changes. */ GoogleMapsApiLoader.listeners = []; var _iteratorSymbol = typeof Symbol !== "undefined" ? Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator")) : "@@iterator"; function _settle(pact, state, value) { if (!pact.s) { if (value instanceof _Pact) { if (value.s) { if (state & 1) { state = value.s; } value = value.v; } else { value.o = _settle.bind(null, pact, state); return; } } if (value && value.then) { value.then(_settle.bind(null, pact, state), _settle.bind(null, pact, 2)); return; } pact.s = state; pact.v = value; var observer = pact.o; if (observer) { observer(pact); } } } var _Pact = /*#__PURE__*/function () { function _Pact() {} _Pact.prototype.then = function (onFulfilled, onRejected) { var result = new _Pact(); var state = this.s; if (state) { var callback = state & 1 ? onFulfilled : onRejected; if (callback) { try { _settle(result, 1, callback(this.v)); } catch (e) { _settle(result, 2, e); } return result; } else { return this; } } this.o = function (_this) { try { var value = _this.v; if (_this.s & 1) { _settle(result, 1, onFulfilled ? onFulfilled(value) : value); } else if (onRejected) { _settle(result, 1, onRejected(value)); } else { _settle(result, 2, value); } } catch (e) { _settle(result, 2, e); } }; return result; }; return _Pact; }(); function _isSettledPact(thenable) { return thenable instanceof _Pact && thenable.s & 1; } function _forTo(array, body, check) { var i = -1, pact, reject; function _cycle(result) { try { while (++i < array.length && (!check || !check())) { result = body(i); if (result && result.then) { if (_isSettledPact(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle.bind(null, pact = new _Pact(), 2))); return; } } } if (pact) { _settle(pact, 1, result); } else { pact = result; } } catch (e) { _settle(pact || (pact = new _Pact()), 2, e); } } _cycle(); return pact; } var _excluded$3 = ["onLoad", "onError", "apiKey", "version", "libraries"], _excluded2$1 = ["children"]; function _forOf(target, body, check) { if (typeof target[_iteratorSymbol] === "function") { var _cycle2 = function _cycle(result) { try { while (!(step = iterator.next()).done && (!check || !check())) { result = body(step.value); if (result && result.then) { if (_isSettledPact(result)) { result = result.v; } else { result.then(_cycle2, reject || (reject = _settle.bind(null, pact = new _Pact(), 2))); return; } } } if (pact) { _settle(pact, 1, result); } else { pact = result; } } catch (e) { _settle(pact || (pact = new _Pact()), 2, e); } }; var iterator = target[_iteratorSymbol](), step, pact, reject; _cycle2(); if (iterator["return"]) { var _fixup = function _fixup(value) { try { if (!step.done) { iterator["return"](); } } catch (e) {} return value; }; if (pact && pact.then) { return pact.then(_fixup, function (e) { throw _fixup(e); }); } _fixup(); } return pact; } // No support for Symbol.iterator if (!("length" in target)) { throw new TypeError("Object is not iterable"); } // Handle live collections properly var values = []; for (var i = 0; i < target.length; i++) { values.push(target[i]); } return _forTo(values, function (i) { return body(values[i]); }, check); } var DEFAULT_SOLUTION_CHANNEL = 'GMP_visgl_rgmlibrary_v1_default'; function _catch(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } /** * local hook to set up the map-instance management context. */ var APIProviderContext = React__default["default"].createContext(null); function useMapInstances() { var _useState = React.useState({}), mapInstances = _useState[0], setMapInstances = _useState[1]; var addMapInstance = function addMapInstance(mapInstance, id) { if (id === void 0) { id = 'default'; } setMapInstances(function (instances) { var _extends2; return _extends({}, instances, (_extends2 = {}, _extends2[id] = mapInstance, _extends2)); }); }; var removeMapInstance = function removeMapInstance(id) { if (id === void 0) { id = 'default'; } // eslint-disable-next-line @typescript-eslint/no-unused-vars setMapInstances(function (_ref) { var remaining = _objectWithoutPropertiesLoose(_ref, [id].map(_toPropertyKey)); return remaining; }); }; var clearMapInstances = function clearMapInstances() { setMapInstances({}); }; return { mapInstances: mapInstances, addMapInstance: addMapInstance, removeMapInstance: removeMapInstance, clearMapInstances: clearMapInstances }; } /** * local hook to handle the loading of the maps API, returns the current loading status * @param props */ function useGoogleMapsApiLoader(props) { var onLoad = props.onLoad, onError = props.onError, apiKey = props.apiKey, version = props.version, _props$libraries = props.libraries, libraries = _props$libraries === void 0 ? [] : _props$libraries, otherApiParams = _objectWithoutPropertiesLoose(props, _excluded$3); var _useState2 = React.useState(GoogleMapsApiLoader.loadingStatus), status = _useState2[0], setStatus = _useState2[1]; var _useReducer = React.useReducer(function (loadedLibraries, action) { var _extends3; return loadedLibraries[action.name] ? loadedLibraries : _extends({}, loadedLibraries, (_extends3 = {}, _extends3[action.name] = action.value, _extends3)); }, {}), loadedLibraries = _useReducer[0], addLoadedLibrary = _useReducer[1]; var librariesString = React.useMemo(function () { return libraries == null ? void 0 : libraries.join(','); }, [libraries]); var serializedParams = React.useMemo(function () { return JSON.stringify(_extends({ apiKey: apiKey, version: version }, otherApiParams)); }, [apiKey, version, otherApiParams]); var importLibrary = React.useCallback(function (name) { try { var _google; if (loadedLibraries[name]) { return Promise.resolve(loadedLibraries[name]); } if (!((_google = google) != null && (_google = _google.maps) != null && _google.importLibrary)) { throw new Error('[api-provider-internal] importLibrary was called before ' + 'google.maps.importLibrary was defined.'); } return Promise.resolve(window.google.maps.importLibrary(name)).then(function (res) { addLoadedLibrary({ name: name, value: res }); return res; }); } catch (e) { return Promise.reject(e); } }, [loadedLibraries]); React.useEffect(function () { (function () { try { var _temp3 = _catch(function () { var params = _extends({ key: apiKey }, otherApiParams); if (version) params.v = version; if ((librariesString == null ? void 0 : librariesString.length) > 0) params.libraries = librariesString; if (params.channel === undefined || params.channel < 0 || params.channel > 999) delete params.channel; if (params.solutionChannel === undefined) params.solutionChannel = DEFAULT_SOLUTION_CHANNEL;else if (params.solutionChannel === '') delete params.solutionChannel; return Promise.resolve(GoogleMapsApiLoader.load(params, function (status) { return setStatus(status); })).then(function () { function _temp2() { if (onLoad) { onLoad(); } } var _temp = _forOf(['core', 'maps'].concat(libraries), function (name) { return Promise.resolve(importLibrary(name)).then(function () {}); }); return _temp && _temp.then ? _temp.then(_temp2) : _temp2(_temp); }); }, function (error) { if (onError) { onError(error); } else { console.error('<ApiProvider> failed to load the Google Maps JavaScript API', error); } }); return _temp3 && _temp3.then ? _temp3.then(function () {}) : void 0; } catch (e) { Promise.reject(e); } })(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [apiKey, librariesString, serializedParams]); return { status: status, loadedLibraries: loadedLibraries, importLibrary: importLibrary }; } /** * Component to wrap the components from this library and load the Google Maps JavaScript API */ var APIProvider = function APIProvider(props) { var children = props.children, loaderProps = _objectWithoutPropertiesLoose(props, _excluded2$1); var _useMapInstances = useMapInstances(), mapInstances = _useMapInstances.mapInstances, addMapInstance = _useMapInstances.addMapInstance, removeMapInstance = _useMapInstances.removeMapInstance, clearMapInstances = _useMapInstances.clearMapInstances; var _useGoogleMapsApiLoad = useGoogleMapsApiLoader(loaderProps), status = _useGoogleMapsApiLoad.status, loadedLibraries = _useGoogleMapsApiLoad.loadedLibraries, importLibrary = _useGoogleMapsApiLoad.importLibrary; var contextValue = React.useMemo(function () { return { mapInstances: mapInstances, addMapInstance: addMapInstance, removeMapInstance: removeMapInstance, clearMapInstances: clearMapInstances, status: status, loadedLibraries: loadedLibraries, importLibrary: importLibrary }; }, [mapInstances, addMapInstance, removeMapInstance, clearMapInstances, status, loadedLibraries, importLibrary]); return /*#__PURE__*/React__default["default"].createElement(APIProviderContext.Provider, { value: contextValue }, children); }; /** * Sets up effects to bind event-handlers for all event-props in MapEventProps. * @internal */ function useMapEvents(map, props) { var _loop = function _loop() { var propName = _step.value; // fixme: this cast is essentially a 'trust me, bro' for typescript, but // a proper solution seems way too complicated right now var handler = props[propName]; var eventType = propNameToEventType[propName]; // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(function () { if (!map) return; if (!handler) return; var listener = google.maps.event.addListener(map, eventType, function (ev) { handler(createMapEvent(eventType, map, ev)); }); return function () { return listener.remove(); }; }, [map, eventType, handler]); }; // note: calling a useEffect hook from within a loop is prohibited by the // rules of hooks, but it's ok here since it's unconditional and the number // and order of iterations is always strictly the same. // (see https://legacy.reactjs.org/docs/hooks-rules.html) for (var _iterator = _createForOfIteratorHelperLoose(eventPropNames), _step; !(_step = _iterator()).done;) { _loop(); } } /** * Create the wrapped map-events used for the event-props. * @param type the event type as it is specified to the maps api * @param map the map instance the event originates from * @param srcEvent the source-event if there is one. */ function createMapEvent(type, map, srcEvent) { var ev = { type: type, map: map, detail: {}, stoppable: false, stop: function stop() {} }; if (cameraEventTypes.includes(type)) { var camEvent = ev; var center = map.getCenter(); var zoom = map.getZoom(); var heading = map.getHeading() || 0; var tilt = map.getTilt() || 0; var bounds = map.getBounds(); if (!center || !bounds || !Number.isFinite(zoom)) { console.warn('[createEvent] at least one of the values from the map ' + 'returned undefined. This is not expected to happen. Please ' + 'report an issue at https://github.com/visgl/react-google-maps/issues/new'); } camEvent.detail = { center: (center == null ? void 0 : center.toJSON()) || { lat: 0, lng: 0 }, zoom: zoom || 0, heading: heading, tilt: tilt, bounds: (bounds == null ? void 0 : bounds.toJSON()) || { north: 90, east: 180, south: -90, west: -180 } }; return camEvent; } else if (mouseEventTypes.includes(type)) { var _srcEvent$latLng; if (!srcEvent) throw new Error('[createEvent] mouse events must provide a srcEvent'); var mouseEvent = ev; mouseEvent.domEvent = srcEvent.domEvent; mouseEvent.stoppable = true; mouseEvent.stop = function () { return srcEvent.stop(); }; mouseEvent.detail = { latLng: ((_srcEvent$latLng = srcEvent.latLng) == null ? void 0 : _srcEvent$latLng.toJSON()) || null, placeId: srcEvent.placeId }; return mouseEvent; } return ev; } /** * maps the camelCased names of event-props to the corresponding event-types * used in the maps API. */ var propNameToEventType = { onBoundsChanged: 'bounds_changed', onCenterChanged: 'center_changed', onClick: 'click', onContextmenu: 'contextmenu', onDblclick: 'dblclick', onDrag: 'drag', onDragend: 'dragend', onDragstart: 'dragstart', onHeadingChanged: 'heading_changed', onIdle: 'idle', onIsFractionalZoomEnabledChanged: 'isfractionalzoomenabled_changed', onMapCapabilitiesChanged: 'mapcapabilities_changed', onMapTypeIdChanged: 'maptypeid_changed', onMousemove: 'mousemove', onMouseout: 'mouseout', onMouseover: 'mouseover', onProjectionChanged: 'projection_changed', onRenderingTypeChanged: 'renderingtype_changed', onTilesLoaded: 'tilesloaded', onTiltChanged: 'tilt_changed', onZoomChanged: 'zoom_changed', // note: onCameraChanged is an alias for the bounds_changed event, // since that is going to be fired in every situation where the camera is // updated. onCameraChanged: 'bounds_changed' }; var cameraEventTypes = ['bounds_changed', 'center_changed', 'heading_changed', 'tilt_changed', 'zoom_changed']; var mouseEventTypes = ['click', 'contextmenu', 'dblclick', 'mousemove', 'mouseout', 'mouseover']; var eventPropNames = Object.keys(propNameToEventType); function useMemoized(value, isEqual) { var ref = React.useRef(value); if (!isEqual(value, ref.current)) { ref.current = value; } return ref.current; } function useCustomCompareEffect(effect, dependencies, isEqual) { // eslint-disable-next-line react-hooks/exhaustive-deps React.useEffect(effect, [useMemoized(dependencies, isEqual)]); } function useDeepCompareEffect(effect, dependencies) { useCustomCompareEffect(effect, dependencies, isDeepEqual__default["default"]); } var mapOptionKeys = new Set(['backgroundColor', 'clickableIcons', 'controlSize', 'disableDefaultUI', 'disableDoubleClickZoom', 'draggable', 'draggableCursor', 'draggingCursor', 'fullscreenControl', 'fullscreenControlOptions', 'gestureHandling', 'headingInteractionEnabled', 'isFractionalZoomEnabled', 'keyboardShortcuts', 'mapTypeControl', 'mapTypeControlOptions', 'mapTypeId', 'maxZoom', 'minZoom', 'noClear', 'panControl', 'panControlOptions', 'restriction', 'rotateControl', 'rotateControlOptions', 'scaleControl', 'scaleControlOptions', 'scrollwheel', 'streetView', 'streetViewControl', 'streetViewControlOptions', 'styles', 'tiltInteractionEnabled', 'zoomControl', 'zoomControlOptions']); /** * Internal hook to update the map-options when props are changed. * * @param map the map instance * @param mapProps the props to update the map-instance with * @internal */ function useMapOptions(map, mapProps) { /* eslint-disable react-hooks/exhaustive-deps -- * * The following effects aren't triggered when the map is changed. * In that case, the values will be or have been passed to the map * constructor via mapOptions. */ var mapOptions = {}; var keys = Object.keys(mapProps); for (var _i = 0, _keys = keys; _i < _keys.length; _i++) { var key = _keys[_i]; if (!mapOptionKeys.has(key)) continue; mapOptions[key] = mapProps[key]; } // update the map options when mapOptions is changed // Note: due to the destructuring above, mapOptions will be seen as changed // with every re-render, so we're assuming the maps-api will properly // deal with unchanged option-values passed into setOptions. useDeepCompareEffect(function () { if (!map) return; map.setOptions(mapOptions); }, [mapOptions]); /* eslint-enable react-hooks/exhaustive-deps */ } function useApiLoadingStatus() { var _useContext; return ((_useContext = React.useContext(APIProviderContext)) == null ? void 0 : _useContext.status) || APILoadingStatus.NOT_LOADED; } /** * Internal hook that updates the camera when deck.gl viewState changes. * @internal */ function useDeckGLCameraUpdate(map, props) { var viewport = props.viewport, viewState = props.viewState; var isDeckGlControlled = !!viewport; React.useLayoutEffect(function () { if (!map || !viewState) return; var latitude = viewState.latitude, longitude = viewState.longitude, heading = viewState.bearing, tilt = viewState.pitch, zoom = viewState.zoom; map.moveCamera({ center: { lat: latitude, lng: longitude }, heading: heading, tilt: tilt, zoom: zoom + 1 }); }, [map, viewState]); return isDeckGlControlled; } function isLatLngLiteral(obj) { if (!obj || typeof obj !== 'object') return false; if (!('lat' in obj && 'lng' in obj)) return false; return Number.isFinite(obj.lat) && Number.isFinite(obj.lng); } function latLngEquals(a, b) { if (!a || !b) return false; var A = toLatLngLiteral(a); var B = toLatLngLiteral(b); if (A.lat !== B.lat || A.lng !== B.lng) return false; return true; } function toLatLngLiteral(obj) { if (isLatLngLiteral(obj)) return obj; return obj.toJSON(); } function useMapCameraParams(map, cameraStateRef, mapProps) { var center = mapProps.center ? toLatLngLiteral(mapProps.center) : null; var lat = null; var lng = null; if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) { lat = center.lat; lng = center.lng; } var zoom = Number.isFinite(mapProps.zoom) ? mapProps.zoom : null; var heading = Number.isFinite(mapProps.heading) ? mapProps.heading : null; var tilt = Number.isFinite(mapProps.tilt) ? mapProps.tilt : null; // the following effect runs for every render of the map component and checks // if there are differences between the known state of the map instance // (cameraStateRef, which is updated by all bounds_changed events) and the // desired state in the props. React.useLayoutEffect(function () { if (!map) return; var nextCamera = {}; var needsUpdate = false; if (lat !== null && lng !== null && (cameraStateRef.current.center.lat !== lat || cameraStateRef.current.center.lng !== lng)) { nextCamera.center = { lat: lat, lng: lng }; needsUpdate = true; } if (zoom !== null && cameraStateRef.current.zoom !== zoom) { nextCamera.zoom = zoom; needsUpdate = true; } if (heading !== null && cameraStateRef.current.heading !== heading) { nextCamera.heading = heading; needsUpdate = true; } if (tilt !== null && cameraStateRef.current.tilt !== tilt) { nextCamera.tilt = tilt; needsUpdate = true; } if (needsUpdate) { map.moveCamera(nextCamera); } }); } var AuthFailureMessage = function AuthFailureMessage() { var style = { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, zIndex: 999, display: 'flex', flexFlow: 'column nowrap', textAlign: 'center', justifyContent: 'center', fontSize: '.8rem', color: 'rgba(0,0,0,0.6)', background: '#dddddd', padding: '1rem 1.5rem' }; return /*#__PURE__*/React__default["default"].createElement("div", { style: style }, /*#__PURE__*/React__default["default"].createElement("h2", null, "Error: AuthFailure"), /*#__PURE__*/React__default["default"].createElement("p", null, "A problem with your API key prevents the map from rendering correctly. Please make sure the value of the ", /*#__PURE__*/React__default["default"].createElement("code", null, "APIProvider.apiKey"), " prop is correct. Check the error-message in the console for further details.")); }; function useCallbackRef() { var _useState = React.useState(null), el = _useState[0], setEl = _useState[1]; var ref = React.useCallback(function (value) { return setEl(value); }, [setEl]); return [el, ref]; } /** * Hook to check if the Maps JavaScript API is loaded */ function useApiIsLoaded() { var status = useApiLoadingStatus(); return status === APILoadingStatus.LOADED; } function useForceUpdate() { var _useReducer = React.useReducer(function (x) { return x + 1; }, 0), forceUpdate = _useReducer[1]; return forceUpdate; } function handleBoundsChange(map, ref) { var center = map.getCenter(); var zoom = map.getZoom(); var heading = map.getHeading() || 0; var tilt = map.getTilt() || 0; var bounds = map.getBounds(); if (!center || !bounds || !Number.isFinite(zoom)) { console.warn('[useTrackedCameraState] at least one of the values from the map ' + 'returned undefined. This is not expected to happen. Please ' + 'report an issue at https://github.com/visgl/react-google-maps/issues/new'); } // fixme: do we need the `undefined` cases for the camera-params? When are they used in the maps API? Object.assign(ref.current, { center: (center == null ? void 0 : center.toJSON()) || { lat: 0, lng: 0 }, zoom: zoom || 0, heading: heading, tilt: tilt }); } /** * Creates a mutable ref object to track the last known state of the map camera. * This is used in `useMapCameraParams` to reduce stuttering in normal operation * by avoiding updates of the map camera with values that have already been processed. */ function useTrackedCameraStateRef(map) { var forceUpdate = useForceUpdate(); var ref = React.useRef({ center: { lat: 0, lng: 0 }, heading: 0, tilt: 0, zoom: 0 }); // Record camera state with every bounds_changed event dispatched by the map. // This data is used to prevent feeding these values back to the // map-instance when a typical "controlled component" setup (state variable is // fed into and updated by the map). React.useEffect(function () { if (!map) return; var listener = google.maps.event.addListener(map, 'bounds_changed', function () { handleBoundsChange(map, ref); // When an event is occured, we have to update during the next cycle. // The application could decide to ignore the event and not update any // camera props of the map, meaning that in that case we will have to // 'undo' the change to the camera. forceUpdate(); }); return function () { return listener.remove(); }; }, [map, forceUpdate]); return ref; } var _excluded$2 = ["id", "defaultBounds", "defaultCenter", "defaultZoom", "defaultHeading", "defaultTilt", "reuseMaps", "renderingType", "colorScheme"], _excluded2 = ["padding"]; /** * Stores a stack of map-instances for each mapId. Whenever an * instance is used, it is removed from the stack while in use, * and returned to the stack when the component unmounts. * This allows us to correctly implement caching for multiple * maps om the same page, while reusing as much as possible. * * FIXME: while it should in theory be possible to reuse maps solely * based on the mapId (as all other parameters can be changed at * runtime), we don't yet have good enough tracking of options to * reliably unset all the options that have been set. */ var CachedMapStack = /*#__PURE__*/function () { function CachedMapStack() {} CachedMapStack.has = function has(key) { return this.entries[key] && this.entries[key].length > 0; }; CachedMapStack.pop = function pop(key) { if (!this.entries[key]) return null; return this.entries[key].pop() || null; }; CachedMapStack.push = function push(key, value) { if (!this.entries[key]) this.entries[key] = []; this.entries[key].push(value); }; return CachedMapStack; }(); /** * The main hook takes care of creating map-instances and registering them in * the api-provider context. * @return a tuple of the map-instance created (or null) and the callback * ref that will be used to pass the map-container into this hook. * @internal */ CachedMapStack.entries = {}; function useMapInstance(props, context) { var apiIsLoaded = useApiIsLoaded(); var _useState = React.useState(null), map = _useState[0], setMap = _useState[1]; var _useCallbackRef = useCallbackRef(), container = _useCallbackRef[0], containerRef = _useCallbackRef[1]; var cameraStateRef = useTrackedCameraStateRef(map); var id = props.id, defaultBounds = props.defaultBounds, defaultCenter = props.defaultCenter, defaultZoom = props.defaultZoom, defaultHeading = props.defaultHeading, defaultTilt = props.defaultTilt, reuseMaps = props.reuseMaps, renderingType = props.renderingType, colorScheme = props.colorScheme, mapOptions = _objectWithoutPropertiesLoose(props, _excluded$2); var hasZoom = props.zoom !== undefined || props.defaultZoom !== undefined; var hasCenter = props.center !== undefined || props.defaultCenter !== undefined; if (!defaultBounds && (!hasZoom || !hasCenter)) { console.warn('<Map> component is missing configuration. ' + 'You have to provide zoom and center (via the `zoom`/`defaultZoom` and ' + '`center`/`defaultCenter` props) or specify the region to show using ' + '`defaultBounds`. See ' + 'https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required'); } // apply default camera props if available and not overwritten by controlled props if (!mapOptions.center && defaultCenter) mapOptions.center = defaultCenter; if (!mapOptions.zoom && Number.isFinite(defaultZoom)) mapOptions.zoom = defaultZoom; if (!mapOptions.heading && Number.isFinite(defaultHeading)) mapOptions.heading = defaultHeading; if (!mapOptions.tilt && Number.isFinite(defaultTilt)) mapOptions.tilt = defaultTilt; for (var _i = 0, _Object$keys = Object.keys(mapOptions); _i < _Object$keys.length; _i++) { var key = _Object$keys[_i]; if (mapOptions[key] === undefined) delete mapOptions[key]; } var savedMapStateRef = React.useRef(undefined); // create the map instance and register it in the context React.useEffect(function () { if (!container || !apiIsLoaded) return; var addMapInstance = context.addMapInstance, removeMapInstance = context.removeMapInstance; // note: colorScheme (upcoming feature) isn't yet in the typings, remove once that is fixed: var mapId = props.mapId; var cacheKey = (mapId || 'default') + ":" + (renderingType || 'default') + ":" + (colorScheme || 'LIGHT'); var mapDiv; var map; if (reuseMaps && CachedMapStack.has(cacheKey)) { map = CachedMapStack.pop(cacheKey); mapDiv = map.getDiv(); container.appendChild(mapDiv); map.setOptions(mapOptions); // detaching the element from the DOM lets the map fall back to its default // size, setting the center will trigger reloading the map. setTimeout(function () { return map.setCenter(map.getCenter()); }, 0); } else { mapDiv = document.createElement('div'); mapDiv.style.height = '100%'; container.appendChild(mapDiv); map = new google.maps.Map(mapDiv, _extends({}, mapOptions, renderingType ? { renderingType: renderingType } : {}, colorScheme ? { colorScheme: colorScheme } : {})); } setMap(map); addMapInstance(map, id); if (defaultBounds) { var padding = defaultBounds.padding, defBounds = _objectWithoutPropertiesLoose(defaultBounds, _excluded2); map.fitBounds(defBounds, padding); } // prevent map not rendering due to missing configuration else if (!hasZoom || !hasCenter) { map.fitBounds({ east: 180, west: -180, south: -90, north: 90 }); } // the savedMapState is used to restore the camera parameters when the mapId is changed if (savedMapStateRef.current) { var _savedMapStateRef$cur = savedMapStateRef.current, savedMapId = _savedMapStateRef$cur.mapId, savedCameraState = _savedMapStateRef$cur.cameraState; if (savedMapId !== mapId) { map.setOptions(savedCameraState); } } return function () { savedMapStateRef.current = { mapId: mapId, // eslint-disable-next-line react-hooks/exhaustive-deps cameraState: cameraStateRef.current }; // detach the map-div from the dom mapDiv.remove(); if (reuseMaps) { // push back on the stack CachedMapStack.push(cacheKey, map); } else { // remove all event-listeners to minimize the possibility of memory-leaks google.maps.event.clearInstanceListeners(map); } setMap(null); removeMapInstance(id); }; }, // some dependencies are ignored in the list below: // - defaultBounds and the default* camera props will only be used once, and // changes should be ignored // - mapOptions has special hooks that take care of updating the options // eslint-disable-next-line react-hooks/exhaustive-deps [container, apiIsLoaded, id, // these props can't be changed after initialization and require a new // instance to be created props.mapId, props.renderingType, props.colorScheme]); return [map, containerRef, cameraStateRef]; } var GoogleMapsContext = React__default["default"].createContext(null); // ColorScheme and RenderingType are redefined here to make them usable before the // maps API has been fully loaded. var ColorScheme = { DARK: 'DARK', LIGHT: 'LIGHT', FOLLOW_SYSTEM: 'FOLLOW_SYSTEM' }; var RenderingType = { VECTOR: 'VECTOR', RASTER: 'RASTER', UNINITIALIZED: 'UNINITIALIZED' }; var Map = function Map(props) { var children = props.children, id = props.id, className = props.className, style = props.style; var context = React.useContext(APIProviderContext); var loadingStatus = useApiLoadingStatus(); if (!context) { throw new Error('<Map> can only be used inside an <ApiProvider> component.'); } var _useMapInstance = useMapInstance(props, context), map = _useMapInstance[0], mapRef = _useMapInstance[1], cameraStateRef = _useMapInstance[2]; useMapCameraParams(map, cameraStateRef, props); useMapEvents(map, props); useMapOptions(map, props); var isDeckGlControlled = useDeckGLCameraUpdate(map, props); var isControlledExternally = !!props.controlled; // disable interactions with the map for externally controlled maps React.useEffect(function () { if (!map) return; // fixme: this doesn't seem to belong here (and it's mostly there for convenience anyway). // The reasoning is that a deck.gl canvas will be put on top of the map, rendering // any default map controls pretty much useless if (isDeckGlControlled) { map.setOptions({ disableDefaultUI: true }); } // disable all control-inputs when the map is controlled externally if (isDeckGlControlled || isControlledExternally) { map.setOptions({ gestureHandling: 'none', keyboardShortcuts: false }); } return function () { map.setOptions({ gestureHandling: props.gestureHandling, keyboardShortcuts: props.keyboardShortcuts }); }; }, [map, isDeckGlControlled, isControlledExternally, props.gestureHandling, props.keyboardShortcuts]); // setup a stable cameraOptions object that can be used as dependency var center = props.center ? toLatLngLiteral(props.center) : null; var lat = null; var lng = null; if (center && Number.isFinite(center.lat) && Number.isFinite(center.lng)) { lat = center.lat; lng = center.lng; } var cameraOptions = React.useMemo(function () { var _lat, _lng, _props$zoom, _props$heading, _props$tilt; return { center: { lat: (_lat = lat) != null ? _lat : 0, lng: (_lng = lng) != null ? _lng : 0 }, zoom: (_props$zoom = props.zoom) != null ? _props$zoom : 0, heading: (_props$heading = props.heading) != null ? _props$heading : 0, tilt: (_props$tilt = props.tilt) != null ? _props$tilt : 0 }; }, [lat, lng, props.zoom, props.heading, props.tilt]); // externally controlled mode: reject all camera changes that don't correspond to changes in props React.useLayoutEffect(function () { if (!map || !isControlledExternally) return; map.moveCamera(cameraOptions); var listener = map.addListener('bounds_changed', function () { map.moveCamera(cameraOptions); }); return function () { return listener.remove(); }; }, [map, isControlledExternally, cameraOptions]); var combinedStyle = React.useMemo(function () { return _extends({ width: '100%', height: '100%', position: 'relative', // when using deckgl, the map should be sent to the back zIndex: isDeckGlControlled ? -1 : 0 }, style); }, [style, isDeckGlControlled]); var contextValue = React.useMemo(function () { return { map: map }; }, [map]); if (loadingStatus === APILoadingStatus.AUTH_FAILURE) { return /*#__PURE__*/React__default["default"].createElement("div", { style: _extends({ position: 'relative' }, className ? {} : combinedStyle), className: className }, /*#__PURE__*/React__default["default"].createElement(AuthFailureMessage, null)); } return /*#__PURE__*/React__default["default"].createElement("div", _extends({ ref: mapRef, "data-testid": 'map', style: className ? undefined : combinedStyle, className: className }, id ? { id: id } : {}), map ? /*#__PURE__*/React__default["default"].createElement(GoogleMapsContext.Provider, { value: contextValue }, children) : null); }; // The deckGLViewProps flag here indicates to deck.gl that the Map component is // able to handle viewProps from deck.gl when deck.gl is used to control the map. // eslint-disable-next-line @typescript-eslint/no-explicit-any Map.deckGLViewProps = true; var shownMessages = new Set(); function logErrorOnce() { var args = [].slice.call(arguments); var key = JSON.stringify(args); if (!shownMessages.has(key)) { var _console; shownMessages.add(key); (_console = console).error.apply(_console, args); } } /** * Retrieves a map-instance from the context. This is either an instance * identified by id or the parent map instance if no id is specified. * Returns null if neither can be found. */ var useMap = function useMap(id) { if (id === void 0) { id = null; } var ctx = React.useContext(APIProviderContext); var _ref = React.useContext(GoogleMapsContext) || {}, map = _ref.map; if (ctx === null) { logErrorOnce('useMap(): failed to retrieve APIProviderContext. ' + 'Make sure that the <APIProvider> component exists and that the ' + 'component you are calling `useMap()` from is a sibling of the ' + '<APIProvider>.'); return null; } var mapInstances = ctx.mapInstances; // if an id is specified, the corresponding map or null is returned if (id !== null) return mapInstances[id] || null; // otherwise, return the closest ancestor if (map) return map; // finally, return the default map instance return mapInstances['default'] || null; }; function useMapsLibrary(name) { var apiIsLoaded = useApiIsLoaded(); var ctx = React.useContext(APIProviderContext); React.useEffect(function () { if (!apiIsLoaded || !ctx) return; // Trigger loading the libraries via our proxy-method. // The returned promise is ignored, since importLibrary will update loadedLibraries // list in the context, triggering a re-render. void ctx.importLibrary(name); }, [apiIsLoaded, ctx, name]); return (ctx == null ? void 0 : ctx.loadedLibraries[name]) || null; } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Internally used to bind events to Maps JavaScript API objects. * @internal */ function useMapsEventListener(target, name, callback) { React.useEffect(function () { if (!target || !name || !callback) return; var listener = google.maps.event.addListener(target, name, callback); return function () { return listener.remove(); }; }, [target, name, callback]); } /** * Internally used to copy values from props into API-Objects * whenever they change. * * @example * usePropBinding(marker, 'position', position); * * @internal */ function usePropBinding(object, prop, value) { React.useEffect(function () { if (!object) return; object[prop] = value; }, [object, prop,