├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── example ├── src │ ├── example.jsx │ └── index.jsx ├── web │ ├── example.html │ └── index.html └── webpack.config.js ├── index.js ├── package.json ├── src ├── evented.jsx ├── facades │ ├── map.jsx │ └── transform.jsx ├── index.js ├── map-events.jsx ├── map.jsx └── utils │ ├── index.jsx │ ├── map.jsx │ ├── styles.jsx │ └── transform.jsx └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "react", "es2015", "stage-1"], 3 | "plugins": [ ], 4 | "env": { 5 | "test": { 6 | "plugins": ["istanbul"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | example/webpack.config.js 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "modules": true, 8 | "jsx": true, 9 | "experimentalObjectRestSpread": true, 10 | "destructuring": true 11 | } 12 | }, 13 | "rules": { 14 | "new-cap": [2, { "capIsNewExceptions": ["List", "Map", "Seq", "Immutable", "Dimensions", "ViewportMercator", "Promise"] }], 15 | "one-var": [0], 16 | "no-underscore-dangle": [0], 17 | "no-console": [0], 18 | "max-len": [0] 19 | }, 20 | "env": { 21 | "browser": true, 22 | "es6": true, 23 | "mocha": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Example dist 40 | example/web/dist 41 | example/web/bundle.js 42 | 43 | # Output 44 | dist/ 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cameron Manderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | 25 | This contains code from react-map-gl 26 | 27 | Copyright (c) 2015 Uber Technologies, Inc. 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in 37 | all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 45 | THE SOFTWARE. 46 | 47 | --- 48 | 49 | This contains code from MapboxGL-js 50 | 51 | Copyright (c) 2014, Mapbox 52 | 53 | All rights reserved. 54 | Redistribution and use in source and binary forms, with or without modification, 55 | are permitted provided that the following conditions are met: 56 | 57 | * Redistributions of source code must retain the above copyright notice, 58 | this list of conditions and the following disclaimer. 59 | * Redistributions in binary form must reproduce the above copyright notice, 60 | this list of conditions and the following disclaimer in the documentation 61 | and/or other materials provided with the distribution. 62 | * Neither the name of Mapbox GL JS nor the names of its contributors 63 | may be used to endorse or promote products derived from this software 64 | without specific prior written permission. 65 | 66 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 67 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 68 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 69 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 70 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 71 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 72 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 73 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 74 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 75 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 76 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 77 | 78 | ------------------------------------------------------------------------------- 79 | 80 | Contains glmatrix.js 81 | 82 | Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. 83 | 84 | Redistribution and use in source and binary forms, with or without modification, 85 | are permitted provided that the following conditions are met: 86 | 87 | * Redistributions of source code must retain the above copyright notice, this 88 | list of conditions and the following disclaimer. 89 | * Redistributions in binary form must reproduce the above copyright notice, 90 | this list of conditions and the following disclaimer in the documentation 91 | and/or other materials provided with the distribution. 92 | 93 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 94 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 95 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 96 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 97 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 98 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 99 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 100 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 101 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 102 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 103 | 104 | ------------------------------------------------------------------------------- 105 | 106 | Contains Hershey Simplex Font: http://paulbourke.net/dataformats/hershey/ 107 | 108 | ------------------------------------------------------------------------------- 109 | 110 | Contains code from glfx.js 111 | 112 | Copyright (C) 2011 by Evan Wallace 113 | 114 | Permission is hereby granted, free of charge, to any person obtaining a copy 115 | of this software and associated documentation files (the "Software"), to deal 116 | in the Software without restriction, including without limitation the rights 117 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 118 | copies of the Software, and to permit persons to whom the Software is 119 | furnished to do so, subject to the following conditions: 120 | 121 | The above copyright notice and this permission notice shall be included in 122 | all copies or substantial portions of the Software. 123 | 124 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 125 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 126 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 127 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 128 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 129 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 130 | THE SOFTWARE. 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React WebGL Maps with Mapbox GL JS 2 | 3 | *react-map-gl-alt* provides a [React](http://facebook.github.io/react/) friendly 4 | API wrapper around [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/). A webGl 5 | based vector tile mapping library. 6 | 7 | This library is "bare-bones" without the kitchen-sink, for more direct API 8 | interactions with the mapbox API. 9 | 10 | [![NPM](https://nodei.co/npm/react-map-gl-alt.png?downloads=true&downloadRank=true)](https://nodei.co/npm/react-map-gl-alt/) 11 | ![Dependency Management](https://david-dm.org/AlpacaTravel/react-map-gl-alt.svg) 12 | 13 | ## Overview 14 | 15 | This project looks at improved programmatic bindings to the API, motion control, 16 | event handling and style management (such as Redux controlled state opposed to 17 | React or style URLs). 18 | 19 | At a basic level, this library provides your React project with beautiful WebGL 20 | maps that can be used for data visualiations and improved interactivity. 21 | 22 | If you plan on creating simple maps and are new to mapbox gl js, you may find 23 | simpler integrations using [react-mapbox-gl](https://github.com/alex3165/react-mapbox-gl). 24 | If you require finer control of your style, API and want to leverage more of the 25 | API this project we think will help you. 26 | 27 | ## Overview 28 | 29 | ### Installation 30 | 31 | ``` 32 | npm install react-map-gl-alt --save 33 | ``` 34 | 35 | This package works with compatible mapbox-gl-js build approaches, including 36 | webpack, browserify, etc. 37 | 38 | #### Using Webpack 39 | 40 | You will need to set your resolve "alias" section to include the mapbox gl js 41 | dist file directly. Mapbox GL JS has recommended this approach since version 42 | 0.25.0. 43 | 44 | See [webpack configuration example](https://github.com/AlpacaTravel/react-map-gl-alt/blob/master/example/webpack.config.js) 45 | 46 | ### Simple Usage 47 | 48 | ```jsx 49 | import MapGL from 'react-map-gl-alt'; 50 | 51 | // Simple react component returning your webGL map 52 | export const MapExample = () => ( 53 | 57 | ); 58 | ``` 59 | 60 | ## Running the examples 61 | 62 | Clone the example to your local fs. The example shows a simple full screen 63 | interaction controlled by this component (built using webpack etc). 64 | 65 | ``` 66 | npm install 67 | npm start 68 | ``` 69 | 70 | ### Usage (showing map accessor interaction with map events) 71 | 72 | ```jsx 73 | const hover = (e) => { 74 | // Access features under cursor through safe non-mutable map facade 75 | const features = e.target.queryRenderedFeatures(e.point); 76 | } 77 | const move = (e) => { 78 | // Differentiate user interaction versus flyTo 79 | if (e.originalEvent) { 80 | console.log('A human moved the map'); 81 | } 82 | // Access map props (no mutation possible) 83 | console.log(e.target.getCenter(), e.target.getZoom()); 84 | } 85 | 86 | // Can update center/zoom etc to move 87 | return ( 88 | ({ command: 'flyTo', args: [{ 96 | ...target, 97 | // Use animation options, duration etc. 98 | speed: 1.5, 99 | curve: 1.8, 100 | }])} 101 | scrollZoomDisabled={true} 102 | > 103 | { this.setState({ loaded: true }); }} 105 | onError={console.error} 106 | onMouseMove={hover} 107 | onMove={move} 108 | onClick={...} 109 | /> 110 | 111 | ); 112 | ``` 113 | 114 | ## Orienting the viewport 115 | 116 | You can provide a number of props to help control the location of the map. This 117 | includes providing center, longitude latitude or a bounds. 118 | 119 | * Supply a center or longitude/latitude props (LngLat like center is prefered) 120 | * Supply bounds (e.g. LngLatBounds like) 121 | * Supply zoom/bearing/pitch 122 | 123 | 124 | ```jsx 125 | // Using a bounds 126 | return ( 127 | ({ 130 | command: 'fitBounds', 131 | args: [ 132 | target.bounds, 133 | { animate: false, padding: 50 } 134 | ], 135 | })} 136 | /> 137 | ); 138 | ``` 139 | 140 | Note: Mapbox API does not support instantiating a map with just a bounds, 141 | you need to mount the component with a center and then apply your bounds. 142 | 143 | ## Map Options 144 | 145 | You can adjust the map following map options through the `````` exposed 146 | props; 147 | 148 | * scrollZoomDisabled (default false) 149 | * dragRotateDisabled (default false) 150 | * dragPanDisabled (default false) 151 | * keyboardDisabled (default false) 152 | * doubleClickZoomDisabled (default false) 153 | * touchZoomRotateDisabled (default false) 154 | * trackResizeDisabled (default false) 155 | * interactiveDisabled (default false) 156 | * pitchWithRotateDisabled (deafult false) 157 | * attributionControlDisabled (default false) 158 | * failIfMajorPerformanceCaveatDisabled (default false) 159 | * preserveDrawingBuffer (default true) 160 | * bearingSnap (default 7) 161 | * mapClasses (default []) 162 | 163 | There are also some enhanced behaviours to assist. 164 | 165 | * worldCopyJumpDisabled (default true) 166 | * trackResizeContainerDisabled (default true) 167 | * crossSourceCollisionsDisabled (default false) 168 | 169 | 170 | ## The Map Facade 171 | 172 | To reduce the state managed outside of this component, the component offers a 173 | facade to the map object. The facade provides all accessor based methods for 174 | those familiar with the mapbox API spec (e.g. getCenter, getCanvas etc) 175 | 176 | ```jsx 177 | const click = (e) => { 178 | // Access the read only properties of the map 179 | const map = e.target; 180 | } 181 | 182 | ... 183 | return ( 184 | 185 | 186 | 187 | ); 188 | ``` 189 | 190 | This is included along with events from the component/api, replacing the 191 | exposed 'target' event. 192 | 193 | Note: This Map Facade is available to children context. 194 | 195 | ### Transform 196 | 197 | The map facade also offers access to the map transform. This is done through 198 | a Transform Facade. 199 | 200 | ```jsx 201 | // Access the transform facade (read only) through the map facade 202 | const transform = map.transform; 203 | 204 | // Optionally use a clone to have a working transform (detached); 205 | const clonedTransform = map.cloneTransform(); // version unlocked clone.. 206 | ``` 207 | 208 | The cloned transform can provide the ability to calculate zoom around and other 209 | various viewport calculations in a interaction controlled state. 210 | 211 | ### Available Events 212 | 213 | All the current documented events are accessible in this implementation. All 214 | are available through using the MapEvents component with the prefix 'on', e.g. 215 | onClick, onLoad, onMouseMove, onDrag etc. 216 | 217 | * onStyleLoad 218 | * onResize 219 | * onWebGLContextLost 220 | * onWebGLContextRestored 221 | * onRemove 222 | * onDataLoading 223 | * onRender 224 | * onLoad 225 | * onData 226 | * onError 227 | * onMouseOut 228 | * onMouseDown 229 | * onMouseUp 230 | * onMouseMove 231 | * onTouchStart 232 | * onTouchEnd 233 | * onTouchMove 234 | * onTouchCancel 235 | * onClick 236 | * onDblClick 237 | * onContextMenu 238 | * onMoveStart 239 | * onMove 240 | * onMoveEnd 241 | * onZoomStart 242 | * onZoomEnd 243 | * onZoom 244 | * onBoxZoomCancel 245 | * onBoxZoomEnd 246 | * onBoxZoomStart 247 | * onRotateStart 248 | * onRotateEnd 249 | * onDragStart 250 | * onDragEnd 251 | * onDrag 252 | * onPitch 253 | 254 | Note: This component can be mounted many times in the children of the MapGL 255 | component, enabling you to have many different function interactions depending 256 | on what is mounted. There is support for multiple 'onClick' etc. listeners 257 | using this method. 258 | 259 | ## Controlling move animations 260 | 261 | You can provide new viewport details to the map, and as well have the 262 | opportunity to control the animation. This allows you to control the Camera and 263 | Animation options, as well as customise the usual Mapbox API args. 264 | 265 | ### Supported move animations 266 | 267 | * flyTo 268 | * jumpTo 269 | * panTo 270 | * zoomTo 271 | * easeTo 272 | * rotateTo 273 | * fitBounds 274 | * panTo 275 | * fitScreenCoordinates 276 | 277 | Other possible move control include zoomIn/zoomOut, snapToNorth, resetNorth. Be 278 | cautious using these as you need to anticipate their state. 279 | 280 | ### Usage with move function 281 | 282 | ```jsx 283 | const move = (target) => ({ 284 | command: 'flyTo', 285 | args: [ 286 | { 287 | ...target, // Standard target viewport 288 | // Additional camera/animation options 289 | speed: 0.2, // Slow it down 290 | curve: 1, 291 | // Control the easing 292 | easing: function(t) { 293 | return t; 294 | } 295 | } 296 | ] 297 | }); 298 | 299 | return ( 300 | 304 | ); 305 | ``` 306 | 307 | ### Controlled animation 308 | 309 | Supporting stateless behaviour, this library can use React-Motion or any other 310 | animation prop controled easing/animation library. 311 | 312 | ```jsx 313 | 317 | {({ latitude, longitude }) => } 323 | 324 | ``` 325 | 326 | ### Controlling viewport interactions (stateless map viewport) 327 | 328 | If you chose to fully control the viewstate component and control your own 329 | interactions, you can wrap this component in your own interaction component. 330 | 331 | The child context provides access to the read only Map Facade, which supports 332 | read-only project/unproject/transform as well as a cloneTransform etc. 333 | 334 | ```jsx 335 | return ( 336 | 340 | this.setState({ viewport })} 342 | /> 343 | 344 | ); 345 | ``` 346 | 347 | ### featureStates 348 | 349 | You can pass the prop of featureState with an array of feature states. 350 | 351 | ``` 352 | // References to the feature state 353 | const feature = { source: 'mySource', sourceLayer: 'default', id: '123' }; 354 | const state = { selected: true }; 355 | 356 | const featureStates = [ 357 | { feature, state } 358 | ]; 359 | 360 | // Apply to the prop featureStates 361 | 362 | ``` 363 | 364 | ### Controlling Attribution 365 | 366 | * attributionControlDisabled (default false) 367 | * logoPosition (default 'bottom-left') 368 | * customAttribution (default null, accepts string|array) 369 | 370 | ### Additional Props for initiation of maps 371 | 372 | In addition to the API offered above, the additional props are exposed for 373 | initiating the map: 374 | 375 | * collectResourceTimingDisabled (default true) 376 | * transformRequest (default null) 377 | * localIdeographFontFamily (default null) 378 | * maxTileCacheSize (default null) 379 | * clickTolerance (default 3) 380 | 381 | ### worldCopyJumpDisabled (Experimental - default false) 382 | 383 | When enabled, the map tracks the pan to another copy of the world and resets 384 | the center. This is to assist emulating the behaviour of Leaflet 385 | world copy jump behaviour. This behaviour will reset the center to the wrapped 386 | center after the end of a move behaviour. 387 | 388 | This can help the projection of your overlays (popups and custom markers etc) 389 | that typically have problems projecting with illegal lng/lat values. 390 | 391 | ### trackResizeContainerDisabled (Experimental - default false) 392 | 393 | When enabled, when the container is resized (opposed to the window), the map 394 | will have a resize() call applied. This can assist where a transition happens 395 | to the map component and it is important to call resize(). 396 | 397 | ### crossSourceCollisionsDisabled (default false) 398 | 399 | With the introduction of 0.46.0, Mapbox GL JS support for disabling cross source 400 | collissions has been introduced. Apply this prop to the map to apply the map 401 | option. 402 | -------------------------------------------------------------------------------- /example/src/example.jsx: -------------------------------------------------------------------------------- 1 | const mapboxgl = require('mapbox-gl'); 2 | 3 | mapboxgl.accessToken = 'pk.eyJ1IjoiYWxwYWNhdHJhdmVsIiwiYSI6ImNpazY0aTB6czAwbGhoZ20ycHN2Ynhlc28ifQ.GwAeDuQVIUb4_U1mT-QUig'; 4 | const map = new mapboxgl.Map({ 5 | container: 'map', // container id 6 | style: 'mapbox://styles/mapbox/streets-v9', // stylesheet location 7 | center: [-74.50, 40], // starting position [lng, lat] 8 | zoom: 9, 9 | }); 10 | console.log(map); 11 | -------------------------------------------------------------------------------- /example/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import MapGL from '../../src/map'; 4 | import MapEvents from '../../src/map-events'; 5 | 6 | const mapboxApiAccessToken = process.env.MAPBOX_API_ACCESS_TOKEN; 7 | 8 | const flyTo = (target) => ({ 9 | command: 'flyTo', 10 | args: [{ 11 | ...target, 12 | // Use animation options, duration etc. 13 | duration: 1000, 14 | curve: 1.8, 15 | }], 16 | }); 17 | 18 | const fitBounds = (target) => ({ 19 | command: 'fitBounds', 20 | args: [target.bounds, { duration: 0 }] 21 | }); 22 | 23 | const resetNorth = (target) => ({ 24 | command: 'resetNorth', 25 | args: [{ 26 | ...target, 27 | duration: 200, 28 | }], 29 | }); 30 | 31 | class Example extends React.Component { 32 | constructor(props, context) { 33 | super(props, context); 34 | this.state = { 35 | loaded: false, 36 | target: { 37 | center: [ 38 | 144.9633200, 39 | -37.8140000, 40 | ], 41 | zoom: 5, 42 | }, 43 | motion: flyTo, 44 | flex: 1, 45 | featureStates: [], 46 | }; 47 | 48 | this._onClick = this._onClick.bind(this); 49 | this._onChangeViewport = this._onChangeViewport.bind(this); 50 | } 51 | 52 | _onChangeViewport(viewport) { 53 | this.setState({ viewport }); 54 | } 55 | 56 | _onClick(e) { 57 | // Access features under cursor through safe non-mutable map facade 58 | const features = e.target.queryRenderedFeatures(e.point); 59 | console.log(e, features); 60 | } 61 | 62 | render() { 63 | // Can update center/zoom etc to move 64 | return ( 65 |
68 |
69 | 72 | 75 | 78 | 81 | 84 | 87 | 90 |
91 | 105 | { this.setState({ loaded: true }); }} 107 | onError={console.error} 108 | onClick={this._onClick} 109 | /> 110 | 111 |
112 | ); 113 | } 114 | } 115 | 116 | ReactDOM.render( 117 | , 118 | document.getElementById('react-map-gl-alt') 119 | ); 120 | -------------------------------------------------------------------------------- /example/web/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Display a map 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 |
17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-map-gl-alt 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | webpack = require("webpack"); 3 | 4 | var mapboxApiAccessToken = process.env.MAPBOX_API_ACCESS_TOKEN || 5 | 'pk.eyJ1IjoiYWxwYWNhdHJhdmVsIiwiYSI6ImNpazY0aTB6czAwbGhoZ20ycHN2Ynhlc28ifQ.GwAeDuQVIUb4_U1mT-QUig'; 6 | 7 | var config = { 8 | entry: [ 9 | path.resolve(__dirname, 'src/index.jsx'), 10 | ], 11 | output: { 12 | path: path.resolve(__dirname, 'web'), 13 | publicPath: '/', 14 | filename: 'bundle.js' 15 | }, 16 | target: "web", 17 | plugins: [ 18 | new webpack.DefinePlugin({ 19 | 'process.env': { 20 | 'MAPBOX_API_ACCESS_TOKEN': JSON.stringify(mapboxApiAccessToken), 21 | } 22 | }) 23 | ], 24 | module: { 25 | loaders: [{ 26 | test: /\.jsx?$/, 27 | exclude: /node_modules/, 28 | loader: 'babel', 29 | query: { 30 | presets: ['react', 'es2015', 'stage-1'], 31 | } 32 | }, { 33 | test: /\.json$/, 34 | loader: 'json', 35 | }], 36 | noParse: [ 37 | // /mapbox-gl-style-spec\/migrations.+/, 38 | /jsonlint-lines.+/ 39 | ], 40 | }, 41 | resolve: { 42 | extensions: ['', '.webpack.js', '.web.js', '.jsx', '.js'], 43 | alias: { 44 | 'mapbox-gl': path.resolve(__dirname, '../node_modules/mapbox-gl/dist/mapbox-gl.js') 45 | } 46 | }, 47 | devServer: { 48 | historyApiFallback: true, 49 | contentBase: path.resolve(__dirname, 'web'), 50 | watchOptions: { 51 | poll: true 52 | } 53 | } 54 | }; 55 | 56 | module.exports = config; 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-map-gl-alt", 3 | "version": "1.1.1", 4 | "description": "A React wrapper for Mapbox-GL-JS", 5 | "main": "index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config example/webpack.config.js --host 0.0.0.0 --port 8080 --open", 11 | "lint": "node ./node_modules/eslint/bin/eslint.js src/**/*", 12 | "compile": "babel -d dist/ src/", 13 | "prepublish": "npm run compile" 14 | }, 15 | "repository": "https://github.com/AlpacaTravel/react-map-gl-alt.git", 16 | "keywords": [ 17 | "react", 18 | "mapbox-gl", 19 | "mapbox", 20 | "vector", 21 | "map" 22 | ], 23 | "author": "Cam Manderson", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/AlpacaTravel/react-map-gl-alt/issues" 27 | }, 28 | "homepage": "https://github.com/AlpacaTravel/react-map-gl-alt#readme", 29 | "dependencies": { 30 | "@mapbox/mapbox-gl-style-spec": "^10.0.0", 31 | "element-resize-event": "^3.0.3", 32 | "lodash.has": "^4.5.2", 33 | "lodash.isequal": "^4.5.0", 34 | "prop-types": "^15.6.2", 35 | "react": "^16.2.0", 36 | "react-dom": "^16.2.0" 37 | }, 38 | "peerDependencies": { 39 | "mapbox-gl": "^1.5.0" 40 | }, 41 | "devDependencies": { 42 | "@mapbox/mapbox-gl-style-spec": "^10.0.0", 43 | "babel-cli": "^6.16.0", 44 | "babel-core": "^6.17.0", 45 | "babel-loader": "^6.2.5", 46 | "babel-preset-es2015": "^6.16.0", 47 | "babel-preset-react": "^6.16.0", 48 | "babel-preset-stage-1": "^6.16.0", 49 | "element-resize-event": "^3.0.3", 50 | "eslint": "^2.13.1", 51 | "eslint-config-airbnb": "^9.0.1", 52 | "eslint-plugin-import": "^1.8.1", 53 | "eslint-plugin-jsx-a11y": "^1.5.3", 54 | "eslint-plugin-react": "^5.1.1", 55 | "gl": "^4.0.2", 56 | "istanbul": "^0.4.5", 57 | "jsdom": "^9.6.0", 58 | "json-loader": "^0.5.4", 59 | "json2mq": "^0.2.0", 60 | "lodash.has": "^4.5.2", 61 | "lodash.isequal": "^4.5.0", 62 | "mapbox-gl": "^1.5.0", 63 | "prop-types": "^15.6.2", 64 | "react": "^16.2.0", 65 | "react-dom": "^16.2.0", 66 | "webpack": "^1.13.2", 67 | "webpack-dev-server": "^1.16.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/evented.jsx: -------------------------------------------------------------------------------- 1 | export default class Evented { 2 | constructor(map, mapAccessor) { 3 | this._map = map; 4 | this._mapAccessor = mapAccessor; 5 | this._listeners = {}; 6 | 7 | // Register the listeners for the map events 8 | this._map.on('resize', (...args) => { this.notifyListeners('resize', args); }); 9 | this._map.on('remove', (...args) => { this.notifyListeners('remove', args); }); 10 | this._map.on('render', (...args) => { this.notifyListeners('render', args); }); 11 | this._map.on('load', (...args) => { this.notifyListeners('load', args); }); 12 | this._map.on('error', (...args) => { this.notifyListeners('error', args); }); 13 | this._map.on('movestart', (...args) => { this.notifyListeners('movestart', args); }); 14 | this._map.on('moveend', (...args) => { this.notifyListeners('moveend', args); }); 15 | this._map.on('boxzoomend', (...args) => { this.notifyListeners('boxzoomend', args); }); 16 | this._map.on('boxzoomstart', (...args) => { this.notifyListeners('boxzoomstart', args); }); 17 | this._map.on('dragstart', (...args) => { this.notifyListeners('dragstart', args); }); 18 | this._map.on('dragend', (...args) => { this.notifyListeners('dragend', args); }); 19 | this._map.on('webglcontextlost', (...args) => { this.notifyListeners('webglcontextlost', args); }); 20 | this._map.on('webglcontextrestored', (...args) => { this.notifyListeners('webglcontextrestored', args); }); 21 | this._map.on('dataloading', (...args) => { this.notifyListeners('dataloading', args); }); 22 | this._map.on('mouseout', (...args) => { this.notifyListeners('mouseout', args); }); 23 | this._map.on('mousedown', (...args) => { this.notifyListeners('mousedown', args); }); 24 | this._map.on('mouseup', (...args) => { this.notifyListeners('mouseup', args); }); 25 | this._map.on('mousemove', (...args) => { this.notifyListeners('mousemove', args); }); 26 | this._map.on('click', (...args) => { this.notifyListeners('click', args); }); 27 | this._map.on('dblclick', (...args) => { this.notifyListeners('dblclick', args); }); 28 | this._map.on('contextmenu', (...args) => { this.notifyListeners('contextmenu', args); }); 29 | this._map.on('touchstart', (...args) => { this.notifyListeners('touchstart', args); }); 30 | this._map.on('touchend', (...args) => { this.notifyListeners('touchend', args); }); 31 | this._map.on('touchcanel', (...args) => { this.notifyListeners('touchcanel', args); }); 32 | this._map.on('move', (...args) => { this.notifyListeners('move', args); }); 33 | this._map.on('zoomstart', (...args) => { this.notifyListeners('zoomstart', args); }); 34 | this._map.on('zoomend', (...args) => { this.notifyListeners('zoomend', args); }); 35 | this._map.on('zoom', (...args) => { this.notifyListeners('zoom', args); }); 36 | this._map.on('rotatestart', (...args) => { this.notifyListeners('rotatestart', args); }); 37 | this._map.on('rotate', (...args) => { this.notifyListeners('rotate', args); }); 38 | this._map.on('rotateend', (...args) => { this.notifyListeners('rotateend', args); }); 39 | this._map.on('drag', (...args) => { this.notifyListeners('drag', args); }); 40 | this._map.on('pitch', (...args) => { this.notifyListeners('pitch', args); }); 41 | } 42 | 43 | notifyListeners(type, args) { 44 | const listeners = this._listeners[type] || []; 45 | if (listeners.length) { 46 | switch (type) { 47 | case 'resize': 48 | case 'remove': 49 | listeners.forEach((listener) => { 50 | listener(); 51 | }); 52 | break; 53 | case 'render': 54 | case 'load': 55 | listeners.forEach((listener) => { 56 | listener({ target: this._mapAccessor }); 57 | }); 58 | break; 59 | // Safe errors 60 | case 'error': 61 | listeners.forEach((listener) => { 62 | listener(...args); 63 | }); 64 | break; 65 | // WebGLContextEvent 66 | case 'webglcontextlost': 67 | case 'webglcontextrestored': 68 | listeners.forEach((listener) => { 69 | listener(args[0]); 70 | }); 71 | break; 72 | // MapDataEvent 73 | case 'dataloading': 74 | listeners.forEach((listener) => { 75 | listener(args[0]); 76 | }); 77 | break; 78 | // MapMouseEvent 79 | case 'mouseout': 80 | case 'mousedown': 81 | case 'mouseup': 82 | case 'mousemove': 83 | case 'click': 84 | case 'dblclick': 85 | case 'contextmenu': 86 | this.notifyMapEventListeners(listeners, args); 87 | break; 88 | // MapTouchEvents 89 | case 'touchstart': 90 | case 'touchend': 91 | case 'touchcanel': 92 | this.notifyMapEventListeners(listeners, args); 93 | break; 94 | // MapMouseEvents|MouseTouchEvents 95 | case 'movestart': 96 | case 'moveend': 97 | case 'boxzoomend': 98 | case 'boxzoomstart': 99 | case 'dragstart': 100 | case 'dragend': 101 | case 'move': 102 | case 'zoomstart': 103 | case 'zoomend': 104 | case 'zoom': 105 | case 'rotatestart': 106 | case 'rotate': 107 | case 'rotateend': 108 | case 'drag': 109 | case 'pitch': 110 | this.notifyMapEventListeners(listeners, args); 111 | break; 112 | 113 | default: 114 | listeners.forEach((listener) => { 115 | listener(); 116 | }); 117 | break; 118 | } 119 | } 120 | } 121 | 122 | notifyMapEventListeners(listeners, args) { 123 | const mapEventClone = args[0]; 124 | mapEventClone.target = this._mapAccessor; // Accessor map only! 125 | listeners.forEach((listener) => { 126 | listener(mapEventClone); 127 | }); 128 | } 129 | 130 | on(type, listener) { 131 | this._listeners[type] = this._listeners[type] || []; 132 | this._listeners[type].push(listener); 133 | } 134 | 135 | off(type, listener) { 136 | if (this._listeners && this._listeners[type]) { 137 | const index = this._listeners[type].indexOf(listener); 138 | if (index !== -1) { 139 | this._listeners[type].splice(index, 1); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/facades/map.jsx: -------------------------------------------------------------------------------- 1 | import Evented from '../evented'; 2 | import { cloneTransform } from '../utils/transform'; 3 | import Transform from './transform'; 4 | 5 | export default class Map { 6 | constructor(map) { 7 | this._map = map; 8 | this._evented = new Evented(map, this); 9 | this.transform = new Transform(map.transform); 10 | 11 | // Bind the context to this 12 | this.on = this.on.bind(this); 13 | this.off = this.off.bind(this); 14 | this.hasClass = this.hasClass.bind(this); 15 | this.getClasses = this.getClasses.bind(this); 16 | this.getBounds = this.getBounds.bind(this); 17 | this.project = this.project.bind(this); 18 | this.unproject = this.unproject.bind(this); 19 | this.queryRenderedFeatures = this.queryRenderedFeatures.bind(this); 20 | this.querySourceFeatures = this.querySourceFeatures.bind(this); 21 | this.getContainer = this.getContainer.bind(this); 22 | this.getCanvasContainer = this.getCanvasContainer.bind(this); 23 | this.loaded = this.loaded.bind(this); 24 | this.getCenter = this.getCenter.bind(this); 25 | this.getZoom = this.getZoom.bind(this); 26 | this.getBearing = this.getBearing.bind(this); 27 | this.getPitch = this.getPitch.bind(this); 28 | this.cloneTransform = this.cloneTransform.bind(this); 29 | } 30 | on(type, action) { 31 | this._evented.on(type, action); 32 | return this; 33 | } 34 | off(type, action) { 35 | this._evented.off(type, action); 36 | return this; 37 | } 38 | hasClass(klass) { 39 | return this._map.hasClass(klass); 40 | } 41 | getClasses() { 42 | return this._map.getClasses(); 43 | } 44 | getBounds() { 45 | return this._map.getBounds(); 46 | } 47 | project(lnglat) { 48 | return this._map.project(lnglat); 49 | } 50 | unproject(point) { 51 | return this._map.unproject(point); 52 | } 53 | queryRenderedFeatures(...args) { 54 | return this._map.queryRenderedFeatures(...args); 55 | } 56 | querySourceFeatures(...args) { 57 | return this._map.querySourceFeatures(...args); 58 | } 59 | getContainer() { 60 | return this._map.getContainer(); 61 | } 62 | getCanvasContainer() { 63 | return this._map.getCanvasContainer(); 64 | } 65 | getCanvas() { 66 | return this._map.getCanvas(); 67 | } 68 | loaded() { 69 | return this._map.loaded(); 70 | } 71 | getCenter() { 72 | return this._map.getCenter(); 73 | } 74 | getZoom() { 75 | return this._map.getZoom(); 76 | } 77 | getBearing() { 78 | return this._map.getBearing(); 79 | } 80 | getPitch() { 81 | return this._map.getPitch(); 82 | } 83 | cloneTransform() { 84 | return cloneTransform(this._map.transform); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/facades/transform.jsx: -------------------------------------------------------------------------------- 1 | export default class Transform { 2 | constructor(transform) { 3 | this._transform = transform; 4 | 5 | // Bind functions to this 6 | this.coveringZoomLevel = this.coveringZoomLevel.bind(this); 7 | this.coveringTiles = this.coveringTiles.bind(this); 8 | this.zoomScale = this.zoomScale.bind(this); 9 | this.scaleZoom = this.scaleZoom.bind(this); 10 | this.lngX = this.lngX.bind(this); 11 | this.latY = this.latY.bind(this); 12 | this.xLng = this.xLng.bind(this); 13 | this.yLng = this.yLng.bind(this); 14 | this.locationPoint = this.locationPoint.bind(this); 15 | this.pointLocation = this.pointLocation.bind(this); 16 | this.locationCoordinate = this.locationCoordinate.bind(this); 17 | this.coordinateLocation = this.coordinateLocation.bind(this); 18 | this.pointCoordinate = this.pointCoordinate.bind(this); 19 | this.coordinatePoint = this.coordinatePoint.bind(this); 20 | this.calculatePosMatrix = this.calculatePosMatrix.bind(this); 21 | } 22 | get minZoom() { 23 | return this._transform.minZoom; 24 | } 25 | get maxZoom() { 26 | return this._transform.maxZoom; 27 | } 28 | get worldSize() { 29 | return this._transform.worldSize; 30 | } 31 | get scale() { 32 | return this._transform.scale; 33 | } 34 | get centerPoint() { 35 | return this._transform.centerPoint; 36 | } 37 | get size() { 38 | return this._transform.size; 39 | } 40 | get bearing() { 41 | return this._transform.bearing; 42 | } 43 | get pitch() { 44 | return this._transform.pitch; 45 | } 46 | get altitude() { 47 | return this._transform.altitude; 48 | } 49 | get zoom() { 50 | return this._transform.zoom; 51 | } 52 | get center() { 53 | return this._transform.center; 54 | } 55 | coveringZoomLevel(options) { 56 | return this._transform.coveringZoomLevel(options); 57 | } 58 | coveringTiles(options) { 59 | return this._transform.coveringTiles(options); 60 | } 61 | zoomScale(zoom) { 62 | return this._transform.zoomScale(zoom); 63 | } 64 | scaleZoom(scale) { 65 | return this._transform.scaleZoom(scale); 66 | } 67 | get x() { 68 | return this._transform.x; 69 | } 70 | get y() { 71 | return this._transform.y; 72 | } 73 | get point() { 74 | return this._transform.point; 75 | } 76 | lngX(lng, worldSize) { 77 | return this._transform.lngX(lng, worldSize); 78 | } 79 | latY(lat, worldSize) { 80 | return this._transform.latY(lat, worldSize); 81 | } 82 | xLng(x, worldSize) { 83 | return this._transform.xLng(x, worldSize); 84 | } 85 | yLng(y, worldSize) { 86 | return this._transform.yLng(y, worldSize); 87 | } 88 | locationPoint(lnglat) { 89 | return this._transform.locationPoint(lnglat); 90 | } 91 | pointLocation(p) { 92 | return this._transform.pointLocation(p); 93 | } 94 | locationCoordinate(lnglat) { 95 | return this._transform.locationCoordinate(lnglat); 96 | } 97 | coordinateLocation(coord) { 98 | return this._transform.coordinateLocation(coord); 99 | } 100 | pointCoordinate(p) { 101 | return this._transform.pointCoordinate(p); 102 | } 103 | coordinatePoint(coord) { 104 | return this._transform.coordinatePoint(coord); 105 | } 106 | calculatePosMatrix(coord, maxZoom) { 107 | return this._transform.calculatePosMatrix(coord, maxZoom); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Map from './map'; 2 | import MapEvents from './map-events'; 3 | 4 | module.exports = Map; 5 | module.exports.MapEvents = MapEvents; 6 | -------------------------------------------------------------------------------- /src/map-events.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { diff } from "./utils"; 4 | 5 | class MapEvents extends React.Component { 6 | componentDidMount() { 7 | this._updateListeners({}, this.props); 8 | } 9 | 10 | componentDidUpdate(nextProps) { 11 | this._updateListeners(this.props, nextProps); 12 | } 13 | 14 | componentWillUnmount() { 15 | this._updateListeners(this.props, {}); 16 | } 17 | 18 | _updateListener(type, current, next) { 19 | if (diff(type, current, next)) { 20 | const mapType = type.substr(2).toLowerCase(); 21 | if (current[type]) { 22 | this.context.map.off(mapType, current[type]); 23 | } 24 | if (next[type]) { 25 | this.context.map.on(mapType, next[type]); 26 | } 27 | } 28 | } 29 | 30 | _updateListeners(current, next) { 31 | this._updateListener("onStyleLoad", current, next); 32 | this._updateListener("onResize", current, next); 33 | this._updateListener("onWebGLContextLost", current, next); 34 | this._updateListener("onWebGLContextRestored", current, next); 35 | this._updateListener("onRemove", current, next); 36 | this._updateListener("onDataLoading", current, next); 37 | this._updateListener("onRender", current, next); 38 | this._updateListener("onLoad", current, next); 39 | this._updateListener("onData", current, next); 40 | this._updateListener("onError", current, next); 41 | this._updateListener("onMouseOut", current, next); 42 | this._updateListener("onMouseDown", current, next); 43 | this._updateListener("onMouseUp", current, next); 44 | this._updateListener("onMouseMove", current, next); 45 | this._updateListener("onTouchStart", current, next); 46 | this._updateListener("onTouchEnd", current, next); 47 | this._updateListener("onTouchMove", current, next); 48 | this._updateListener("onTouchCancel", current, next); 49 | this._updateListener("onClick", current, next); 50 | this._updateListener("onDblClick", current, next); 51 | this._updateListener("onContextMenu", current, next); 52 | this._updateListener("onMoveStart", current, next); 53 | this._updateListener("onMove", current, next); 54 | this._updateListener("onMoveEnd", current, next); 55 | this._updateListener("onZoomStart", current, next); 56 | this._updateListener("onZoomEnd", current, next); 57 | this._updateListener("onZoom", current, next); 58 | this._updateListener("onBoxZoomCancel", current, next); 59 | this._updateListener("onBoxZoomEnd", current, next); 60 | this._updateListener("onBoxZoomStart", current, next); 61 | this._updateListener("onRotateStart", current, next); 62 | this._updateListener("onRotateEnd", current, next); 63 | this._updateListener("onDragStart", current, next); 64 | this._updateListener("onDragEnd", current, next); 65 | this._updateListener("onDrag", current, next); 66 | this._updateListener("onPitchStart", current, next); 67 | this._updateListener("onPitchEnd", current, next); 68 | this._updateListener("onPitch", current, next); 69 | this._updateListener("onWheel", current, next); 70 | this._updateListener("onIdle", current, next); 71 | this._updateListener("onStyleData", current, next); 72 | this._updateListener("onSourceData", current, next); 73 | this._updateListener("onDataLoading", current, next); 74 | this._updateListener("onStyleDataLoading", current, next); 75 | this._updateListener("onSourceDataLoading", current, next); 76 | this._updateListener("onStyleImageMissing", current, next); 77 | } 78 | 79 | render() { 80 | return null; 81 | } 82 | } 83 | 84 | MapEvents.propTypes = { 85 | // Map events 86 | onStyleLoad: PropTypes.func, 87 | onResize: PropTypes.func, 88 | onWebGLContextLost: PropTypes.func, 89 | onWebGLContextRestored: PropTypes.func, 90 | onRemove: PropTypes.func, 91 | onDataLoading: PropTypes.func, 92 | onRender: PropTypes.func, 93 | onLoad: PropTypes.func, 94 | onIdle: PropTypes.func, 95 | onData: PropTypes.func, 96 | onError: PropTypes.func, 97 | onStyleData: PropTypes.func, 98 | onSourceData: PropTypes.func, 99 | onStyleDataLoading: PropTypes.func, 100 | onSourceDataLoading: PropTypes.func, 101 | onStyleImageMissing: PropTypes.func, 102 | 103 | // Interactive events 104 | onMouseOut: PropTypes.func, 105 | onMouseDown: PropTypes.func, 106 | onMouseUp: PropTypes.func, 107 | onMouseMove: PropTypes.func, 108 | onWheel: PropTypes.func, 109 | 110 | onTouchStart: PropTypes.func, 111 | onTouchEnd: PropTypes.func, 112 | onTouchMove: PropTypes.func, 113 | onTouchCancel: PropTypes.func, 114 | 115 | onClick: PropTypes.func, 116 | onDblClick: PropTypes.func, 117 | onContextMenu: PropTypes.func, 118 | 119 | onMoveStart: PropTypes.func, 120 | onMove: PropTypes.func, 121 | onMoveEnd: PropTypes.func, 122 | 123 | onZoomStart: PropTypes.func, 124 | onZoomEnd: PropTypes.func, 125 | onZoom: PropTypes.func, 126 | 127 | onBoxZoomCancel: PropTypes.func, 128 | onBoxZoomEnd: PropTypes.func, 129 | onBoxZoomStart: PropTypes.func, 130 | 131 | onRotateStart: PropTypes.func, 132 | onRotateEnd: PropTypes.func, 133 | 134 | onDragStart: PropTypes.func, 135 | onDragEnd: PropTypes.func, 136 | onDrag: PropTypes.func, 137 | 138 | onPitchStart: PropTypes.func, 139 | onPitchEnd: PropTypes.func, 140 | onPitch: PropTypes.func 141 | }; 142 | 143 | MapEvents.contextTypes = { 144 | map: PropTypes.object.isRequired 145 | }; 146 | 147 | export default MapEvents; 148 | -------------------------------------------------------------------------------- /src/map.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import mapboxgl from "mapbox-gl"; 4 | import { 5 | default as elementResizedEvent, 6 | unbind as removeResizeListener 7 | } from "element-resize-event"; 8 | 9 | import MapFacade from "./facades/map"; 10 | import { diff, has, mod, lngLatArray } from "./utils"; 11 | import { 12 | updateOptions as updateMapOptions, 13 | performMoveAction 14 | } from "./utils/map"; 15 | import { getInteractiveLayerIds, update as updateStyle } from "./utils/styles"; 16 | 17 | const defaultMoveAction = target => ({ command: "flyTo", args: [target] }); 18 | const defaultFitBoundsAction = target => ({ 19 | command: "fitBounds", 20 | args: [target.bounds, { animate: false, duration: 100 }] 21 | }); 22 | 23 | class Map extends React.Component { 24 | static supported() { 25 | return mapboxgl.supported(); 26 | } 27 | 28 | constructor(props, context) { 29 | super(props, context); 30 | 31 | this.state = { 32 | isSupported: Map.supported(), 33 | isDragging: false, 34 | isTouching: false, 35 | isZooming: false, 36 | isMoving: false, 37 | isLoaded: false, 38 | isStyleLoaded: false, 39 | startDragLngLat: null, 40 | startTouchLngLat: null, 41 | startZoomLngLat: null, // same as startZoom? 42 | startMoveLngLat: null, 43 | startRotatingLngLat: null, // same as startBearing? 44 | startBearing: null, 45 | startPitch: null, 46 | startZoom: null, 47 | userControlled: false, 48 | featureStates: null 49 | }; 50 | 51 | const accessToken = 52 | props.mapboxApiAccessToken || context.mapboxApiAccessToken; 53 | if (accessToken) { 54 | mapboxgl.accessToken = accessToken; 55 | } 56 | 57 | this._simpleQuery = this._simpleQuery.bind(this); 58 | this._simpleClick = this._simpleClick.bind(this); 59 | this._simpleHover = this._simpleHover.bind(this); 60 | this._onChangeViewport = this._onChangeViewport.bind(this); 61 | this._resizedContainer = this._resizedContainer.bind(this); 62 | } 63 | 64 | // Scrub map access to events 65 | getChildContext() { 66 | return { 67 | map: this._mapFacade 68 | }; 69 | } 70 | 71 | componentDidMount() { 72 | // Create the local map 73 | const mapStyle = 74 | (this.props.mapStyle && 75 | this.props.mapStyle.toJS && 76 | this.props.mapStyle.toJS()) || 77 | this.props.mapStyle; 78 | 79 | // Optionally use longitude/latitude without a center present 80 | const options = { 81 | container: this.container, 82 | style: mapStyle, 83 | hash: !this.props.hashDisabled, 84 | interactive: !this.props.interactiveDisabled, 85 | bearingSnap: this.props.bearingSnap, 86 | pitchWithRotate: this.props.pitchWithRotate, 87 | clickTolerance: this.props.clickTolerance, 88 | attributionControl: !this.props.attributionControlDisabled, 89 | customAttribution: this.props.customAttribution, 90 | logoPosition: this.props.logoPosition, 91 | failIfMajorPerformanceCaveat: !this.props 92 | .failIfMajorPerformanceCaveatDisabled, 93 | preserveDrawingBuffer: !this.props.preserveDrawingBufferDisabled, 94 | refreshExpiredTiles: !this.props.refreshExpiredTilesDisabled, 95 | trackResize: !this.props.trackResizeDisabled, 96 | 97 | bounds: this.props.bounds, 98 | fitBoundsOptions: this.props.fitBoundsOptions, 99 | maxBounds: this.props.maxBounds, 100 | renderWorldCopies: !this.props.renderWorldCopiesDisabled, 101 | maxTileCacheSize: this.props.maxTileCacheSize, 102 | localIdeographFontFamily: this.props.localIdeographFontFamily, 103 | transformRequest: this.props.transformRequest, 104 | collectResourceTiming: !this.props.collectResourceTimingDisabled, 105 | fadeDuration: this.props.fadeDuration, 106 | crossSourceCollisions: !this.props.crossSourceCollisionsDisabled 107 | }; 108 | 109 | // Initialise the viewport 110 | let updateMapViewport = null; // If we are going to call update on load 111 | if (lngLatArray(this.props) && this.props.zoom) { 112 | Object.assign(options, { 113 | center: lngLatArray(this.props), 114 | zoom: this.props.zoom, 115 | bearing: this.props.bearing, 116 | pitch: this.props.pitch 117 | }); 118 | // We have a bounds also set, let's move to it 119 | if (has(this.props, "bounds")) { 120 | updateMapViewport = { 121 | ...this.props, 122 | move: this.props.move || defaultFitBoundsAction 123 | }; 124 | } 125 | // If we have just a bounds on init 126 | } else if (has(this.props, "bounds")) { 127 | Object.assign(options, { 128 | bounds: this.props.bounds 129 | }); 130 | } 131 | 132 | // Create the map and configure the map options 133 | this._map = new mapboxgl.Map(options); 134 | this._mapFacade = new MapFacade(this._map); 135 | if (this.props.onMap && typeof this.props.onMap === "function") { 136 | this.props.onMap(this._mapFacade); 137 | } 138 | 139 | // If we have a next viewport action 140 | if (updateMapViewport) { 141 | this._updateMapViewport({}, updateMapViewport); 142 | } 143 | 144 | // Initial actions 145 | this._updateConvenienceHandlers({}, this.props); 146 | this._updateMapOptions({}, this.props); 147 | this._updateFeatureState({}, this.props); 148 | 149 | // Listen to some of the dispatched events 150 | this._listenStateEvents(); 151 | 152 | // Add in event listeners for the container 153 | elementResizedEvent(this.container, this._resizedContainer); 154 | } 155 | 156 | componentWillReceiveProps(nextProps) { 157 | this._updateConvenienceHandlers(this.props, nextProps); 158 | this._updateStyle(this.props.mapStyle, nextProps.mapStyle); 159 | this._updateMapOptions(this.props, nextProps); 160 | this._updateMapViewport(this.props, nextProps); 161 | this._updateFeatureState(this.props, nextProps); 162 | } 163 | 164 | componentWillUnmount() { 165 | // Remove in event listeners for the container 166 | // Pending; https://github.com/KyleAMathews/element-resize-event/issues/2 167 | if (removeResizeListener) { 168 | removeResizeListener(this.container, this._resizedContainer); 169 | } 170 | 171 | // Remove the map instance through the API 172 | if (this._map) { 173 | this._map.remove(); 174 | this._map = null; 175 | } 176 | } 177 | 178 | _getQueryParams() { 179 | return { 180 | layerIds: getInteractiveLayerIds(this.props.mapStyle) 181 | }; 182 | } 183 | 184 | _simpleQuery(geometry, callback) { 185 | if (!callback) { 186 | return; 187 | } 188 | const features = this._mapFacade.queryRenderedFeatures( 189 | geometry, 190 | this._getQueryParams() 191 | ); 192 | if (!features.length && this.props.ignoreEmptyFeatures) { 193 | return; 194 | } 195 | callback(features); 196 | } 197 | 198 | _simpleClick(e) { 199 | // Query the map and call the this.prop.onClickFeatures 200 | if (!this.props.onClickFeatures) { 201 | return; 202 | } 203 | const boxSize = this.props.clickRadius; 204 | const bbox = [ 205 | [e.point.x - boxSize, e.point.y - boxSize], 206 | [e.point.x + boxSize, e.point.y + boxSize] 207 | ]; 208 | this._simpleQuery(bbox, this.props.onClickFeatures); 209 | } 210 | 211 | _resizedContainer() { 212 | if (!this.props.trackResizeContainerDisabled && this._map) { 213 | this._map.resize(); 214 | if (!this.props.forceResizeContainerViewportDisabled && this._map) { 215 | this._updateMapViewport(this.props, { 216 | ...this.props, 217 | timestamp: Date.now() 218 | }); 219 | } 220 | } 221 | } 222 | 223 | _simpleHover(e) { 224 | // Query the map and call the this.prop.onHoverFeatures 225 | if (!this.props.onHoverFeatures) { 226 | return; 227 | } 228 | this._simpleQuery(e.point, this.props.onHoverFeatures); 229 | } 230 | 231 | _onChangeViewport(e) { 232 | // Obtain map viewport and call the this.prop.onChangeViewport 233 | if (!this.props.onChangeViewport) { 234 | return; 235 | } 236 | 237 | const { lng, lat } = e.target.getCenter(); 238 | this.props.onChangeViewport({ 239 | longitude: mod(lng + 180, 360) - 180, 240 | latitude: lat, 241 | center: e.target.getCenter(), 242 | zoom: e.target.getZoom(), 243 | pitch: e.target.getPitch(), 244 | bearing: mod(e.target.getBearing() + 180, 360) - 180, 245 | isDragging: this.state.isDragging, 246 | isTouching: this.state.isTouching, 247 | isZooming: this.state.isZooming, 248 | isMoving: this.state.isMoving, 249 | startDragLngLat: this.state.startDragLngLat, 250 | startTouchLngLat: this.state.startTouchLngLat, 251 | startZoomLngLat: this.state.startZoomLngLat, 252 | startMoveLngLat: this.state.startMoveLngLat, 253 | startRotatingLngLat: this.state.startRotatingLngLat, 254 | startPitch: this.state.startPitch, 255 | startBearing: this.state.startBearing, 256 | startZoom: this.state.startZoom, 257 | isUserControlled: this.state.userControlled, 258 | map: this._mapFacade 259 | }); 260 | } 261 | 262 | _listenStateEvents() { 263 | this._map.on("movestart", event => { 264 | this.setState({ 265 | startMoveLngLat: event.target.getCenter(), 266 | startBearing: event.target.getBearing(), 267 | startPitch: event.target.getPitch(), 268 | startZoom: event.target.getZoom(), 269 | isUserControlled: has(event, "originalEvent") 270 | }); 271 | }); 272 | this._map.on("moveend", e => { 273 | // Attempt to keep world within normal legal lng values 274 | // https://github.com/mapbox/mapbox-gl-js/issues/2071 275 | if (this.props.worldCopyJumpDisabled !== true) { 276 | const map = this._map; // Use map from the event 277 | if (!e.snapWorldMove && map) { 278 | map.setCenter(map.getCenter().wrap(), { snapWorldMove: true }); 279 | } 280 | } 281 | this.setState({ 282 | startMoveLngLat: null, 283 | startBearing: null, 284 | startPitch: null, 285 | startZoom: null, 286 | isUserControlled: false 287 | }); 288 | }); 289 | 290 | this._map.on("dragstart", event => { 291 | this.setState({ isDragging: true, startDragLngLat: event.lngLat }); 292 | }); 293 | this._map.on("dragend", () => { 294 | this.setState({ isDragging: false, startDragLngLat: null }); 295 | }); 296 | this._map.on("zoomstart", event => { 297 | this.setState({ isZooming: true, startZoomLngLat: event.lngLat }); 298 | }); 299 | this._map.on("zoomend", () => { 300 | this.setState({ isZooming: false, startZoomLngLat: null }); 301 | }); 302 | this._map.on("touchstart", event => { 303 | this.setState({ isTouching: true, startTouchLngLat: event.lngLat }); 304 | }); 305 | this._map.on("touchend", () => { 306 | this.setState({ isTouching: false, startTouchLngLat: null }); 307 | }); 308 | this._map.on("rotatestart", event => { 309 | this.setState({ isRotating: true, startRotatingLngLat: event.lngLat }); 310 | }); 311 | this._map.on("rotateend", () => { 312 | this.setState({ isRotating: false, startRotatingLngLat: null }); 313 | }); 314 | this._map.on("load", () => { 315 | this.setState({ isLoaded: true }); 316 | this._onChangeViewport({ target: this._mapFacade }); 317 | }); 318 | } 319 | 320 | _updateStyle(previousStyle, nextStyle) { 321 | updateStyle(this._map, previousStyle, nextStyle); 322 | } 323 | 324 | _updateMapOptions(previous, next) { 325 | updateMapOptions(this._map, previous, next); 326 | } 327 | 328 | _updateFeatureState(prevProps, nextProps) { 329 | if (diff("featureStates", prevProps, nextProps)) { 330 | // Transform the function from [{ feature, state }] to { [feature]: { feature, state } } 331 | const featureStates = fs => 332 | (fs && 333 | Array.isArray(fs) && 334 | fs.reduce( 335 | (c, t) => 336 | Object.assign({}, c, { 337 | [`${t.feature.source}:${t.feature.sourceLayer || ""}:${ 338 | t.feature.id 339 | }`]: t 340 | }), 341 | {} 342 | )) || 343 | {}; 344 | 345 | // Reset existin 346 | const current = this.state.featureStates; 347 | const next = featureStates(nextProps.featureStates); 348 | if (current) { 349 | // Any non-matching states can be reset 350 | Object.keys(current) 351 | .filter(key => !next[key]) 352 | .forEach(key => { 353 | // https://github.com/mapbox/mapbox-gl-js/issues/6889 354 | const blankState = Object.keys(current[key].state).reduce( 355 | (c, t) => Object.assign({}, c, { [t]: null }), 356 | {} 357 | ); 358 | this._map.setFeatureState(current[key].feature, blankState); 359 | }); 360 | } 361 | 362 | // Any non-matching states can be reset 363 | Object.keys(next).forEach(key => 364 | this._map.setFeatureState(next[key].feature, next[key].state) 365 | ); 366 | 367 | // Hold the feature state 368 | this.setState({ featureStates: next }); 369 | } 370 | } 371 | 372 | _updateConvenienceHandlers(prevProps, nextProps) { 373 | if (diff("onClickFeatures", prevProps, nextProps)) { 374 | if (nextProps.onClickFeatures) { 375 | this._map.on("click", this._simpleClick); 376 | } else { 377 | this._map.off("click", this._simpleClick); 378 | } 379 | } 380 | if (diff("onHoverFeatures", prevProps, nextProps)) { 381 | if (nextProps.onHoverFeatures) { 382 | this._map.on("mousemove", this._simpleHover); 383 | } else { 384 | this._map.off("mousemove", this._simpleHover); 385 | } 386 | } 387 | if (diff("onChangeViewport", prevProps, nextProps)) { 388 | if (nextProps.onChangeViewport) { 389 | this._map.on("move", this._onChangeViewport); 390 | } else { 391 | this._map.off("move", this._onChangeViewport); 392 | } 393 | } 394 | } 395 | 396 | _updateMapViewport(prior, next) { 397 | // Check if the user is controlling this currently 398 | if (this.state.userControlled === true) { 399 | return; 400 | } 401 | 402 | // Obtain the center 403 | const propsCenter = lngLatArray(prior); 404 | const nextPropsCenter = lngLatArray(next); 405 | 406 | const viewportChanged = 407 | diff("center", { center: propsCenter }, { center: nextPropsCenter }) || 408 | diff("bounds", prior, next) || 409 | diff("zoom", prior, next) || 410 | // diff('altitude', this.props, nextProps) || 411 | diff("bearing", prior, next) || 412 | diff("pitch", prior, next) || 413 | diff("timestamp", prior, next); 414 | 415 | if (viewportChanged) { 416 | const target = { 417 | center: nextPropsCenter, 418 | longitude: nextPropsCenter[0], 419 | latitude: nextPropsCenter[1], 420 | bounds: next.bounds, 421 | zoom: next.zoom, 422 | // altitude: nextProps.altitude, 423 | bearing: next.bearing, 424 | pitch: next.pitch, 425 | ...this.state 426 | }; 427 | 428 | // Use a move 429 | const moveAction = next.move || defaultMoveAction; 430 | const result = moveAction(target); 431 | if (Array.isArray(result)) { 432 | result.forEach(action => performMoveAction(this._map, action)); 433 | } else { 434 | performMoveAction(this._map, result); 435 | } 436 | } 437 | } 438 | 439 | render() { 440 | return ( 441 |
{ 443 | this.container = container; 444 | }} 445 | className="map" 446 | style={this.props.style} 447 | > 448 | {this._map && this.props.children} 449 |
450 | ); 451 | } 452 | } 453 | 454 | Map.propTypes = { 455 | style: PropTypes.object, 456 | 457 | // Mapbox access token 458 | mapboxApiAccessToken: PropTypes.string, 459 | 460 | // Main style 461 | mapStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) 462 | .isRequired, 463 | 464 | // Move control actions 465 | move: PropTypes.func, 466 | 467 | // Control Interactions 468 | scrollZoomDisabled: PropTypes.bool, 469 | boxZoomDisabled: PropTypes.bool, 470 | dragPanDisabled: PropTypes.bool, 471 | dragRotateDisabled: PropTypes.bool, 472 | keyboardDisabled: PropTypes.bool, 473 | doubleClickZoomDisabled: PropTypes.bool, 474 | touchZoomRotateDisabled: PropTypes.bool, 475 | trackResizeDisabled: PropTypes.bool, 476 | trackResizeContainerDisabled: PropTypes.bool, 477 | worldCopyJumpDisabled: PropTypes.bool, 478 | forceResizeContainerViewportDisabled: PropTypes.bool, 479 | crossSourceCollisionsDisabled: PropTypes.bool, 480 | 481 | // Convenience implementations 482 | onChangeViewport: PropTypes.func, 483 | onHoverFeatures: PropTypes.func, 484 | ignoreEmptyFeatures: PropTypes.bool, 485 | onClickFeatures: PropTypes.func, 486 | clickRadius: PropTypes.number, 487 | longitude: PropTypes.number, 488 | latitude: PropTypes.number, 489 | 490 | // Target controls 491 | center: PropTypes.oneOfType([ 492 | PropTypes.arrayOf(PropTypes.number), 493 | PropTypes.instanceOf(mapboxgl.LngLat) 494 | ]), 495 | zoom: PropTypes.number, 496 | // altitude: PropTypes.number, 497 | bearing: PropTypes.number, 498 | pitch: PropTypes.number, 499 | bounds: PropTypes.oneOfType([ 500 | PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), 501 | PropTypes.arrayOf(PropTypes.number), 502 | PropTypes.instanceOf(mapboxgl.LngLatBounds) 503 | ]), 504 | fitBoundsOptions: PropTypes.object, 505 | 506 | minZoom: PropTypes.number, 507 | maxZoom: PropTypes.number, 508 | maxBounds: PropTypes.object, 509 | hashDisabled: PropTypes.bool, 510 | interactiveDisabled: PropTypes.bool, 511 | bearingSnap: PropTypes.number, 512 | mapClasses: PropTypes.arrayOf(PropTypes.string), 513 | logoPosition: PropTypes.string, 514 | pitchWithRotate: PropTypes.bool, 515 | clickTolerance: PropTypes.number, 516 | customAttribution: PropTypes.oneOf([PropTypes.string, PropTypes.array]), 517 | fadeDuration: PropTypes.number, 518 | localIdeographFontFamily: PropTypes.string, 519 | maxTileCacheSize: PropTypes.number, 520 | collectResourceTimingDisabled: PropTypes.bool, 521 | transformRequest: PropTypes.func, 522 | renderWorldCopiesDisabled: PropTypes.bool, 523 | refreshExpiredTilesDisabled: PropTypes.bool, 524 | 525 | attributionControlDisabled: PropTypes.bool, 526 | failIfMajorPerformanceCaveatDisabled: PropTypes.bool, 527 | preserveDrawingBufferDisabled: PropTypes.bool, 528 | children: PropTypes.any, 529 | 530 | featureStates: PropTypes.arrayOf(PropTypes.object), 531 | onMap: PropTypes.func 532 | }; 533 | 534 | Map.defaultProps = { 535 | minZoom: 0, 536 | maxZoom: 20, 537 | 538 | scrollZoomDisabled: false, 539 | dragRotateDisabled: false, 540 | dragPanDisabled: false, 541 | keyboardDisabled: false, 542 | doubleClickZoomDisabled: false, 543 | touchZoomRotateDisabled: false, 544 | trackResizeContainerDisabled: true, 545 | trackResizeDisabled: false, 546 | hashDisabled: true, 547 | interactiveDisabled: false, 548 | attributionControlDisabled: false, 549 | failIfMajorPerformanceCaveatDisabled: false, 550 | preserveDrawingBufferDisabled: true, 551 | worldCopyJumpDisabled: true, 552 | forceResizeContainerViewportDisabled: false, 553 | crossSourceCollisionsDisabled: false, 554 | logoPosition: "bottom-left", 555 | refreshExpiredTilesDisabled: false, 556 | pitchWithRotate: true, 557 | clickTolerance: 3, 558 | customAttribution: null, 559 | fadeDuration: 300, 560 | localIdeographFontFamily: null, 561 | maxTileCacheSize: null, 562 | collectResourceTimingDisabled: true, 563 | transformRequest: null, 564 | renderWorldCopiesDisabled: false, 565 | fitBoundsOptions: null, 566 | onMap: null, 567 | 568 | featureStates: [], 569 | 570 | bearingSnap: 7, 571 | mapClasses: [], 572 | 573 | center: [144.96332, -37.814], 574 | 575 | zoom: 1, 576 | bearing: 0, 577 | pitch: 0, 578 | 579 | clickRadius: 15 580 | }; 581 | 582 | Map.childContextTypes = { 583 | map: PropTypes.object 584 | }; 585 | 586 | Map.contextTypes = { 587 | mapboxApiAccessToken: PropTypes.string 588 | }; 589 | 590 | export default Map; 591 | -------------------------------------------------------------------------------- /src/utils/index.jsx: -------------------------------------------------------------------------------- 1 | import _has from 'lodash.has'; 2 | import _isEqual from 'lodash.isequal'; 3 | import mapboxgl from 'mapbox-gl'; 4 | 5 | export const isEqual = (prop1, prop2) => (_isEqual(prop1, prop2)); 6 | export const has = (obj, key) => (_has(obj, key)); 7 | 8 | export const diff = (prop, obj1, obj2) => { 9 | const obj1HasProp = has(obj1, prop); 10 | const obj2HasProp = has(obj2, prop); 11 | if (!obj1HasProp && !obj2HasProp) { 12 | return false; 13 | } else if ( 14 | (obj1HasProp && !obj2HasProp) || 15 | (!obj1HasProp && obj2HasProp) 16 | ) { 17 | return true; 18 | } 19 | return !isEqual(obj1[prop], obj2[prop]); 20 | }; 21 | 22 | export const mod = (value, divisor) => { 23 | const modulus = value % divisor; 24 | return modulus < 0 ? divisor + modulus : modulus; 25 | }; 26 | 27 | export const lngLatBoundsArray = (props) => { 28 | if (!props.bounds) { 29 | return null; 30 | } 31 | 32 | if (Array.isArray(props.bounds)) { 33 | return props.bounds; 34 | } 35 | 36 | if (props.bounds instanceof mapboxgl.LngLatBounds) { 37 | return props.bounds.toArray(); 38 | } 39 | 40 | return null; 41 | }; 42 | 43 | export const lngLatArray = (props) => { 44 | if (!(props.longitude && props.latitude) && !props.center) { 45 | return null; 46 | } 47 | 48 | if (props.latitude && props.longitude && !props.center) { 49 | return [props.longitude, props.latitude]; 50 | } 51 | 52 | if (Array.isArray(props.center)) { 53 | return props.center; 54 | } 55 | 56 | if (props.center instanceof mapboxgl.LngLat) { 57 | return props.center.toArray(); 58 | } 59 | 60 | return null; 61 | }; 62 | -------------------------------------------------------------------------------- /src/utils/map.jsx: -------------------------------------------------------------------------------- 1 | import { diff } from './index'; 2 | 3 | export const updateOptions = (map, current, next) => { 4 | if (diff('minZoom', current, next)) { 5 | map.setMinZoom(next.minZoom); 6 | } 7 | if (diff('maxZoom', current, next)) { 8 | map.setMaxZoom(next.maxZoom); 9 | } 10 | if (diff('mapClasses', current, next) && map.setClasses) { 11 | map.setClasses(next.mapClasses); 12 | } 13 | if (diff('maxBounds', current, next)) { 14 | map.setMaxBounds(next.maxBounds); 15 | } 16 | if (diff('scrollZoomDisabled', current, next)) { 17 | if (next.scrollZoomDisabled === true) { 18 | map.scrollZoom.disable(); 19 | } else { 20 | map.scrollZoom.enable(); 21 | } 22 | } 23 | if (diff('boxZoomDisabled', current, next)) { 24 | if (next.boxZoomDisabled === true) { 25 | map.boxZoom.disable(); 26 | } else { 27 | map.boxZoom.enable(); 28 | } 29 | } 30 | if (diff('dragRotateDisabled', current, next)) { 31 | if (next.dragRotateDisabled === true) { 32 | map.dragRotate.disable(); 33 | } else { 34 | map.dragRotate.enable(); 35 | } 36 | } 37 | if (diff('dragPanDisabled', current, next)) { 38 | if (next.dragPanDisabled === true) { 39 | map.dragPan.disable(); 40 | } else { 41 | map.dragPan.enable(); 42 | } 43 | } 44 | if (diff('keyboardDisabled', current, next)) { 45 | if (next.keyboardDisabled === true) { 46 | map.keyboard.disable(); 47 | } else { 48 | map.keyboard.enable(); 49 | } 50 | } 51 | if (diff('doubleClickZoomDisabled', current, next)) { 52 | if (next.doubleClickZoomDisabled === true) { 53 | map.doubleClickZoom.disable(); 54 | } else { 55 | map.doubleClickZoom.enable(); 56 | } 57 | } 58 | if (diff('touchZoomRotateDisabled', current, next)) { 59 | if (next.touchZoomRotateDisabled === true) { 60 | map.touchZoomRotate.disable(); 61 | } else { 62 | map.touchZoomRotate.enable(); 63 | } 64 | } 65 | }; 66 | 67 | export const performMoveAction = (map, action) => { 68 | if (action.command && action.args) { 69 | switch (action.command) { 70 | case 'flyTo': 71 | map.flyTo(...action.args); 72 | break; 73 | case 'fitBounds': 74 | map.fitBounds(...action.args); 75 | break; 76 | case 'jumpTo': 77 | map.jumpTo(...action.args); 78 | break; 79 | case 'panTo': 80 | map.panTo(...action.args); 81 | break; 82 | case 'zoomTo': 83 | map.zoomTo(...action.args); 84 | break; 85 | case 'zoomIn': 86 | map.zoomIn(...action.args); 87 | break; 88 | case 'zoomOut': 89 | map.zoomOut(...action.args); 90 | break; 91 | case 'rotateTo': 92 | map.rotateTo(...action.args); 93 | break; 94 | case 'resetNorth': 95 | map.resetNorth(...action.args); 96 | break; 97 | case 'snapToNorth': 98 | map.snapToNorth(...action.args); 99 | break; 100 | case 'easeTo': 101 | map.easeTo(...action.args); 102 | break; 103 | case 'fitScreenCoordinates': 104 | map.fitScreenCoordinates(...action.args); 105 | break; 106 | default: break; 107 | } 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /src/utils/styles.jsx: -------------------------------------------------------------------------------- 1 | import { default as diffStyles } from '@mapbox/mapbox-gl-style-spec/diff'; 2 | import { isEqual, has } from './index'; 3 | 4 | export const getInteractiveLayerIds = (style) => { 5 | const mapStyle = (style.toJS && style.toJS()) || style; 6 | 7 | if (Array.isArray(mapStyle.layers)) { 8 | return mapStyle.layers 9 | .filter(l => l.interactive) 10 | .map(l => l.id); 11 | } 12 | 13 | return []; 14 | }; 15 | 16 | export const areGeoJSONSourcePropertiesSimilar = (source, newSource) => { 17 | const compareableOriginal = {}; 18 | const compareableNew = {}; 19 | const geojsonVtOptionsExtent = source.workerOptions.geojsonVtOptions.extent; 20 | const tileSize = source.tileSize; 21 | const scale = geojsonVtOptionsExtent / tileSize; 22 | if (has(newSource, 'buffer')) { 23 | compareableOriginal.buffer = source.workerOptions.geojsonVtOptions.buffer / scale; 24 | compareableNew.buffer = newSource.buffer; 25 | } 26 | if (has(newSource, 'tolerance')) { 27 | compareableOriginal.tolerance = source.workerOptions.geojsonVtOptions.tolerance / scale; 28 | compareableNew.tolerance = newSource.tolerance; 29 | } 30 | if (has(newSource, 'maxzoom')) { 31 | compareableOriginal.maxzoom = source.workerOptions.geojsonVtOptions.maxZoom; 32 | compareableNew.maxzoom = newSource.maxzoom; 33 | } 34 | if (has(newSource, 'cluster')) { 35 | compareableOriginal.cluster = source.workerOptions.cluster; 36 | compareableNew.cluster = newSource.cluster; 37 | } 38 | if (has(newSource, 'clusterMaxZoom')) { 39 | compareableOriginal.clusterMaxZoom = source.workerOptions.superclusterOptions.maxZoom; 40 | compareableNew.clusterMaxZoom = newSource.clusterMaxZoom; 41 | } 42 | if (has(newSource, 'clusterRadius')) { 43 | compareableOriginal.clusterRadius = source.workerOptions.superclusterOptions.radius / scale; 44 | compareableNew.clusterRadius = newSource.clusterRadius; 45 | } 46 | return (source.type === 'geojson' && newSource.type === 'geojson') && isEqual(compareableOriginal, compareableNew); 47 | }; 48 | 49 | export const processStyleChanges = (map, changes, nextMapStyle) => { 50 | changes.forEach((change) => { 51 | const targetSource = change.args[0]; 52 | 53 | // Check if we are just updating the data 54 | if (change.command === 'setGeoJSONSourceData') { 55 | if (nextMapStyle.sources && nextMapStyle.sources[targetSource]) { 56 | const source = map.getSource(targetSource); 57 | const data = change.args[1]; 58 | if (source) { 59 | // Ensure we have the source before attempting to call data on it 60 | source.setData(data); 61 | return; 62 | } 63 | } 64 | } 65 | if (map[change.command]) { 66 | map[change.command].apply(map, change.args); 67 | } 68 | }); 69 | }; 70 | 71 | // Identify style or source updates 72 | export const update = (map, mapStyle, nextMapStyle) => { 73 | // String styles 74 | if (mapStyle !== nextMapStyle && (typeof nextMapStyle !== 'object')) { 75 | map.setStyle(nextMapStyle); 76 | return; 77 | } 78 | 79 | // If we can compare quickly 80 | if (nextMapStyle && nextMapStyle.equals && nextMapStyle.equals(mapStyle)) { 81 | return; 82 | } 83 | 84 | // If we are dealing with immutable elements 85 | const before = (mapStyle && mapStyle.toJS && mapStyle.toJS()) || mapStyle; 86 | const after = (nextMapStyle && nextMapStyle.toJS && nextMapStyle.toJS()) || nextMapStyle; 87 | 88 | // Process the style differences 89 | processStyleChanges(map, diffStyles(before, after), after); 90 | }; 91 | -------------------------------------------------------------------------------- /src/utils/transform.jsx: -------------------------------------------------------------------------------- 1 | import mapboxgl from 'mapbox-gl'; 2 | 3 | export const cloneTransform = (transform) => { 4 | const clonedTransform = Object.create(Object.getPrototypeOf(transform)); 5 | clonedTransform.tileSize = transform.tileSize; // Constant 6 | clonedTransform.minZoom = transform.minZoom; 7 | clonedTransform.maxZoom = transform.maxZoom; 8 | clonedTransform.latRange = transform.latRange; 9 | clonedTransform.width = transform.width; 10 | clonedTransform.height = transform.height; 11 | clonedTransform.scale = transform.scale; 12 | clonedTransform.tileZoom = transform.tileZoom; 13 | clonedTransform.zoomFraction = transform.zoomFraction; 14 | clonedTransform.angle = transform.angle; 15 | // Ensure we don't hold onto the original object reference 16 | // must access '_' as modifier/accessors perform actions on the transform 17 | clonedTransform._center = mapboxgl.LngLat.convert([transform.center.lng, transform.center.lat]); 18 | clonedTransform._altitude = transform.altitude; 19 | clonedTransform._pitch = transform.pitch; 20 | clonedTransform._unmodified = transform._unmodified; 21 | clonedTransform._renderWorldCopies = transform._renderWorldCopies; 22 | clonedTransform._fov = transform._fov; 23 | // Last modifier calls calculatePosMatrix 24 | clonedTransform.zoom = transform.zoom; 25 | return clonedTransform; 26 | }; 27 | --------------------------------------------------------------------------------