diff --git a/loko/assets/locale/en/LC_MESSAGES/django.mo b/loko/assets/locale/en/LC_MESSAGES/django.mo index 4d3b7fe..dda1a38 100644 Binary files a/loko/assets/locale/en/LC_MESSAGES/django.mo and b/loko/assets/locale/en/LC_MESSAGES/django.mo differ diff --git a/loko/assets/locale/en/LC_MESSAGES/django.po b/loko/assets/locale/en/LC_MESSAGES/django.po index d1a0a55..93fee18 100644 --- a/loko/assets/locale/en/LC_MESSAGES/django.po +++ b/loko/assets/locale/en/LC_MESSAGES/django.po @@ -8588,6 +8588,87 @@ msgstr "Archive existing (do not recreate)" msgid "Archiver l'existant" msgstr "Archive existing" +msgid "Aide au tracé des câbles" +msgstr "Cable tracing help" + +msgid "Tracer" +msgstr "Trace" + +msgid "Double-clic sur la carte pour ajouter un point." +msgstr "Double-click on the map to add a point." + +msgid "Ajouter un nœud" +msgstr "Add a node" + +msgid "Double-clic sur un segment du câble." +msgstr "Double-click on a cable segment." + +msgid "Supprimer un nœud" +msgstr "Delete a node" + +msgid "Double-clic sur un nœud (poignée orange)." +msgstr "Double-click on a node (orange handle)." + +msgid "Déplacer" +msgstr "Move" + +msgid "Glisser-déposer les poignées pour ajuster." +msgstr "Drag and drop handles to adjust." + +msgid "Copier tracé" +msgstr "Copy trace" + +msgid "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." +msgstr "Copy the trace of another cable following the same path (e.g. 48 and tel.)." + +msgid "Copier tracé…" +msgstr "Copy trace…" + +msgid "Copier le tracé d'un autre câble" +msgstr "Copy another cable's trace" + +msgid "Appliquer aux autres câbles…" +msgstr "Apply to other cables…" + +msgid "Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud pour le supprimer • Glisser pour déplacer" +msgstr "Double-click a segment to add a node • Double-click a node to delete • Drag to move" + +msgid "Appliquer le tracé à d'autres câbles" +msgstr "Apply trace to other cables" + +msgid "Copier le tracé de" +msgstr "Copy trace from" + +msgid "vers les câbles sélectionnés ci-dessous :" +msgstr "to the selected cables below:" + +msgid "Copier le tracé" +msgstr "Copy trace" + +msgid "Le câble source ne possède aucun tracé valide à copier." +msgstr "The source cable has no valid trace to copy." + +msgid "Aucun tracé disponible" +msgstr "No trace available" + +msgid "Tracé copié de" +msgstr "Trace copied from" + +msgid "vers" +msgstr "to" + +msgid "Veuillez d'abord tracer au moins deux points pour ce câble avant de le dupliquer." +msgstr "Please trace at least two points for this cable before duplicating." + +msgid "Aucun autre câble disponible dans ce carrefour." +msgstr "No other cable available at this intersection." + +msgid "Veuillez sélectionner au moins un câble cible." +msgstr "Please select at least one target cable." + +msgid "câble(s) mis à jour avec ce tracé !" +msgstr "cable(s) updated with this trace!" + #~ msgid "Absent du fichier Excel (Sera conservé ou archivé)" #~ msgstr "Missing from Excel file (Will be kept or archived)" diff --git a/loko/assets/locale/fr/LC_MESSAGES/django.mo b/loko/assets/locale/fr/LC_MESSAGES/django.mo index 6254f81..e9c4865 100644 Binary files a/loko/assets/locale/fr/LC_MESSAGES/django.mo and b/loko/assets/locale/fr/LC_MESSAGES/django.mo differ diff --git a/loko/assets/locale/fr/LC_MESSAGES/django.po b/loko/assets/locale/fr/LC_MESSAGES/django.po index c4257d3..8439dbe 100644 --- a/loko/assets/locale/fr/LC_MESSAGES/django.po +++ b/loko/assets/locale/fr/LC_MESSAGES/django.po @@ -8639,6 +8639,87 @@ msgstr "Archiver l'existant (Ne pas recréer)" msgid "Archiver l'existant" msgstr "Archiver l'existant" +msgid "Aide au tracé des câbles" +msgstr "Aide au tracé des câbles" + +msgid "Tracer" +msgstr "Tracer" + +msgid "Double-clic sur la carte pour ajouter un point." +msgstr "Double-clic sur la carte pour ajouter un point." + +msgid "Ajouter un nœud" +msgstr "Ajouter un nœud" + +msgid "Double-clic sur un segment du câble." +msgstr "Double-clic sur un segment du câble." + +msgid "Supprimer un nœud" +msgstr "Supprimer un nœud" + +msgid "Double-clic sur un nœud (poignée orange)." +msgstr "Double-clic sur un nœud (poignée orange)." + +msgid "Déplacer" +msgstr "Déplacer" + +msgid "Glisser-déposer les poignées pour ajuster." +msgstr "Glisser-déposer les poignées pour ajuster." + +msgid "Copier tracé" +msgstr "Copier tracé" + +msgid "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." +msgstr "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." + +msgid "Copier tracé…" +msgstr "Copier tracé…" + +msgid "Copier le tracé d'un autre câble" +msgstr "Copier le tracé d'un autre câble" + +msgid "Appliquer aux autres câbles…" +msgstr "Appliquer aux autres câbles…" + +msgid "Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud pour le supprimer • Glisser pour déplacer" +msgstr "Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud pour le supprimer • Glisser pour déplacer" + +msgid "Appliquer le tracé à d'autres câbles" +msgstr "Appliquer le tracé à d'autres câbles" + +msgid "Copier le tracé de" +msgstr "Copier le tracé de" + +msgid "vers les câbles sélectionnés ci-dessous :" +msgstr "vers les câbles sélectionnés ci-dessous :" + +msgid "Copier le tracé" +msgstr "Copier le tracé" + +msgid "Le câble source ne possède aucun tracé valide à copier." +msgstr "Le câble source ne possède aucun tracé valide à copier." + +msgid "Aucun tracé disponible" +msgstr "Aucun tracé disponible" + +msgid "Tracé copié de" +msgstr "Tracé copié de" + +msgid "vers" +msgstr "vers" + +msgid "Veuillez d'abord tracer au moins deux points pour ce câble avant de le dupliquer." +msgstr "Veuillez d'abord tracer au moins deux points pour ce câble avant de le dupliquer." + +msgid "Aucun autre câble disponible dans ce carrefour." +msgstr "Aucun autre câble disponible dans ce carrefour." + +msgid "Veuillez sélectionner au moins un câble cible." +msgstr "Veuillez sélectionner au moins un câble cible." + +msgid "câble(s) mis à jour avec ce tracé !" +msgstr "câble(s) mis à jour avec ce tracé !" + #~ msgid "Absent du fichier Excel (Sera conservé ou archivé)" #~ msgstr "Absent du fichier Excel (Sera conservé ou archivé)" diff --git a/loko/assets/locale/nl/LC_MESSAGES/django.mo b/loko/assets/locale/nl/LC_MESSAGES/django.mo index 9196b46..c3779c3 100644 Binary files a/loko/assets/locale/nl/LC_MESSAGES/django.mo and b/loko/assets/locale/nl/LC_MESSAGES/django.mo differ diff --git a/loko/assets/locale/nl/LC_MESSAGES/django.po b/loko/assets/locale/nl/LC_MESSAGES/django.po index e1c4ac1..9ce774f 100644 --- a/loko/assets/locale/nl/LC_MESSAGES/django.po +++ b/loko/assets/locale/nl/LC_MESSAGES/django.po @@ -8587,6 +8587,87 @@ msgstr "Bestaande archiveren (niet opnieuw aanmaken)" msgid "Archiver l'existant" msgstr "Bestaande archiveren" +msgid "Aide au tracé des câbles" +msgstr "Hulp bij het traceren van kabels" + +msgid "Tracer" +msgstr "Traceren" + +msgid "Double-clic sur la carte pour ajouter un point." +msgstr "Dubbelklik op de kaart om een punt toe te voegen." + +msgid "Ajouter un nœud" +msgstr "Knooppunt toevoegen" + +msgid "Double-clic sur un segment du câble." +msgstr "Dubbelklik op een segment van de kabel." + +msgid "Supprimer un nœud" +msgstr "Knooppunt verwijderen" + +msgid "Double-clic sur un nœud (poignée orange)." +msgstr "Dubbelklik op een knooppunt (oranje handgreep)." + +msgid "Déplacer" +msgstr "Verplaatsen" + +msgid "Glisser-déposer les poignées pour ajuster." +msgstr "Sleep de handgrepen om aan te passen." + +msgid "Copier tracé" +msgstr "Tracé kopiëren" + +msgid "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." +msgstr "Kopieer het tracé van een andere kabel die hetzelfde traject volgt (bv. 48 en tel.)." + +msgid "Copier tracé…" +msgstr "Tracé kopiëren…" + +msgid "Copier le tracé d'un autre câble" +msgstr "Het tracé van een andere kabel kopiëren" + +msgid "Appliquer aux autres câbles…" +msgstr "Toepassen op andere kabels…" + +msgid "Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud pour le supprimer • Glisser pour déplacer" +msgstr "Dubbelklik op een segment om een knooppunt toe te voegen • Dubbelklik op een knooppunt om te verwijderen • Sleep om te verplaatsen" + +msgid "Appliquer le tracé à d'autres câbles" +msgstr "Tracé toepassen op andere kabels" + +msgid "Copier le tracé de" +msgstr "Tracé kopiëren van" + +msgid "vers les câbles sélectionnés ci-dessous :" +msgstr "naar de onderstaande geselecteerde kabels:" + +msgid "Copier le tracé" +msgstr "Tracé kopiëren" + +msgid "Le câble source ne possède aucun tracé valide à copier." +msgstr "De bronkabel heeft geen geldig tracé om te kopiëren." + +msgid "Aucun tracé disponible" +msgstr "Geen tracé beschikbaar" + +msgid "Tracé copié de" +msgstr "Tracé gekopieerd van" + +msgid "vers" +msgstr "naar" + +msgid "Veuillez d'abord tracer au moins deux points pour ce câble avant de le dupliquer." +msgstr "Teken eerst ten minste twee punten voor deze kabel alvorens te dupliceren." + +msgid "Aucun autre câble disponible dans ce carrefour." +msgstr "Geen andere kabel beschikbaar op dit kruispunt." + +msgid "Veuillez sélectionner au moins un câble cible." +msgstr "Selecteer ten minste één doelkabel." + +msgid "câble(s) mis à jour avec ce tracé !" +msgstr "kabel(s) bijgewerkt met dit tracé!" + #~ msgid "Absent du fichier Excel (Sera conservé ou archivé)" #~ msgstr "Ontbreekt in het Excel-bestand (Wordt bewaard of gearchiveerd)" diff --git a/loko/assets/templates/assets/trafficlights_import_cross_plan.html b/loko/assets/templates/assets/trafficlights_import_cross_plan.html index 349b975..3a37e93 100644 --- a/loko/assets/templates/assets/trafficlights_import_cross_plan.html +++ b/loko/assets/templates/assets/trafficlights_import_cross_plan.html @@ -661,6 +661,11 @@ {% else %} {% translate "Non tracé" %} {% endif %} + {% if item.status != 'db_only' %} + + {% endif %} {{ item.deduced_model_name|default:"—" }} {{ item.db_model_name|default:"—" }} @@ -874,13 +879,19 @@
-
-
- {% translate "Tracé des câbles" %} +
+
+ + {% translate "Aide au tracé des câbles" %} +
-

