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