UNPKG

@mapbox/react-map-gl

Version:

A React wrapper for MapboxGL-js and overlay API.

538 lines (421 loc) 16.4 kB
import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck"; import _createClass from "@babel/runtime/helpers/esm/createClass"; import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; // Copyright (c) 2015 Uber Technologies, Inc. // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. /* global window, process, HTMLCanvasElement */ import PropTypes from 'prop-types'; import { document } from '../utils/globals'; function noop() {} function defaultOnError(event) { if (event) { console.error(event.error); // eslint-disable-line } } var propTypes = { // Creation parameters container: PropTypes.object, /** The container to have the map. */ gl: PropTypes.object, /** External WebGLContext to use */ mapboxApiAccessToken: PropTypes.string, /** Mapbox API access token for Mapbox tiles/styles. */ mapboxApiUrl: PropTypes.string, attributionControl: PropTypes.bool, /** Show attribution control or not. */ preserveDrawingBuffer: PropTypes.bool, /** Useful when you want to export the canvas as a PNG. */ reuseMaps: PropTypes.bool, transformRequest: PropTypes.func, /** The transformRequest callback for the map */ mapOptions: PropTypes.object, /** Extra options to pass to Mapbox constructor. See #545. **/ mapStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), /** The Mapbox style. A string url to a MapboxGL style */ visible: PropTypes.bool, /** Whether the map is visible */ asyncRender: PropTypes.bool, /** Whether mapbox should manage its own render cycle */ onLoad: PropTypes.func, /** The onLoad callback for the map */ onError: PropTypes.func, /** The onError callback for the map */ // Map view state width: PropTypes.number, /** The width of the map. */ height: PropTypes.number, /** The height of the map. */ viewState: PropTypes.object, /** object containing lng/lat/zoom/bearing/pitch */ longitude: PropTypes.number, /** The longitude of the center of the map. */ latitude: PropTypes.number, /** The latitude of the center of the map. */ zoom: PropTypes.number, /** The tile zoom level of the map. */ bearing: PropTypes.number, /** Specify the bearing of the viewport */ pitch: PropTypes.number, /** Specify the pitch of the viewport */ // Note: Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137 altitude: PropTypes.number /** Altitude of the viewport camera. Default 1.5 "screen heights" */ }; var defaultProps = { container: document.body, mapboxApiAccessToken: getAccessToken(), mapboxApiUrl: 'https://api.mapbox.com', preserveDrawingBuffer: false, attributionControl: true, reuseMaps: false, mapOptions: {}, mapStyle: 'mapbox://styles/mapbox/light-v8', visible: true, asyncRender: false, onLoad: noop, onError: defaultOnError, width: 0, height: 0, longitude: 0, latitude: 0, zoom: 0, bearing: 0, pitch: 0 }; // Try to get access token from URL, env, local storage or config export function getAccessToken() { var accessToken = null; if (typeof window !== 'undefined' && window.location) { var match = window.location.search.match(/access_token=([^&\/]*)/); accessToken = match && match[1]; } if (!accessToken && typeof process !== 'undefined') { // Note: This depends on bundler plugins (e.g. webpack) importing environment correctly accessToken = accessToken || process.env.MapboxAccessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN; // eslint-disable-line } // Prevents mapbox from throwing return accessToken || 'no-token'; } // Helper function to merge defaultProps and check prop types function checkPropTypes(props) { var component = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'component'; // TODO - check for production (unless done by prop types package?) if (props.debug) { PropTypes.checkPropTypes(propTypes, props, 'prop', component); } } // A small wrapper class for mapbox-gl // - Provides a prop style interface (that can be trivially used by a React wrapper) // - Makes sure mapbox doesn't crash under Node // - Handles map reuse (to work around Mapbox resource leak issues) // - Provides support for specifying tokens during development var Mapbox = /*#__PURE__*/ function () { function Mapbox(props) { var _this = this; _classCallCheck(this, Mapbox); _defineProperty(this, "mapboxgl", void 0); _defineProperty(this, "props", defaultProps); _defineProperty(this, "_map", null); _defineProperty(this, "width", 0); _defineProperty(this, "height", 0); _defineProperty(this, "_fireLoadEvent", function () { _this.props.onLoad({ type: 'load', target: _this._map }); }); if (!props.mapboxgl) { throw new Error('Mapbox not available'); } this.mapboxgl = props.mapboxgl; if (!Mapbox.initialized) { Mapbox.initialized = true; // Version detection using babel plugin // $FlowFixMe // const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'untranspiled source'; // TODO - expose version for debug this._checkStyleSheet(this.mapboxgl.version); } this._initialize(props); } _createClass(Mapbox, [{ key: "finalize", value: function finalize() { this._destroy(); return this; } }, { key: "setProps", value: function setProps(props) { this._update(this.props, props); return this; } // Mapbox's map.resize() reads size from DOM, so DOM element must already be resized // In a system like React we must wait to read size until after render // (e.g. until "componentDidUpdate") }, { key: "resize", value: function resize() { this._map.resize(); return this; } // Force redraw the map now. Typically resize() and jumpTo() is reflected in the next // render cycle, which is managed by Mapbox's animation loop. // This removes the synchronization issue caused by requestAnimationFrame. }, { key: "redraw", value: function redraw() { var map = this._map; // map._render will throw error if style does not exist // https://github.com/mapbox/mapbox-gl-js/blob/fb9fc316da14e99ff4368f3e4faa3888fb43c513 // /src/ui/map.js#L1834 if (map.style) { // cancel the scheduled update if (map._frame) { map._frame.cancel(); map._frame = null; } // the order is important - render() may schedule another update map._render(); } } // External apps can access map this way }, { key: "getMap", value: function getMap() { return this._map; } // PRIVATE API }, { key: "_reuse", value: function _reuse(props) { this._map = Mapbox.savedMap; // When reusing the saved map, we need to reparent the map(canvas) and other child nodes // intoto the new container from the props. // Step1: reparenting child nodes from old container to new container var oldContainer = this._map.getContainer(); var newContainer = props.container; newContainer.classList.add('mapboxgl-map'); while (oldContainer.childNodes.length > 0) { newContainer.appendChild(oldContainer.childNodes[0]); } // Step2: replace the internal container with new container from the react component this._map._container = newContainer; Mapbox.savedMap = null; // Step3: update style and call onload again if (props.mapStyle) { this._map.setStyle(props.mapStyle, { diff: true }); } // call onload event handler after style fully loaded when style needs update if (this._map.isStyleLoaded()) { this._fireLoadEvent(); } else { this._map.once('styledata', this._fireLoadEvent); } } }, { key: "_create", value: function _create(props) { // Reuse a saved map, if available if (props.reuseMaps && Mapbox.savedMap) { this._reuse(props); } else { if (props.gl) { var getContext = HTMLCanvasElement.prototype.getContext; // Hijack canvas.getContext to return our own WebGLContext // This will be called inside the mapboxgl.Map constructor // $FlowFixMe HTMLCanvasElement.prototype.getContext = function () { // Unhijack immediately // $FlowFixMe HTMLCanvasElement.prototype.getContext = getContext; return props.gl; }; } var mapOptions = { container: props.container, center: [0, 0], zoom: 8, pitch: 0, bearing: 0, maxZoom: 24, style: props.mapStyle, interactive: false, trackResize: false, attributionControl: props.attributionControl, preserveDrawingBuffer: props.preserveDrawingBuffer }; // We don't want to pass a null or no-op transformRequest function. if (props.transformRequest) { mapOptions.transformRequest = props.transformRequest; } this._map = new this.mapboxgl.Map(Object.assign({}, mapOptions, props.mapOptions)); // Attach optional onLoad function this._map.once('load', props.onLoad); this._map.on('error', props.onError); } return this; } }, { key: "_destroy", value: function _destroy() { if (!this._map) { return; } if (!Mapbox.savedMap) { Mapbox.savedMap = this._map; // deregister the mapbox event listeners this._map.off('load', this.props.onLoad); this._map.off('error', this.props.onError); this._map.off('styledata', this._fireLoadEvent); } else { this._map.remove(); } this._map = null; } }, { key: "_initialize", value: function _initialize(props) { var _this2 = this; props = Object.assign({}, defaultProps, props); checkPropTypes(props, 'Mapbox'); // Creation only props this.mapboxgl.accessToken = props.mapboxApiAccessToken || defaultProps.mapboxApiAccessToken; this.mapboxgl.baseApiUrl = props.mapboxApiUrl; this._create(props); // Hijack dimension properties // This eliminates the timing issue between calling resize() and DOM update /* eslint-disable accessor-pairs */ var _props = props, container = _props.container; // $FlowFixMe Object.defineProperty(container, 'offsetWidth', { get: function get() { return _this2.width; } }); // $FlowFixMe Object.defineProperty(container, 'clientWidth', { get: function get() { return _this2.width; } }); // $FlowFixMe Object.defineProperty(container, 'offsetHeight', { get: function get() { return _this2.height; } }); // $FlowFixMe Object.defineProperty(container, 'clientHeight', { get: function get() { return _this2.height; } }); // Disable outline style var canvas = this._map.getCanvas(); if (canvas) { canvas.style.outline = 'none'; } this._updateMapViewport({}, props); this._updateMapSize({}, props); this.props = props; } }, { key: "_update", value: function _update(oldProps, newProps) { if (!this._map) { return; } newProps = Object.assign({}, this.props, newProps); checkPropTypes(newProps, 'Mapbox'); var viewportChanged = this._updateMapViewport(oldProps, newProps); var sizeChanged = this._updateMapSize(oldProps, newProps); if (!newProps.asyncRender && (viewportChanged || sizeChanged)) { this.redraw(); } this.props = newProps; } // Note: needs to be called after render (e.g. in componentDidUpdate) }, { key: "_updateMapSize", value: function _updateMapSize(oldProps, newProps) { var sizeChanged = oldProps.width !== newProps.width || oldProps.height !== newProps.height; if (sizeChanged) { this.width = newProps.width; this.height = newProps.height; this.resize(); } return sizeChanged; } }, { key: "_updateMapViewport", value: function _updateMapViewport(oldProps, newProps) { var oldViewState = this._getViewState(oldProps); var newViewState = this._getViewState(newProps); var viewportChanged = newViewState.latitude !== oldViewState.latitude || newViewState.longitude !== oldViewState.longitude || newViewState.zoom !== oldViewState.zoom || newViewState.pitch !== oldViewState.pitch || newViewState.bearing !== oldViewState.bearing || newViewState.altitude !== oldViewState.altitude; if (viewportChanged) { this._map.jumpTo(this._viewStateToMapboxProps(newViewState)); // TODO - jumpTo doesn't handle altitude if (newViewState.altitude !== oldViewState.altitude) { this._map.transform.altitude = newViewState.altitude; } } return viewportChanged; } }, { key: "_getViewState", value: function _getViewState(props) { var _ref = props.viewState || props, longitude = _ref.longitude, latitude = _ref.latitude, zoom = _ref.zoom, _ref$pitch = _ref.pitch, pitch = _ref$pitch === void 0 ? 0 : _ref$pitch, _ref$bearing = _ref.bearing, bearing = _ref$bearing === void 0 ? 0 : _ref$bearing, _ref$altitude = _ref.altitude, altitude = _ref$altitude === void 0 ? 1.5 : _ref$altitude; return { longitude: longitude, latitude: latitude, zoom: zoom, pitch: pitch, bearing: bearing, altitude: altitude }; } }, { key: "_checkStyleSheet", value: function _checkStyleSheet() { var mapboxVersion = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '0.47.0'; if (typeof document === 'undefined') { return; } // check mapbox styles try { var testElement = document.createElement('div'); testElement.className = 'mapboxgl-map'; testElement.style.display = 'none'; document.body.append(testElement); var isCssLoaded = window.getComputedStyle(testElement).position !== 'static'; if (!isCssLoaded) { // attempt to insert mapbox stylesheet var link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('href', "https://api.tiles.mapbox.com/mapbox-gl-js/v".concat(mapboxVersion, "/mapbox-gl.css")); document.head.append(link); } } catch (error) {// do nothing } } }, { key: "_viewStateToMapboxProps", value: function _viewStateToMapboxProps(viewState) { return { center: [viewState.longitude, viewState.latitude], zoom: viewState.zoom, bearing: viewState.bearing, pitch: viewState.pitch }; } }]); return Mapbox; }(); _defineProperty(Mapbox, "initialized", false); _defineProperty(Mapbox, "propTypes", propTypes); _defineProperty(Mapbox, "defaultProps", defaultProps); _defineProperty(Mapbox, "savedMap", null); export { Mapbox as default }; //# sourceMappingURL=mapbox.js.map