diff --git a/src/public/index.html b/src/public/index.html
index 63d0fb6..302ed4b 100644
--- a/src/public/index.html
+++ b/src/public/index.html
@@ -27,6 +27,10 @@
+
+
+
+
@@ -2025,9 +2029,14 @@
var openStreetMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22, // increase from 18 to 22
- attribution: '© OpenStreetMap | Data from Meshtastic',
+ attribution: 'Tiles © OpenStreetMap | Data from Meshtastic',
}).addTo(map);
+ var esriWorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
+ maxZoom: 21, // esri doesn't have tiles closer than this
+ attribution: 'Tiles © Esri | Data from Meshtastic'
+ });
+
// create layer groups
var nodesLayerGroup = new L.LayerGroup();
var neighboursLayerGroup = new L.LayerGroup();
@@ -2068,16 +2077,9 @@
return div;
};
- var baseLayers = {
- "Nodes (All)": nodesLayerGroup,
- "Nodes (Clustered)": nodesClusteredLayerGroup,
- "Nodes (Hide All)": new L.LayerGroup(),
- };
-
- var overlays = {
- "Legend": legendLayerGroup,
- "Neighbours": neighboursLayerGroup,
- "Waypoints": waypointsLayerGroup,
+ var tileLayers = {
+ "Map": openStreetMap,
+ "Satellite": esriWorldImagery,
};
// handle adding/remove legend on map (can't use L.Control as an overlay, so we toggle an empty L.LayerGroup)
@@ -2092,7 +2094,21 @@
});
// add layers to control ui
- L.control.layers(baseLayers, overlays).addTo(map);
+ L.control.groupedLayers(tileLayers, {
+ "Nodes": {
+ "All": nodesLayerGroup,
+ "Clustered": nodesClusteredLayerGroup,
+ "None": new L.LayerGroup(),
+ },
+ "Overlays": {
+ "Legend": legendLayerGroup,
+ "Neighbours": neighboursLayerGroup,
+ "Waypoints": waypointsLayerGroup,
+ },
+ }, {
+ // make the "Nodes" group exclusive (use radio inputs instead of checkbox)
+ exclusiveGroups: ["Nodes"],
+ }).addTo(map);
// enable base layers
nodesClusteredLayerGroup.addTo(map);
diff --git a/src/public/plugins/leaflet.groupedlayercontrol/leaflet.groupedlayercontrol.css b/src/public/plugins/leaflet.groupedlayercontrol/leaflet.groupedlayercontrol.css
new file mode 100644
index 0000000..28b169b
--- /dev/null
+++ b/src/public/plugins/leaflet.groupedlayercontrol/leaflet.groupedlayercontrol.css
@@ -0,0 +1,14 @@
+.leaflet-control-layers-group-name {
+ font-weight: bold;
+ margin-bottom: .2em;
+ margin-left: 3px;
+}
+
+.leaflet-control-layers-group {
+ margin-bottom: .5em;
+}
+
+.leaflet-control-layers-scrollbar {
+ overflow-y: scroll;
+ padding-right: 10px;
+}
diff --git a/src/public/plugins/leaflet.groupedlayercontrol/leaflet.groupedlayercontrol.js b/src/public/plugins/leaflet.groupedlayercontrol/leaflet.groupedlayercontrol.js
new file mode 100644
index 0000000..7f0aaa8
--- /dev/null
+++ b/src/public/plugins/leaflet.groupedlayercontrol/leaflet.groupedlayercontrol.js
@@ -0,0 +1,374 @@
+/* global L */
+
+// A layer control which provides for layer groupings.
+// Author: Ishmael Smyrnow
+L.Control.GroupedLayers = L.Control.extend({
+
+ options: {
+ collapsed: true,
+ position: 'topright',
+ autoZIndex: true,
+ exclusiveGroups: [],
+ groupCheckboxes: false
+ },
+
+ initialize: function (baseLayers, groupedOverlays, options) {
+ var i, j;
+ L.Util.setOptions(this, options);
+
+ this._layers = [];
+ this._lastZIndex = 0;
+ this._handlingClick = false;
+ this._groupList = [];
+ this._domGroups = [];
+
+ for (i in baseLayers) {
+ this._addLayer(baseLayers[i], i);
+ }
+
+ for (i in groupedOverlays) {
+ for (j in groupedOverlays[i]) {
+ this._addLayer(groupedOverlays[i][j], j, i, true);
+ }
+ }
+ },
+
+ onAdd: function (map) {
+ this._initLayout();
+ this._update();
+
+ map
+ .on('layeradd', this._onLayerChange, this)
+ .on('layerremove', this._onLayerChange, this);
+
+ return this._container;
+ },
+
+ onRemove: function (map) {
+ map
+ .off('layeradd', this._onLayerChange, this)
+ .off('layerremove', this._onLayerChange, this);
+ },
+
+ addBaseLayer: function (layer, name) {
+ this._addLayer(layer, name);
+ this._update();
+ return this;
+ },
+
+ addOverlay: function (layer, name, group) {
+ this._addLayer(layer, name, group, true);
+ this._update();
+ return this;
+ },
+
+ removeLayer: function (layer) {
+ var id = L.Util.stamp(layer);
+ var _layer = this._getLayer(id);
+ if (_layer) {
+ delete this._layers[this._layers.indexOf(_layer)];
+ }
+ this._update();
+ return this;
+ },
+
+ _getLayer: function (id) {
+ for (var i = 0; i < this._layers.length; i++) {
+ if (this._layers[i] && L.stamp(this._layers[i].layer) === id) {
+ return this._layers[i];
+ }
+ }
+ },
+
+ _initLayout: function () {
+ var className = 'leaflet-control-layers',
+ container = this._container = L.DomUtil.create('div', className);
+
+ // Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
+ container.setAttribute('aria-haspopup', true);
+
+ if (L.Browser.touch) {
+ L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
+ } else {
+ L.DomEvent.disableClickPropagation(container);
+ L.DomEvent.on(container, 'wheel', L.DomEvent.stopPropagation);
+ }
+
+ var form = this._form = L.DomUtil.create('form', className + '-list');
+
+ if (this.options.collapsed) {
+ if (!L.Browser.android) {
+ L.DomEvent
+ .on(container, 'mouseover', this._expand, this)
+ .on(container, 'mouseout', this._collapse, this);
+ }
+ var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container);
+ link.href = '#';
+ link.title = 'Layers';
+
+ if (L.Browser.touch) {
+ L.DomEvent
+ .on(link, 'click', L.DomEvent.stop)
+ .on(link, 'click', this._expand, this);
+ } else {
+ L.DomEvent.on(link, 'focus', this._expand, this);
+ }
+
+ this._map.on('click', this._collapse, this);
+ // TODO keyboard accessibility
+ } else {
+ this._expand();
+ }
+
+ this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
+ this._separator = L.DomUtil.create('div', className + '-separator', form);
+ this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
+
+ container.appendChild(form);
+ },
+
+ _addLayer: function (layer, name, group, overlay) {
+ var id = L.Util.stamp(layer);
+
+ var _layer = {
+ layer: layer,
+ name: name,
+ overlay: overlay
+ };
+ this._layers.push(_layer);
+
+ group = group || '';
+ var groupId = this._indexOf(this._groupList, group);
+
+ if (groupId === -1) {
+ groupId = this._groupList.push(group) - 1;
+ }
+
+ var exclusive = (this._indexOf(this.options.exclusiveGroups, group) !== -1);
+
+ _layer.group = {
+ name: group,
+ id: groupId,
+ exclusive: exclusive
+ };
+
+ if (this.options.autoZIndex && layer.setZIndex) {
+ this._lastZIndex++;
+ layer.setZIndex(this._lastZIndex);
+ }
+ },
+
+ _update: function () {
+ if (!this._container) {
+ return;
+ }
+
+ this._baseLayersList.innerHTML = '';
+ this._overlaysList.innerHTML = '';
+ this._domGroups.length = 0;
+
+ var baseLayersPresent = false,
+ overlaysPresent = false,
+ i, obj;
+
+ for (var i = 0; i < this._layers.length; i++) {
+ obj = this._layers[i];
+ this._addItem(obj);
+ overlaysPresent = overlaysPresent || obj.overlay;
+ baseLayersPresent = baseLayersPresent || !obj.overlay;
+ }
+
+ this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
+ },
+
+ _onLayerChange: function (e) {
+ var obj = this._getLayer(L.Util.stamp(e.layer)),
+ type;
+
+ if (!obj) {
+ return;
+ }
+
+ if (!this._handlingClick) {
+ this._update();
+ }
+
+ if (obj.overlay) {
+ type = e.type === 'layeradd' ? 'overlayadd' : 'overlayremove';
+ } else {
+ type = e.type === 'layeradd' ? 'baselayerchange' : null;
+ }
+
+ if (type) {
+ this._map.fire(type, obj);
+ }
+ },
+
+ // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe)
+ _createRadioElement: function (name, checked) {
+ var radioHtml = '';
+
+ var radioFragment = document.createElement('div');
+ radioFragment.innerHTML = radioHtml;
+
+ return radioFragment.firstChild;
+ },
+
+ _addItem: function (obj) {
+ var label = document.createElement('label'),
+ input,
+ checked = this._map.hasLayer(obj.layer),
+ container,
+ groupRadioName;
+
+ if (obj.overlay) {
+ if (obj.group.exclusive) {
+ groupRadioName = 'leaflet-exclusive-group-layer-' + obj.group.id;
+ input = this._createRadioElement(groupRadioName, checked);
+ } else {
+ input = document.createElement('input');
+ input.type = 'checkbox';
+ input.className = 'leaflet-control-layers-selector';
+ input.defaultChecked = checked;
+ }
+ } else {
+ input = this._createRadioElement('leaflet-base-layers', checked);
+ }
+
+ input.layerId = L.Util.stamp(obj.layer);
+ input.groupID = obj.group.id;
+ L.DomEvent.on(input, 'click', this._onInputClick, this);
+
+ var name = document.createElement('span');
+ name.innerHTML = ' ' + obj.name;
+
+ label.appendChild(input);
+ label.appendChild(name);
+
+ if (obj.overlay) {
+ container = this._overlaysList;
+
+ var groupContainer = this._domGroups[obj.group.id];
+
+ // Create the group container if it doesn't exist
+ if (!groupContainer) {
+ groupContainer = document.createElement('div');
+ groupContainer.className = 'leaflet-control-layers-group';
+ groupContainer.id = 'leaflet-control-layers-group-' + obj.group.id;
+
+ var groupLabel = document.createElement('label');
+ groupLabel.className = 'leaflet-control-layers-group-label';
+
+ if (obj.group.name !== '' && !obj.group.exclusive) {
+ // ------ add a group checkbox with an _onInputClickGroup function
+ if (this.options.groupCheckboxes) {
+ var groupInput = document.createElement('input');
+ groupInput.type = 'checkbox';
+ groupInput.className = 'leaflet-control-layers-group-selector';
+ groupInput.groupID = obj.group.id;
+ groupInput.legend = this;
+ L.DomEvent.on(groupInput, 'click', this._onGroupInputClick, groupInput);
+ groupLabel.appendChild(groupInput);
+ }
+ }
+
+ var groupName = document.createElement('span');
+ groupName.className = 'leaflet-control-layers-group-name';
+ groupName.innerHTML = obj.group.name;
+ groupLabel.appendChild(groupName);
+
+ groupContainer.appendChild(groupLabel);
+ container.appendChild(groupContainer);
+
+ this._domGroups[obj.group.id] = groupContainer;
+ }
+
+ container = groupContainer;
+ } else {
+ container = this._baseLayersList;
+ }
+
+ container.appendChild(label);
+
+ return label;
+ },
+
+ _onGroupInputClick: function () {
+ var i, input, obj;
+
+ var this_legend = this.legend;
+ this_legend._handlingClick = true;
+
+ var inputs = this_legend._form.getElementsByTagName('input');
+ var inputsLen = inputs.length;
+
+ for (i = 0; i < inputsLen; i++) {
+ input = inputs[i];
+ if (input.groupID === this.groupID && input.className === 'leaflet-control-layers-selector') {
+ input.checked = this.checked;
+ obj = this_legend._getLayer(input.layerId);
+ if (input.checked && !this_legend._map.hasLayer(obj.layer)) {
+ this_legend._map.addLayer(obj.layer);
+ } else if (!input.checked && this_legend._map.hasLayer(obj.layer)) {
+ this_legend._map.removeLayer(obj.layer);
+ }
+ }
+ }
+
+ this_legend._handlingClick = false;
+ },
+
+ _onInputClick: function () {
+ var i, input, obj,
+ inputs = this._form.getElementsByTagName('input'),
+ inputsLen = inputs.length;
+
+ this._handlingClick = true;
+
+ for (i = 0; i < inputsLen; i++) {
+ input = inputs[i];
+ if (input.className === 'leaflet-control-layers-selector') {
+ obj = this._getLayer(input.layerId);
+
+ if (input.checked && !this._map.hasLayer(obj.layer)) {
+ this._map.addLayer(obj.layer);
+ } else if (!input.checked && this._map.hasLayer(obj.layer)) {
+ this._map.removeLayer(obj.layer);
+ }
+ }
+ }
+
+ this._handlingClick = false;
+ },
+
+ _expand: function () {
+ L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
+ // permits to have a scrollbar if overlays heighter than the map.
+ var acceptableHeight = this._map._size.y - (this._container.offsetTop * 4);
+ if (acceptableHeight < this._form.clientHeight) {
+ L.DomUtil.addClass(this._form, 'leaflet-control-layers-scrollbar');
+ this._form.style.height = acceptableHeight + 'px';
+ }
+ },
+
+ _collapse: function () {
+ this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
+ },
+
+ _indexOf: function (arr, obj) {
+ for (var i = 0, j = arr.length; i < j; i++) {
+ if (arr[i] === obj) {
+ return i;
+ }
+ }
+ return -1;
+ }
+});
+
+L.control.groupedLayers = function (baseLayers, groupedOverlays, options) {
+ return new L.Control.GroupedLayers(baseLayers, groupedOverlays, options);
+};