+
+ id="dxf-detail-layer-${layer.name}" data-layer="${layer.name}" ${autoCheckAll ? 'checked' : ''} style="cursor: pointer; width: 28px; height: 14px;">
`;
container.appendChild(div);
-
+
const toggle = div.querySelector('.dxf-vector-layer-toggle');
toggle.addEventListener('change', () => {
applyDxfVectorFilters();
- updateButtonActiveState();
+ updatePlansActiveState();
});
});
+
+ const totalCountEl = document.getElementById('dxf-total-layers-count');
+ if (totalCountEl) totalCountEl.textContent = layers.length;
+
+ updatePlansActiveState();
}
-if (dxfFileSelect) {
- dxfFileSelect.addEventListener('change', () => {
- fetchDetailLayers();
+function updatePlansActiveState() {
+ let activeImages = 0;
+ document.querySelectorAll('.image-plan-toggle').forEach(cb => {
+ if (cb.checked) activeImages++;
});
-
- // Auto-load if there is a default select value or single dxf
- if (dxfFileSelect.value) {
- fetchDetailLayers();
+
+ let activeDxfLayers = 0;
+ let totalDxfLayers = 0;
+ document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => {
+ totalDxfLayers++;
+ if (cb.checked) activeDxfLayers++;
+ });
+
+ const activeCount = activeImages + (activeDxfLayers > 0 ? 1 : 0);
+ const hasAnyActive = activeImages > 0 || activeDxfLayers > 0;
+
+ const countBadge = document.getElementById('plans-active-count-badge');
+ if (countBadge) {
+ if (hasAnyActive) {
+ countBadge.style.display = 'inline-block';
+ countBadge.textContent = `${activeCount} actif${activeCount > 1 ? 's' : ''}`;
+ } else {
+ countBadge.style.display = 'none';
+ }
+ }
+
+ const dxfActiveCountEl = document.getElementById('dxf-active-layers-count');
+ if (dxfActiveCountEl) dxfActiveCountEl.textContent = activeDxfLayers;
+
+ const dxfTotalCountEl = document.getElementById('dxf-total-layers-count');
+ if (dxfTotalCountEl) dxfTotalCountEl.textContent = totalDxfLayers;
+
+ const switchDxfMaster = document.getElementById('switch-dxf-master');
+ if (switchDxfMaster) {
+ switchDxfMaster.checked = activeDxfLayers > 0;
+ }
+
+ const planDxfCard = document.getElementById('plan-dxf-card');
+ if (planDxfCard) {
+ planDxfCard.classList.toggle('active', activeDxfLayers > 0);
+ }
+
+ const footerActions = document.getElementById('plans-footer-actions');
+ if (footerActions) {
+ footerActions.style.display = hasAnyActive ? 'block' : 'none';
+ }
+
+ const dxfToggleAllBtn = document.getElementById('dxf-detail-toggle-all-layers');
+ if (dxfToggleAllBtn && totalDxfLayers > 0) {
+ dxfToggleAllBtn.textContent = (activeDxfLayers === totalDxfLayers) ? "Décocher tout" : "Tout cocher";
+ }
+
+ if (layerSwitcher && typeof layerSwitcher.setOverlayActive === 'function') {
+ layerSwitcher.setOverlayActive(hasAnyActive);
}
}
-const btnToggleAllLayers = document.getElementById('dxf-detail-toggle-all-layers');
-if (btnToggleAllLayers) {
- btnToggleAllLayers.addEventListener('click', () => {
- const checkboxes = document.querySelectorAll('.dxf-vector-layer-toggle');
- const allChecked = Array.from(checkboxes).every(cb => cb.checked);
- checkboxes.forEach(cb => {
- cb.checked = !allChecked;
- });
- btnToggleAllLayers.textContent = allChecked ? "Tout cocher" : "Décocher tout";
- applyDxfVectorFilters();
- updateButtonActiveState();
- });
-}
+function renderCrossroadPlansSection(container, control) {
+ const importDxfUrl = crossroadPlansConfig && crossroadPlansConfig.importDxfUrl;
+ const hasImages = georeferencedDocuments && georeferencedDocuments.length > 0;
+ const hasDxf = dxfFiles && dxfFiles.length > 0;
-const btnDisableAllPlans = document.getElementById('btn-disable-all-plans');
-if (btnDisableAllPlans) {
- btnDisableAllPlans.addEventListener('click', () => {
+ // 1. Header with title & active badge
+ const headerEl = document.createElement('div');
+ headerEl.className = 'layer-switcher-subtitle';
+ headerEl.innerHTML = `
+
+
+ Plans du carrefour
+
+
0 actif
+ `;
+ container.appendChild(headerEl);
+
+ // Empty state if neither image nor DXF
+ if (!hasImages && !hasDxf) {
+ const emptyEl = document.createElement('div');
+ emptyEl.className = 'text-center py-2 px-1';
+ emptyEl.innerHTML = `
+
Aucun plan géoréférencé
+ ${importDxfUrl ? `
Importer un plan DXF` : ''}
+ `;
+ container.appendChild(emptyEl);
+ return;
+ }
+
+ // 2. Image Plans Sub-section
+ if (hasImages) {
+ const imgTitleEl = document.createElement('div');
+ imgTitleEl.className = 'layer-switcher-subtitle text-muted';
+ imgTitleEl.style.fontSize = '9px';
+ imgTitleEl.innerHTML = `
Plans images (${georeferencedDocuments.length})`;
+ container.appendChild(imgTitleEl);
+
+ georeferencedDocuments.forEach(doc => {
+ const card = document.createElement('div');
+ card.className = 'layer-switcher-card';
+ card.id = `plan-img-card-${doc.id}`;
+ card.innerHTML = `
+
+
+
+
+
+
+
+
+
+ 95%
+
+ `;
+ container.appendChild(card);
+
+ const toggle = card.querySelector('.image-plan-toggle');
+ const opacityRow = card.querySelector('.layer-switcher-slider-row');
+ const slider = card.querySelector('.image-plan-opacity-slider');
+ const opacityVal = card.querySelector('.opacity-val');
+
+ toggle.addEventListener('change', () => {
+ const active = toggle.checked;
+ card.classList.toggle('active', active);
+ opacityRow.style.display = active ? 'flex' : 'none';
+ const opacity = parseFloat(slider.value) / 100.0;
+ showGeorefImage(doc, active, opacity);
+ updatePlansActiveState();
+ });
+
+ slider.addEventListener('input', () => {
+ const val = slider.value;
+ opacityVal.textContent = val + '%';
+ const opacity = parseFloat(val) / 100.0;
+ const lyrId = `georef-image-overlay-layer-${doc.id}`;
+ if (map.getLayer(lyrId)) {
+ map.setPaintProperty(lyrId, 'raster-opacity', opacity);
+ }
+ });
+ });
+ }
+
+ // 3. DXF Vector Plan Sub-section
+ if (hasDxf) {
+ const dxfTitleEl = document.createElement('div');
+ dxfTitleEl.className = 'layer-switcher-subtitle text-muted mt-1';
+ dxfTitleEl.style.fontSize = '9px';
+ dxfTitleEl.innerHTML = `
Plan CAO (DXF)`;
+ container.appendChild(dxfTitleEl);
+
+ const dxfCard = document.createElement('div');
+ dxfCard.className = 'layer-switcher-card';
+ dxfCard.id = 'plan-dxf-card';
+
+ let dxfSelectHtml = '';
+ if (dxfFiles.length === 1) {
+ dxfSelectHtml = `
+
+
+
+ ${dxfFiles[0]}
+
+
+
+
+
+ `;
+ } else {
+ dxfSelectHtml = `
+
+
+
+
+
+
+ `;
+ }
+
+ dxfCard.innerHTML = `
+ ${dxfSelectHtml}
+
+
+
+
+
+
+
+
+
+
+ `;
+ container.appendChild(dxfCard);
+
+ // Wire up DXF select and master toggle
+ const dxfSelectEl = dxfCard.querySelector('#select-dxf-file');
+ const dxfMasterSwitch = dxfCard.querySelector('#switch-dxf-master');
+ const accordionBtn = dxfCard.querySelector('#btn-toggle-dxf-accordion');
+ const accordionChevron = dxfCard.querySelector('#dxf-accordion-chevron');
+ const layersListEl = dxfCard.querySelector('#dxf-detail-layers-list');
+ const layersAccordion = dxfCard.querySelector('#dxf-layers-accordion');
+ const toggleAllBtn = dxfCard.querySelector('#dxf-detail-toggle-all-layers');
+
+ let isAccordionExpanded = true;
+
+ if (accordionBtn) {
+ accordionBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ isAccordionExpanded = !isAccordionExpanded;
+ layersListEl.style.display = isAccordionExpanded ? 'flex' : 'none';
+ if (toggleAllBtn) toggleAllBtn.style.display = isAccordionExpanded ? 'inline' : 'none';
+ accordionChevron.style.transform = isAccordionExpanded ? 'rotate(0deg)' : 'rotate(-90deg)';
+ });
+ }
+
+ if (dxfMasterSwitch) {
+ dxfMasterSwitch.addEventListener('change', (e) => {
+ e.stopPropagation();
+ const isMasterOn = dxfMasterSwitch.checked;
+ dxfCard.classList.toggle('active', isMasterOn);
+
+ const file = dxfSelectEl ? dxfSelectEl.value : '';
+ if (!file) {
+ dxfMasterSwitch.checked = false;
+ return;
+ }
+
+ if (!detailLayersFetched[file]) {
+ fetchDetailLayers(file, isMasterOn);
+ } else {
+ const checkboxes = dxfCard.querySelectorAll('.dxf-vector-layer-toggle');
+ checkboxes.forEach(cb => {
+ cb.checked = isMasterOn;
+ });
+ applyDxfVectorFilters();
+ updatePlansActiveState();
+ }
+
+ if (isMasterOn) {
+ layersAccordion.style.display = 'block';
+ }
+ });
+ }
+
+ if (dxfSelectEl && dxfSelectEl.tagName === 'SELECT') {
+ dxfSelectEl.addEventListener('change', (e) => {
+ e.stopPropagation();
+ const file = dxfSelectEl.value;
+ if (file) {
+ fetchDetailLayers(file, dxfMasterSwitch.checked);
+ } else {
+ layersAccordion.style.display = 'none';
+ if (dxfMasterSwitch) dxfMasterSwitch.checked = false;
+ applyDxfVectorFilters();
+ updatePlansActiveState();
+ }
+ });
+ }
+
+ if (toggleAllBtn) {
+ toggleAllBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const checkboxes = dxfCard.querySelectorAll('.dxf-vector-layer-toggle');
+ const allChecked = Array.from(checkboxes).every(cb => cb.checked);
+ checkboxes.forEach(cb => {
+ cb.checked = !allChecked;
+ });
+ toggleAllBtn.textContent = allChecked ? "Tout cocher" : "Décocher tout";
+ applyDxfVectorFilters();
+ updatePlansActiveState();
+ });
+ }
+
+ // If single DXF, prepare/fetch layers
+ if (dxfSelectEl && dxfSelectEl.value) {
+ fetchDetailLayers(dxfSelectEl.value, false);
+ }
+ }
+
+ // 4. Footer Actions (Désactiver tous les plans)
+ const footerEl = document.createElement('div');
+ footerEl.id = 'plans-footer-actions';
+ footerEl.style.display = 'none';
+ footerEl.style.marginTop = '6px';
+ footerEl.innerHTML = `
+
+ `;
+ container.appendChild(footerEl);
+
+ const btnDisableAll = footerEl.querySelector('#btn-disable-all-plans');
+ btnDisableAll.addEventListener('click', (e) => {
+ e.stopPropagation();
+ // Turn off image plans
document.querySelectorAll('.image-plan-toggle').forEach(cb => {
if (cb.checked) {
cb.checked = false;
cb.dispatchEvent(new Event('change'));
}
});
-
+
+ // Turn off DXF layers
document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => {
- if (cb.checked) {
- cb.checked = false;
- }
+ cb.checked = false;
});
+ const switchMaster = document.getElementById('switch-dxf-master');
+ if (switchMaster) switchMaster.checked = false;
+ const dxfCard = document.getElementById('plan-dxf-card');
+ if (dxfCard) dxfCard.classList.remove('active');
+
applyDxfVectorFilters();
-
- const modalEl = document.getElementById('georefPlanModal');
- if (modalEl) {
- const modal = bootstrap.Modal.getInstance(modalEl);
- if (modal) modal.hide();
+ updatePlansActiveState();
+ });
+}
+
+// Register section with LayerSwitcherControl
+if (layerSwitcher) {
+ layerSwitcher.addCustomSection({
+ id: 'crossroad-plans',
+ render: (container, control) => {
+ renderCrossroadPlansSection(container, control);
}
-
- updateButtonActiveState();
});
}
\ No newline at end of file
diff --git a/loko/assets/templates/assets/trafficlights_intersection_detail.html b/loko/assets/templates/assets/trafficlights_intersection_detail.html
index d4924cd..3be8be6 100644
--- a/loko/assets/templates/assets/trafficlights_intersection_detail.html
+++ b/loko/assets/templates/assets/trafficlights_intersection_detail.html
@@ -241,6 +241,12 @@
+ {{ dxf_files|json_script:"dxfFilesData" }}
+
@@ -1108,107 +1114,6 @@
{% endif %}
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% translate "Aucun plan d'image géoréférencé disponible" %}
-
{% translate "Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la fiche." %}
-
-
-
-
-
-
-
- {% if not dxf_files %}
-
-
-
{% translate "Aucun fichier DXF disponible pour ce carrefour" %}
-
{% translate "Veuillez d'abord en importer un." %}
-
- {% translate "Importer un plan DXF" %}
-
-
- {% else %}
-
-
-
- {% if dxf_files|length == 1 %}
-
- {{ dxf_files.0 }}
-
-
- {% else %}
-
- {% endif %}
-
-
-
-
-
-
-
-
{% translate "Lecture des couches du plan..." %}
-
-
-
-
-
- {% endif %}
-
-
-
-
-
-
-
-
-
{% endblock content%}
diff --git a/loko/common/static/common/layer-switcher-control.js b/loko/common/static/common/layer-switcher-control.js
index 9f5f219..5e37a1d 100644
--- a/loko/common/static/common/layer-switcher-control.js
+++ b/loko/common/static/common/layer-switcher-control.js
@@ -73,14 +73,42 @@
padding: 8px;
z-index: 9999;
display: none;
- min-width: 260px;
- max-width: 320px;
+ min-width: 280px;
+ max-width: 340px;
+ max-height: min(85vh, 620px);
+ overflow-y: auto;
+ overflow-x: hidden;
+ scrollbar-width: thin;
+ scrollbar-color: #cbd5e1 transparent;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #1e293b;
user-select: none;
animation: layerSwitcherFadeIn 0.18s ease-out;
}
+ .layer-switcher-popover::-webkit-scrollbar {
+ width: 5px;
+ }
+ .layer-switcher-popover::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ .layer-switcher-popover::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 3px;
+ }
+
+ .layer-switcher-dot {
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ width: 7px;
+ height: 7px;
+ background-color: #2563eb;
+ border-radius: 50%;
+ border: 1.5px solid #ffffff;
+ pointer-events: none;
+ }
+
@keyframes layerSwitcherFadeIn {
from { opacity: 0; transform: scale(0.96) translateX(4px); }
to { opacity: 1; transform: scale(1) translateX(0); }
@@ -215,12 +243,136 @@
.badge-region-bru { background-color: #dbeafe; color: #2563eb; border: 1px solid #bfdbfe; }
.badge-region-vlg { background-color: #fef3c7; color: #d97706; border: 1px solid #fde68a; }
.badge-region-all { background-color: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }
+
+ /* Custom Sections & Plan Elements */
+ .layer-switcher-divider {
+ height: 1px;
+ background-color: #e2e8f0;
+ margin: 8px 0 6px 0;
+ }
+
+ .layer-switcher-custom-section {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .layer-switcher-subtitle {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: #64748b;
+ padding: 4px 6px 2px 6px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .layer-switcher-card {
+ background-color: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ padding: 7px 9px;
+ margin-bottom: 3px;
+ transition: all 0.15s ease;
+ }
+
+ .layer-switcher-card:hover {
+ border-color: #cbd5e1;
+ background-color: #f1f5f9;
+ }
+
+ .layer-switcher-card.active {
+ border-color: #bfdbfe;
+ background-color: #f0f7ff;
+ }
+
+ .layer-switcher-slider-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 5px;
+ padding-top: 4px;
+ border-top: 1px dashed #e2e8f0;
+ }
+
+ .layer-switcher-slider-row label {
+ font-size: 10px;
+ font-weight: 600;
+ color: #64748b;
+ margin: 0;
+ white-space: nowrap;
+ }
+
+ .layer-switcher-slider-row input[type="range"] {
+ flex-grow: 1;
+ height: 4px;
+ accent-color: #2563eb;
+ cursor: pointer;
+ }
+
+ .layer-switcher-slider-val {
+ font-size: 10.5px;
+ font-weight: 700;
+ color: #2563eb;
+ min-width: 28px;
+ text-align: right;
+ }
+
+ .layer-switcher-btn-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ width: 100%;
+ padding: 5px 8px;
+ border-radius: 5px;
+ font-size: 11px;
+ font-weight: 600;
+ border: 1px solid #e2e8f0;
+ background-color: #ffffff;
+ color: #475569;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ text-decoration: none;
+ }
+
+ .layer-switcher-btn-action:hover {
+ background-color: #f8fafc;
+ border-color: #cbd5e1;
+ color: #0f172a;
+ }
+
+ .layer-switcher-btn-action.btn-danger-light {
+ color: #dc2626;
+ border-color: #fecaca;
+ background-color: #fef2f2;
+ }
+
+ .layer-switcher-btn-action.btn-danger-light:hover {
+ background-color: #fee2e2;
+ border-color: #fca5a5;
+ }
+
+ .layer-switcher-collapse-content {
+ display: none;
+ margin-top: 6px;
+ border-top: 1px solid #e2e8f0;
+ padding-top: 6px;
+ }
+
+ .layer-switcher-collapse-content.expanded {
+ display: block;
+ }
`;
class LayerSwitcherControl {
constructor(options = {}) {
this.options = options;
this.layers = options.layers || [];
+ this.customSections = options.customSections || [];
+ this.hasActiveOverlays = false;
this.map = null;
this.container = null;
this.popover = null;
@@ -381,9 +533,70 @@
});
}
+ // Render custom sections if configured
+ if (this.customSections && this.customSections.length > 0) {
+ this.customSections.forEach(section => {
+ this._renderCustomSection(section, popover);
+ });
+ }
+
return popover;
}
+ _renderCustomSection(section, popoverEl = this.popover) {
+ if (!popoverEl) return;
+
+ const divider = document.createElement('div');
+ divider.className = 'layer-switcher-divider';
+ popoverEl.appendChild(divider);
+
+ if (section.title) {
+ const sectionTitle = document.createElement('div');
+ sectionTitle.className = 'layer-switcher-section-title';
+ sectionTitle.innerHTML = section.title;
+ popoverEl.appendChild(sectionTitle);
+ }
+
+ const container = document.createElement('div');
+ container.className = 'layer-switcher-custom-section';
+ if (section.id) container.id = `layer-switcher-section-${section.id}`;
+ popoverEl.appendChild(container);
+
+ if (typeof section.render === 'function') {
+ section.render(container, this);
+ }
+ }
+
+ addCustomSection(section) {
+ this.customSections.push(section);
+ if (this.popover) {
+ this._renderCustomSection(section, this.popover);
+ }
+ }
+
+ setOverlayActive(active) {
+ this.hasActiveOverlays = !!active;
+ this._updateButtonState();
+ }
+
+ _updateButtonState() {
+ if (!this.button) return;
+ const isBasemapActive = this.activeBasemapId && this.activeBasemapId !== 'default';
+ const isActive = isBasemapActive || this.hasActiveOverlays;
+ this.button.classList.toggle('active', isActive);
+
+ let dot = this.button.querySelector('.layer-switcher-dot');
+ if (this.hasActiveOverlays) {
+ if (!dot) {
+ dot = document.createElement('span');
+ dot.className = 'layer-switcher-dot';
+ this.button.appendChild(dot);
+ }
+ } else if (dot) {
+ dot.remove();
+ }
+ }
+
_createBasemapItem(bg, iconSvg) {
const item = document.createElement('div');
item.className = 'layer-switcher-item' + (this.activeBasemapId === bg.id ? ' active' : '');
@@ -458,9 +671,7 @@
}
// Update button active style
- if (this.button) {
- this.button.classList.toggle('active', id !== 'default');
- }
+ this._updateButtonState();
// Apply to map
if (id === 'default') {
@@ -478,7 +689,7 @@
const bgConfig = this.basemaps.find(b => b.id === this.activeBasemapId);
if (bgConfig) {
this._applyRasterBasemap(bgConfig);
- if (this.button) this.button.classList.add('active');
+ this._updateButtonState();
return;
}
}
@@ -487,7 +698,7 @@
if (defaultBg) {
this.selectBasemap(defaultBg.id);
} else {
- if (this.button) this.button.classList.remove('active');
+ this._updateButtonState();
}
}