diff --git a/loko/assets/templates/assets/trafficlights_import_cross_plan.html b/loko/assets/templates/assets/trafficlights_import_cross_plan.html index 5cef657..29b70c8 100644 --- a/loko/assets/templates/assets/trafficlights_import_cross_plan.html +++ b/loko/assets/templates/assets/trafficlights_import_cross_plan.html @@ -597,7 +597,7 @@ {% translate "Code Asset" %} {% translate "Nom" %} {% translate "Statut" %} - {% translate "Géométrie (DB)" %} + {% translate "Géométrie" %} {% translate "Modèle Déduit (Excel)" %} {% translate "Modèle Actuel (DB)" %} {% translate "Décision" %} @@ -628,8 +628,12 @@ {% endfor %} - - {% if item.has_db_geom %} + + {% if item.geom_modified %} + + {% translate "Tracé modifié" %} + + {% elif item.has_db_geom %} {{ item.db_geom_length }} m @@ -1235,6 +1239,8 @@ document.addEventListener('DOMContentLoaded', () => { { code: "{{ item.code|escapejs }}", name: "{{ item.name|escapejs }}", + status: "{{ item.status|escapejs }}", + geom_modified: {{ item.geom_modified|yesno:"true,false" }}, connected_poles: {{ item.connected_poles|safe|default:"[]" }} }, {% endfor %} @@ -1265,9 +1271,26 @@ document.addEventListener('DOMContentLoaded', () => { let activePlacementPole = null; let activeEditCable = null; - const placedPoles = {}; - const cableGeometries = {}; let cableHandles = []; + const cableGeometries = {}; + + function markCableAsModified(code) { + const selectAction = document.querySelector(`select[name="action_cable_${code}"]`); + if (selectAction) { + if (selectAction.querySelector('option[value="modify"]')) { + selectAction.value = 'modify'; + } else if (selectAction.querySelector('option[value="add"]')) { + selectAction.value = 'add'; + } + } + 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é" %}`; + } + } const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]); @@ -1530,7 +1553,7 @@ document.addEventListener('DOMContentLoaded', () => { // Automatically place markers for poles that have known coordinates (e.g. from DXF or previous georeferencing) polesList.forEach(p => { if (p.status !== 'db_only' && p.new_lon !== null && p.new_lat !== null) { - placePoleMarker(p.code, p.new_lon, p.new_lat, p.new_x_3812, p.new_y_3812, false); + placePoleMarker(p.code, p.new_lon, p.new_lat, p.new_x_3812, p.new_y_3812, false, true); } }); @@ -1585,7 +1608,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('placed-poles-count').textContent = `${placed} / ${total}`; } - function markPoleAsGeoreferenced(code, lon, lat, x_3812, y_3812) { + function markPoleAsGeoreferenced(code, lon, lat, x_3812, y_3812, isInitialLoad = false) { const badge = document.getElementById(`georef_badge_${code}`); if (badge) { badge.classList.remove('bg-danger'); @@ -1602,24 +1625,49 @@ document.addEventListener('DOMContentLoaded', () => { const inputY = document.getElementById(`y_3812_pole_${code}`); if (inputY) inputY.value = (y_3812 !== undefined && y_3812 !== null) ? y_3812 : ""; - // Dynamically update the table row for this pole if position changed - const poleMeta = polesList.find(p => p.code === code); - if (poleMeta) { - const selectAction = document.querySelector(`select[name="action_pole_${code}"]`); - if (selectAction) { - if (selectAction.querySelector('option[value="modify"]')) { - selectAction.value = 'modify'; + // If not initial load (i.e. user moved or placed the pole interactively), update decision and geom cell + if (!isInitialLoad) { + const poleMeta = polesList.find(p => p.code === code); + if (poleMeta) { + const selectAction = document.querySelector(`select[name="action_pole_${code}"]`); + const geomCell = document.getElementById(`geom_cell_pole_${code}`); + const hasDbPos = (poleMeta.db_x_3812 !== null && poleMeta.db_y_3812 !== null && x_3812 !== null && y_3812 !== null); + let dist = hasDbPos ? Math.sqrt((poleMeta.db_x_3812 - x_3812)**2 + (poleMeta.db_y_3812 - y_3812)**2) : null; + const isIdenticalPos = (hasDbPos && dist < 0.05); + + if (isIdenticalPos && poleMeta.status === 'identical') { + // Position identical and attributes identical: propose "Ne rien faire" + if (selectAction && selectAction.querySelector('option[value="keep"]')) { + selectAction.value = 'keep'; + } + if (geomCell) { + geomCell.innerHTML = ` {% translate "Inchangée" %} `; + } + } else { + // Position changed or newly placed: propose "Mise à jour" + if (selectAction) { + if (selectAction.querySelector('option[value="modify"]')) { + selectAction.value = 'modify'; + } else if (selectAction.querySelector('option[value="add"]')) { + selectAction.value = 'add'; + } + } + const cb = document.querySelector(`input[name="selected_pole_${code}"]`); + if (cb) cb.checked = true; + + if (geomCell) { + let distText = (dist !== null) ? ` (${dist.toFixed(1)} m)` : ""; + geomCell.innerHTML = ` {% translate "Position modifiée" %}${distText} `; + } + + // Any cable connected to this pole also has its geometry modified + cablesList.forEach(c => { + if (c.connected_poles && c.connected_poles.includes(code)) { + markCableAsModified(c.code); + } + }); } } - const geomCell = document.getElementById(`geom_cell_pole_${code}`); - if (geomCell) { - let distText = ""; - if (poleMeta.db_x_3812 !== null && poleMeta.db_y_3812 !== null && x_3812 && y_3812) { - const d = Math.sqrt((poleMeta.db_x_3812 - x_3812)**2 + (poleMeta.db_y_3812 - y_3812)**2); - distText = ` (${d.toFixed(1)} m)`; - } - geomCell.innerHTML = ` {% translate "Positionné sur carte" %}${distText} `; - } } redrawCablesOnMap(); @@ -1633,10 +1681,10 @@ document.addEventListener('DOMContentLoaded', () => { const originalCoords = [e.lngLat.lng, e.lngLat.lat]; const snapped = getSnappedCoords(originalCoords[0], originalCoords[1]); - placePoleMarker(activePlacementPole, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1]); + placePoleMarker(activePlacementPole, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1], true, false); }); - function placePoleMarker(code, lon, lat, x_3812, y_3812, autoAdvance = true) { + function placePoleMarker(code, lon, lat, x_3812, y_3812, autoAdvance = true, isInitialLoad = false) { if (placedPoles[code]) { placedPoles[code].marker.remove(); } @@ -1678,10 +1726,10 @@ document.addEventListener('DOMContentLoaded', () => { placedPoles[code].lat = snapped.coords[1]; placedPoles[code].x_3812 = snapped.lambert[0]; placedPoles[code].y_3812 = snapped.lambert[1]; - markPoleAsGeoreferenced(code, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1]); + markPoleAsGeoreferenced(code, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1], false); }); - markPoleAsGeoreferenced(code, lon, lat, x_3812, y_3812); + markPoleAsGeoreferenced(code, lon, lat, x_3812, y_3812, isInitialLoad); const mapPane = document.getElementById('map-poles-pane'); mapPane.querySelectorAll('.pole-config-item').forEach(item => item.classList.remove('bg-light')); @@ -1821,6 +1869,7 @@ document.addEventListener('DOMContentLoaded', () => { cableGeometries[code][index] = snapped.coords; redrawCablesOnMap(); saveCablesToForm(); + markCableAsModified(code); }); el.addEventListener('dblclick', function(e) { @@ -1829,6 +1878,7 @@ document.addEventListener('DOMContentLoaded', () => { drawCableHandles(code); redrawCablesOnMap(); saveCablesToForm(); + markCableAsModified(code); }); }); } @@ -1885,6 +1935,7 @@ document.addEventListener('DOMContentLoaded', () => { drawCableHandles(activeEditCable); redrawCablesOnMap(); saveCablesToForm(); + markCableAsModified(activeEditCable); }); map.on('dblclick', function(e) { @@ -1907,6 +1958,7 @@ document.addEventListener('DOMContentLoaded', () => { drawCableHandles(activeEditCable); redrawCablesOnMap(); saveCablesToForm(); + markCableAsModified(activeEditCable); }); // Hover mouse styles for active cable line @@ -1929,6 +1981,7 @@ document.addEventListener('DOMContentLoaded', () => { drawCableHandles(activeEditCable); redrawCablesOnMap(); saveCablesToForm(); + markCableAsModified(activeEditCable); } }); diff --git a/loko/assets/tests.py b/loko/assets/tests.py index 0177e82..ab84d5e 100644 --- a/loko/assets/tests.py +++ b/loko/assets/tests.py @@ -2715,6 +2715,164 @@ class TrafficLightCrossPlanImportTest(TestCase): if os.path.exists(tmp_path): os.remove(tmp_path) + def test_import_cross_plan_default_decisions_identical_vs_modified(self): + """ + Verify that: + - When pole & cable position and model are identical: status is 'identical', + default proposed decision in HTML is 'Ne rien faire (Identique)'. + - When pole position changed (DXF shift): status is 'modified', + default proposed decision in HTML is 'Mettre à jour l'asset'. + - When connected pole moved, the cable status becomes 'modified', + default proposed decision in HTML is 'Mettre à jour l'asset'. + """ + import openpyxl + import tempfile + from unittest.mock import patch + from django.contrib.gis.geos import Point as DjangoPoint, LineString as DjangoLineString + from assets.models import ( + TrafficLightPole, TrafficLightCable, + TrafficLightPoleModel, TrafficLightCableModel + ) + from django.core.files.uploadedfile import SimpleUploadedFile + + pole_model = TrafficLightPoleModel.objects.first() + cable_model = TrafficLightCableModel.objects.first() + + # Pole A01: position will be identical (DXF matches DB) + pole_a = TrafficLightPole.objects.create( + intersection=self.intersection, + code="SWB01_A01", + name_fr="Poteau A01", + geom=DjangoPoint(149200.0, 170500.0, srid=3812), + lon=4.350, + lat=50.840, + model=pole_model, + status="active" + ) + # Pole B01: position will change (DXF differs by 2.0m) + pole_b = TrafficLightPole.objects.create( + intersection=self.intersection, + code="SWB01_B01", + name_fr="Poteau B01", + geom=DjangoPoint(149300.0, 170600.0, srid=3812), + lon=4.351, + lat=50.841, + model=pole_model, + status="active" + ) + # Cable 01: connects A01 and B01 + cable_01 = TrafficLightCable.objects.create( + intersection=self.intersection, + code="SWB01_CAB01", + name_fr="Câble 01", + geom=DjangoLineString([(149200.0, 170500.0), (149300.0, 170600.0)], srid=3812), + model=cable_model, + status="active" + ) + + # Create mock workbook + 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=f"Câble 01 {cable_model.name_fr}") + 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=2, value="1") + ws.cell(row=8, column=3, value="A") + ws.cell(row=8, column=4, value="V") + 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) + + # Mock DXF poles: A01 identical (0.0m diff), B01 shifted by 2.0m + mock_dxf_poles = [ + {'label': 'A01', 'x_3812': 149200.0, 'y_3812': 170500.0, 'lon': 4.350, 'lat': 50.840}, + {'label': 'B01', 'x_3812': 149302.0, 'y_3812': 170600.0, 'lon': 4.35102, 'lat': 50.841}, + ] + + try: + self.client.force_login(self.user) + with open(tmp_path, "rb") as f: + uploaded_file = SimpleUploadedFile("plan_test_decisions.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + with patch('assets.views.trafficlights.resolve_dxf_file_path', return_value='/dummy/test.dxf'), \ + patch('assets.views.trafficlights.parse_dxf_poles', return_value=mock_dxf_poles): + url_import = reverse("assets:import_cross_plan", args=[self.intersection.id]) + response = self.client.post(url_import, { + "cross_plan_file_upload": uploaded_file, + "selected_dxf": "test.dxf" + }) + self.assertEqual(response.status_code, 200) + + # Context assertions + poles = response.context["poles"] + cables = response.context["cables"] + + p_a = next(p for p in poles if p["code"] == "SWB01_A01") + p_b = next(p for p in poles if p["code"] == "SWB01_B01") + c_1 = next(c for c in cables if c["code"] == "SWB01_CAB01") + + # Pole A: Identical position & model + self.assertEqual(p_a["status"], "identical") + self.assertFalse(p_a["geom_modified"]) + + # Pole B: Position shifted + self.assertEqual(p_b["status"], "modified") + self.assertTrue(p_b["geom_modified"]) + self.assertEqual(p_b["distance_diff"], 2.0) + + # Cable 1: Connected pole B01 moved -> Cable position is modified! + self.assertEqual(c_1["status"], "modified") + self.assertTrue(c_1["geom_modified"]) + self.assertTrue(any("déplacement de poteaux connectés" in d for d in c_1["details_diff"])) + + # HTML rendered assertions + content = response.content.decode("utf-8") + # Pole A should have 'keep' (Ne rien faire) selected + self.assertIn('name="action_pole_SWB01_A01"', content) + self.assertRegex(content, r']*>[\s\S]*?