@mapbox/react-map-gl
Version:
A React wrapper for MapboxGL-js and overlay API.
538 lines (421 loc) • 16.4 kB
JavaScript
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