├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── index.html ├── mapboxgl-control-minimap.js └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://www.paypal.me/aesqe 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bruno Babic 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapbox GL Minimap Control 2 | 3 | ![npm (scoped)](https://img.shields.io/npm/v/@aesqe/mapboxgl-minimap) 4 | 5 | ## Demo 6 | [Demo on GitHub pages](http://aesqe.github.io/mapboxgl-minimap/) 7 | 8 | **--- work in progress; overall performance can probably be improved ---** 9 | 10 | ## How to use it 11 | 12 | ```javascript 13 | var map = new mapboxgl.Map({ 14 | container: "map", 15 | style: "mapbox://styles/mapbox/streets-v8", 16 | center: [-73.94656812952897, 40.72912351406106], 17 | zoom: 7 18 | }); 19 | 20 | map.on("style.load", function () { 21 | // Possible position values are 'bottom-left', 'bottom-right', 'top-left', 'top-right' 22 | map.addControl(new mapboxgl.Minimap(), 'top-right'); 23 | }); 24 | ``` 25 | 26 | ## Options 27 | 28 | ```javascript 29 | { 30 | id: "mapboxgl-minimap", 31 | width: "320px", 32 | height: "180px", 33 | style: "mapbox://styles/mapbox/streets-v8", 34 | center: [0, 0], 35 | zoom: 6, 36 | 37 | // should be a function; will be bound to Minimap 38 | zoomAdjust: null, 39 | 40 | // if parent map zoom >= 18 and minimap zoom >= 14, set minimap zoom to 16 41 | zoomLevels: [ 42 | [18, 14, 16], 43 | [16, 12, 14], 44 | [14, 10, 12], 45 | [12, 8, 10], 46 | [10, 6, 8] 47 | ], 48 | 49 | lineColor: "#08F", 50 | lineWidth: 1, 51 | lineOpacity: 1, 52 | 53 | fillColor: "#F80", 54 | fillOpacity: 0.25, 55 | 56 | dragPan: false, 57 | scrollZoom: false, 58 | boxZoom: false, 59 | dragRotate: false, 60 | keyboard: false, 61 | doubleClickZoom: false, 62 | touchZoomRotate: false 63 | } 64 | ``` 65 | 66 | ## Compatibility 67 | 68 | The latest version should be compatible with maboxgl 0.54.0 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapbox GL Minimap 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /mapboxgl-control-minimap.js: -------------------------------------------------------------------------------- 1 | function Minimap ( options ) 2 | { 3 | Object.assign(this.options, options); 4 | 5 | this._ticking = false; 6 | this._lastMouseMoveEvent = null; 7 | this._parentMap = null; 8 | this._isDragging = false; 9 | this._isCursorOverFeature = false; 10 | this._previousPoint = [0, 0]; 11 | this._currentPoint = [0, 0]; 12 | this._trackingRectCoordinates = [[[], [], [], [], []]]; 13 | } 14 | 15 | Minimap.prototype = Object.assign({}, mapboxgl.NavigationControl.prototype, { 16 | 17 | options: { 18 | id: "mapboxgl-minimap", 19 | width: "320px", 20 | height: "180px", 21 | style: "mapbox://styles/mapbox/streets-v8", 22 | center: [0, 0], 23 | zoom: 6, 24 | 25 | // should be a function; will be bound to Minimap 26 | zoomAdjust: null, 27 | 28 | // if parent map zoom >= 18 and minimap zoom >= 14, set minimap zoom to 16 29 | zoomLevels: [ 30 | [18, 14, 16], 31 | [16, 12, 14], 32 | [14, 10, 12], 33 | [12, 8, 10], 34 | [10, 6, 8] 35 | ], 36 | 37 | lineColor: "#08F", 38 | lineWidth: 1, 39 | lineOpacity: 1, 40 | 41 | fillColor: "#F80", 42 | fillOpacity: 0.25, 43 | 44 | dragPan: false, 45 | scrollZoom: false, 46 | boxZoom: false, 47 | dragRotate: false, 48 | keyboard: false, 49 | doubleClickZoom: false, 50 | touchZoomRotate: false 51 | }, 52 | 53 | onAdd: function ( parentMap ) 54 | { 55 | this._parentMap = parentMap; 56 | 57 | var opts = this.options; 58 | var container = this._container = this._createContainer(parentMap); 59 | var miniMap = this._miniMap = new mapboxgl.Map({ 60 | attributionControl: false, 61 | container: container, 62 | style: opts.style, 63 | zoom: opts.zoom, 64 | center: opts.center 65 | }); 66 | 67 | if (opts.maxBounds) miniMap.setMaxBounds(opts.maxBounds); 68 | 69 | miniMap.on("load", this._load.bind(this)); 70 | 71 | return this._container; 72 | }, 73 | 74 | _load: function () 75 | { 76 | var opts = this.options; 77 | var parentMap = this._parentMap; 78 | var miniMap = this._miniMap; 79 | var interactions = [ 80 | "dragPan", "scrollZoom", "boxZoom", "dragRotate", 81 | "keyboard", "doubleClickZoom", "touchZoomRotate" 82 | ]; 83 | 84 | interactions.forEach(function(i){ 85 | if( opts[i] !== true ) { 86 | miniMap[i].disable(); 87 | } 88 | }); 89 | 90 | if( typeof opts.zoomAdjust === "function" ) { 91 | this.options.zoomAdjust = opts.zoomAdjust.bind(this); 92 | } else if( opts.zoomAdjust === null ) { 93 | this.options.zoomAdjust = this._zoomAdjust.bind(this); 94 | } 95 | 96 | var bounds = miniMap.getBounds(); 97 | 98 | this._convertBoundsToPoints(bounds); 99 | 100 | miniMap.addSource("trackingRect", { 101 | "type": "geojson", 102 | "data": { 103 | "type": "Feature", 104 | "properties": { 105 | "name": "trackingRect" 106 | }, 107 | "geometry": { 108 | "type": "Polygon", 109 | "coordinates": this._trackingRectCoordinates 110 | } 111 | } 112 | }); 113 | 114 | miniMap.addLayer({ 115 | "id": "trackingRectOutline", 116 | "type": "line", 117 | "source": "trackingRect", 118 | "layout": {}, 119 | "paint": { 120 | "line-color": opts.lineColor, 121 | "line-width": opts.lineWidth, 122 | "line-opacity": opts.lineOpacity 123 | } 124 | }); 125 | 126 | // needed for dragging 127 | miniMap.addLayer({ 128 | "id": "trackingRectFill", 129 | "type": "fill", 130 | "source": "trackingRect", 131 | "layout": {}, 132 | "paint": { 133 | "fill-color": opts.fillColor, 134 | "fill-opacity": opts.fillOpacity 135 | } 136 | }); 137 | 138 | this._trackingRect = this._miniMap.getSource("trackingRect"); 139 | 140 | this._update(); 141 | 142 | parentMap.on("move", this._update.bind(this)); 143 | 144 | miniMap.on("mousemove", this._mouseMove.bind(this)); 145 | miniMap.on("mousedown", this._mouseDown.bind(this)); 146 | miniMap.on("mouseup", this._mouseUp.bind(this)); 147 | 148 | miniMap.on("touchmove", this._mouseMove.bind(this)); 149 | miniMap.on("touchstart", this._mouseDown.bind(this)); 150 | miniMap.on("touchend", this._mouseUp.bind(this)); 151 | 152 | this._miniMapCanvas = miniMap.getCanvasContainer(); 153 | this._miniMapCanvas.addEventListener("wheel", this._preventDefault); 154 | this._miniMapCanvas.addEventListener("mousewheel", this._preventDefault); 155 | }, 156 | 157 | _mouseDown: function ( e ) 158 | { 159 | if( this._isCursorOverFeature ) 160 | { 161 | this._isDragging = true; 162 | this._previousPoint = this._currentPoint; 163 | this._currentPoint = [e.lngLat.lng, e.lngLat.lat]; 164 | } 165 | }, 166 | 167 | _mouseMove: function (e) 168 | { 169 | this._ticking = false; 170 | 171 | var miniMap = this._miniMap; 172 | var features = miniMap.queryRenderedFeatures(e.point, { 173 | layers: ["trackingRectFill"] 174 | }); 175 | 176 | // don't update if we're still hovering the area 177 | if( ! (this._isCursorOverFeature && features.length > 0) ) 178 | { 179 | this._isCursorOverFeature = features.length > 0; 180 | this._miniMapCanvas.style.cursor = this._isCursorOverFeature ? "move" : ""; 181 | } 182 | 183 | if( this._isDragging ) 184 | { 185 | this._previousPoint = this._currentPoint; 186 | this._currentPoint = [e.lngLat.lng, e.lngLat.lat]; 187 | 188 | var offset = [ 189 | this._previousPoint[0] - this._currentPoint[0], 190 | this._previousPoint[1] - this._currentPoint[1] 191 | ]; 192 | 193 | var newBounds = this._moveTrackingRect(offset); 194 | 195 | this._parentMap.fitBounds(newBounds, { 196 | duration: 80, 197 | noMoveStart: true 198 | }); 199 | } 200 | }, 201 | 202 | _mouseUp: function () 203 | { 204 | this._isDragging = false; 205 | this._ticking = false; 206 | }, 207 | 208 | _moveTrackingRect: function ( offset ) 209 | { 210 | var source = this._trackingRect; 211 | var data = source._data; 212 | var bounds = data.properties.bounds; 213 | 214 | bounds._ne.lat -= offset[1]; 215 | bounds._ne.lng -= offset[0]; 216 | bounds._sw.lat -= offset[1]; 217 | bounds._sw.lng -= offset[0]; 218 | 219 | this._convertBoundsToPoints(bounds); 220 | source.setData(data); 221 | 222 | return bounds; 223 | }, 224 | 225 | _setTrackingRectBounds: function ( bounds ) 226 | { 227 | var source = this._trackingRect; 228 | var data = source._data; 229 | 230 | data.properties.bounds = bounds; 231 | this._convertBoundsToPoints(bounds); 232 | source.setData(data); 233 | }, 234 | 235 | _convertBoundsToPoints: function ( bounds ) 236 | { 237 | var ne = bounds._ne; 238 | var sw = bounds._sw; 239 | var trc = this._trackingRectCoordinates; 240 | 241 | trc[0][0][0] = ne.lng; 242 | trc[0][0][1] = ne.lat; 243 | trc[0][1][0] = sw.lng; 244 | trc[0][1][1] = ne.lat; 245 | trc[0][2][0] = sw.lng; 246 | trc[0][2][1] = sw.lat; 247 | trc[0][3][0] = ne.lng; 248 | trc[0][3][1] = sw.lat; 249 | trc[0][4][0] = ne.lng; 250 | trc[0][4][1] = ne.lat; 251 | }, 252 | 253 | _update: function ( e ) 254 | { 255 | if( this._isDragging ) { 256 | return; 257 | } 258 | 259 | var parentBounds = this._parentMap.getBounds(); 260 | 261 | this._setTrackingRectBounds(parentBounds); 262 | 263 | if( typeof this.options.zoomAdjust === "function" ) { 264 | this.options.zoomAdjust(); 265 | } 266 | }, 267 | 268 | _zoomAdjust: function () 269 | { 270 | var miniMap = this._miniMap; 271 | var parentMap = this._parentMap; 272 | var miniZoom = parseInt(miniMap.getZoom(), 10); 273 | var parentZoom = parseInt(parentMap.getZoom(), 10); 274 | var levels = this.options.zoomLevels; 275 | var found = false; 276 | 277 | levels.forEach(function(zoom) 278 | { 279 | if( ! found && parentZoom >= zoom[0] ) 280 | { 281 | if( miniZoom >= zoom[1] ) { 282 | miniMap.setZoom(zoom[2]); 283 | } 284 | 285 | miniMap.setCenter(parentMap.getCenter()); 286 | found = true; 287 | } 288 | }); 289 | 290 | if( ! found && miniZoom !== this.options.zoom ) 291 | { 292 | if( typeof this.options.bounds === "object" ) { 293 | miniMap.fitBounds(this.options.bounds, {duration: 50}); 294 | } 295 | 296 | miniMap.setZoom(this.options.zoom) 297 | } 298 | }, 299 | 300 | _createContainer: function ( parentMap ) 301 | { 302 | var opts = this.options; 303 | var container = document.createElement("div"); 304 | 305 | container.className = "mapboxgl-ctrl-minimap mapboxgl-ctrl"; 306 | container.setAttribute('style', 'width: ' + opts.width + '; height: ' + opts.height + ';'); 307 | container.addEventListener("contextmenu", this._preventDefault); 308 | 309 | parentMap.getContainer().appendChild(container); 310 | 311 | if( opts.id !== "" ) { 312 | container.id = opts.id; 313 | } 314 | 315 | return container; 316 | }, 317 | 318 | _preventDefault: function ( e ) { 319 | e.preventDefault(); 320 | } 321 | }); 322 | 323 | mapboxgl.Minimap = Minimap; 324 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aesqe/mapboxgl-minimap", 3 | "version": "1.0.0", 4 | "description": "Mapbox GL Minimap Control", 5 | "main": "mapboxgl-control-minimap.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/aesqe/mapboxgl-minimap.git" 12 | }, 13 | "keywords": [ 14 | "mapbox", 15 | "mapboxgl", 16 | "minimap" 17 | ], 18 | "author": "aesqe", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/aesqe/mapboxgl-minimap/issues" 22 | }, 23 | "homepage": "https://github.com/aesqe/mapboxgl-minimap#readme" 24 | } --------------------------------------------------------------------------------