- 1 :
'use strict';
- 2 :
- 3 :
/* global L, log -- eslint */
- 4 :
- 5 :
/**
- 6 :
* Represents a control for selecting layers on the map. It extends the Leaflet's L.Control.Layers class.
- 7 :
* This control not only manages layer visibility but also provides persistence of layer display states between sessions.
- 8 :
* The class has been enhanced with additional options and methods for more flexible layer management.
- 9 :
*
- 10 :
* @memberof L
- 11 :
* @class LayerChooser
- 12 :
* @extends L.Control.Layers
- 13 :
*/
- 14 :
var LayerChooser = L.Control.Layers.extend({
- 15 :
options: {
- 16 :
/**
- 17 :
* @property {Boolean} sortLayers=true - Ensures stable sort order (based on initial), while still providing
- 18 :
* ability to enforce specific order with `addBaseLayer`/`addOverlay`
- 19 :
* `sortPriority` option.
- 20 :
*/
- 21 :
sortLayers: true,
- 22 :
- 23 :
/**
- 24 :
* @property {Function} sortFunction - A compare function that will be used for sorting the layers,
- 25 :
* when `sortLayers` is `true`. The function receives objects with
- 26 :
* the layer's data.
- 27 :
* @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
- 28 :
*/
- 29 :
sortFunction: function (A, B) {
- 30 :
var a = A.sortPriority;
- 31 :
var b = B.sortPriority;
- 32 :
return a < b ? -1 : b < a ? 1 : 0;
- 33 :
},
- 34 :
},
- 35 :
- 36 :
/**
- 37 :
* Initializes a new instance of the LayerChooser control.
- 38 :
*
- 39 :
* @memberof LayerChooser
- 40 :
* @method
- 41 :
* @param {L.Layer[]} baseLayers - Array of base layers to include in the chooser.
- 42 :
* @param {L.Layer[]} overlays - Array of overlay layers to include in the chooser.
- 43 :
* @param {Object} [options] - Additional options for the LayerChooser control.
- 44 :
*/
- 45 :
initialize: function (baseLayers, overlays, options) {
- 46 :
this._overlayStatus = {};
- 47 :
var layersJSON = localStorage['ingress.intelmap.layergroupdisplayed'];
- 48 :
if (layersJSON) {
- 49 :
try {
- 50 :
this._overlayStatus = JSON.parse(layersJSON);
- 51 :
} catch (e) {
- 52 :
log.error(e);
- 53 :
}
- 54 :
}
- 55 :
this._mapToAdd = options && options.map;
- 56 :
this.lastBaseLayerName = localStorage['iitc-base-map'];
- 57 :
this._lastPriority = -1000; // initial layers get priority <0
- 58 :
L.Control.Layers.prototype.initialize.apply(this, arguments);
- 59 :
this._lastPriority = 0; // any following gets >0
- 60 :
},
- 61 :
- 62 :
_addLayer: function (layer, name, overlay, options) {
- 63 :
options = options || {};
- 64 :
// _chooser property stores layerChooser data after layer removal
- 65 :
// (in case if it's meant to be re-added)
- 66 :
var data = layer._chooser;
- 67 :
if (!data) {
- 68 :
data = {
- 69 :
layer: layer,
- 70 :
// name should be unique, otherwise behavior of other methods is undefined
- 71 :
// (typically: first found will be taken)
- 72 :
name: name,
- 73 :
// label: name,
- 74 :
overlay: overlay,
- 75 :
persistent: 'persistent' in options ? options.persistent : true,
- 76 :
};
- 77 :
} else {
- 78 :
delete layer._chooser;
- 79 :
}
- 80 :
// provide stable sort order
- 81 :
if ('sortPriority' in options) {
- 82 :
data.sortPriority = options.sortPriority;
- 83 :
} else if (!('sortPriority' in data)) {
- 84 :
this._lastPriority = this._lastPriority + 10;
- 85 :
data.sortPriority = this._lastPriority;
- 86 :
}
- 87 :
// *** adapted from L.Control.Layers.prototype._addLayer.call(this, layer, name, overlay);
- 88 :
if (this._map) {
- 89 :
layer.on('add remove', this._onLayerChange, this);
- 90 :
}
- 91 :
- 92 :
this._layers.push(data);
- 93 :
- 94 :
if (this.options.sortLayers) {
- 95 :
this._layers.sort(this.options.sortFunction);
- 96 :
}
- 97 :
- 98 :
if (this.options.autoZIndex && layer.setZIndex) {
- 99 :
this._lastZIndex++;
- 100 :
layer.setZIndex(this._lastZIndex);
- 101 :
}
- 102 :
- 103 :
this._expandIfNotCollapsed();
- 104 :
// ***
- 105 :
- 106 :
if (data.overlay) {
- 107 :
data.default = 'default' in options ? options.default : true;
- 108 :
}
- 109 :
var map = this._map || this._mapToAdd;
- 110 :
if (!data.persistent) {
- 111 :
if (!data.overlay) {
- 112 :
return;
- 113 :
}
- 114 :
if ('enable' in options ? options.enable : data.default) {
- 115 :
layer.addTo(map);
- 116 :
}
- 117 :
return;
- 118 :
}
- 119 :
if (overlay) {
- 120 :
data.statusTracking = function (e) {
- 121 :
this._storeOverlayState(data.name, e.type === 'add');
- 122 :
};
- 123 :
layer.on('add remove', data.statusTracking, this);
- 124 :
if ('enable' in options) {
- 125 :
// do as explicitly specified
- 126 :
map[options.enable ? 'addLayer' : 'removeLayer'](layer);
- 127 :
} else if (layer._map) {
- 128 :
// already on map, only store state
- 129 :
this._storeOverlayState(data.name, true);
- 130 :
} else {
- 131 :
// restore at recorded state
- 132 :
if (this._isOverlayDisplayed(data.name, data.default)) {
- 133 :
layer.addTo(map);
- 134 :
}
- 135 :
}
- 136 :
} else {
- 137 :
data.statusTracking = function () {
- 138 :
localStorage['iitc-base-map'] = data.name;
- 139 :
};
- 140 :
layer.on('add', data.statusTracking);
- 141 :
}
- 142 :
},
- 143 :
- 144 :
_addItem: function (obj) {
- 145 :
var labelEl = L.Control.Layers.prototype._addItem.call(this, {
- 146 :
layer: obj.layer,
- 147 :
overlay: obj.overlay,
- 148 :
name: obj.label || obj.name,
- 149 :
});
- 150 :
obj.labelEl = labelEl;
- 151 :
// obj.inputEl = this._layerControlInputs[this._layerControlInputs.length-1];
- 152 :
return labelEl;
- 153 :
},
- 154 :
- 155 :
/**
- 156 :
* Adds a base layer (radio button entry) with the given name to the control.
- 157 :
*
- 158 :
* @memberof LayerChooser
- 159 :
* @param {L.Layer} layer - The layer to be added.
- 160 :
* @param {String} name - The name of the layer.
- 161 :
* @param {Object} [options] - Additional options for the layer entry.
- 162 :
* @param {Boolean} [options.persistent=true] - When set to `false`, the base layer's status is not tracked.
- 163 :
* @param {Number} [options.sortPriority] - Enforces a specific order in the control. Lower value means
- 164 :
* higher position in the list. If not specified, the value
- 165 :
* will be assigned implicitly in an increasing manner.
- 166 :
* @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
- 167 :
*/
- 168 :
addBaseLayer: function (layer, name, options) {
- 169 :
this._addLayer(layer, name, false, options);
- 170 :
return this._map ? this._update() : this;
- 171 :
},
- 172 :
- 173 :
/**
- 174 :
* Adds an overlay (checkbox entry) with the given name to the control.
- 175 :
*
- 176 :
* @memberof LayerChooser
- 177 :
* @param {L.Layer} layer - The overlay layer to be added.
- 178 :
* @param {String} name - The name of the overlay.
- 179 :
* @param {Object} [options] - Additional options for the overlay entry.
- 180 :
* @param {Boolean} [options.persistent=true] - When `true` (or not specified), the overlay is added to the map
- 181 :
* if its last state was active. If no previous state is recorded,
- 182 :
* the value specified in the `default` option is used.
- 183 :
* When `false`, the overlay status is not tracked,
- 184 :
* but the `default` option is still honored.
- 185 :
* @param {Boolean} [options.default=true] - The default state of the overlay, used only when no record
- 186 :
* of the previous state is found.
- 187 :
* @param {Boolean} [options.enable] - If set, enforces the specified state, ignoring any previously saved state.
- 188 :
* @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
- 189 :
*/
- 190 :
addOverlay: function (layer, name, options) {
- 191 :
this._addLayer(layer, name, true, options);
- 192 :
return this._map ? this._update() : this;
- 193 :
},
- 194 :
- 195 :
/**
- 196 :
* Removes the given layer from the control.
- 197 :
*
- 198 :
* @memberof LayerChooser
- 199 :
* @param {L.Layer|String} layer - The layer to be removed, either as a Leaflet layer object or its name.
- 200 :
* @param {Object} [options] - Additional options, including `keepOnMap` to keep the layer on the map.
- 201 :
* @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
- 202 :
*/
- 203 :
removeLayer: function (layer, options) {
- 204 :
layer = this.getLayer(layer);
- 205 :
var data = this.layerInfo(layer);
- 206 :
if (data) {
- 207 :
options = options || {};
- 208 :
if (data.statusTracking) {
- 209 :
data.layer.off('add remove', data.statusTracking, this);
- 210 :
delete data.statusTracking;
- 211 :
}
- 212 :
L.Control.Layers.prototype.removeLayer.apply(this, arguments);
- 213 :
if (this._map && !options.keepOnMap) {
- 214 :
window.map.removeLayer(data.layer);
- 215 :
}
- 216 :
delete data.labelEl;
- 217 :
// delete data.inputEl;
- 218 :
layer._chooser = data;
- 219 :
} else {
- 220 :
log.warn('Layer not found: ', layer);
- 221 :
}
- 222 :
return this;
- 223 :
},
- 224 :
- 225 :
_storeOverlayState: function (name, isDisplayed) {
- 226 :
this._overlayStatus[name] = isDisplayed;
- 227 :
localStorage['ingress.intelmap.layergroupdisplayed'] = JSON.stringify(this._overlayStatus);
- 228 :
},
- 229 :
- 230 :
_isOverlayDisplayed: function (name, defaultState) {
- 231 :
if (name in this._overlayStatus) {
- 232 :
return this._overlayStatus[name];
- 233 :
}
- 234 :
return defaultState;
- 235 :
},
- 236 :
- 237 :
__byName: function (data) {
- 238 :
var name = this.toString();
- 239 :
return data.name === name || data.label === name;
- 240 :
},
- 241 :
- 242 :
__byLayer: function (data) {
- 243 :
return data.layer === this;
- 244 :
},
- 245 :
- 246 :
__byLabelEl: function (data) {
- 247 :
return data.labelEl === this;
- 248 :
},
- 249 :
- 250 :
// @method layerInfo(name: String|Layer): Layer
- 251 :
// Returns layer info by it's name in the control, or by layer object itself,
- 252 :
// or label html element.
- 253 :
// Info is internal data object with following properties:
- 254 :
// `layer`, `name`, `label`, `overlay`, `sortPriority`, `persistent`, `default`,
- 255 :
// `labelEl`, `inputEl`, `statusTracking`.
- 256 :
/**
- 257 :
* Retrieves layer info by its name in the control, or by the layer object itself, or its label HTML element.
- 258 :
*
- 259 :
* @memberof LayerChooser
- 260 :
* @param {String|L.Layer|HTMLElement} layer - The name, layer object, or label element of the layer.
- 261 :
* @returns {Object} Layer info object with following properties: `layer`, `name`, `label`, `overlay`, `sortPriority`,
- 262 :
* `persistent`, `default`, `labelEl`, `inputEl`, `statusTracking`.
- 263 :
*/
- 264 :
layerInfo: function (layer) {
- 265 :
var fn = layer instanceof L.Layer ? this.__byLayer : layer instanceof HTMLElement ? this.__byLabelEl : this.__byName;
- 266 :
return this._layers.find(fn, layer);
- 267 :
},
- 268 :
- 269 :
/**
- 270 :
* Returns the Leaflet layer object based on its name in the control, or the layer object itself,
- 271 :
* or its label HTML element. The latter can be used to ensure the layer is in layerChooser.
- 272 :
*
- 273 :
* @memberof LayerChooser
- 274 :
* @param {String|L.Layer|HTMLElement} layer - The name, layer object, or label element of the layer.
- 275 :
* @returns {L.Layer} The corresponding Leaflet layer object.
- 276 :
*/
- 277 :
getLayer: function (layer) {
- 278 :
var data = this.layerInfo(layer);
- 279 :
return data && data.layer;
- 280 :
},
- 281 :
- 282 :
/**
- 283 :
* Shows or hides a specified basemap or overlay layer. The layer can be specified by its ID, name, or layer object.
- 284 :
* If the display parameter is not provided, the layer will be shown by default.
- 285 :
* When showing a base layer, it ensures that no other base layers are displayed at the same time.
- 286 :
*
- 287 :
* @memberof LayerChooser
- 288 :
* @param {L.Layer|String|Number} layer - The layer to show or hide. This can be a Leaflet layer object,
- 289 :
* a layer name, or a layer ID.
- 290 :
* @param {Boolean} [display=true] - Pass `false` to hide the layer, or `true`/omit to show it.
- 291 :
* @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
- 292 :
*/
- 293 :
showLayer: function (layer, display) {
- 294 :
var data = this._layers[layer]; // layer is index, private use only
- 295 :
if (!data) {
- 296 :
data = this.layerInfo(layer);
- 297 :
if (!data) {
- 298 :
log.warn('Layer not found: ', layer);
- 299 :
return this;
- 300 :
}
- 301 :
}
- 302 :
var map = this._map;
- 303 :
if (display || arguments.length === 1) {
- 304 :
if (!map.hasLayer(data.layer)) {
- 305 :
if (!data.overlay) {
- 306 :
// if it's a base layer, remove any others
- 307 :
this._layers.forEach(function (el) {
- 308 :
if (!el.overlay && el.layer !== data.layer) {
- 309 :
map.removeLayer(el.layer);
- 310 :
}
- 311 :
});
- 312 :
}
- 313 :
map.addLayer(data.layer);
- 314 :
}
- 315 :
} else {
- 316 :
map.removeLayer(data.layer);
- 317 :
}
- 318 :
return this;
- 319 :
},
- 320 :
- 321 :
/**
- 322 :
* Sets the label of a layer in the control.
- 323 :
*
- 324 :
* @memberof LayerChooser
- 325 :
* @param {String|L.Layer} layer - The name or layer object.
- 326 :
* @param {String} [label] - The label text (HTML allowed) to set. Resets to original name if not provided.
- 327 :
* @returns {LayerChooser} Returns the `LayerChooser` instance for chaining.
- 328 :
*/
- 329 :
setLabel: function (layer, label) {
- 330 :
var data = this.layerInfo(layer);
- 331 :
if (!data) {
- 332 :
log.warn('Layer not found: ', layer);
- 333 :
return this;
- 334 :
}
- 335 :
data.label = label;
- 336 :
var nameEl = data.labelEl.querySelector('span');
- 337 :
nameEl.innerHTML = ' ' + label;
- 338 :
return this;
- 339 :
},
- 340 :
- 341 :
_onLongClick: function (data, originalEvent) {
- 342 :
var defaultPrevented;
- 343 :
- 344 :
// @miniclass LayersControlInteractionEvent (LayerChooser)
- 345 :
// @inherits Event
- 346 :
// @property layer: L.Layer
- 347 :
// The layer that was interacted in LayerChooser control.
- 348 :
// @property control: LayerChooser
- 349 :
// LayerChooser control instance (just handy shortcut for window.layerChooser).
- 350 :
// @property data: Object
- 351 :
// Internal data object TODO
- 352 :
// @property originalEvent: DOMEvent
- 353 :
// The original mouse/jQuery event that triggered this Leaflet event.
- 354 :
// @method preventDefault: Function
- 355 :
// Method to prevent default action of event (like overlays toggling), otherwise handled by layerChooser.
- 356 :
var obj = {
- 357 :
control: this,
- 358 :
data: data,
- 359 :
originalEvent: originalEvent || { type: 'taphold' },
- 360 :
preventDefault: function () {
- 361 :
defaultPrevented = true;
- 362 :
this.defaultPrevented = true;
- 363 :
},
- 364 :
};
- 365 :
- 366 :
// @namespace Layer
- 367 :
// @section Layers control interaction events
- 368 :
// Fired when the overlay's label is long-clicked in the layers control.
- 369 :
- 370 :
// @section Layers control interaction events
- 371 :
// @event longclick: LayersControlInteractionEvent
- 372 :
// Fired on layer
- 373 :
data.layer.fire('longclick', obj);
- 374 :
if (!defaultPrevented) {
- 375 :
this._toggleOverlay(data);
- 376 :
}
- 377 :
// @namespace LayerChooser
- 378 :
},
- 379 :
- 380 :
// adds listeners to the overlays list to make inputs toggleable.
- 381 :
_initLayout: function () {
- 382 :
L.Control.Layers.prototype._initLayout.call(this);
- 383 :
$(this._overlaysList).on(
- 384 :
'click taphold',
- 385 :
'label',
- 386 :
function (e) {
- 387 :
if (!(e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.type === 'taphold')) {
- 388 :
return;
- 389 :
}
- 390 :
// e.preventDefault(); // seems no effect
- 391 :
var labelEl = e.target.closest('label');
- 392 :
this._onLongClick(this.layerInfo(labelEl), e);
- 393 :
}.bind(this)
- 394 :
);
- 395 :
},
- 396 :
- 397 :
_filterOverlays: function (data) {
- 398 :
return data.overlay && ['DEBUG Data Tiles', 'Resistance', 'Enlightened'].indexOf(data.name) === -1;
- 399 :
},
- 400 :
- 401 :
// Hides all the control's overlays except given one,
- 402 :
// or restores all, if it was the only one displayed (or none was displayed).
- 403 :
_toggleOverlay: function (data) {
- 404 :
if (!data || !data.overlay) {
- 405 :
log.warn('Overlay not found: ', data);
- 406 :
return;
- 407 :
}
- 408 :
var map = this._map;
- 409 :
- 410 :
var isChecked = map.hasLayer(data.layer);
- 411 :
var checked = 0;
- 412 :
var overlays = this._layers.filter(this._filterOverlays);
- 413 :
overlays.forEach(function (el) {
- 414 :
if (map.hasLayer(el.layer)) {
- 415 :
checked++;
- 416 :
}
- 417 :
});
- 418 :
- 419 :
if (checked === 0 || (isChecked && checked === 1)) {
- 420 :
// if nothing is selected, or specified overlay is exclusive,
- 421 :
// assume all boxes should be checked again
- 422 :
overlays.forEach(function (el) {
- 423 :
if (el.default) {
- 424 :
map.addLayer(el.layer);
- 425 :
}
- 426 :
});
- 427 :
} else {
- 428 :
// uncheck all, check specified
- 429 :
overlays.forEach(function (el) {
- 430 :
if (el.layer === data.layer) {
- 431 :
map.addLayer(el.layer);
- 432 :
} else {
- 433 :
map.removeLayer(el.layer);
- 434 :
}
- 435 :
});
- 436 :
}
- 437 :
},
- 438 :
- 439 :
_stripHtmlTags: function (str) {
- 440 :
return str.replace(/(<([^>]+)>)/gi, ''); // https://css-tricks.com/snippets/javascript/strip-html-tags-in-javascript/
- 441 :
},
- 442 :
- 443 :
/**
- 444 :
* Retrieves the current state of base and overlay layers managed by this control.
- 445 :
* This method is deprecated and should be used with caution.
- 446 :
*
- 447 :
* The method returns an object with two properties: 'baseLayers' and 'overlayLayers'.
- 448 :
* Each array contains objects representing the respective layers with properties: 'layerId', 'name', and 'active'.
- 449 :
* 'layerId' is an internal identifier for the layer, 'name' is the layer's name, and 'active' is a boolean indicating
- 450 :
* if the layer is currently active on the map.
- 451 :
*
- 452 :
* @memberof LayerChooser
- 453 :
* @deprecated
- 454 :
* @returns {{overlayLayers: Array, baseLayers: Array}} An object containing arrays of base and overlay layers.
- 455 :
*/
- 456 :
getLayers: function () {
- 457 :
var baseLayers = [];
- 458 :
var overlayLayers = [];
- 459 :
this._layers.forEach(function (data, idx) {
- 460 :
(data.overlay ? overlayLayers : baseLayers).push({
- 461 :
layerId: idx,
- 462 :
name: this._stripHtmlTags(data.label || data.name), // IITCm does not support html in layers labels
- 463 :
active: this._map.hasLayer(data.layer),
- 464 :
});
- 465 :
}, this);
- 466 :
- 467 :
return {
- 468 :
baseLayers: baseLayers,
- 469 :
overlayLayers: overlayLayers,
- 470 :
};
- 471 :
},
- 472 :
});
- 473 :
- 474 :
window.LayerChooser = LayerChooser;
- 475 :
- 476 :
// contains current status(on/off) of overlay layerGroups.
- 477 :
// !!deprecated: use `map.hasLayer` instead (https://leafletjs.com/reference.html#map-haslayer)
- 478 :
window.overlayStatus = {}; // to be set in constructor
- 479 :
- 480 :
// Reads recorded layerGroup status (as it may not be added to map yet),
- 481 :
// return `defaultDisplay` if no record found.
- 482 :
// !!deprecated: for most use cases prefer `getLayer()` method
- 483 :
// or `map.hasLayer` (https://leafletjs.com/reference.html#map-haslayer)
- 484 :
// window.isLayerGroupDisplayed = function (name, defaultDisplay) { // ...
- 485 :
window.isLayerGroupDisplayed = L.Util.falseFn; // to be set in constructor
- 486 :
- 487 :
LayerChooser.addInitHook(function () {
- 488 :
window.overlayStatus = this._overlayStatus;
- 489 :
window.isLayerGroupDisplayed = this._isOverlayDisplayed.bind(this);
- 490 :
});
- 491 :
- 492 :
// !!deprecated: use `layerChooser.addOverlay` directly
- 493 :
window.addLayerGroup = function (name, layerGroup, defaultDisplay) {
- 494 :
var options = { default: defaultDisplay };
- 495 :
if (arguments.length < 3) {
- 496 :
options = undefined;
- 497 :
}
- 498 :
window.layerChooser.addOverlay(layerGroup, name, options);
- 499 :
};
- 500 :
- 501 :
// !!deprecated: use `layerChooser.removeLayer` directly
- 502 :
// our method differs from inherited (https://leafletjs.com/reference.html#control-layers-removelayer),
- 503 :
// as (by default) layer is removed from the map as well, see description for more details.
- 504 :
window.removeLayerGroup = function (layerGroup) {
- 505 :
window.layerChooser.removeLayer(layerGroup);
- 506 :
};