├── .eslintrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── flow-remove-types-register.js ├── index.js ├── package.json └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "mourner", 3 | "parserOptions": { 4 | "sourceType": "script", 5 | }, 6 | "rules": { 7 | "array-bracket-spacing": "off", 8 | "block-scoped-var": "error", 9 | "consistent-return": "off", 10 | "global-require": "off", 11 | "key-spacing": "off", 12 | "no-eq-null": "off", 13 | "no-new": "off", 14 | "no-warning-comments": "error", 15 | "object-curly-spacing": "off", 16 | "quotes": "off", 17 | "space-before-function-paren": "off", 18 | "template-curly-spacing": "error" 19 | }, 20 | "env": { 21 | "es6": true, 22 | "browser": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./node_modules 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapbox GL Layer Group Management Plugin 2 | -------------------------------------------------------------------------------- /flow-remove-types-register.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This is a hacked up version of "flow-remove-types/register" which allows 4 | // us to include "mapbox-gl" directly. 5 | 6 | var Module = require('module'); 7 | var removeTypes = require('flow-remove-types'); 8 | 9 | var _compileSuper = Module.prototype._compile; 10 | Module.prototype._compile = function _compile(source, filename) { 11 | var transformedSource = filename.indexOf('node_modules/mapbox-gl') !== -1 ? removeTypes(source) : source; 12 | _compileSuper.call(this, transformedSource, filename); 13 | }; 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var assign = require('lodash.assign'); 4 | 5 | /** 6 | * Add a layer group to the map. 7 | * 8 | * @param {Map} map 9 | * @param {string} id The id of the new group 10 | * @param {Array} layers The Mapbox style spec layers of the new group 11 | * @param {string} [beforeId] The layer id or group id after which the group 12 | * will be inserted. If ommitted the group is added to the bottom of the 13 | * style. 14 | */ 15 | function addGroup(map, id, layers, beforeId) { 16 | var beforeLayerId = normalizeBeforeId(map, beforeId); 17 | for (var i = 0; i < layers.length; i++) { 18 | addLayerToGroup(map, id, layers[i], beforeLayerId, true); 19 | } 20 | } 21 | 22 | /** 23 | * Add a single layer to an existing layer group. 24 | * 25 | * @param {Map} map 26 | * @param {string} groupId The id of group 27 | * @param {Object} layer The Mapbox style spec layer 28 | * @param {string} [beforeId] An existing layer id after which the new layer 29 | * will be inserted. If ommitted the layer is added to the bottom of 30 | * the group. 31 | */ 32 | function addLayerToGroup(map, groupId, layer, beforeId) { 33 | var ignoreBeforeIdCheck = arguments[4]; 34 | 35 | if (beforeId && !ignoreBeforeIdCheck && (!isLayer(map, beforeId) || getLayerGroup(map, beforeId) !== groupId)) { 36 | throw new Error('beforeId must be the id of a layer within the same group'); 37 | } else if (!beforeId && !ignoreBeforeIdCheck) { 38 | beforeId = getLayerIdFromIndex(map, getGroupFirstLayerId(map, groupId) - 1); 39 | } 40 | 41 | var groupedLayer = assign({}, layer, {metadata: assign({}, layer.metadata || {}, {group: groupId})}); 42 | map.addLayer(groupedLayer, beforeId); 43 | } 44 | 45 | /** 46 | * Remove a layer group and all of its layers from the map. 47 | * 48 | * @param {Map} map 49 | * @param {string} id The id of the group to be removed. 50 | */ 51 | function removeGroup(map, id) { 52 | var layers = map.getStyle().layers; 53 | for (var i = 0; i < layers.length; i++) { 54 | if (layers[i].metadata.group === id) { 55 | map.removeLayer(layers[i].id); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Remove a layer group and all of its layers from the map. 62 | * 63 | * @param {Map} map 64 | * @param {string} id The id of the group to be removed. 65 | */ 66 | function moveGroup(map, id, beforeId) { 67 | var beforeLayerId = normalizeBeforeId(map, beforeId); 68 | 69 | var layers = map.getStyle().layers; 70 | for (var i = 0; i < layers.length; i++) { 71 | if (layers[i].metadata.group === id) { 72 | map.moveLayer(layers[i].id, beforeLayerId); 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Get the id of the first layer in a group. 79 | * 80 | * @param {Map} map 81 | * @param {string} id The id of the group. 82 | * @returns {string} 83 | */ 84 | function getGroupFirstLayerId(map, id) { 85 | return getLayerIdFromIndex(map, getGroupFirstLayerIndex(map, id)); 86 | } 87 | 88 | /** 89 | * Get the id of the last layer in a group. 90 | * 91 | * @param {Map} map 92 | * @param {string} id The id of the group. 93 | * @returns {string} 94 | */ 95 | function getGroupLastLayerId(map, id) { 96 | return getLayerIdFromIndex(map, getGroupLastLayerIndex(map, id)); 97 | } 98 | 99 | function getGroupFirstLayerIndex(map, id) { 100 | var layers = map.getStyle().layers; 101 | for (var i = 0; i < layers.length; i++) { 102 | if (layers[i].metadata.group === id) return i; 103 | } 104 | return -1; 105 | } 106 | 107 | function getGroupLastLayerIndex(map, id) { 108 | var layers = map.getStyle().layers; 109 | var i = getGroupFirstLayerIndex(map, id); 110 | if (i === -1) return -1; 111 | while (i < layers.length && (layers[i].id === id || layers[i].metadata.group === id)) i++; 112 | return i - 1; 113 | } 114 | 115 | function getLayerIdFromIndex(map, index) { 116 | if (index === -1) return undefined; 117 | var layers = map.getStyle().layers; 118 | return layers[index] && layers[index].id; 119 | } 120 | 121 | function getLayerGroup(map, id) { 122 | return map.getLayer(id).metadata.group; 123 | } 124 | 125 | function isLayer(map, id) { 126 | return !!map.getLayer(id); 127 | } 128 | 129 | function normalizeBeforeId(map, beforeId) { 130 | if (beforeId && !isLayer(map, beforeId)) { 131 | return getGroupFirstLayerId(map, beforeId); 132 | } else if (beforeId && getLayerGroup(map, beforeId)) { 133 | return getGroupFirstLayerId(map, getLayerGroup(map, beforeId)); 134 | } else { 135 | return beforeId; 136 | } 137 | } 138 | 139 | module.exports = { 140 | addGroup, 141 | removeGroup, 142 | moveGroup, 143 | addLayerToGroup, 144 | getGroupFirstLayer: getGroupFirstLayerId, 145 | getGroupLastLayer: getGroupLastLayerId 146 | }; 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapbox-gl-js-layer-groups", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint . && tap test.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "lodash.assign": "^4.2.0" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^3.9.0", 16 | "eslint-config-mourner": "^2.0.1", 17 | "flow-remove-types": "1.0.4", 18 | "gl": "^4.0.2", 19 | "jsdom": "^9.8.3", 20 | "mapbox-gl": "^0.28.0", 21 | "sinon": "^1.17.6", 22 | "tap": "^8.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require('./flow-remove-types-register'); 4 | 5 | var t = require('tap'); 6 | var mapboxgl = require('mapbox-gl'); 7 | var window = require('mapbox-gl/js/util/window'); 8 | var groups = require('./index'); 9 | 10 | t.test('getGroupFirstLayer', function(t) { 11 | 12 | t.test('for a non-existent id', function(t) { 13 | createMap([], function(err, map) { 14 | t.error(err); 15 | 16 | t.equal(groups.getGroupFirstLayer(map, 'nonexistent'), undefined); 17 | 18 | t.end(); 19 | }); 20 | }); 21 | 22 | t.test('for a group', function(t) { 23 | createMap([l('layer1'), l('layer2', 'group1')], function(err, map) { 24 | t.error(err); 25 | 26 | t.equal(groups.getGroupFirstLayer(map, 'group1'), 'layer2'); 27 | 28 | t.end(); 29 | }); 30 | }); 31 | 32 | t.end(); 33 | }); 34 | 35 | t.test('getGroupLastLayer', function(t) { 36 | 37 | t.test('for a non-existent id', function(t) { 38 | createMap([], function(err, map) { 39 | t.error(err); 40 | 41 | t.equal(groups.getGroupLastLayer(map, 'nonexistent'), undefined); 42 | 43 | t.end(); 44 | }); 45 | }); 46 | 47 | t.test('for a group', function(t) { 48 | createMap([l('layer1', 'group1'), l('layer2', 'group1')], function(err, map) { 49 | t.error(err); 50 | 51 | t.equal(groups.getGroupLastLayer(map, 'group1'), 'layer2'); 52 | 53 | t.end(); 54 | }); 55 | }); 56 | 57 | t.end(); 58 | }); 59 | 60 | t.test('addGroup', function(t) { 61 | 62 | t.test('to an empty style', function(t) { 63 | createMap([], function(err, map) { 64 | t.error(err); 65 | 66 | groups.addGroup(map, 'group1', [l('layer1')]); 67 | 68 | t.deepEqual( 69 | map.getStyle().layers, 70 | [l('layer1', 'group1')] 71 | ); 72 | 73 | t.end(); 74 | }); 75 | }); 76 | 77 | t.test('at the bottom of a style (without a "beforeId")', function(t) { 78 | createMap( 79 | [l('layer1')], 80 | function(err, map) { 81 | t.error(err); 82 | 83 | groups.addGroup(map, 'group1', [l('layer2')]); 84 | 85 | t.deepEqual(map.getStyle().layers, [ 86 | l('layer1'), 87 | l('layer2', 'group1'), 88 | ]); 89 | 90 | t.end(); 91 | } 92 | ); 93 | }); 94 | 95 | t.test('before a layer', function(t) { 96 | createMap( 97 | [l('layer1'), l('layer2')], 98 | function(err, map) { 99 | t.error(err); 100 | 101 | groups.addGroup(map, 'group1', [l('layer3')], 'layer2'); 102 | 103 | t.deepEqual(map.getStyle().layers, [ 104 | l('layer1'), 105 | l('layer3', 'group1'), 106 | l('layer2') 107 | ]); 108 | 109 | t.end(); 110 | } 111 | ); 112 | }); 113 | 114 | t.test('before a group', function(t) { 115 | createMap( 116 | [l('layer1', 'group1'), l('layer2', 'group2')], 117 | function(err, map) { 118 | t.error(err); 119 | 120 | groups.addGroup(map, 'group3', [l('layer3')], 'group2'); 121 | 122 | t.deepEqual(map.getStyle().layers, [ 123 | l('layer1', 'group1'), 124 | l('layer3', 'group3'), 125 | l('layer2', 'group2') 126 | ]); 127 | 128 | t.end(); 129 | } 130 | ); 131 | }); 132 | 133 | 134 | t.test('before a layer in the middle of another group', function(t) { 135 | createMap( 136 | [l('layer1', 'group1'), l('layer2', 'group1')], 137 | function(err, map) { 138 | t.error(err); 139 | 140 | groups.addGroup(map, 'group2', [l('layer3')], 'layer2'); 141 | 142 | t.deepEqual(map.getStyle().layers, [ 143 | l('layer3', 'group2'), 144 | l('layer1', 'group1'), 145 | l('layer2', 'group1') 146 | ]); 147 | 148 | t.end(); 149 | } 150 | ); 151 | }); 152 | 153 | t.end(); 154 | }); 155 | 156 | t.test('addLayerToGroup', function(t) { 157 | 158 | t.test('at the bottom of a group (without a "beforeId")', function(t) { 159 | createMap( 160 | [l('layer1', 'group1')], 161 | function(err, map) { 162 | t.error(err); 163 | 164 | groups.addLayerToGroup(map, 'group1', l('layer2')); 165 | 166 | t.deepEqual(map.getStyle().layers, [ 167 | l('layer1', 'group1'), 168 | l('layer2', 'group1') 169 | ]); 170 | 171 | t.end(); 172 | } 173 | ); 174 | }); 175 | 176 | t.test('before a layer within the same group', function(t) { 177 | createMap( 178 | [l('layer1', 'group1')], 179 | function(err, map) { 180 | t.error(err); 181 | 182 | groups.addLayerToGroup(map, 'group1', l('layer2'), 'layer1'); 183 | 184 | t.deepEqual(map.getStyle().layers, [ 185 | l('layer2', 'group1'), 186 | l('layer1', 'group1') 187 | ]); 188 | 189 | t.end(); 190 | } 191 | ); 192 | }); 193 | 194 | t.test('to a non-existant group', function(t) { 195 | createMap([l('layer1', 'group1')], function(err, map) { 196 | t.error(err); 197 | 198 | groups.addLayerToGroup(map, 'group2', l('layer2')); 199 | 200 | t.deepEqual(map.getStyle().layers, [ 201 | l('layer1', 'group1'), 202 | l('layer2', 'group2') 203 | ]); 204 | 205 | t.end(); 206 | }); 207 | }); 208 | 209 | t.test('before a layer within a different group', function(t) { 210 | createMap( 211 | [l('layer1', 'group1'), l('layer2', 'group2')], 212 | function(err, map) { 213 | t.error(err); 214 | 215 | t.throws(function() { 216 | groups.addLayerToGroup(map, 'group1', l('layer3'), 'layer2'); 217 | }); 218 | 219 | t.end(); 220 | } 221 | ); 222 | }); 223 | 224 | t.test('before a group', function(t) { 225 | createMap( 226 | [l('layer1', 'group1')], 227 | function(err, map) { 228 | t.error(err); 229 | 230 | t.throws(function() { 231 | groups.addLayerToGroup(map, 'group1', l('layer3'), 'group1'); 232 | }); 233 | 234 | t.end(); 235 | } 236 | ); 237 | }); 238 | 239 | t.test('before a non-existant group', function(t) { 240 | createMap([], function(err, map) { 241 | t.error(err); 242 | 243 | t.throws(function() { 244 | groups.addLayerToGroup(map, 'group1', l('layer1'), 'group1'); 245 | }); 246 | 247 | t.end(); 248 | }); 249 | }); 250 | 251 | t.end(); 252 | }); 253 | 254 | t.test('removeGroup', function(t) { 255 | 256 | t.test('a non-existent id', function(t) { 257 | createMap([], function(err, map) { 258 | t.error(err); 259 | groups.removeGroup(map, 'nonexistent'); 260 | t.deepEqual(map.getStyle().layers, []); 261 | t.end(); 262 | } 263 | ); 264 | }); 265 | 266 | t.test('a group', function(t) { 267 | createMap( 268 | [ 269 | l('layer1'), 270 | l('layer2', 'group1'), 271 | l('layer3', 'group1'), 272 | l('layer4') 273 | ], 274 | function(err, map) { 275 | t.error(err); 276 | groups.removeGroup(map, 'group1'); 277 | t.deepEqual(map.getStyle().layers, [l('layer1'), l('layer4')]); 278 | t.end(); 279 | } 280 | ); 281 | }); 282 | 283 | t.end(); 284 | }); 285 | 286 | t.test('moveGroup', function(t) { 287 | 288 | t.test('a non-existent id', function(t) { 289 | createMap([], function(err, map) { 290 | t.error(err); 291 | groups.moveGroup(map, 'nonexistent'); 292 | t.deepEqual(map.getStyle().layers, []); 293 | t.end(); 294 | }); 295 | }); 296 | 297 | t.test('a group before an ungrouped layer', function(t) { 298 | createMap( 299 | [ 300 | l('layer1'), 301 | l('layer2', 'group1'), 302 | l('layer3', 'group1') 303 | ], 304 | function(err, map) { 305 | t.error(err); 306 | groups.moveGroup(map, 'group1', 'layer1'); 307 | t.deepEqual(map.getStyle().layers, [ 308 | l('layer2', 'group1'), 309 | l('layer3', 'group1'), 310 | l('layer1') 311 | ]); 312 | t.end(); 313 | } 314 | ); 315 | }); 316 | 317 | t.test('a group before a grouped layer', function(t) { 318 | createMap( 319 | [ 320 | l('layer1', 'group1'), 321 | l('layer2', 'group1'), 322 | l('layer3', 'group2'), 323 | l('layer4', 'group2') 324 | ], 325 | function(err, map) { 326 | t.error(err); 327 | groups.moveGroup(map, 'group2', 'layer2'); 328 | t.deepEqual(map.getStyle().layers, [ 329 | l('layer3', 'group2'), 330 | l('layer4', 'group2'), 331 | l('layer1', 'group1'), 332 | l('layer2', 'group1') 333 | ]); 334 | t.end(); 335 | } 336 | ); 337 | }); 338 | 339 | t.test('a group before a group', function(t) { 340 | createMap( 341 | [ 342 | l('layer1', 'group1'), 343 | l('layer2', 'group2'), 344 | l('layer3', 'group2') 345 | ], 346 | function(err, map) { 347 | t.error(err); 348 | groups.moveGroup(map, 'group2', 'group1'); 349 | t.deepEqual(map.getStyle().layers, [ 350 | l('layer2', 'group2'), 351 | l('layer3', 'group2'), 352 | l('layer1', 'group1') 353 | ]); 354 | t.end(); 355 | } 356 | ); 357 | }); 358 | 359 | t.test('a group to the end', function(t) { 360 | createMap( 361 | [ 362 | l('layer1', 'group1'), 363 | l('layer2', 'group1'), 364 | l('layer3') 365 | ], 366 | function(err, map) { 367 | t.error(err); 368 | groups.moveGroup(map, 'group1'); 369 | t.deepEqual(map.getStyle().layers, [ 370 | l('layer3'), 371 | l('layer1', 'group1'), 372 | l('layer2', 'group1') 373 | ]); 374 | t.end(); 375 | } 376 | ); 377 | }); 378 | 379 | t.end(); 380 | }); 381 | 382 | function l(layerId, groupId) { 383 | return {id: layerId, type: 'background', metadata: {group: groupId}}; 384 | } 385 | 386 | function createMap(layers, callback) { 387 | const container = window.document.createElement('div'); 388 | container.offsetWidth = 200; 389 | container.offsetHeight = 200; 390 | 391 | const map = new mapboxgl.Map({ 392 | container: container, 393 | interactive: false, 394 | attributionControl: false, 395 | style: { 396 | "version": 8, 397 | "sources": {}, 398 | "layers": layers 399 | } 400 | }); 401 | 402 | if (callback) map.on('load', () => { 403 | callback(null, map); 404 | }); 405 | } 406 | --------------------------------------------------------------------------------