@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
JavaScript
(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,