- {% translate "Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer la ligne du câble." %} -

+
    +
  • {% translate "Tracer" %} : {% translate "Double-clic sur la carte pour ajouter un point." %}
  • +
  • {% translate "Ajouter un nœud" %} : {% translate "Double-clic sur un segment du câble." %}
  • +
  • {% translate "Supprimer un nœud" %} : {% translate "Double-clic sur un nœud (poignée orange)." %}
  • +
  • {% translate "Déplacer" %} : {% translate "Glisser-déposer les poignées pour ajuster." %}
  • +
  • {% translate "Copier tracé" %} : {% translate "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." %}
  • +
@@ -889,12 +900,23 @@
{{ item.name }} + + … +
{{ item.deduced_model_name }}
-
+
+
{% endif %} @@ -914,10 +936,17 @@
{% translate "Mode: Lecture" %} +
+
@@ -926,6 +955,46 @@
+ + + {% endif %}
@@ -1084,30 +1153,47 @@ document.addEventListener('DOMContentLoaded', () => { const tableView = document.getElementById('table-view'); const mapView = document.getElementById('map-view'); + function switchToMapView(targetTab, cableCodeToEdit) { + tableView.classList.remove('d-block'); + tableView.classList.add('d-none'); + + mapView.classList.remove('d-none'); + mapView.classList.add('d-block'); + + // Trigger MapLibre GL canvas resize recalculation + if (map) { + map.resize(); + } + + // Activate corresponding tab in map sidebar + if (targetTab === 'poles') { + const tabEl = document.getElementById('map-poles-tab'); + if (tabEl) new bootstrap.Tab(tabEl).show(); + } else if (targetTab === 'cables') { + const tabEl = document.getElementById('map-cables-tab'); + if (tabEl) new bootstrap.Tab(tabEl).show(); + if (cableCodeToEdit) { + activateCableEditingMode(cableCodeToEdit); + const item = document.querySelector(`.cable-config-item[data-code="${cableCodeToEdit}"]`); + if (item) { + item.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + } + } + document.querySelectorAll('.show-map-btn').forEach(btn => { btn.addEventListener('click', function(e) { e.preventDefault(); - const targetTab = this.dataset.targetTab; - - tableView.classList.remove('d-block'); - tableView.classList.add('d-none'); - - mapView.classList.remove('d-none'); - mapView.classList.add('d-block'); - - // Trigger MapLibre GL canvas resize recalculation - if (map) { - map.resize(); - } - - // Activate corresponding tab in map sidebar - if (targetTab === 'poles') { - const tabEl = document.getElementById('map-poles-tab'); - if (tabEl) new bootstrap.Tab(tabEl).show(); - } else if (targetTab === 'cables') { - const tabEl = document.getElementById('map-cables-tab'); - if (tabEl) new bootstrap.Tab(tabEl).show(); - } + switchToMapView(this.dataset.targetTab); + }); + }); + + document.querySelectorAll('.show-cable-on-map-btn').forEach(btn => { + btn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + switchToMapView('cables', this.dataset.code); }); }); @@ -1178,7 +1264,11 @@ document.addEventListener('DOMContentLoaded', () => { name: "{{ item.name|escapejs }}", status: "{{ item.status|escapejs }}", geom_modified: {{ item.geom_modified|yesno:"true,false" }}, - connected_poles: {{ item.connected_poles|safe|default:"[]" }} + connected_poles: {{ item.connected_poles|safe|default:"[]" }}, + db_coords: {{ item.db_coords|safe|default:"[]" }}, + has_db_geom: {{ item.has_db_geom|yesno:"true,false" }}, + db_geom_length: {% if item.db_geom_length is not None %}{{ item.db_geom_length|unlocalize }}{% else %}null{% endif %}, + deduced_model_name: "{{ item.deduced_model_name|default:''|escapejs }}" }, {% endfor %} ]; @@ -1213,6 +1303,87 @@ document.addEventListener('DOMContentLoaded', () => { const placedPoles = {}; let layerSwitcher = null; + function calculateLineLengthMeters(coords) { + if (!coords || coords.length < 2) return 0; + const R = 6371000; + let total = 0; + for (let i = 0; i < coords.length - 1; i++) { + const lon1 = coords[i][0] * Math.PI / 180; + const lat1 = coords[i][1] * Math.PI / 180; + const lon2 = coords[i+1][0] * Math.PI / 180; + const lat2 = coords[i+1][1] * Math.PI / 180; + const dLat = lat2 - lat1; + const dLon = lon2 - lon1; + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + total += R * c; + } + return Math.round(total * 10) / 10; + } + + function getCableCoords(code) { + if (cableGeometries[code] && cableGeometries[code].length >= 2) { + return cableGeometries[code]; + } + const c = cablesList.find(item => item.code === code); + if (!c) return []; + if (c.db_coords && c.db_coords.length >= 2) { + return c.db_coords; + } + const coords = []; + const connected = c.connected_poles || []; + connected.forEach(pCode => { + if (placedPoles[pCode]) { + coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]); + } + }); + return coords; + } + + function updateCableBadge(code) { + const badge = document.getElementById(`badge-cable-status-${code}`); + const coords = getCableCoords(code); + const c = cablesList.find(item => item.code === code); + if (!badge || !c) return; + + if (coords && coords.length >= 2) { + const len = calculateLineLengthMeters(coords); + const isModified = cableGeometries[code] !== undefined || c.geom_modified; + if (isModified) { + badge.className = 'badge bg-warning text-dark border border-warning-subtle font-monospace cable-geom-status-badge'; + badge.innerHTML = ` ${coords.length} pts (${len} m)`; + } else { + badge.className = 'badge bg-info-subtle text-info-emphasis border border-info-subtle font-monospace cable-geom-status-badge'; + badge.innerHTML = ` ${coords.length} pts (${len} m)`; + } + } else { + badge.className = 'badge bg-light text-muted border font-monospace cable-geom-status-badge'; + badge.textContent = `{% translate "Non tracé" %}`; + } + } + + function updateTableCableGeomCell(code) { + const geomCell = document.getElementById(`geom_cell_cable_${code}`); + if (!geomCell) return; + const coords = getCableCoords(code); + if (coords && coords.length >= 2) { + const len = calculateLineLengthMeters(coords); + geomCell.innerHTML = ` {% translate "Tracé modifié" %} (${len} m) `; + } else { + geomCell.innerHTML = `{% translate "Non tracé" %} `; + } + const btn = geomCell.querySelector('.show-cable-on-map-btn'); + if (btn) { + btn.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + switchToMapView('cables', code); + }); + } + } + function markCableAsModified(code) { const selectAction = document.querySelector(`select[name="action_cable_${code}"]`); if (selectAction) { @@ -1225,10 +1396,8 @@ document.addEventListener('DOMContentLoaded', () => { const cb = document.querySelector(`input[name="selected_cable_${code}"]`); if (cb) cb.checked = true; - const geomCell = document.getElementById(`geom_cell_cable_${code}`); - if (geomCell) { - geomCell.innerHTML = ` {% translate "Tracé modifié" %}`; - } + updateTableCableGeomCell(code); + updateCableBadge(code); } const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]); @@ -1718,8 +1887,12 @@ document.addEventListener('DOMContentLoaded', () => { deactivateCableEditingMode(); activeEditCable = code; - document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Tracé" %} ${cablesList.find(c => c.code === code).name}`; + const cableInfo = cablesList.find(c => c.code === code); + const cableName = cableInfo ? cableInfo.name : code; + document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Tracé" %} ${cableName}`; document.getElementById('btn-clear-trace').style.display = 'block'; + document.getElementById('btn-duplicate-trace').style.display = 'block'; + document.getElementById('cable-edit-tip').style.display = 'block'; const mapPane = document.getElementById('map-cables-pane'); mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light')); @@ -1728,25 +1901,21 @@ document.addEventListener('DOMContentLoaded', () => { let coords = cableGeometries[code]; if (!coords || coords.length === 0) { - const cableInfo = cablesList.find(c => c.code === code); - const connected = cableInfo.connected_poles || []; - - coords = []; - connected.forEach(pCode => { - if (placedPoles[pCode]) { - coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]); - } - }); + const defaultCoords = getCableCoords(code); + coords = defaultCoords && defaultCoords.length > 0 ? defaultCoords.map(pt => [pt[0], pt[1]]) : []; cableGeometries[code] = coords; } drawCableHandles(code); redrawCablesOnMap(); + updateCableBadge(code); } function deactivateCableEditingMode() { activeEditCable = null; document.getElementById('btn-clear-trace').style.display = 'none'; + document.getElementById('btn-duplicate-trace').style.display = 'none'; + document.getElementById('cable-edit-tip').style.display = 'none'; document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Lecture" %}`; cableHandles.forEach(h => h.remove()); @@ -1795,6 +1964,7 @@ document.addEventListener('DOMContentLoaded', () => { redrawCablesOnMap(); saveCablesToForm(); markCableAsModified(code); + populateCopyCableMenus(); }); el.addEventListener('dblclick', function(e) { @@ -1804,6 +1974,7 @@ document.addEventListener('DOMContentLoaded', () => { redrawCablesOnMap(); saveCablesToForm(); markCableAsModified(code); + populateCopyCableMenus(); }); }); } @@ -1861,6 +2032,7 @@ document.addEventListener('DOMContentLoaded', () => { redrawCablesOnMap(); saveCablesToForm(); markCableAsModified(activeEditCable); + populateCopyCableMenus(); }); map.on('dblclick', function(e) { @@ -1884,6 +2056,7 @@ document.addEventListener('DOMContentLoaded', () => { redrawCablesOnMap(); saveCablesToForm(); markCableAsModified(activeEditCable); + populateCopyCableMenus(); }); // Hover mouse styles for active cable line @@ -1907,20 +2080,192 @@ document.addEventListener('DOMContentLoaded', () => { redrawCablesOnMap(); saveCablesToForm(); markCableAsModified(activeEditCable); + populateCopyCableMenus(); } }); + function copyCableGeometry(sourceCode, targetCode) { + const srcCoords = getCableCoords(sourceCode); + if (!srcCoords || srcCoords.length < 2) { + alert("{% translate "Le câble source ne possède aucun tracé valide à copier." %}"); + return false; + } + cableGeometries[targetCode] = srcCoords.map(pt => [pt[0], pt[1]]); + saveCablesToForm(); + markCableAsModified(targetCode); + populateCopyCableMenus(); + if (activeEditCable === targetCode) { + drawCableHandles(targetCode); + redrawCablesOnMap(); + } + return true; + } + + function populateCopyCableMenus() { + document.querySelectorAll('.copy-cable-menu').forEach(menu => { + const targetCode = menu.dataset.targetCode; + menu.innerHTML = ''; + + let hasAvailable = false; + cablesList.forEach(other => { + if (other.code === targetCode || other.status === 'db_only') return; + const coords = getCableCoords(other.code); + if (coords && coords.length >= 2) { + hasAvailable = true; + const len = calculateLineLengthMeters(coords); + const li = document.createElement('li'); + li.innerHTML = ` + + `; + menu.appendChild(li); + } + }); + + if (!hasAvailable) { + const li = document.createElement('li'); + li.className = 'dropdown-item text-muted small fst-italic py-2'; + li.textContent = `{% translate "Aucun tracé disponible" %}`; + menu.appendChild(li); + } + }); + + // Rebind click listeners for dynamically populated dropdown items + document.querySelectorAll('.copy-from-cable-btn').forEach(btn => { + btn.onclick = function(e) { + e.preventDefault(); + const source = this.dataset.source; + const target = this.dataset.target; + const success = copyCableGeometry(source, target); + if (success) { + const targetCable = cablesList.find(c => c.code === target); + const sourceCable = cablesList.find(c => c.code === source); + const targetName = targetCable ? targetCable.name : target; + const sourceName = sourceCable ? sourceCable.name : source; + const tipEl = document.getElementById('cable-edit-tip'); + if (tipEl) { + tipEl.style.display = 'block'; + const origContent = tipEl.innerHTML; + tipEl.innerHTML = `{% translate "Tracé copié de" %} ${sourceName} {% translate "vers" %} ${targetName}`; + setTimeout(() => { + if (!activeEditCable) tipEl.style.display = 'none'; + else tipEl.innerHTML = origContent; + }, 3000); + } + } + }; + }); + } + + // Modal Duplicate Trace logic + const duplicateModalEl = document.getElementById('duplicateCableModal'); + const duplicateModal = duplicateModalEl ? new bootstrap.Modal(duplicateModalEl) : null; + + const btnDuplicateTrace = document.getElementById('btn-duplicate-trace'); + if (btnDuplicateTrace) { + btnDuplicateTrace.addEventListener('click', function() { + if (!activeEditCable) return; + const currentCable = cablesList.find(c => c.code === activeEditCable); + const coords = getCableCoords(activeEditCable); + if (!coords || coords.length < 2) { + alert("{% translate "Veuillez d'abord tracer au moins deux points pour ce câble avant de le dupliquer." %}"); + return; + } + + const len = calculateLineLengthMeters(coords); + const srcNameEl = document.getElementById('modal-source-cable-name'); + const srcInfoEl = document.getElementById('modal-source-cable-info'); + if (srcNameEl) srcNameEl.textContent = currentCable ? currentCable.name : activeEditCable; + if (srcInfoEl) srcInfoEl.textContent = `${coords.length} pts • ${len} m`; + + const listContainer = document.getElementById('modal-target-cables-list'); + if (listContainer) { + listContainer.innerHTML = ''; + const otherCables = cablesList.filter(c => c.code !== activeEditCable && c.status !== 'db_only'); + if (otherCables.length === 0) { + listContainer.innerHTML = `
{% translate "Aucun autre câble disponible dans ce carrefour." %}
`; + const confirmBtn = document.getElementById('btn-confirm-duplicate-cable'); + if (confirmBtn) confirmBtn.disabled = true; + } else { + const confirmBtn = document.getElementById('btn-confirm-duplicate-cable'); + if (confirmBtn) confirmBtn.disabled = false; + otherCables.forEach(c => { + const cCoords = getCableCoords(c.code); + const hasGeom = cCoords && cCoords.length >= 2; + const cLen = hasGeom ? calculateLineLengthMeters(cCoords) : null; + const geomBadge = hasGeom + ? `${cCoords.length} pts (${cLen} m)` + : `{% translate "Non tracé" %}`; + + const itemDiv = document.createElement('div'); + itemDiv.className = 'form-check d-flex align-items-center p-2 rounded border bg-white'; + itemDiv.innerHTML = ` + + + `; + listContainer.appendChild(itemDiv); + }); + } + } + + const selectAllCb = document.getElementById('modal-select-all-cables'); + if (selectAllCb) selectAllCb.checked = false; + + if (duplicateModal) duplicateModal.show(); + }); + } + + const modalSelectAllCables = document.getElementById('modal-select-all-cables'); + if (modalSelectAllCables) { + modalSelectAllCables.addEventListener('change', function() { + const checked = this.checked; + document.querySelectorAll('.modal-cable-target-cb').forEach(cb => { + cb.checked = checked; + }); + }); + } + + const btnConfirmDuplicateCable = document.getElementById('btn-confirm-duplicate-cable'); + if (btnConfirmDuplicateCable) { + btnConfirmDuplicateCable.addEventListener('click', function() { + if (!activeEditCable) return; + const selectedCbs = document.querySelectorAll('.modal-cable-target-cb:checked'); + if (selectedCbs.length === 0) { + alert("{% translate "Veuillez sélectionner au moins un câble cible." %}"); + return; + } + + let appliedCount = 0; + selectedCbs.forEach(cb => { + const targetCode = cb.value; + copyCableGeometry(activeEditCable, targetCode); + appliedCount++; + }); + + if (duplicateModal) duplicateModal.hide(); + + const tipEl = document.getElementById('cable-edit-tip'); + if (tipEl) { + const origContent = tipEl.innerHTML; + tipEl.innerHTML = `${appliedCount} {% translate "câble(s) mis à jour avec ce tracé !" %}`; + setTimeout(() => { + if (activeEditCable) tipEl.innerHTML = origContent; + }, 3000); + } + }); + } + function saveCablesToForm() { cablesList.forEach(c => { let coords = cableGeometries[c.code]; if (!coords || coords.length === 0) { - coords = []; - const connected = c.connected_poles || []; - connected.forEach(pCode => { - if (placedPoles[pCode]) { - coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]); - } - }); + coords = getCableCoords(c.code); } const input = document.getElementById(`geom_cable_${c.code}`); if (input && coords && coords.length >= 2) { @@ -1953,13 +2298,7 @@ document.addEventListener('DOMContentLoaded', () => { let coords = cableGeometries[c.code]; if (!coords || coords.length === 0) { - coords = []; - const connected = c.connected_poles || []; - connected.forEach(pCode => { - if (placedPoles[pCode]) { - coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]); - } - }); + coords = getCableCoords(c.code); } if (coords && coords.length >= 2) { @@ -1987,6 +2326,12 @@ document.addEventListener('DOMContentLoaded', () => { map.triggerRepaint(); } + // Initial cable badges and copy menus update + cablesList.forEach(c => { + updateCableBadge(c.code); + }); + populateCopyCableMenus(); + // ── Georeferenced Plans & DXF Background Loader ── const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData'); let georeferencedDocuments = []; diff --git a/loko/assets/tests.py b/loko/assets/tests.py index d22c291..7016db8 100644 --- a/loko/assets/tests.py +++ b/loko/assets/tests.py @@ -3325,6 +3325,88 @@ class TrafficLightCrossPlanImportTest(TestCase): if os.path.exists(tmp_path): os.remove(tmp_path) + def test_compared_cables_db_coords_and_integrate_cable_geometry(self): + import openpyxl + import tempfile + import json + from django.contrib.gis.geos import LineString + from assets.models import TrafficLightCable, TrafficLightCableModel + from django.core.files.uploadedfile import SimpleUploadedFile + from pyproj import Transformer + + cable_model = TrafficLightCableModel.objects.first() + transformer = Transformer.from_crs(4326, 3812, always_xy=True) + p1 = transformer.transform(4.3508, 50.8465) + p2 = transformer.transform(4.3518, 50.8475) + + # Create active cable in DB with Lambert 3812 LineString + existing_cable = TrafficLightCable.objects.create( + intersection=self.intersection, + code="SWB01_CAB01", + model=cable_model, + status="active", + geom=LineString([p1, p2], srid=3812) + ) + + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "kruisjesplan" + ws.cell(row=1, column=1, value="Sleutel:") + ws.cell(row=1, column=2, value="SWB01") + ws.cell(row=5, column=1, value="Câble 01 SVAVB 61x1,5mm²") + ws.cell(row=6, column=3, value="richting") + ws.cell(row=6, column=4, value="kringen") + ws.cell(row=7, column=5, value="A01") + ws.cell(row=7, column=6, value="B01") + ws.cell(row=8, column=1, value="1") + ws.cell(row=8, column=5, value="X") + ws.cell(row=8, column=6, value="X") + + with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: + tmp_path = tmp.name + wb.save(tmp_path) + + try: + self.client.force_login(self.user) + with open(tmp_path, "rb") as f: + uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file}) + self.assertEqual(response.status_code, 200) + + cables_preview = response.context["cables"] + cab01 = next((c for c in cables_preview if c["code"] == "SWB01_CAB01"), None) + self.assertIsNotNone(cab01) + self.assertTrue(cab01["has_db_geom"]) + self.assertIsNotNone(cab01["db_coords"]) + self.assertEqual(len(cab01["db_coords"]), 2) + # Coordinates in WGS84 lon, lat + self.assertAlmostEqual(cab01["db_coords"][0][0], 4.35, delta=0.5) + + # Test integrate with modified cable geometry + url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id]) + new_geom_coords = [[4.3508, 50.8465], [4.3510, 50.8470], [4.3515, 50.8475]] + post_data = { + "cross_plan_filename": response.context["cross_plan_filename"], + "selected_cable_SWB01_CAB01": "on", + "action_cable_SWB01_CAB01": "modify", + "db_id_cable_SWB01_CAB01": str(existing_cable.id), + "geom_cable_SWB01_CAB01": json.dumps(new_geom_coords), + } + res_int = self.client.post(url_integrate, post_data) + self.assertEqual(res_int.status_code, 302) + + existing_cable.refresh_from_db() + self.assertIsNotNone(existing_cable.geom) + self.assertEqual(len(existing_cable.geom.coords), 3) + self.assertEqual(existing_cable.geom.srid, 3812) + + finally: + import os + if os.path.exists(tmp_path): + os.remove(tmp_path) + diff --git a/loko/assets/views/trafficlights.py b/loko/assets/views/trafficlights.py index 3cb0a5c..656fc68 100644 --- a/loko/assets/views/trafficlights.py +++ b/loko/assets/views/trafficlights.py @@ -4401,6 +4401,15 @@ def import_cross_plan(request, intersection_id): geom_modified = True details_diff.append(_("Nouveau tracé déduit des poteaux")) + db_coords = [] + if has_geom: + try: + gw = ec.geom.clone() + gw.transform(4326) + db_coords = [[round(c[0], 6), round(c[1], 6)] for c in gw.coords] + except Exception: + db_coords = [] + status = 'modified' if (is_diff or geom_modified) else 'identical' compared_cables.append({ 'code': code, @@ -4414,6 +4423,7 @@ def import_cross_plan(request, intersection_id): 'has_db_geom': has_geom, 'geom_modified': geom_modified, 'db_geom_length': round(ec.geom.length, 1) if has_geom else None, + 'db_coords': db_coords, }) existing_cables.pop(code) else: @@ -4432,9 +4442,18 @@ def import_cross_plan(request, intersection_id): 'has_db_geom': False, 'geom_modified': False, 'db_geom_length': None, + 'db_coords': [], }) for code, ec in existing_cables.items(): has_geom = ec.geom is not None + db_coords = [] + if has_geom: + try: + gw = ec.geom.clone() + gw.transform(4326) + db_coords = [[round(c[0], 6), round(c[1], 6)] for c in gw.coords] + except Exception: + db_coords = [] compared_cables.append({ 'code': code, 'name': ec.name_fr or code, @@ -4446,6 +4465,7 @@ def import_cross_plan(request, intersection_id): 'has_db_geom': has_geom, 'geom_modified': False, 'db_geom_length': round(ec.geom.length, 1) if has_geom else None, + 'db_coords': db_coords, }) # ── 3. LANTERNS COMPARISON ──