672 lines
27 KiB
JavaScript
672 lines
27 KiB
JavaScript
/**
|
|
* Layer Switcher Control for Maplibre GL
|
|
*
|
|
* A reusable control to toggle visibility of additional map layers.
|
|
* Supports raster layers (WMS, WMTS) and vector layers.
|
|
*
|
|
* @version 1.0.0
|
|
* @requires maplibre-gl
|
|
*/
|
|
|
|
(function(window) {
|
|
'use strict';
|
|
|
|
// Default icons for layer types
|
|
const ICONS = {
|
|
// Satellite/Ortho icon
|
|
satellite: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>`,
|
|
|
|
// Map/Layers icon
|
|
layers: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>`,
|
|
|
|
// 3D/Cube icon
|
|
cube: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline><polyline points="7.5 19.79 7.5 14.6 3 12"></polyline><polyline points="21 12 16.5 14.6 16.5 19.79"></polyline><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>`,
|
|
|
|
// Eye icon for visibility
|
|
eye: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>`,
|
|
|
|
// Grid/Tiles icon
|
|
grid: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>`,
|
|
|
|
// Tree icon for vegetation/green surfaces
|
|
tree: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-7"></path><path d="M9 22h6"></path><path d="M12 15a6 6 0 0 0 6-6c0-1.5-.5-2.9-1.4-4H7.4C6.5 6.1 6 7.5 6 9a6 6 0 0 0 6 6z"></path><path d="M12 2a3 3 0 0 0-3 3h6a3 3 0 0 0-3-3z"></path></svg>`
|
|
};
|
|
|
|
// CSS styles for the control
|
|
const CONTROL_STYLES = `
|
|
.layer-switcher-ctrl {
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
box-shadow: 0 0 0 2px rgba(0,0,0,.1);
|
|
overflow: visible;
|
|
}
|
|
|
|
.layer-switcher-btn {
|
|
width: 26px;
|
|
height: 26px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
border: 0;
|
|
outline: none;
|
|
box-sizing: border-box;
|
|
background-color: transparent;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid rgba(0,0,0,.1);
|
|
}
|
|
|
|
.layer-switcher-btn:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.layer-switcher-btn:hover {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.layer-switcher-btn.active {
|
|
background-color: #e7f3ff;
|
|
}
|
|
|
|
.layer-switcher-btn.active:hover {
|
|
background-color: #cce5ff;
|
|
}
|
|
|
|
.layer-switcher-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
stroke: #333;
|
|
}
|
|
|
|
.layer-switcher-btn.active svg {
|
|
stroke: #0066ff;
|
|
}
|
|
|
|
/* Bootstrap Icons support */
|
|
.layer-switcher-btn .bi {
|
|
font-size: 14px;
|
|
color: #333;
|
|
}
|
|
|
|
.layer-switcher-btn.active .bi {
|
|
color: #0066ff;
|
|
}
|
|
|
|
/* Sublayer selector styles - positioned to the right of the control */
|
|
.layer-switcher-sublayer-selector {
|
|
position: absolute;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
padding: 6px;
|
|
z-index: 9999;
|
|
display: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.layer-switcher-sublayer-selector.visible {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.layer-switcher-sublayer-btn {
|
|
padding: 4px 8px;
|
|
border: 2px solid #ddd;
|
|
background: white;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.layer-switcher-sublayer-btn:hover {
|
|
background: #f8f9fa;
|
|
border-color: #0d6efd;
|
|
}
|
|
|
|
.layer-switcher-sublayer-btn.active {
|
|
background: #0d6efd;
|
|
color: white;
|
|
border-color: #0d6efd;
|
|
}
|
|
`;
|
|
|
|
/**
|
|
* LayerSwitcherControl class
|
|
*
|
|
* @param {Object} options - Configuration options
|
|
* @param {Array} options.layers - Array of layer configurations
|
|
* @param {string} options.layers[].id - Unique layer ID
|
|
* @param {string} options.layers[].name - Display name for tooltip
|
|
* @param {string} options.layers[].icon - Icon type: 'satellite', 'layers', 'cube', 'eye', 'grid' or custom SVG
|
|
* @param {Object} options.layers[].source - MapLibre source configuration (for new layers)
|
|
* @param {Object} options.layers[].layer - MapLibre layer configuration without id and source (for new layers)
|
|
* @param {string[]} options.layers[].existingLayerIds - Array of existing layer IDs to toggle (for existing layers)
|
|
* @param {string} options.layers[].beforeLayer - ID of layer to insert before (optional)
|
|
* @param {boolean} options.layers[].visible - Initial visibility (default: false for new layers, true for existing)
|
|
* @param {boolean} options.layers[].exclusive - If true, enabling this layer disables others in same group
|
|
* @param {string} options.layers[].group - Group name for exclusive layers
|
|
* @param {Array} options.layers[].sublayers - Array of sublayer configurations for multi-source layers
|
|
* @param {string} options.layers[].sublayers[].id - Unique sublayer ID
|
|
* @param {string} options.layers[].sublayers[].name - Display name for sublayer
|
|
* @param {Object} options.layers[].sublayers[].source - MapLibre source configuration
|
|
* @param {Object} options.layers[].sublayers[].layer - MapLibre layer configuration
|
|
* @param {boolean} options.layers[].sublayers[].default - If true, this sublayer is selected by default
|
|
*/
|
|
class LayerSwitcherControl {
|
|
constructor(options = {}) {
|
|
this.layers = options.layers || [];
|
|
this.map = null;
|
|
this.container = null;
|
|
this.buttons = {};
|
|
this.sublayerSelectors = {}; // Fixed position selectors for sublayers
|
|
this.layerStates = new Map(); // Track layer visibility states
|
|
this.activeSublayers = new Map(); // Track active sublayer for each parent layer
|
|
|
|
this._injectStyles();
|
|
}
|
|
|
|
/**
|
|
* Inject CSS styles
|
|
*/
|
|
_injectStyles() {
|
|
if (document.getElementById('layer-switcher-styles')) return;
|
|
|
|
const style = document.createElement('style');
|
|
style.id = 'layer-switcher-styles';
|
|
style.textContent = CONTROL_STYLES;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
/**
|
|
* Add control to map (IControl interface)
|
|
*/
|
|
onAdd(map) {
|
|
this.map = map;
|
|
|
|
// Create container
|
|
this.container = document.createElement('div');
|
|
this.container.className = 'maplibregl-ctrl maplibregl-ctrl-group layer-switcher-ctrl';
|
|
this.container.style.position = 'relative';
|
|
|
|
// Create buttons for each layer
|
|
this.layers.forEach(layerConfig => {
|
|
// For layers with sublayers, set default active sublayer
|
|
if (layerConfig.sublayers && layerConfig.sublayers.length > 0) {
|
|
const defaultSublayer = layerConfig.sublayers.find(s => s.default) || layerConfig.sublayers[0];
|
|
this.activeSublayers.set(layerConfig.id, defaultSublayer.id);
|
|
}
|
|
|
|
const button = this._createButton(layerConfig);
|
|
this.container.appendChild(button);
|
|
this.buttons[layerConfig.id] = button;
|
|
|
|
// Create sublayer selector for sublayers if needed
|
|
// Appended to the map container to escape the 29px maplibregl-ctrl-group constraint
|
|
if (layerConfig.sublayers && layerConfig.sublayers.length > 0) {
|
|
const selector = this._createSublayerSelector(layerConfig);
|
|
map.getContainer().appendChild(selector);
|
|
this.sublayerSelectors[layerConfig.id] = selector;
|
|
}
|
|
|
|
// Initialize layer state
|
|
// For existing layers, default to visible (they already exist on the map)
|
|
const isExistingLayer = layerConfig.existingLayerIds && layerConfig.existingLayerIds.length > 0;
|
|
const initialVisibility = layerConfig.visible !== undefined
|
|
? layerConfig.visible
|
|
: isExistingLayer; // existing layers are visible by default
|
|
|
|
this.layerStates.set(layerConfig.id, {
|
|
visible: initialVisibility,
|
|
sourceAdded: isExistingLayer, // existing layers don't need source
|
|
layerAdded: isExistingLayer // existing layers are already added
|
|
});
|
|
|
|
// Update button state immediately for existing visible layers
|
|
if (initialVisibility) {
|
|
this._updateButtonState(layerConfig.id, true);
|
|
}
|
|
});
|
|
|
|
// Initialize layers if map is already loaded
|
|
if (map.loaded()) {
|
|
this._initializeLayers();
|
|
} else {
|
|
map.on('load', () => this._initializeLayers());
|
|
}
|
|
|
|
return this.container;
|
|
}
|
|
|
|
/**
|
|
* Remove control from map
|
|
*/
|
|
onRemove() {
|
|
// Remove layers and sources
|
|
this.layers.forEach(layerConfig => {
|
|
const layerId = `layer-switcher-${layerConfig.id}`;
|
|
const sourceId = `layer-switcher-source-${layerConfig.id}`;
|
|
|
|
if (this.map.getLayer(layerId)) {
|
|
this.map.removeLayer(layerId);
|
|
}
|
|
if (this.map.getSource(sourceId)) {
|
|
this.map.removeSource(sourceId);
|
|
}
|
|
});
|
|
|
|
// Remove sublayer selectors from body
|
|
Object.values(this.sublayerSelectors).forEach(selector => {
|
|
if (selector && selector.parentNode) {
|
|
selector.parentNode.removeChild(selector);
|
|
}
|
|
});
|
|
|
|
if (this.container && this.container.parentNode) {
|
|
this.container.parentNode.removeChild(this.container);
|
|
}
|
|
|
|
this.map = null;
|
|
this.container = null;
|
|
}
|
|
|
|
/**
|
|
* Create a button for a layer
|
|
*/
|
|
_createButton(layerConfig) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'layer-switcher-btn';
|
|
button.title = layerConfig.name || layerConfig.id;
|
|
|
|
// Set icon
|
|
if (layerConfig.icon) {
|
|
// Check if it's a Bootstrap Icon (format: "bi:icon-name")
|
|
if (layerConfig.icon.startsWith('bi:')) {
|
|
const iconName = layerConfig.icon.substring(3);
|
|
button.innerHTML = `<i class="bi bi-${iconName}"></i>`;
|
|
}
|
|
// Check if it's a built-in icon
|
|
else if (ICONS[layerConfig.icon]) {
|
|
button.innerHTML = ICONS[layerConfig.icon];
|
|
}
|
|
// Otherwise treat as custom SVG
|
|
else {
|
|
button.innerHTML = layerConfig.icon;
|
|
}
|
|
} else {
|
|
// Default to layers icon
|
|
button.innerHTML = ICONS.layers;
|
|
}
|
|
|
|
// Click handler - simply toggle the layer on/off
|
|
button.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.toggleLayer(layerConfig.id);
|
|
});
|
|
|
|
return button;
|
|
}
|
|
|
|
/**
|
|
* Create a fixed-position sublayer selector (bottom left)
|
|
*/
|
|
_createSublayerSelector(layerConfig) {
|
|
const selector = document.createElement('div');
|
|
selector.className = 'layer-switcher-sublayer-selector';
|
|
selector.dataset.parentId = layerConfig.id;
|
|
|
|
layerConfig.sublayers.forEach(sublayer => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'layer-switcher-sublayer-btn';
|
|
btn.dataset.sublayerId = sublayer.id;
|
|
btn.textContent = sublayer.name || sublayer.id;
|
|
|
|
// Mark default as active
|
|
const activeSublayerId = this.activeSublayers.get(layerConfig.id);
|
|
if (sublayer.id === activeSublayerId) {
|
|
btn.classList.add('active');
|
|
}
|
|
|
|
// Click handler
|
|
btn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this._selectSublayer(layerConfig.id, sublayer.id);
|
|
});
|
|
|
|
selector.appendChild(btn);
|
|
});
|
|
|
|
return selector;
|
|
}
|
|
|
|
/**
|
|
* Show sublayer selector, positioned to the right of the control button
|
|
*/
|
|
_showSublayerSelector(layerId) {
|
|
const selector = this.sublayerSelectors[layerId];
|
|
if (!selector) return;
|
|
|
|
const btn = this.buttons[layerId];
|
|
if (btn && this.map) {
|
|
const mapContainer = this.map.getContainer();
|
|
const mapRect = mapContainer.getBoundingClientRect();
|
|
const btnRect = btn.getBoundingClientRect();
|
|
|
|
// If the button is on the right half of the map, open the panel to the left
|
|
const onRightSide = (btnRect.left - mapRect.left) > mapRect.width / 2;
|
|
if (onRightSide) {
|
|
selector.style.left = '';
|
|
selector.style.right = (mapRect.right - btnRect.left + 4) + 'px';
|
|
} else {
|
|
selector.style.right = '';
|
|
selector.style.left = (btnRect.right - mapRect.left + 4) + 'px';
|
|
}
|
|
selector.style.top = (btnRect.top - mapRect.top) + 'px';
|
|
}
|
|
|
|
selector.classList.add('visible');
|
|
}
|
|
|
|
/**
|
|
* Hide sublayer selector
|
|
*/
|
|
_hideSublayerSelector(layerId) {
|
|
const selector = this.sublayerSelectors[layerId];
|
|
if (selector) {
|
|
selector.classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select a sublayer
|
|
*/
|
|
_selectSublayer(parentLayerId, sublayerId) {
|
|
const layerConfig = this.layers.find(l => l.id === parentLayerId);
|
|
if (!layerConfig || !layerConfig.sublayers) return;
|
|
|
|
const sublayer = layerConfig.sublayers.find(s => s.id === sublayerId);
|
|
if (!sublayer) return;
|
|
|
|
const state = this.layerStates.get(parentLayerId);
|
|
if (!state) return;
|
|
|
|
// Store current active sublayer
|
|
this.activeSublayers.set(parentLayerId, sublayerId);
|
|
|
|
// Update selector button states
|
|
const selector = this.sublayerSelectors[parentLayerId];
|
|
if (selector) {
|
|
selector.querySelectorAll('.layer-switcher-sublayer-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.sublayerId === sublayerId);
|
|
});
|
|
}
|
|
|
|
// If layer is visible, switch the source
|
|
if (state.visible) {
|
|
const mapLayerId = `layer-switcher-${parentLayerId}`;
|
|
|
|
// Remove old layer if exists
|
|
if (this.map.getLayer(mapLayerId)) {
|
|
this.map.removeLayer(mapLayerId);
|
|
}
|
|
|
|
// Add new source if needed
|
|
const sourceId = `layer-switcher-source-${parentLayerId}`;
|
|
if (this.map.getSource(sourceId)) {
|
|
this.map.removeSource(sourceId);
|
|
}
|
|
|
|
this.map.addSource(sourceId, sublayer.source);
|
|
|
|
// Determine where to insert the layer
|
|
let beforeLayer = layerConfig.beforeLayer;
|
|
if (!beforeLayer) {
|
|
const layers = this.map.getStyle().layers;
|
|
for (const layer of layers) {
|
|
if (layer.id.startsWith('gl-draw-') ||
|
|
layer.id.startsWith('custom-draw-') ||
|
|
layer.id.startsWith('polygon-') ||
|
|
layer.id === 'green-surfaces') {
|
|
beforeLayer = layer.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add layer with sublayer configuration
|
|
const mapLayerConfig = {
|
|
id: mapLayerId,
|
|
source: sourceId,
|
|
...(sublayer.layer || layerConfig.layer)
|
|
};
|
|
|
|
this.map.addLayer(mapLayerConfig, beforeLayer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize layers (add sources)
|
|
*/
|
|
_initializeLayers() {
|
|
this.layers.forEach(layerConfig => {
|
|
// Add source if not exists
|
|
const sourceId = `layer-switcher-source-${layerConfig.id}`;
|
|
if (!this.map.getSource(sourceId) && layerConfig.source) {
|
|
this.map.addSource(sourceId, layerConfig.source);
|
|
|
|
const state = this.layerStates.get(layerConfig.id);
|
|
if (state) state.sourceAdded = true;
|
|
}
|
|
|
|
// If layer should be visible initially, add it
|
|
if (layerConfig.visible) {
|
|
this._addLayer(layerConfig);
|
|
this._updateButtonState(layerConfig.id, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a layer to the map
|
|
*/
|
|
_addLayer(layerConfig) {
|
|
const layerId = `layer-switcher-${layerConfig.id}`;
|
|
const sourceId = `layer-switcher-source-${layerConfig.id}`;
|
|
|
|
if (this.map.getLayer(layerId)) return;
|
|
|
|
// For layers with sublayers, use the active sublayer's source
|
|
let sourceConfig = layerConfig.source;
|
|
let layerStyle = layerConfig.layer;
|
|
|
|
if (layerConfig.sublayers && layerConfig.sublayers.length > 0) {
|
|
const activeSublayerId = this.activeSublayers.get(layerConfig.id);
|
|
const activeSublayer = layerConfig.sublayers.find(s => s.id === activeSublayerId)
|
|
|| layerConfig.sublayers[0];
|
|
sourceConfig = activeSublayer.source;
|
|
layerStyle = activeSublayer.layer || layerConfig.layer;
|
|
}
|
|
|
|
// Ensure source exists before adding layer
|
|
if (!this.map.getSource(sourceId) && sourceConfig) {
|
|
this.map.addSource(sourceId, sourceConfig);
|
|
const state = this.layerStates.get(layerConfig.id);
|
|
if (state) state.sourceAdded = true;
|
|
}
|
|
|
|
// Determine where to insert the layer
|
|
let beforeLayer = layerConfig.beforeLayer;
|
|
|
|
// If no beforeLayer specified, try to insert before draw layers
|
|
if (!beforeLayer) {
|
|
const layers = this.map.getStyle().layers;
|
|
for (const layer of layers) {
|
|
if (layer.id.startsWith('gl-draw-') ||
|
|
layer.id.startsWith('custom-draw-') ||
|
|
layer.id.startsWith('polygon-') ||
|
|
layer.id === 'green-surfaces') {
|
|
beforeLayer = layer.id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build layer config
|
|
const mapLayerConfig = {
|
|
id: layerId,
|
|
source: sourceId,
|
|
...layerStyle
|
|
};
|
|
|
|
this.map.addLayer(mapLayerConfig, beforeLayer);
|
|
|
|
const state = this.layerStates.get(layerConfig.id);
|
|
if (state) {
|
|
state.layerAdded = true;
|
|
state.visible = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle layer visibility
|
|
*/
|
|
toggleLayer(layerId) {
|
|
const layerConfig = this.layers.find(l => l.id === layerId);
|
|
if (!layerConfig) return;
|
|
|
|
const state = this.layerStates.get(layerId);
|
|
if (!state) return;
|
|
|
|
// Check if there's a custom toggle callback
|
|
if (layerConfig.onToggle && typeof layerConfig.onToggle === 'function') {
|
|
// Use custom callback instead of default behavior
|
|
layerConfig.onToggle();
|
|
// Toggle the state manually
|
|
state.visible = !state.visible;
|
|
this._updateButtonState(layerId, state.visible);
|
|
return;
|
|
}
|
|
|
|
// Check if this is an existing layer configuration
|
|
if (layerConfig.existingLayerIds && layerConfig.existingLayerIds.length > 0) {
|
|
// Toggle existing layers
|
|
const newVisibility = state.visible ? 'none' : 'visible';
|
|
layerConfig.existingLayerIds.forEach(existingLayerId => {
|
|
if (this.map.getLayer(existingLayerId)) {
|
|
this.map.setLayoutProperty(existingLayerId, 'visibility', newVisibility);
|
|
}
|
|
});
|
|
state.visible = !state.visible;
|
|
} else {
|
|
// Handle new layers created by this control
|
|
const mapLayerId = `layer-switcher-${layerId}`;
|
|
|
|
if (state.visible) {
|
|
// Hide layer
|
|
if (this.map.getLayer(mapLayerId)) {
|
|
this.map.setLayoutProperty(mapLayerId, 'visibility', 'none');
|
|
}
|
|
state.visible = false;
|
|
|
|
// Hide sublayer selector if exists
|
|
if (layerConfig.sublayers && layerConfig.sublayers.length > 0) {
|
|
this._hideSublayerSelector(layerId);
|
|
}
|
|
} else {
|
|
// Handle exclusive layers
|
|
if (layerConfig.exclusive && layerConfig.group) {
|
|
this._hideGroupLayers(layerConfig.group, layerId);
|
|
}
|
|
|
|
// Show layer
|
|
if (this.map.getLayer(mapLayerId)) {
|
|
this.map.setLayoutProperty(mapLayerId, 'visibility', 'visible');
|
|
} else {
|
|
this._addLayer(layerConfig);
|
|
}
|
|
state.visible = true;
|
|
|
|
// Show sublayer selector if exists
|
|
if (layerConfig.sublayers && layerConfig.sublayers.length > 0) {
|
|
this._showSublayerSelector(layerId);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._updateButtonState(layerId, state.visible);
|
|
}
|
|
|
|
/**
|
|
* Hide all layers in a group except the specified one
|
|
*/
|
|
_hideGroupLayers(group, exceptLayerId) {
|
|
this.layers.forEach(layerConfig => {
|
|
if (layerConfig.group === group && layerConfig.id !== exceptLayerId) {
|
|
const mapLayerId = `layer-switcher-${layerConfig.id}`;
|
|
const state = this.layerStates.get(layerConfig.id);
|
|
|
|
if (state && state.visible && this.map.getLayer(mapLayerId)) {
|
|
this.map.setLayoutProperty(mapLayerId, 'visibility', 'none');
|
|
state.visible = false;
|
|
this._updateButtonState(layerConfig.id, false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update button visual state
|
|
*/
|
|
_updateButtonState(layerId, isActive) {
|
|
const button = this.buttons[layerId];
|
|
if (button) {
|
|
button.classList.toggle('active', isActive);
|
|
|
|
const layerConfig = this.layers.find(l => l.id === layerId);
|
|
if (layerConfig) {
|
|
button.title = isActive
|
|
? `Masquer ${layerConfig.name || layerConfig.id}`
|
|
: `Afficher ${layerConfig.name || layerConfig.id}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Programmatically show a layer
|
|
*/
|
|
showLayer(layerId) {
|
|
const state = this.layerStates.get(layerId);
|
|
if (state && !state.visible) {
|
|
this.toggleLayer(layerId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Programmatically hide a layer
|
|
*/
|
|
hideLayer(layerId) {
|
|
const state = this.layerStates.get(layerId);
|
|
if (state && state.visible) {
|
|
this.toggleLayer(layerId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a layer is visible
|
|
*/
|
|
isLayerVisible(layerId) {
|
|
const state = this.layerStates.get(layerId);
|
|
return state ? state.visible : false;
|
|
}
|
|
}
|
|
|
|
// Export to global scope
|
|
window.LayerSwitcherControl = LayerSwitcherControl;
|
|
|
|
})(window);
|