refactor: remove georeferenced plans modal and custom map control from traffic lights import cross plan template
This commit is contained in:
parent
e30faa6ad6
commit
e591b735f3
2 changed files with 412 additions and 284 deletions
|
|
@ -899,103 +899,6 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Georeferenced Plans Modal -->
|
||||
<div class="modal fade" id="georefPlanModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded-3 border-0 shadow-lg">
|
||||
<div class="modal-header bg-light border-bottom-0 py-3">
|
||||
<h5 class="modal-title font-weight-bold text-dark"><i class="bi bi-geo-alt-fill text-primary me-2"></i>{% translate "Plans géoréférencés" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body py-3">
|
||||
<!-- Nav Tabs -->
|
||||
<ul class="nav nav-tabs nav-fill mb-3" id="georefPlanTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active font-weight-bold" id="tab-image-plan" data-bs-toggle="tab" data-bs-target="#panel-image-plan" type="button" role="tab" aria-controls="panel-image-plan" aria-selected="true">
|
||||
<i class="bi bi-file-earmark-image me-1"></i>{% translate "Plan image" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link font-weight-bold" id="tab-vector-plan" data-bs-toggle="tab" data-bs-target="#panel-vector-plan" type="button" role="tab" aria-controls="panel-vector-plan" aria-selected="false">
|
||||
<i class="bi bi-file-earmark-code me-1"></i>{% translate "Plan CAO" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content pt-2" id="georefPlanTabContent">
|
||||
<!-- Tab 1: Image Plan -->
|
||||
<div class="tab-pane fade show active" id="panel-image-plan" role="tabpanel" aria-labelledby="tab-image-plan">
|
||||
<div id="image-plan-selection-container">
|
||||
<div id="image-plans-list" class="d-flex flex-column gap-3 mb-2">
|
||||
<!-- Dynamically populated list of images with switches and sliders -->
|
||||
</div>
|
||||
<div id="no-georef-images-alert" class="text-center py-4" style="display: none;">
|
||||
<div class="text-muted mb-3"><i class="bi bi-info-circle" style="font-size: 2.5rem;"></i></div>
|
||||
<p class="text-dark font-weight-semibold mb-1">{% translate "Aucun plan d'image géoréférencé disponible" %}</p>
|
||||
<p class="text-muted small">{% translate "Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la fiche." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Vector Plan (DXF) -->
|
||||
<div class="tab-pane fade" id="panel-vector-plan" role="tabpanel" aria-labelledby="tab-vector-plan">
|
||||
<div id="dxf-selection-container">
|
||||
{% if not dxf_files %}
|
||||
<div class="text-center py-4">
|
||||
<div class="text-muted mb-3"><i class="bi bi-exclamation-triangle" style="font-size: 2.5rem;"></i></div>
|
||||
<p class="text-dark font-weight-semibold mb-1">{% translate "Aucun fichier DXF disponible pour ce carrefour" %}</p>
|
||||
<p class="text-muted small">{% translate "Veuillez d'abord en importer un." %}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- File Selection -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label font-weight-semibold text-muted small uppercase">{% translate "Sélectionner le plan DXF" %}</label>
|
||||
{% if dxf_files|length == 1 %}
|
||||
<div class="p-3 bg-light border rounded-3 font-weight-semibold text-dark mb-3">
|
||||
<i class="bi bi-file-earmark-check-fill text-success me-2"></i>{{ dxf_files.0 }}
|
||||
<input type="hidden" id="select-dxf-file" value="{{ dxf_files.0 }}">
|
||||
</div>
|
||||
{% else %}
|
||||
<select id="select-dxf-file" class="form-select rounded-3 p-3 mb-3">
|
||||
<option value="">{% translate "-- Sélectionner un plan CAO (DXF) --" %}</option>
|
||||
{% for f in dxf_files %}
|
||||
<option value="{{ f }}">{{ f }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Layers selection checklist -->
|
||||
<div id="dxf-detail-layers-section" style="display: none;" class="mb-3">
|
||||
<label class="form-label font-weight-semibold text-muted small uppercase d-flex justify-content-between align-items-center mb-2">
|
||||
<span>{% translate "Couches vectorielles CAO" %}</span>
|
||||
<button type="button" id="dxf-detail-toggle-all-layers" class="btn btn-link btn-sm p-0 text-decoration-none" style="font-size: 0.75rem;">
|
||||
{% translate "Décocher tout" %}
|
||||
</button>
|
||||
</label>
|
||||
<div id="dxf-detail-layers-spinner" class="text-center py-4" style="display: none;">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
<div class="text-muted small mt-2">{% translate "Lecture des couches du plan..." %}</div>
|
||||
</div>
|
||||
<div id="dxf-detail-layers-list" class="border rounded-3 p-3 bg-white" style="max-height: 250px; overflow-y: auto;">
|
||||
<!-- Checkboxes will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-top-0 pt-0 pb-4 justify-content-between px-4">
|
||||
<button type="button" id="btn-disable-all-plans" class="btn btn-outline-danger px-3 rounded-3">
|
||||
<i class="bi bi-eye-slash-fill me-1"></i>{% translate "Désactiver tout" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary px-4 rounded-3 font-weight-semibold shadow-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
@ -1005,8 +908,15 @@
|
|||
{{ intersection.lon|json_script:"intersection_lon" }}
|
||||
{{ intersection.lat|json_script:"intersection_lat" }}
|
||||
{{ dxf_poles|json_script:"dxf_poles" }}
|
||||
{{ dxf_files|json_script:"dxf_files" }}
|
||||
{{ georeferenced_documents_json|json_script:"georeferencedDocumentsData" }}
|
||||
<script type="application/json" id="georeferencedDocumentsData">
|
||||
{{ georeferenced_documents_json|safe }}
|
||||
</script>
|
||||
{{ dxf_files|json_script:"dxfFilesData" }}
|
||||
<script type="application/json" id="crossroadPlansConfig">
|
||||
{
|
||||
"importDxfUrl": "{% url 'assets:import_dxf' intersection.id %}"
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
|
|
@ -1273,6 +1183,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
let activeEditCable = null;
|
||||
let cableHandles = [];
|
||||
const cableGeometries = {};
|
||||
const placedPoles = {};
|
||||
let layerSwitcher = null;
|
||||
|
||||
function markCableAsModified(code) {
|
||||
const selectAction = document.querySelector(`select[name="action_cable_${code}"]`);
|
||||
|
|
@ -1304,38 +1216,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
map.addControl(searchCtrl, 'top-left');
|
||||
}
|
||||
|
||||
// Custom Plans Control in Top Right (first, at the top)
|
||||
class CustomPlansControl {
|
||||
onAdd(map) {
|
||||
this._container = document.createElement('div');
|
||||
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.id = 'btn-load-georef-plan';
|
||||
button.className = 'btn btn-xs btn-light bg-white border shadow-sm d-flex align-items-center gap-1';
|
||||
button.style.border = 'none';
|
||||
button.style.padding = '8px 12px';
|
||||
button.style.height = 'auto';
|
||||
button.style.width = 'auto';
|
||||
button.style.fontWeight = 'bold';
|
||||
button.style.fontSize = '0.75rem';
|
||||
button.innerHTML = '<i class="bi bi-geo-alt-fill text-primary"></i> Plans';
|
||||
|
||||
// Toggle bootstrap modal natively
|
||||
button.setAttribute('data-bs-toggle', 'modal');
|
||||
button.setAttribute('data-bs-target', '#georefPlanModal');
|
||||
|
||||
this._container.appendChild(button);
|
||||
return this._container;
|
||||
}
|
||||
onRemove() {
|
||||
this._container.parentNode.removeChild(this._container);
|
||||
this._map = undefined;
|
||||
}
|
||||
}
|
||||
map.addControl(new CustomPlansControl(), 'top-right');
|
||||
|
||||
// ── Helper to determine where to place the orthophoto layer ──
|
||||
function getOrthoBeforeLayer(m) {
|
||||
if (!m || !m.getStyle) return undefined;
|
||||
|
|
@ -1413,9 +1293,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Contrôle photo aérienne (LayerSwitcherControl) --- (second, below Plans button)
|
||||
// --- Contrôle photo aérienne (LayerSwitcherControl) ---
|
||||
if (typeof LayerSwitcherControl !== 'undefined') {
|
||||
const layerSwitcher = new LayerSwitcherControl({
|
||||
layerSwitcher = new LayerSwitcherControl({
|
||||
layers: [
|
||||
{
|
||||
id: 'ortho',
|
||||
|
|
@ -1478,12 +1358,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
type: 'circle',
|
||||
source: 'existing-poles',
|
||||
paint: {
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#6b7280',
|
||||
'circle-radius': 5,
|
||||
'circle-color': '#9ca3af',
|
||||
'circle-stroke-width': 1.5,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'existing-poles-labels',
|
||||
type: 'symbol',
|
||||
source: 'existing-poles',
|
||||
layout: {
|
||||
'text-field': ['coalesce', ['get', 'short_code'], ['get', 'code']],
|
||||
'text-font': ['Noto Sans Regular'],
|
||||
'text-size': 11,
|
||||
'text-anchor': 'top',
|
||||
'text-offset': [0, 0.6],
|
||||
'text-allow-overlap': true
|
||||
},
|
||||
paint: {
|
||||
'text-color': '#6b7280',
|
||||
'text-halo-color': 'rgba(255, 255, 255, 0.9)',
|
||||
'text-halo-width': 1.5
|
||||
}
|
||||
});
|
||||
|
||||
// Add DXF snapping points source and layer
|
||||
const dxfPolesFeatures = initialDxfPoles.map(dp => ({
|
||||
|
|
@ -2067,7 +1965,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
let georeferencedDocuments = [];
|
||||
if (georeferencedDocumentsDataEl) {
|
||||
try {
|
||||
const parsed = JSON.parse(georeferencedDocumentsDataEl.textContent);
|
||||
let parsed = JSON.parse(georeferencedDocumentsDataEl.textContent);
|
||||
if (typeof parsed === 'string') {
|
||||
parsed = JSON.parse(parsed);
|
||||
}
|
||||
if (Array.isArray(parsed)) {
|
||||
georeferencedDocuments = parsed;
|
||||
}
|
||||
|
|
@ -2076,7 +1977,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
|
||||
const dxfFilesDataEl = document.getElementById('dxfFilesData');
|
||||
let availableDxfFiles = [];
|
||||
if (dxfFilesDataEl) {
|
||||
try {
|
||||
availableDxfFiles = JSON.parse(dxfFilesDataEl.textContent) || [];
|
||||
} catch (e) {
|
||||
console.error("Error parsing dxf files data:", e);
|
||||
}
|
||||
}
|
||||
if (!availableDxfFiles.length && typeof window.dxfFiles !== 'undefined' && Array.isArray(window.dxfFiles)) {
|
||||
availableDxfFiles = window.dxfFiles;
|
||||
}
|
||||
|
||||
const crossroadPlansConfigEl = document.getElementById('crossroadPlansConfig');
|
||||
let crossroadPlansConfig = {};
|
||||
if (crossroadPlansConfigEl) {
|
||||
try {
|
||||
crossroadPlansConfig = JSON.parse(crossroadPlansConfigEl.textContent);
|
||||
} catch (e) {
|
||||
console.error("Error parsing crossroad plans config:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function showGeorefImage(doc, active, opacity) {
|
||||
const srcId = `georef-image-overlay-${doc.id}`;
|
||||
|
|
@ -2123,11 +2045,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
// Zoom/fitBounds to document
|
||||
const coords = doc.coords;
|
||||
const bounds = coords.reduce((acc, coord) => {
|
||||
return acc.extend(coord);
|
||||
}, new maplibregl.LngLatBounds(coords[0], coords[0]));
|
||||
map.fitBounds(bounds, { padding: 40, maxZoom: 19 });
|
||||
if (doc.coords && doc.coords.length > 0) {
|
||||
const coords = doc.coords;
|
||||
const bounds = coords.reduce((acc, coord) => {
|
||||
return acc.extend(coord);
|
||||
}, new maplibregl.LngLatBounds(coords[0], coords[0]));
|
||||
map.fitBounds(bounds, { padding: 40, maxZoom: 19 });
|
||||
}
|
||||
}
|
||||
|
||||
let fullDxfGeoJson = null;
|
||||
|
|
@ -2172,9 +2096,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (map.getSource('dxf-layout')) {
|
||||
map.getSource('dxf-layout').setData(filteredData);
|
||||
|
||||
const isVisible = selectedLayers.length > 0;
|
||||
['dxf-layout-fills', 'dxf-layout-lines', 'dxf-layout-symbols'].forEach(lId => {
|
||||
if (map.getLayer(lId)) {
|
||||
map.setLayoutProperty(lId, 'visibility', 'visible');
|
||||
map.setLayoutProperty(lId, 'visibility', isVisible ? 'visible' : 'none');
|
||||
}
|
||||
});
|
||||
reorderCrossPlanLayers();
|
||||
|
|
@ -2249,110 +2174,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
reorderCrossPlanLayers();
|
||||
}
|
||||
|
||||
function updateButtonActiveState() {
|
||||
const btn = document.getElementById('btn-load-georef-plan');
|
||||
if (!btn) return;
|
||||
|
||||
let anyImageActive = false;
|
||||
document.querySelectorAll('.image-plan-toggle').forEach(cb => {
|
||||
if (cb.checked) anyImageActive = true;
|
||||
});
|
||||
|
||||
let anyVectorActive = false;
|
||||
document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => {
|
||||
if (cb.checked) anyVectorActive = true;
|
||||
});
|
||||
|
||||
if (anyImageActive || anyVectorActive) {
|
||||
btn.classList.remove('btn-light', 'bg-white', 'border');
|
||||
btn.classList.add('btn-primary');
|
||||
} else {
|
||||
btn.classList.remove('btn-primary');
|
||||
btn.classList.add('btn-light', 'bg-white', 'border');
|
||||
}
|
||||
}
|
||||
|
||||
const noGeorefImagesAlert = document.getElementById('no-georef-images-alert');
|
||||
const imagePlansList = document.getElementById('image-plans-list');
|
||||
if (imagePlansList) {
|
||||
imagePlansList.innerHTML = '';
|
||||
if (georeferencedDocuments.length === 0) {
|
||||
if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'block';
|
||||
} else {
|
||||
if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'none';
|
||||
|
||||
georeferencedDocuments.forEach(doc => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'image-plan-item p-3 bg-light border rounded-3 d-flex flex-column gap-2';
|
||||
item.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-dark font-weight-bold" style="font-size: 0.9rem;">
|
||||
<i class="bi ${doc.is_dxf ? 'bi-file-earmark-code text-success' : 'bi-file-earmark-image text-primary'} me-2"></i>${doc.title}
|
||||
</span>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input image-plan-toggle" type="checkbox" role="switch" id="switch-georef-img-${doc.id}" data-id="${doc.id}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-plan-opacity-container" id="opacity-container-${doc.id}" style="display: none;">
|
||||
<label class="form-label font-weight-semibold text-muted small uppercase d-flex justify-content-between mb-1">
|
||||
<span>Opacité</span>
|
||||
<span class="opacity-val text-primary font-weight-bold">95%</span>
|
||||
</label>
|
||||
<input type="range" class="form-range image-plan-opacity-slider" min="0" max="100" value="95" id="range-georef-img-opacity-${doc.id}" data-id="${doc.id}">
|
||||
</div>
|
||||
`;
|
||||
imagePlansList.appendChild(item);
|
||||
|
||||
const toggle = item.querySelector('.image-plan-toggle');
|
||||
const opacityContainer = item.querySelector('.image-plan-opacity-container');
|
||||
const slider = item.querySelector('.image-plan-opacity-slider');
|
||||
const opacityVal = item.querySelector('.opacity-val');
|
||||
|
||||
toggle.addEventListener('change', () => {
|
||||
const active = toggle.checked;
|
||||
if (active) {
|
||||
opacityContainer.style.display = 'block';
|
||||
} else {
|
||||
opacityContainer.style.display = 'none';
|
||||
}
|
||||
const opacity = parseFloat(slider.value) / 100.0;
|
||||
showGeorefImage(doc, active, opacity);
|
||||
updateButtonActiveState();
|
||||
});
|
||||
|
||||
slider.addEventListener('input', () => {
|
||||
const val = slider.value;
|
||||
opacityVal.textContent = val + '%';
|
||||
const opacity = parseFloat(val) / 100.0;
|
||||
showGeorefImage(doc, toggle.checked, opacity);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dxfFileSelect = document.getElementById('select-dxf-file');
|
||||
let detailLayersFetched = {};
|
||||
let detailLayersResponseData = {};
|
||||
|
||||
function fetchDetailLayers() {
|
||||
if (!dxfFileSelect) return;
|
||||
const file = dxfFileSelect.value;
|
||||
if (!file) {
|
||||
const section = document.getElementById('dxf-detail-layers-section');
|
||||
if (section) section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
function fetchDetailLayers(file, autoCheckAll = false) {
|
||||
if (!file) return;
|
||||
|
||||
const layersAccordion = document.getElementById('dxf-layers-accordion');
|
||||
if (layersAccordion) layersAccordion.style.display = 'block';
|
||||
|
||||
if (detailLayersFetched[file]) {
|
||||
renderDetailLayers(file);
|
||||
renderDetailLayers(file, autoCheckAll);
|
||||
fetchDxfGeoJson(file);
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = document.getElementById('dxf-detail-layers-spinner');
|
||||
const list = document.getElementById('dxf-detail-layers-list');
|
||||
const section = document.getElementById('dxf-detail-layers-section');
|
||||
if (section) section.style.display = 'block';
|
||||
if (spinner) spinner.style.display = 'block';
|
||||
if (list) list.style.display = 'none';
|
||||
|
||||
|
|
@ -2361,102 +2199,391 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
.then(data => {
|
||||
detailLayersFetched[file] = data.layers || [];
|
||||
detailLayersResponseData[file] = data;
|
||||
renderDetailLayers(file);
|
||||
renderDetailLayers(file, autoCheckAll);
|
||||
fetchDxfGeoJson(file);
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
if (list) list.style.display = 'block';
|
||||
if (list) list.style.display = 'flex';
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Error fetching detail layers:", err);
|
||||
if (spinner) {
|
||||
spinner.innerHTML = `<div class="text-danger small"><i class="bi bi-exclamation-triangle-fill"></i> Erreur de chargement.</div>`;
|
||||
spinner.innerHTML = `<div class="text-danger" style="font-size: 10px;"><i class="bi bi-exclamation-triangle-fill"></i> Erreur de chargement.</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderDetailLayers(file) {
|
||||
function renderDetailLayers(file, autoCheckAll = false) {
|
||||
const container = document.getElementById('dxf-detail-layers-list');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
const layers = detailLayersFetched[file] || [];
|
||||
const responseData = detailLayersResponseData[file] || {};
|
||||
const selectedLayers = responseData.selected_layers || [];
|
||||
|
||||
layers.forEach(layer => {
|
||||
const isChecked = selectedLayers.includes(layer.name) || selectedLayers.length === 0;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'form-check py-1 d-flex align-items-center justify-content-between';
|
||||
div.className = 'd-flex align-items-center justify-content-between py-1 px-1 rounded';
|
||||
div.style.fontSize = '11px';
|
||||
div.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="color-indicator me-2" style="display: inline-block; width: 14px; height: 14px; border-radius: 3px; background-color: ${layer.color}; border: 1px solid rgba(0,0,0,0.1);"></span>
|
||||
<label class="form-check-label text-dark font-weight-medium small" for="dxf-detail-layer-${layer.name}">
|
||||
<div class="d-flex align-items-center gap-1 text-truncate" style="max-width: 200px;">
|
||||
<span class="color-indicator" style="width: 9px; height: 9px; border-radius: 2px; background-color: ${layer.color}; border: 1px solid rgba(0,0,0,0.15); flex-shrink: 0;"></span>
|
||||
<label class="form-check-label text-truncate mb-0" for="dxf-detail-layer-${layer.name}" style="cursor: pointer; font-size: 11px; color: #334155;" title="${layer.name}">
|
||||
${layer.name}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<div class="form-check form-switch mb-0" style="padding-left: 1.8em;">
|
||||
<input class="form-check-input dxf-vector-layer-toggle" type="checkbox" role="switch"
|
||||
id="dxf-detail-layer-${layer.name}" data-layer="${layer.name}" ${isChecked ? 'checked' : ''}>
|
||||
id="dxf-detail-layer-${layer.name}" data-layer="${layer.name}" ${autoCheckAll ? 'checked' : ''} style="cursor: pointer; width: 28px; height: 14px;">
|
||||
</div>
|
||||
`;
|
||||
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 = availableDxfFiles && availableDxfFiles.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 = `
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#2563eb" 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>
|
||||
Plans du carrefour
|
||||
</span>
|
||||
<span class="badge bg-light text-primary border" id="plans-active-count-badge" style="font-size: 9px; display: none;">0 actif</span>
|
||||
`;
|
||||
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 = `
|
||||
<div class="text-muted" style="font-size: 11px; margin-bottom: 4px;">Aucun plan géoréférencé</div>
|
||||
${importDxfUrl ? `<a href="${importDxfUrl}" class="layer-switcher-btn-action"><i class="bi bi-upload"></i> Importer un plan DXF</a>` : ''}
|
||||
`;
|
||||
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 = `<span><i class="bi bi-file-earmark-image me-1"></i> Plans images (${georeferencedDocuments.length})</span>`;
|
||||
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 = `
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<label class="form-check-label d-flex align-items-center gap-1 mb-0" for="switch-georef-img-${doc.id}" style="cursor: pointer; font-size: 11.5px; font-weight: 600; color: #1e293b; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${doc.title}">
|
||||
<i class="bi ${doc.is_dxf ? 'bi-file-earmark-code text-success' : 'bi-file-earmark-image text-primary'}" style="flex-shrink: 0; font-size: 12px;"></i>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${doc.title}</span>
|
||||
</label>
|
||||
<div class="form-check form-switch mb-0" style="padding-left: 2em;">
|
||||
<input class="form-check-input image-plan-toggle" type="checkbox" role="switch" id="switch-georef-img-${doc.id}" data-id="${doc.id}" style="cursor: pointer; width: 28px; height: 14px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer-switcher-slider-row" id="opacity-container-${doc.id}" style="display: none;">
|
||||
<label>Opacité</label>
|
||||
<input type="range" class="form-range image-plan-opacity-slider" min="0" max="100" value="95" id="range-georef-img-opacity-${doc.id}" data-id="${doc.id}">
|
||||
<span class="layer-switcher-slider-val opacity-val">95%</span>
|
||||
</div>
|
||||
`;
|
||||
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 = `<span><i class="bi bi-file-earmark-code me-1"></i> Plan CAO (DXF)</span>`;
|
||||
container.appendChild(dxfTitleEl);
|
||||
|
||||
const dxfCard = document.createElement('div');
|
||||
dxfCard.className = 'layer-switcher-card';
|
||||
dxfCard.id = 'plan-dxf-card';
|
||||
|
||||
let dxfSelectHtml = '';
|
||||
if (availableDxfFiles.length === 1) {
|
||||
dxfSelectHtml = `
|
||||
<input type="hidden" id="select-dxf-file" value="${availableDxfFiles[0]}">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<span class="small font-weight-bold text-dark text-truncate" style="font-size: 11.5px; max-width: 190px;" title="${availableDxfFiles[0]}">
|
||||
<i class="bi bi-file-earmark-check-fill text-success me-1"></i>${availableDxfFiles[0]}
|
||||
</span>
|
||||
<div class="form-check form-switch mb-0" style="padding-left: 2em;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="switch-dxf-master" style="cursor: pointer; width: 28px; height: 14px;" title="Afficher/masquer le plan CAO">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const selectedVal = window.dxfFile || availableDxfFiles[0] || '';
|
||||
dxfSelectHtml = `
|
||||
<div class="d-flex align-items-center justify-content-between gap-1 mb-2">
|
||||
<select id="select-dxf-file" class="form-select form-select-sm" style="font-size: 11px; padding: 2px 6px; flex-grow: 1;">
|
||||
<option value="">-- Sélectionner un fichier DXF --</option>
|
||||
${availableDxfFiles.map(f => `<option value="${f}" ${f === selectedVal ? 'selected' : ''}>${f}</option>`).join('')}
|
||||
</select>
|
||||
<div class="form-check form-switch mb-0 ps-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="switch-dxf-master" style="cursor: pointer; width: 28px; height: 14px;" title="Afficher/masquer le plan CAO">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
dxfCard.innerHTML = `
|
||||
${dxfSelectHtml}
|
||||
<div id="dxf-layers-accordion" style="display: none; margin-top: 4px;">
|
||||
<div class="d-flex align-items-center justify-content-between py-1 px-1 bg-white border rounded mb-1" style="font-size: 10.5px;">
|
||||
<button type="button" id="btn-toggle-dxf-accordion" class="btn btn-link p-0 text-decoration-none text-dark font-weight-bold d-flex align-items-center gap-1" style="font-size: 10.5px;">
|
||||
<i class="bi bi-chevron-down" id="dxf-accordion-chevron" style="transition: transform 0.2s ease; font-size: 10px;"></i>
|
||||
<span>Couches (<span id="dxf-active-layers-count">0</span>/<span id="dxf-total-layers-count">0</span>)</span>
|
||||
</button>
|
||||
<button type="button" id="dxf-detail-toggle-all-layers" class="btn btn-link p-0 text-decoration-none text-primary font-weight-semibold" style="font-size: 10px;">
|
||||
Tout cocher
|
||||
</button>
|
||||
</div>
|
||||
<div id="dxf-detail-layers-spinner" class="text-center py-2" style="display: none;">
|
||||
<div class="spinner-border spinner-border-sm text-primary" style="width: 14px; height: 14px; border-width: 1.5px;" role="status"></div>
|
||||
<span class="text-muted ms-1" style="font-size: 10px;">Chargement...</span>
|
||||
</div>
|
||||
<div id="dxf-detail-layers-list" style="max-height: 160px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; padding-right: 2px;">
|
||||
<!-- Checkboxes populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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 default or single DXF, prepare 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 = `
|
||||
<button type="button" id="btn-disable-all-plans" class="layer-switcher-btn-action btn-danger-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
||||
Désactiver tous les plans
|
||||
</button>
|
||||
`;
|
||||
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 plans section into layerSwitcher
|
||||
if (layerSwitcher) {
|
||||
layerSwitcher.addCustomSection({
|
||||
id: 'crossroad-plans',
|
||||
render: (container, control) => {
|
||||
renderCrossroadPlansSection(container, control);
|
||||
}
|
||||
|
||||
updateButtonActiveState();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4116,6 +4116,7 @@ def import_cross_plan(request, intersection_id):
|
|||
"properties": {
|
||||
"id": ep.id,
|
||||
"code": ep.code,
|
||||
"short_code": ep.code.split('_')[-1] if '_' in ep.code else ep.code,
|
||||
"model": ep.model.code if ep.model else ""
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue