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' |