Grayscale OSM base tiles, mobile-friendly dashboard and map with touch multi-select and box select, wider road click radius, and strip internal properties from JOSM export
This commit is contained in:
+34
-4
@@ -30,6 +30,7 @@ body {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
min-width: 175px;
|
||||
max-width: 280px;
|
||||
max-height: calc(100vh - 20px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -154,14 +155,17 @@ body {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -100%;
|
||||
width: 80%;
|
||||
max-width: 320px;
|
||||
width: 85%;
|
||||
min-width: 0;
|
||||
max-width: 300px;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
transition: right 0.3s ease;
|
||||
border-radius: 0;
|
||||
box-shadow: -2px 0 10px rgba(0,0,0,0.2);
|
||||
padding-top: 50px;
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.controls.open {
|
||||
@@ -173,7 +177,28 @@ body {
|
||||
}
|
||||
|
||||
.controls label {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.controls input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.controls select {
|
||||
font-size: 14px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
padding: 9px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,3 +230,8 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
.btn-back:hover { background: #2d2d4e; color: #fff; }
|
||||
|
||||
/* Mute the base map so colored overlays (e.g. lime green diffs) stand out. */
|
||||
.base-tiles {
|
||||
filter: grayscale(100%) brightness(1.1) contrast(0.95);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
function initBaseMap(elementId, view, zoom) {
|
||||
const map = L.map(elementId).setView(view || [28.7, -81.7], 12);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France',
|
||||
subdomains: 'abc',
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxNativeZoom: 19,
|
||||
maxZoom: 20
|
||||
maxZoom: 20,
|
||||
className: 'base-tiles'
|
||||
}).addTo(map);
|
||||
|
||||
if (zoom) map.setZoom(zoom);
|
||||
|
||||
+222
-88
@@ -22,6 +22,90 @@ let dragStartPoint = null;
|
||||
let selectionBox = null;
|
||||
let multiSelectMode = false;
|
||||
|
||||
// Render vector layers to canvas with a hit tolerance (extra pixels around
|
||||
// each line that still count as a click/tap) so 3px road lines are easy to
|
||||
// hit. Fingers get a wider margin than a mouse pointer.
|
||||
const IS_TOUCH = window.matchMedia('(pointer: coarse)').matches;
|
||||
const HIT_TOLERANCE = IS_TOUCH ? 16 : 8;
|
||||
|
||||
// Pixel distance from a canvas-rendered layer's edge to a layer point.
|
||||
function distanceToLayer(layer, p) {
|
||||
if (layer instanceof L.CircleMarker) {
|
||||
return Math.max(0, p.distanceTo(layer._point) - layer._radius);
|
||||
}
|
||||
const closed = layer instanceof L.Polygon;
|
||||
let min = Infinity;
|
||||
(layer._parts || []).forEach(part => {
|
||||
if (part.length === 1) min = Math.min(min, p.distanceTo(part[0]));
|
||||
for (let i = 1; i < part.length; i++) {
|
||||
min = Math.min(min, L.LineUtil.pointToSegmentDistance(p, part[i - 1], part[i]));
|
||||
}
|
||||
if (closed && part.length > 2) {
|
||||
min = Math.min(min, L.LineUtil.pointToSegmentDistance(p, part[part.length - 1], part[0]));
|
||||
}
|
||||
});
|
||||
// A hit inside a polygon's fill but away from its outline ranks behind
|
||||
// any line that is within tolerance.
|
||||
if (closed && min > layer._clickTolerance()) return HIT_TOLERANCE;
|
||||
return Math.max(0, min - (layer.options.stroke ? layer.options.weight / 2 : 0));
|
||||
}
|
||||
|
||||
// Leaflet's canvas renderer hands a click/hover to whichever layer within
|
||||
// `tolerance` was drawn last, so with a wide tolerance a neighbouring road
|
||||
// can steal the click. This variant picks the nearest one instead (ties
|
||||
// still go to the topmost). Otherwise mirrors Leaflet 1.9.4's
|
||||
// Canvas._onClick / _handleMouseHover.
|
||||
const NearestHitCanvas = L.Canvas.extend({
|
||||
_nearestLayer: function(point, e) {
|
||||
let best = null, bestDist = Infinity;
|
||||
for (let order = this._drawFirst; order; order = order.next) {
|
||||
const layer = order.layer;
|
||||
if (!layer.options.interactive || !layer._containsPoint(point)) continue;
|
||||
if (e && (e.type === 'click' || e.type === 'preclick') && this._map._draggableMoved(layer)) continue;
|
||||
const d = distanceToLayer(layer, point);
|
||||
if (d <= bestDist) {
|
||||
best = layer;
|
||||
bestDist = d;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
},
|
||||
|
||||
_onClick: function(e) {
|
||||
const clickedLayer = this._nearestLayer(this._map.mouseEventToLayerPoint(e), e);
|
||||
this._fireEvent(clickedLayer ? [clickedLayer] : false, e);
|
||||
},
|
||||
|
||||
_handleMouseHover: function(e, point) {
|
||||
if (this._mouseHoverThrottled) return;
|
||||
|
||||
const candidateHoveredLayer = this._nearestLayer(point);
|
||||
|
||||
if (candidateHoveredLayer !== this._hoveredLayer) {
|
||||
this._handleMouseOut(e);
|
||||
|
||||
if (candidateHoveredLayer) {
|
||||
L.DomUtil.addClass(this._container, 'leaflet-interactive'); // change cursor
|
||||
this._fireEvent([candidateHoveredLayer], e, 'mouseover');
|
||||
this._hoveredLayer = candidateHoveredLayer;
|
||||
}
|
||||
}
|
||||
|
||||
this._fireEvent(this._hoveredLayer ? [this._hoveredLayer] : false, e);
|
||||
|
||||
this._mouseHoverThrottled = true;
|
||||
setTimeout(L.Util.bind(function() {
|
||||
this._mouseHoverThrottled = false;
|
||||
}, this), 32);
|
||||
}
|
||||
});
|
||||
|
||||
const paneRenderers = {};
|
||||
function paneRenderer(pane) {
|
||||
if (!paneRenderers[pane]) paneRenderers[pane] = new NearestHitCanvas({ pane: pane, tolerance: HIT_TOLERANCE });
|
||||
return paneRenderers[pane];
|
||||
}
|
||||
|
||||
// Mobile controls state
|
||||
let loadedCounty = '';
|
||||
let loadedDataType = '';
|
||||
@@ -60,72 +144,94 @@ function initMap() {
|
||||
setupDragSelection();
|
||||
}
|
||||
|
||||
// Setup drag selection functionality
|
||||
// Setup drag selection functionality. A box is drawn by shift-dragging with
|
||||
// the mouse, or by dragging with the mouse or one finger while multi-select
|
||||
// mode is on. In multi-select mode on touch screens, two fingers still pan
|
||||
// and pinch-zoom: a second finger landing cancels the box.
|
||||
// A press that never moves past DRAG_THRESHOLD is left alone so the normal
|
||||
// click/tap handler on the feature (or empty map) runs.
|
||||
const DRAG_THRESHOLD = 5;
|
||||
|
||||
function setupDragSelection() {
|
||||
const mapContainer = map.getContainer();
|
||||
let draggingWasEnabled = false;
|
||||
let activePointerId = null;
|
||||
|
||||
map.on('mousedown', function(e) {
|
||||
// Only start drag selection with shift key or multi-select mode enabled
|
||||
if (!e.originalEvent.shiftKey && !multiSelectMode) return;
|
||||
|
||||
isDragging = true;
|
||||
dragStartPoint = e.containerPoint;
|
||||
|
||||
// Create selection box
|
||||
selectionBox = L.DomUtil.create('div', 'selection-box', mapContainer);
|
||||
selectionBox.style.left = dragStartPoint.x + 'px';
|
||||
selectionBox.style.top = dragStartPoint.y + 'px';
|
||||
|
||||
// Prevent map panning while dragging
|
||||
map.dragging.disable();
|
||||
|
||||
e.originalEvent.preventDefault();
|
||||
});
|
||||
|
||||
map.on('mousemove', function(e) {
|
||||
if (!isDragging || !selectionBox) return;
|
||||
|
||||
const currentPoint = e.containerPoint;
|
||||
const minX = Math.min(dragStartPoint.x, currentPoint.x);
|
||||
const minY = Math.min(dragStartPoint.y, currentPoint.y);
|
||||
const width = Math.abs(currentPoint.x - dragStartPoint.x);
|
||||
const height = Math.abs(currentPoint.y - dragStartPoint.y);
|
||||
|
||||
selectionBox.style.left = minX + 'px';
|
||||
selectionBox.style.top = minY + 'px';
|
||||
selectionBox.style.width = width + 'px';
|
||||
selectionBox.style.height = height + 'px';
|
||||
});
|
||||
|
||||
map.on('mouseup', function(e) {
|
||||
if (!isDragging) return;
|
||||
function pointFor(e) {
|
||||
const rect = mapContainer.getBoundingClientRect();
|
||||
return L.point(e.clientX - rect.left, e.clientY - rect.top);
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
isDragging = false;
|
||||
map.dragging.enable();
|
||||
|
||||
dragStartPoint = null;
|
||||
activePointerId = null;
|
||||
if (selectionBox) {
|
||||
const endPoint = e.containerPoint;
|
||||
|
||||
// Calculate bounds
|
||||
const minX = Math.min(dragStartPoint.x, endPoint.x);
|
||||
const minY = Math.min(dragStartPoint.y, endPoint.y);
|
||||
const maxX = Math.max(dragStartPoint.x, endPoint.x);
|
||||
const maxY = Math.max(dragStartPoint.y, endPoint.y);
|
||||
|
||||
const bounds = L.latLngBounds(
|
||||
map.containerPointToLatLng([minX, minY]),
|
||||
map.containerPointToLatLng([maxX, maxY])
|
||||
);
|
||||
|
||||
// Select features within bounds
|
||||
selectFeaturesInBounds(bounds);
|
||||
|
||||
// Remove selection box
|
||||
mapContainer.removeChild(selectionBox);
|
||||
selectionBox = null;
|
||||
}
|
||||
if (draggingWasEnabled) map.dragging.enable();
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
document.removeEventListener('pointercancel', endDrag);
|
||||
}
|
||||
|
||||
dragStartPoint = null;
|
||||
function onPointerMove(e) {
|
||||
if (e.pointerId !== activePointerId) return;
|
||||
const currentPoint = pointFor(e);
|
||||
if (!selectionBox) {
|
||||
if (currentPoint.distanceTo(dragStartPoint) < DRAG_THRESHOLD) return;
|
||||
selectionBox = L.DomUtil.create('div', 'selection-box', mapContainer);
|
||||
}
|
||||
|
||||
selectionBox.style.left = Math.min(dragStartPoint.x, currentPoint.x) + 'px';
|
||||
selectionBox.style.top = Math.min(dragStartPoint.y, currentPoint.y) + 'px';
|
||||
selectionBox.style.width = Math.abs(currentPoint.x - dragStartPoint.x) + 'px';
|
||||
selectionBox.style.height = Math.abs(currentPoint.y - dragStartPoint.y) + 'px';
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
if (e.pointerId !== activePointerId) return;
|
||||
if (selectionBox) {
|
||||
const endPoint = pointFor(e);
|
||||
const bounds = L.latLngBounds(
|
||||
map.containerPointToLatLng([Math.min(dragStartPoint.x, endPoint.x), Math.min(dragStartPoint.y, endPoint.y)]),
|
||||
map.containerPointToLatLng([Math.max(dragStartPoint.x, endPoint.x), Math.max(dragStartPoint.y, endPoint.y)])
|
||||
);
|
||||
selectFeaturesInBounds(bounds);
|
||||
|
||||
// Swallow the click the browser fires after a box drag so it
|
||||
// doesn't also toggle whatever feature the drag ended on.
|
||||
const swallowClick = ev => { ev.stopPropagation(); ev.preventDefault(); };
|
||||
mapContainer.addEventListener('click', swallowClick, { capture: true, once: true });
|
||||
setTimeout(() => mapContainer.removeEventListener('click', swallowClick, { capture: true }), 0);
|
||||
}
|
||||
endDrag();
|
||||
}
|
||||
|
||||
// pointerdown fires before Leaflet's own mousedown/touchstart, so
|
||||
// disabling dragging here stops the map from panning while the box is drawn.
|
||||
mapContainer.addEventListener('pointerdown', function(e) {
|
||||
// Second finger down: abandon the box and let Leaflet pan/pinch-zoom.
|
||||
if (isDragging) {
|
||||
if (e.pointerType !== 'mouse') endDrag();
|
||||
return;
|
||||
}
|
||||
if (e.pointerType === 'mouse') {
|
||||
if (e.button !== 0 || (!e.shiftKey && !multiSelectMode)) return;
|
||||
} else if (!multiSelectMode) {
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('.leaflet-control, .leaflet-popup')) return;
|
||||
|
||||
isDragging = true;
|
||||
activePointerId = e.pointerId;
|
||||
dragStartPoint = pointFor(e);
|
||||
draggingWasEnabled = map.dragging.enabled();
|
||||
map.dragging.disable();
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
document.addEventListener('pointercancel', endDrag);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,6 +321,7 @@ function clearSelection() {
|
||||
selectedFeatures = [];
|
||||
selectedLayers = [];
|
||||
updateSnapPreview();
|
||||
updateMultiSelectCount();
|
||||
|
||||
if (featurePopup) {
|
||||
map.closePopup(featurePopup);
|
||||
@@ -390,6 +497,7 @@ function createOsmLayer() {
|
||||
style: osmStyle,
|
||||
filter: shouldShowOsmFeature,
|
||||
pane: 'osmPane',
|
||||
renderer: paneRenderer('osmPane'),
|
||||
pointToLayer: function(feature, latlng) {
|
||||
return L.circleMarker(latlng, osmMarkerStyle(feature));
|
||||
},
|
||||
@@ -488,6 +596,7 @@ function createDiffLayer() {
|
||||
style: diffStyle,
|
||||
filter: shouldShowFeature,
|
||||
pane: 'diffPane',
|
||||
renderer: paneRenderer('diffPane'),
|
||||
pointToLayer: function(feature, latlng) {
|
||||
return L.circleMarker(latlng, diffMarkerStyle(feature));
|
||||
},
|
||||
@@ -562,6 +671,7 @@ function createCountyLayer() {
|
||||
style: countyStyle,
|
||||
filter: shouldShowCountyFeature,
|
||||
pane: 'countyPane',
|
||||
renderer: paneRenderer('countyPane'),
|
||||
pointToLayer: function(feature, latlng) {
|
||||
return L.circleMarker(latlng, countyMarkerStyle(feature));
|
||||
},
|
||||
@@ -707,6 +817,7 @@ function selectFeature(feature, layer, e, layerType = 'diff') {
|
||||
function showMultiFeaturePopup(latlng) {
|
||||
if (selectedFeatures.length === 0) return;
|
||||
popupLatlng = latlng;
|
||||
updateMultiSelectCount();
|
||||
|
||||
// Remove old popup without triggering clearSelection
|
||||
if (featurePopup) {
|
||||
@@ -714,7 +825,7 @@ function showMultiFeaturePopup(latlng) {
|
||||
map.closePopup(featurePopup);
|
||||
}
|
||||
|
||||
let html = '<div style="font-size: 12px; max-height: 400px; overflow-y: auto;">';
|
||||
let html = '<div style="font-size: 12px; max-height: min(400px, 50vh); overflow-y: auto;">';
|
||||
|
||||
// Show count
|
||||
html += `<div style="margin-bottom: 8px;"><strong>Selected:</strong> ${selectedFeatures.length} feature${selectedFeatures.length > 1 ? 's' : ''}</div>`;
|
||||
@@ -889,7 +1000,7 @@ function showMultiFeaturePopup(latlng) {
|
||||
|
||||
// Create popup at click location
|
||||
featurePopup = L.popup({
|
||||
maxWidth: 300,
|
||||
maxWidth: Math.min(300, window.innerWidth - 60),
|
||||
closeButton: true,
|
||||
autoClose: false,
|
||||
closeOnClick: false
|
||||
@@ -1487,6 +1598,27 @@ function buildNodeRefCount() {
|
||||
return counts;
|
||||
}
|
||||
|
||||
// Feature properties that are bookkeeping from our pipeline/UI, not OSM tags,
|
||||
// and must never be written into the JOSM export.
|
||||
const NON_OSM_PROPERTIES = new Set([
|
||||
// map viewer state
|
||||
'removed', 'status', 'accepted',
|
||||
// OSM identity/version info carried along from the Overpass download
|
||||
'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions',
|
||||
// compare-addresses.py annotations
|
||||
'conflict', 'county_city', 'county_postal_community',
|
||||
]);
|
||||
|
||||
// Turn feature properties into an OSM tag map, dropping internal keys and
|
||||
// empty values.
|
||||
function osmTagsFrom(props) {
|
||||
const tags = {};
|
||||
Object.entries(props || {}).forEach(([k, v]) => {
|
||||
if (!NON_OSM_PROPERTIES.has(k) && v !== null && v !== undefined && v !== '') tags[k] = String(v);
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
function computeRemovalPlan(removedFeat, acceptedAddedFeats, nodeRefCount) {
|
||||
const props = removedFeat.properties || {};
|
||||
const osmId = props.osm_id || props['@id'];
|
||||
@@ -1498,11 +1630,7 @@ function computeRemovalPlan(removedFeat, acceptedAddedFeats, nodeRefCount) {
|
||||
return { type: 'deleteNode', osmId, osmVersion, description: `Delete node${osmId ? ' ' + osmId : ''}` };
|
||||
}
|
||||
|
||||
const tags = {};
|
||||
Object.entries(props).forEach(([k, v]) => {
|
||||
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
||||
v !== null && v !== undefined && v !== '') tags[k] = String(v);
|
||||
});
|
||||
const tags = osmTagsFrom(props);
|
||||
|
||||
if (!osmNodes || !wayCoords || osmNodes.length !== wayCoords.length) {
|
||||
return { type: 'delete', osmId, osmVersion, wayCoords, tags, description: `Delete way${osmId ? ' ' + osmId : ''} (no node data)` };
|
||||
@@ -1655,11 +1783,7 @@ function generateOsmXml(acceptedAdded, removalPlans, checkedPlanIndices) {
|
||||
const deleteNodeIds = new Map(); // id -> version
|
||||
|
||||
acceptedAdded.forEach(feat => {
|
||||
const tags = {};
|
||||
Object.entries(feat.properties || {}).forEach(([k, v]) => {
|
||||
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
||||
v !== null && v !== undefined && v !== '') tags[k] = String(v);
|
||||
});
|
||||
const tags = osmTagsFrom(feat.properties);
|
||||
|
||||
if (feat.geometry.type === 'Point') {
|
||||
newNodes.push({ id: nextNewId--, coord: feat.geometry.coordinates, tags });
|
||||
@@ -1692,11 +1816,7 @@ function generateOsmXml(acceptedAdded, removalPlans, checkedPlanIndices) {
|
||||
insIdx++;
|
||||
}
|
||||
}
|
||||
const tags = {};
|
||||
Object.entries(feat.properties || {}).forEach(([k, v]) => {
|
||||
if (!['removed', 'status', 'accepted', 'osm_id', 'osm_type', 'osm_nodes', 'osm_version', 'osm_node_versions'].includes(k) &&
|
||||
v !== null && v !== undefined && v !== '') tags[k] = String(v);
|
||||
});
|
||||
const tags = osmTagsFrom(feat.properties);
|
||||
modifiedWays.push({ id: osmId, version: osmVersion, ndRefs, tags });
|
||||
});
|
||||
|
||||
@@ -2092,6 +2212,27 @@ function toggleLayer(layerId, layer) {
|
||||
updateLayerZIndex();
|
||||
}
|
||||
|
||||
function setMultiSelectMode(on) {
|
||||
multiSelectMode = on;
|
||||
const button = document.getElementById('multiSelectToggle');
|
||||
const fab = document.getElementById('multiSelectFab');
|
||||
button.style.background = on ? '#007bff' : '#6c757d';
|
||||
button.textContent = on ? 'Multi-Select: ON' : 'Multi-Select: OFF';
|
||||
fab.classList.toggle('active', on);
|
||||
fab.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
map.getContainer().classList.toggle('multi-select-active', on);
|
||||
// Clear selection when turning off multi-select mode
|
||||
if (!on) clearSelection();
|
||||
updateMultiSelectCount();
|
||||
}
|
||||
|
||||
function updateMultiSelectCount() {
|
||||
const fab = document.getElementById('multiSelectFab');
|
||||
if (!fab) return;
|
||||
const n = selectedFeatures.length;
|
||||
fab.querySelector('.fab-count').textContent = multiSelectMode && n > 0 ? n : '';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMap();
|
||||
@@ -2164,7 +2305,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
loadFiles();
|
||||
// Auto-close controls on mobile after clicking load
|
||||
if (window.innerWidth <= 768) {
|
||||
setTimeout(closeControls, 300);
|
||||
setTimeout(window.closeMobileControls, 300);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2174,21 +2315,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Export to JOSM button
|
||||
document.getElementById('exportOsmButton').addEventListener('click', exportToOsm);
|
||||
|
||||
// Multi-select toggle button
|
||||
// Multi-select toggles: the one in the controls panel, and the floating
|
||||
// on-map button shown on mobile where the panel is tucked away.
|
||||
document.getElementById('multiSelectToggle').addEventListener('click', function() {
|
||||
multiSelectMode = !multiSelectMode;
|
||||
const button = document.getElementById('multiSelectToggle');
|
||||
if (multiSelectMode) {
|
||||
button.style.background = '#007bff';
|
||||
button.textContent = 'Multi-Select: ON';
|
||||
map.getContainer().classList.add('multi-select-active');
|
||||
} else {
|
||||
button.style.background = '#6c757d';
|
||||
button.textContent = 'Multi-Select: OFF';
|
||||
map.getContainer().classList.remove('multi-select-active');
|
||||
// Clear selection when turning off multi-select mode
|
||||
clearSelection();
|
||||
}
|
||||
setMultiSelectMode(!multiSelectMode);
|
||||
});
|
||||
document.getElementById('multiSelectFab').addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
setMultiSelectMode(!multiSelectMode);
|
||||
});
|
||||
|
||||
// Drag and drop for layer reordering
|
||||
|
||||
Reference in New Issue
Block a user