feat: support DXF-based pole alignment in traffic light imports
This commit is contained in:
parent
6e4a304930
commit
d73a9e482d
2 changed files with 612 additions and 37 deletions
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
|
@ -147,10 +148,10 @@
|
|||
<!-- Hidden input fields for poles coordinates -->
|
||||
{% for item in poles %}
|
||||
{% if item.status != 'db_only' %}
|
||||
<input type="hidden" name="lon_pole_{{ item.code }}" id="lon_pole_{{ item.code }}" value="">
|
||||
<input type="hidden" name="lat_pole_{{ item.code }}" id="lat_pole_{{ item.code }}" value="">
|
||||
<input type="hidden" name="x_3812_pole_{{ item.code }}" id="x_3812_pole_{{ item.code }}" value="">
|
||||
<input type="hidden" name="y_3812_pole_{{ item.code }}" id="y_3812_pole_{{ item.code }}" value="">
|
||||
<input type="hidden" name="lon_pole_{{ item.code }}" id="lon_pole_{{ item.code }}" value="{{ item.new_lon|default_if_none:''|unlocalize }}">
|
||||
<input type="hidden" name="lat_pole_{{ item.code }}" id="lat_pole_{{ item.code }}" value="{{ item.new_lat|default_if_none:''|unlocalize }}">
|
||||
<input type="hidden" name="x_3812_pole_{{ item.code }}" id="x_3812_pole_{{ item.code }}" value="{{ item.new_x_3812|default_if_none:''|unlocalize }}">
|
||||
<input type="hidden" name="y_3812_pole_{{ item.code }}" id="y_3812_pole_{{ item.code }}" value="{{ item.new_y_3812|default_if_none:''|unlocalize }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
|
@ -187,11 +188,22 @@
|
|||
<i class="bi bi-dash-square me-1"></i> {% translate "Tout décocher" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm rounded-3 shadow-sm me-1" role="group">
|
||||
<button type="button" class="btn btn-outline-success" id="btn-batch-update" title="{% translate 'Définir toutes les décisions sur Mettre à jour' %}">
|
||||
<i class="bi bi-pencil-square me-1"></i> {% translate "Tout Mettre à jour" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning text-dark" id="btn-batch-replace" title="{% translate 'Définir toutes les décisions sur Remplacer' %}">
|
||||
<i class="bi bi-arrow-repeat me-1"></i> {% translate "Tout Remplacer" %}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm rounded-3 shadow-sm px-3" onclick="triggerGlobalAction('update_all')">
|
||||
<i class="bi bi-arrow-repeat me-1"></i> {% translate "Tout mettre à jour (Conserver existant + MAJ)" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm rounded-3 shadow-sm px-3" onclick="triggerGlobalAction('replace_all')">
|
||||
<i class="bi bi-trash3-fill"></i> {% translate "Tout remplacer (Archiver l'existant + Recréer)" %}
|
||||
<i class="bi bi-trash3-fill me-1"></i> {% translate "Tout remplacer (Archiver l'existant + Recréer)" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm rounded-3 shadow-sm px-4">
|
||||
<i class="bi bi-check2-circle"></i> {% translate "Appliquer les décisions" %}
|
||||
<i class="bi bi-check2-circle me-1"></i> {% translate "Appliquer les décisions" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -253,6 +265,7 @@
|
|||
<th>{% translate "Code Asset" %}</th>
|
||||
<th>{% translate "Nom" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th>{% translate "Position / Géométrie" %}</th>
|
||||
<th>{% translate "Modèle Déduit (Excel)" %}</th>
|
||||
<th>{% translate "Modèle Actuel (DB)" %}</th>
|
||||
<th>{% translate "Décision" %}</th>
|
||||
|
|
@ -283,6 +296,45 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td id="geom_cell_pole_{{ item.code }}">
|
||||
{% if item.geom_modified %}
|
||||
<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1">
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
{% if item.distance_diff is not None %}
|
||||
{% translate "Décalage" %} {{ item.distance_diff }} m
|
||||
{% else %}
|
||||
{% translate "Position modifiée" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<button type="button" class="btn btn-xs btn-outline-primary ms-1 show-pole-on-map-btn" data-code="{{ item.code }}" title="{% translate 'Voir et ajuster sur la carte' %}">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% elif item.new_lon is not None %}
|
||||
{% if item.status == 'new' %}
|
||||
<span class="badge bg-success d-inline-flex align-items-center gap-1">
|
||||
<i class="bi bi-geo-alt-fill"></i> {% if item.new_x_3812 %}DXF{% else %}{% translate "Plan" %}{% endif %}
|
||||
</span>
|
||||
{% elif item.status == 'db_only' %}
|
||||
<span class="badge bg-secondary d-inline-flex align-items-center gap-1">
|
||||
<i class="bi bi-database"></i> DB
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-muted border d-inline-flex align-items-center gap-1">
|
||||
<i class="bi bi-check2"></i> {% translate "Inchangée" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-xs btn-outline-secondary ms-1 show-pole-on-map-btn" data-code="{{ item.code }}" title="{% translate 'Voir sur la carte' %}">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="badge bg-danger d-inline-flex align-items-center gap-1">
|
||||
<i class="bi bi-geo-alt"></i> {% translate "Non positionné" %}
|
||||
</span>
|
||||
<button type="button" class="btn btn-xs btn-outline-primary ms-1 show-pole-on-map-btn" data-code="{{ item.code }}" title="{% translate 'Positionner sur la carte' %}">
|
||||
<i class="bi bi-crosshair"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||
<td>
|
||||
|
|
@ -302,6 +354,7 @@
|
|||
<option value="keep">{% translate "Conserver existant" %}</option>
|
||||
{% elif item.status == 'identical' %}
|
||||
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
||||
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
|
|
@ -328,7 +381,7 @@
|
|||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">{% translate "Aucun poteau détecté." %}</td>
|
||||
<td colspan="9" class="text-center py-4 text-muted">{% translate "Aucun poteau détecté." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -544,6 +597,7 @@
|
|||
<th>{% translate "Code Asset" %}</th>
|
||||
<th>{% translate "Nom" %}</th>
|
||||
<th>{% translate "Statut" %}</th>
|
||||
<th>{% translate "Géométrie (DB)" %}</th>
|
||||
<th>{% translate "Modèle Déduit (Excel)" %}</th>
|
||||
<th>{% translate "Modèle Actuel (DB)" %}</th>
|
||||
<th>{% translate "Décision" %}</th>
|
||||
|
|
@ -574,6 +628,15 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.has_db_geom %}
|
||||
<span class="badge bg-info text-dark d-inline-flex align-items-center gap-1">
|
||||
<i class="bi bi-bezier2"></i> {{ item.db_geom_length }} m
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></td>
|
||||
<td><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||
<td>
|
||||
|
|
@ -593,6 +656,7 @@
|
|||
<option value="keep">{% translate "Conserver existant" %}</option>
|
||||
{% elif item.status == 'identical' %}
|
||||
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
|
||||
<option value="modify">{% translate "Mettre à jour l'asset" %}</option>
|
||||
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
|
|
@ -619,7 +683,7 @@
|
|||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-4 text-muted">{% translate "Aucun câble détecté." %}</td>
|
||||
<td colspan="9" class="text-center py-4 text-muted">{% translate "Aucun câble détecté." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -984,6 +1048,11 @@
|
|||
document.getElementById('global_action_input').value = 'replace_all';
|
||||
document.getElementById('combined-import-form').submit();
|
||||
}
|
||||
} else if (actionType === 'update_all') {
|
||||
if (confirm("{% translate 'Voulez-vous mettre à jour les assets existants sans les archiver, et créer les nouveaux assets présents dans le plan ? Les géométries et modèles modifiés seront appliqués.' %}")) {
|
||||
document.getElementById('global_action_input').value = 'update_all';
|
||||
document.getElementById('combined-import-form').submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1031,6 +1100,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
btnUncheckAllGlobal.addEventListener('click', () => setAllCheckboxesState(false));
|
||||
}
|
||||
|
||||
// Batch Decision Buttons (Tout Mettre à jour / Tout Remplacer)
|
||||
const btnBatchUpdate = document.getElementById('btn-batch-update');
|
||||
if (btnBatchUpdate) {
|
||||
btnBatchUpdate.addEventListener('click', () => {
|
||||
const allDecisionSelects = document.querySelectorAll('select[name^="action_pole_"], select[name^="action_cable_"], select[name^="action_lantern_"], select[name^="action_detector_"]');
|
||||
allDecisionSelects.forEach(sel => {
|
||||
if (sel.name.includes('_db_')) {
|
||||
if (sel.querySelector('option[value="keep"]')) sel.value = 'keep';
|
||||
} else {
|
||||
if (sel.querySelector('option[value="modify"]')) {
|
||||
sel.value = 'modify';
|
||||
} else if (sel.querySelector('option[value="add"]')) {
|
||||
sel.value = 'add';
|
||||
} else if (sel.querySelector('option[value="keep"]')) {
|
||||
sel.value = 'keep';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const btnBatchReplace = document.getElementById('btn-batch-replace');
|
||||
if (btnBatchReplace) {
|
||||
btnBatchReplace.addEventListener('click', () => {
|
||||
const allDecisionSelects = document.querySelectorAll('select[name^="action_pole_"], select[name^="action_cable_"], select[name^="action_lantern_"], select[name^="action_detector_"]');
|
||||
allDecisionSelects.forEach(sel => {
|
||||
if (sel.name.includes('_db_')) {
|
||||
if (sel.querySelector('option[value="archive"]')) sel.value = 'archive';
|
||||
} else {
|
||||
if (sel.querySelector('option[value="replace"]')) {
|
||||
sel.value = 'replace';
|
||||
} else if (sel.querySelector('option[value="add"]')) {
|
||||
sel.value = 'add';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Interactive Switch Logic between Table View and Map View
|
||||
const tableView = document.getElementById('table-view');
|
||||
const mapView = document.getElementById('map-view');
|
||||
|
|
@ -1089,7 +1197,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
{
|
||||
code: "{{ item.code|escapejs }}",
|
||||
short_code: "{{ item.short_code|escapejs }}",
|
||||
status: "{{ item.status|escapejs }}"
|
||||
status: "{{ item.status|escapejs }}",
|
||||
new_lon: {% if item.new_lon is not None %}{{ item.new_lon|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
new_lat: {% if item.new_lat is not None %}{{ item.new_lat|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
new_x_3812: {% if item.new_x_3812 is not None %}{{ item.new_x_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
new_y_3812: {% if item.new_y_3812 is not None %}{{ item.new_y_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
db_lon: {% if item.db_lon is not None %}{{ item.db_lon|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
db_lat: {% if item.db_lat is not None %}{{ item.db_lat|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
db_x_3812: {% if item.db_x_3812 is not None %}{{ item.db_x_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
db_y_3812: {% if item.db_y_3812 is not None %}{{ item.db_y_3812|default_if_none:''|unlocalize }}{% else %}null{% endif %},
|
||||
geom_modified: {{ item.geom_modified|yesno:"true,false" }},
|
||||
distance_diff: {% if item.distance_diff is not None %}{{ item.distance_diff|default_if_none:''|unlocalize }}{% else %}null{% endif %}
|
||||
},
|
||||
{% endfor %}
|
||||
];
|
||||
|
|
@ -1409,9 +1527,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
loadDxfBackground(window.dxfFile);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
updatePolesCounter();
|
||||
});
|
||||
|
||||
// Delegated click handler for "Voir sur la carte" buttons in table
|
||||
document.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.show-pole-on-map-btn');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
const code = btn.dataset.code;
|
||||
|
||||
tableView.classList.remove('d-block');
|
||||
tableView.classList.add('d-none');
|
||||
mapView.classList.remove('d-none');
|
||||
mapView.classList.add('d-block');
|
||||
|
||||
if (map) {
|
||||
map.resize();
|
||||
const tabEl = document.getElementById('map-poles-tab');
|
||||
if (tabEl) new bootstrap.Tab(tabEl).show();
|
||||
|
||||
if (placedPoles[code]) {
|
||||
map.flyTo({ center: [placedPoles[code].lon, placedPoles[code].lat], zoom: 19 });
|
||||
} else {
|
||||
activatePlacementMode(code);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getSnappedCoords(lon, lat) {
|
||||
let bestDistance = Infinity;
|
||||
let bestCoords = [lon, lat];
|
||||
|
|
@ -1443,10 +1593,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
badge.textContent = `{% translate "Positionné" %}`;
|
||||
}
|
||||
|
||||
document.getElementById(`lon_pole_${code}`).value = lon;
|
||||
document.getElementById(`lat_pole_${code}`).value = lat;
|
||||
document.getElementById(`x_3812_pole_${code}`).value = x_3812 !== undefined && x_3812 !== null ? x_3812 : "";
|
||||
document.getElementById(`y_3812_pole_${code}`).value = y_3812 !== undefined && y_3812 !== null ? y_3812 : "";
|
||||
const inputLon = document.getElementById(`lon_pole_${code}`);
|
||||
if (inputLon) inputLon.value = lon;
|
||||
const inputLat = document.getElementById(`lat_pole_${code}`);
|
||||
if (inputLat) inputLat.value = lat;
|
||||
const inputX = document.getElementById(`x_3812_pole_${code}`);
|
||||
if (inputX) inputX.value = (x_3812 !== undefined && x_3812 !== null) ? x_3812 : "";
|
||||
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';
|
||||
}
|
||||
}
|
||||
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 = `<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1"><i class="bi bi-geo-alt-fill"></i> {% translate "Positionné sur carte" %}${distText}</span> <button type="button" class="btn btn-xs btn-outline-primary ms-1 show-pole-on-map-btn" data-code="${code}" title="{% translate 'Voir et ajuster sur la carte' %}"><i class="bi bi-geo-alt"></i></button>`;
|
||||
}
|
||||
}
|
||||
|
||||
redrawCablesOnMap();
|
||||
updatePolesCounter();
|
||||
|
|
@ -1462,7 +1636,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
placePoleMarker(activePlacementPole, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1]);
|
||||
});
|
||||
|
||||
function placePoleMarker(code, lon, lat, x_3812, y_3812) {
|
||||
function placePoleMarker(code, lon, lat, x_3812, y_3812, autoAdvance = true) {
|
||||
if (placedPoles[code]) {
|
||||
placedPoles[code].marker.remove();
|
||||
}
|
||||
|
|
@ -1516,6 +1690,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
listItem.classList.add('bg-light');
|
||||
}
|
||||
|
||||
if (autoAdvance) {
|
||||
const unplaced = polesList.filter(p => p.status !== 'db_only' && !placedPoles[p.code]);
|
||||
if (unplaced.length > 0) {
|
||||
activatePlacementMode(unplaced[0].code);
|
||||
|
|
@ -1523,6 +1698,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
deactivatePlacementMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activatePlacementMode(code) {
|
||||
deactivateCableEditingMode();
|
||||
|
|
|
|||
|
|
@ -1922,6 +1922,54 @@ def get_base_dxf_name(filename):
|
|||
return f"{base_cleaned}{ext}".lower()
|
||||
|
||||
|
||||
def resolve_dxf_file_path(intersection, filename):
|
||||
if not filename:
|
||||
return None
|
||||
filename_clean = os.path.basename(filename)
|
||||
|
||||
# 1. Managed documents
|
||||
try:
|
||||
from documents.models import DocumentAttachment, DocumentVersion
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
ct = ContentType.objects.get_for_model(intersection)
|
||||
doc_ids = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id).values_list('document_id', flat=True)
|
||||
versions = DocumentVersion.objects.filter(document_id__in=doc_ids, file__icontains=filename_clean)
|
||||
for v in versions:
|
||||
if v.file and os.path.exists(v.file.path):
|
||||
return v.file.path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Database documents (legacy)
|
||||
try:
|
||||
for d in intersection.documents.all():
|
||||
if os.path.basename(d.file.name) == filename_clean and os.path.exists(d.file.path):
|
||||
return d.file.path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Private media dxf_files
|
||||
uploaded_dxf_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_files')
|
||||
p = os.path.join(uploaded_dxf_dir, filename_clean)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
|
||||
# 4. External read-only files
|
||||
read_only_dxf_dir = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files')
|
||||
p = os.path.join(read_only_dxf_dir, filename_clean)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
|
||||
# 5. Dxf temp
|
||||
dxf_temp_dir = os.path.join(settings.PRIVATE_MEDIA_ROOT, 'dxf_temp')
|
||||
p = os.path.join(dxf_temp_dir, filename_clean)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@check_thematic_access('trafficlights', edit_permission_required=True)
|
||||
def import_dxf(request, intersection_id):
|
||||
intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id)
|
||||
|
|
@ -4078,43 +4126,142 @@ def import_cross_plan(request, intersection_id):
|
|||
|
||||
deduced = merged_deduced
|
||||
|
||||
# ── RESOLVE ACTIVE DXF & SNAP POINTS ──
|
||||
active_dxf_file = request.POST.get('selected_dxf') or (dxf_files[0] if dxf_files else "")
|
||||
active_dxf_poles = []
|
||||
dxf_poles_by_label = {}
|
||||
if active_dxf_file:
|
||||
dxf_path = resolve_dxf_file_path(intersection, active_dxf_file)
|
||||
if dxf_path:
|
||||
try:
|
||||
active_dxf_poles = parse_dxf_poles(dxf_path)
|
||||
for dp_dxf in active_dxf_poles:
|
||||
lbl = (dp_dxf.get('label') or "").strip().upper()
|
||||
if lbl:
|
||||
dxf_poles_by_label[lbl] = dp_dxf
|
||||
except Exception as e:
|
||||
logger.warning("Could not parse DXF poles from %s: %s", dxf_path, e)
|
||||
|
||||
# ── 1. POLES COMPARISON ──
|
||||
existing_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
|
||||
compared_poles = []
|
||||
from pyproj import Transformer
|
||||
transformer_3812_to_4326 = Transformer.from_crs(3812, 4326, always_xy=True)
|
||||
transformer_4326_to_3812 = Transformer.from_crs(4326, 3812, always_xy=True)
|
||||
|
||||
for dp in deduced["poles"]:
|
||||
code = dp["code"]
|
||||
short_code = dp["short_code"]
|
||||
model_name = dp["model"]
|
||||
db_model = TrafficLightPoleModel.objects.filter(name_fr__iexact=model_name).first() or TrafficLightPoleModel.objects.first()
|
||||
|
||||
dxf_match = dxf_poles_by_label.get(short_code.strip().upper())
|
||||
dxf_lon = dxf_match['lon'] if dxf_match else None
|
||||
dxf_lat = dxf_match['lat'] if dxf_match else None
|
||||
dxf_x_3812 = dxf_match['x_3812'] if dxf_match else None
|
||||
dxf_y_3812 = dxf_match['y_3812'] if dxf_match else None
|
||||
|
||||
if code in existing_poles:
|
||||
ep = existing_poles[code]
|
||||
# Since cross plans do not specify pole physical models (height, color), keep existing DB model
|
||||
status = 'identical'
|
||||
ep_lon = ep.lon
|
||||
ep_lat = ep.lat
|
||||
ep_x_3812 = ep.geom.x if ep.geom else None
|
||||
ep_y_3812 = ep.geom.y if ep.geom else None
|
||||
|
||||
if ep.geom and (ep_lon is None or ep_lat is None):
|
||||
try:
|
||||
ep_lon, ep_lat = transformer_3812_to_4326.transform(ep_x_3812, ep_y_3812)
|
||||
except Exception:
|
||||
pass
|
||||
elif ep.geom is None and ep_lon is not None and ep_lat is not None:
|
||||
try:
|
||||
ep_x_3812, ep_y_3812 = transformer_4326_to_3812.transform(ep_lon, ep_lat)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
details_diff = []
|
||||
geom_modified = False
|
||||
distance_diff = None
|
||||
|
||||
if dxf_match:
|
||||
if ep_x_3812 is not None and ep_y_3812 is not None:
|
||||
import math
|
||||
dist = math.sqrt((ep_x_3812 - dxf_x_3812)**2 + (ep_y_3812 - dxf_y_3812)**2)
|
||||
if dist >= 0.05:
|
||||
geom_modified = True
|
||||
distance_diff = round(dist, 1)
|
||||
details_diff.append(_("Position modifiée (DXF) : décalage de %(dist).1f m") % {'dist': dist})
|
||||
else:
|
||||
geom_modified = True
|
||||
details_diff.append(_("Nouvelle position détectée (DXF)"))
|
||||
|
||||
is_model_diff = (ep.model_id != db_model.id) if (ep.model and db_model) else False
|
||||
if is_model_diff:
|
||||
details_diff.append(_("Modèle : %(db_m)s → %(new_m)s") % {'db_m': ep.model.name_fr if ep.model else 'Aucun', 'new_m': model_name})
|
||||
|
||||
status = 'modified' if (geom_modified or is_model_diff) else 'identical'
|
||||
compared_poles.append({
|
||||
'code': code,
|
||||
'short_code': dp['short_code'],
|
||||
'name': ep.name_fr or dp.get('name', dp['short_code']),
|
||||
'short_code': short_code,
|
||||
'name': ep.name_fr or dp.get('name', short_code),
|
||||
'status': status,
|
||||
'deduced_model_name': ep.model.name_fr if ep.model else model_name,
|
||||
'deduced_model_name': model_name,
|
||||
'db_model_name': ep.model.name_fr if ep.model else '',
|
||||
'db_id': ep.id,
|
||||
'details_diff': []
|
||||
'details_diff': details_diff,
|
||||
'geom_modified': geom_modified,
|
||||
'distance_diff': distance_diff,
|
||||
'db_lon': ep_lon,
|
||||
'db_lat': ep_lat,
|
||||
'db_x_3812': ep_x_3812,
|
||||
'db_y_3812': ep_y_3812,
|
||||
'new_lon': dxf_lon if dxf_match else ep_lon,
|
||||
'new_lat': dxf_lat if dxf_match else ep_lat,
|
||||
'new_x_3812': dxf_x_3812 if dxf_match else ep_x_3812,
|
||||
'new_y_3812': dxf_y_3812 if dxf_match else ep_y_3812,
|
||||
})
|
||||
# remove from processed
|
||||
existing_poles.pop(code)
|
||||
else:
|
||||
details_diff = []
|
||||
if dxf_match:
|
||||
details_diff.append(_("Positionné via DXF"))
|
||||
compared_poles.append({
|
||||
'code': code,
|
||||
'short_code': dp['short_code'],
|
||||
'name': dp.get('name', dp['short_code']),
|
||||
'short_code': short_code,
|
||||
'name': dp.get('name', short_code),
|
||||
'status': 'new',
|
||||
'deduced_model_name': model_name,
|
||||
'db_model_name': '',
|
||||
'db_id': None,
|
||||
'details_diff': []
|
||||
'details_diff': details_diff,
|
||||
'geom_modified': False,
|
||||
'distance_diff': None,
|
||||
'db_lon': None,
|
||||
'db_lat': None,
|
||||
'db_x_3812': None,
|
||||
'db_y_3812': None,
|
||||
'new_lon': dxf_lon,
|
||||
'new_lat': dxf_lat,
|
||||
'new_x_3812': dxf_x_3812,
|
||||
'new_y_3812': dxf_y_3812,
|
||||
})
|
||||
# Add remaining existing active poles (orphans)
|
||||
for code, ep in existing_poles.items():
|
||||
ep_lon = ep.lon
|
||||
ep_lat = ep.lat
|
||||
ep_x_3812 = ep.geom.x if ep.geom else None
|
||||
ep_y_3812 = ep.geom.y if ep.geom else None
|
||||
if ep.geom and (ep_lon is None or ep_lat is None):
|
||||
try:
|
||||
ep_lon, ep_lat = transformer_3812_to_4326.transform(ep_x_3812, ep_y_3812)
|
||||
except Exception:
|
||||
pass
|
||||
elif ep.geom is None and ep_lon is not None and ep_lat is not None:
|
||||
try:
|
||||
ep_x_3812, ep_y_3812 = transformer_4326_to_3812.transform(ep_lon, ep_lat)
|
||||
except Exception:
|
||||
pass
|
||||
compared_poles.append({
|
||||
'code': code,
|
||||
'short_code': code.split('_')[-1] if '_' in code else code,
|
||||
|
|
@ -4123,9 +4270,20 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': '',
|
||||
'db_model_name': ep.model.name_fr if ep.model else '',
|
||||
'db_id': ep.id,
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")],
|
||||
'geom_modified': False,
|
||||
'distance_diff': None,
|
||||
'db_lon': ep_lon,
|
||||
'db_lat': ep_lat,
|
||||
'db_x_3812': ep_x_3812,
|
||||
'db_y_3812': ep_y_3812,
|
||||
'new_lon': ep_lon,
|
||||
'new_lat': ep_lat,
|
||||
'new_x_3812': ep_x_3812,
|
||||
'new_y_3812': ep_y_3812,
|
||||
})
|
||||
|
||||
|
||||
# ── 2. CABLES COMPARISON ──
|
||||
existing_cables = {c.code: c for c in TrafficLightCable.objects.filter(intersection=intersection, status='active')}
|
||||
compared_cables = []
|
||||
|
|
@ -4138,6 +4296,7 @@ def import_cross_plan(request, intersection_id):
|
|||
ec = existing_cables[code]
|
||||
is_diff = ec.model_id != db_model.id
|
||||
status = 'modified' if is_diff else 'identical'
|
||||
has_geom = ec.geom is not None
|
||||
compared_cables.append({
|
||||
'code': code,
|
||||
'name': ec.name_fr or dc['name'],
|
||||
|
|
@ -4146,7 +4305,9 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': ec.model.name_fr if ec.model else '',
|
||||
'db_id': ec.id,
|
||||
'details_diff': [f"Modèle : {ec.model.name_fr if ec.model else 'Aucun'} → {model_name}"] if is_diff else [],
|
||||
'connected_poles': dc.get("connected_poles", [])
|
||||
'connected_poles': dc.get("connected_poles", []),
|
||||
'has_db_geom': has_geom,
|
||||
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
||||
})
|
||||
existing_cables.pop(code)
|
||||
else:
|
||||
|
|
@ -4158,9 +4319,12 @@ def import_cross_plan(request, intersection_id):
|
|||
'db_model_name': '',
|
||||
'db_id': None,
|
||||
'details_diff': [],
|
||||
'connected_poles': dc.get("connected_poles", [])
|
||||
'connected_poles': dc.get("connected_poles", []),
|
||||
'has_db_geom': False,
|
||||
'db_geom_length': None,
|
||||
})
|
||||
for code, ec in existing_cables.items():
|
||||
has_geom = ec.geom is not None
|
||||
compared_cables.append({
|
||||
'code': code,
|
||||
'name': ec.name_fr or code,
|
||||
|
|
@ -4168,7 +4332,9 @@ def import_cross_plan(request, intersection_id):
|
|||
'deduced_model_name': '',
|
||||
'db_model_name': ec.model.name_fr if ec.model else '',
|
||||
'db_id': ec.id,
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")]
|
||||
'details_diff': [_("Absent du fichier Excel (présent en DB)")],
|
||||
'has_db_geom': has_geom,
|
||||
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
||||
})
|
||||
|
||||
# ── 3. LANTERNS COMPARISON ──
|
||||
|
|
@ -4526,8 +4692,8 @@ def import_cross_plan(request, intersection_id):
|
|||
'cable_models': cable_models,
|
||||
'step': 'preview',
|
||||
'thematic_code': 'trafficlights',
|
||||
'dxf_file': "",
|
||||
'dxf_poles': [],
|
||||
'dxf_file': active_dxf_file,
|
||||
'dxf_poles': active_dxf_poles,
|
||||
'existing_poles_geojson': existing_poles_geojson,
|
||||
'dxf_files': dxf_files,
|
||||
'georeferenced_documents_json': georeferenced_documents_json,
|
||||
|
|
@ -4621,6 +4787,40 @@ def integrate_cross_plan(request, intersection_id):
|
|||
)
|
||||
return m
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from documents.models import DocumentAttachment, ManagedDocument
|
||||
|
||||
dxf_files = []
|
||||
ct = ContentType.objects.get_for_model(intersection)
|
||||
attachments = DocumentAttachment.objects.filter(content_type=ct, object_id=intersection.id)
|
||||
managed_dxf_docs = ManagedDocument.objects.filter(
|
||||
id__in=attachments.values_list('document_id', flat=True),
|
||||
is_archived=False
|
||||
).select_related('current_version')
|
||||
for doc in managed_dxf_docs:
|
||||
if doc.current_version and doc.current_version.file:
|
||||
fn = os.path.basename(doc.current_version.file.name)
|
||||
if fn.lower().endswith('.dxf') and fn not in dxf_files:
|
||||
dxf_files.append(fn)
|
||||
for doc in intersection.documents.all():
|
||||
if doc.file.name.lower().endswith('.dxf'):
|
||||
fn = os.path.basename(doc.file.name)
|
||||
if fn not in dxf_files:
|
||||
dxf_files.append(fn)
|
||||
|
||||
active_dxf_file = request.POST.get('selected_dxf') or (dxf_files[0] if dxf_files else "")
|
||||
dxf_poles_by_label = {}
|
||||
if active_dxf_file:
|
||||
dxf_path = resolve_dxf_file_path(intersection, active_dxf_file)
|
||||
if dxf_path:
|
||||
try:
|
||||
for dp_dxf in parse_dxf_poles(dxf_path):
|
||||
lbl = (dp_dxf.get('label') or "").strip().upper()
|
||||
if lbl:
|
||||
dxf_poles_by_label[lbl] = dp_dxf
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def update_pole_geometry(pole, code):
|
||||
def clean_float(val):
|
||||
if not val:
|
||||
|
|
@ -4635,9 +4835,22 @@ def integrate_cross_plan(request, intersection_id):
|
|||
x_3812 = clean_float(request.POST.get(f"x_3812_pole_{code}"))
|
||||
y_3812 = clean_float(request.POST.get(f"y_3812_pole_{code}"))
|
||||
|
||||
if x_3812 is None and lon is None:
|
||||
short_code = code.split('_')[-1] if '_' in code else code
|
||||
dxf_match = dxf_poles_by_label.get(short_code.strip().upper())
|
||||
if dxf_match:
|
||||
lon = dxf_match.get('lon')
|
||||
lat = dxf_match.get('lat')
|
||||
x_3812 = dxf_match.get('x_3812')
|
||||
y_3812 = dxf_match.get('y_3812')
|
||||
|
||||
if x_3812 is not None and y_3812 is not None and x_3812 != 0.0 and y_3812 != 0.0:
|
||||
from django.contrib.gis.geos import Point as DjangoPoint
|
||||
from pyproj import Transformer
|
||||
pole.geom = DjangoPoint(x_3812, y_3812, srid=3812)
|
||||
if lon is None or lat is None:
|
||||
transformer = Transformer.from_crs(3812, 4326, always_xy=True)
|
||||
lon, lat = transformer.transform(x_3812, y_3812)
|
||||
pole.lon = lon
|
||||
pole.lat = lat
|
||||
elif lon is not None and lat is not None:
|
||||
|
|
@ -4762,7 +4975,193 @@ def integrate_cross_plan(request, intersection_id):
|
|||
|
||||
messages.success(request, "Tous les anciens assets ont été archivés et les nouveaux ont été créés sur base du plan de croix.")
|
||||
|
||||
# B. SELECTIVE OR INDIVIDUAL INTEGRATION
|
||||
# B. GLOBAL UPDATE ALL (Preserve existing + Update in-place + Create new)
|
||||
elif global_action == 'update_all':
|
||||
# --- 1. Poles ---
|
||||
created_poles = {}
|
||||
for dp in deduced["poles"]:
|
||||
code = dp["code"]
|
||||
short_code = dp.get('short_code', code.split('_')[-1])
|
||||
existing = TrafficLightPole.objects.filter(intersection=intersection, code=code, status='active').first()
|
||||
if not existing:
|
||||
db_id = request.POST.get(f"db_id_pole_{code}")
|
||||
if db_id:
|
||||
existing = TrafficLightPole.objects.filter(pk=db_id, status='active').first()
|
||||
if not existing:
|
||||
existing = TrafficLightPole.objects.filter(intersection=intersection, code__endswith=f"_{short_code}", status='active').first()
|
||||
|
||||
selected_model_id = request.POST.get(f"model_pole_{code}")
|
||||
db_model = None
|
||||
if selected_model_id:
|
||||
db_model = TrafficLightPoleModel.objects.filter(pk=selected_model_id).first()
|
||||
if not db_model:
|
||||
db_model = resolve_pole_model(dp["model"])
|
||||
|
||||
custom_name = request.POST.get(f"name_pole_{code}", dp.get('name', short_code))
|
||||
|
||||
if existing:
|
||||
existing.code = code
|
||||
existing.name_fr = custom_name
|
||||
existing.name_nl = custom_name
|
||||
if db_model:
|
||||
existing.model = db_model
|
||||
existing.updated_by = request.user
|
||||
update_pole_geometry(existing, code)
|
||||
existing.save()
|
||||
created_poles[code] = existing
|
||||
else:
|
||||
new_pole = TrafficLightPole(
|
||||
intersection=intersection,
|
||||
code=code,
|
||||
name_fr=custom_name,
|
||||
name_nl=custom_name,
|
||||
model=db_model,
|
||||
status='active',
|
||||
validation_status='validated',
|
||||
updated_by=request.user
|
||||
)
|
||||
update_pole_geometry(new_pole, code)
|
||||
new_pole.save()
|
||||
created_poles[code] = new_pole
|
||||
|
||||
# Map active poles for lanterns and detectors parenting
|
||||
active_poles = {p.code: p for p in TrafficLightPole.objects.filter(intersection=intersection, status='active')}
|
||||
for c, p in created_poles.items():
|
||||
active_poles[c] = p
|
||||
|
||||
# --- 2. Cables ---
|
||||
for dc in deduced["cables"]:
|
||||
code = dc["code"]
|
||||
existing = TrafficLightCable.objects.filter(intersection=intersection, code=code, status='active').first()
|
||||
if not existing:
|
||||
db_id = request.POST.get(f"db_id_cable_{code}")
|
||||
if db_id:
|
||||
existing = TrafficLightCable.objects.filter(pk=db_id, status='active').first()
|
||||
|
||||
selected_model_id = request.POST.get(f"model_cable_{code}")
|
||||
db_model = None
|
||||
if selected_model_id:
|
||||
db_model = TrafficLightCableModel.objects.filter(pk=selected_model_id).first()
|
||||
if not db_model:
|
||||
db_model = resolve_cable_model(dc["model_name"])
|
||||
|
||||
custom_name = request.POST.get(f"name_cable_{code}", dc["name"])
|
||||
if existing:
|
||||
existing.code = code
|
||||
existing.name_fr = custom_name
|
||||
existing.name_nl = custom_name
|
||||
if db_model:
|
||||
existing.model = db_model
|
||||
existing.updated_by = request.user
|
||||
update_cable_geometry(existing, code)
|
||||
existing.save()
|
||||
else:
|
||||
new_cable = TrafficLightCable(
|
||||
intersection=intersection,
|
||||
code=code,
|
||||
name_fr=custom_name,
|
||||
name_nl=custom_name,
|
||||
model=db_model,
|
||||
status='active',
|
||||
validation_status='validated',
|
||||
updated_by=request.user
|
||||
)
|
||||
update_cable_geometry(new_cable, code)
|
||||
new_cable.save()
|
||||
|
||||
# --- 3. Lanterns ---
|
||||
for dl in deduced["lanterns"]:
|
||||
code = dl["code"]
|
||||
pole = active_poles.get(dl["pole_code"])
|
||||
existing = TrafficLightLantern.objects.filter(pole__intersection=intersection, code=code, status='active').first()
|
||||
if not existing:
|
||||
db_id = request.POST.get(f"db_id_lantern_{code}")
|
||||
if db_id:
|
||||
existing = TrafficLightLantern.objects.filter(pk=db_id, status='active').first()
|
||||
|
||||
selected_model_id = request.POST.get(f"model_lantern_{code}")
|
||||
db_model = None
|
||||
if selected_model_id:
|
||||
db_model = TrafficLightLanternModel.objects.filter(pk=selected_model_id).first()
|
||||
if not db_model:
|
||||
db_model = resolve_lantern_model(dl["model_name"])
|
||||
|
||||
custom_name = request.POST.get(f"name_lantern_{code}", dl["name"])
|
||||
if existing:
|
||||
existing.code = code
|
||||
if pole:
|
||||
existing.pole = pole
|
||||
if db_model:
|
||||
existing.model = db_model
|
||||
existing.name_fr = custom_name
|
||||
existing.name_nl = custom_name
|
||||
existing.updated_by = request.user
|
||||
existing.save()
|
||||
elif pole:
|
||||
TrafficLightLantern.objects.create(
|
||||
pole=pole,
|
||||
code=code,
|
||||
name_fr=custom_name,
|
||||
name_nl=custom_name,
|
||||
model=db_model,
|
||||
status='active',
|
||||
validation_status='validated',
|
||||
updated_by=request.user
|
||||
)
|
||||
|
||||
# --- 4. Detectors ---
|
||||
for dd in deduced["detectors"]:
|
||||
code = dd["code"]
|
||||
pole = active_poles.get(dd["pole_code"])
|
||||
existing = TrafficLightDetector.objects.filter(intersection=intersection, code=code, status='active').first()
|
||||
if not existing:
|
||||
db_id = request.POST.get(f"db_id_detector_{code}")
|
||||
if db_id:
|
||||
existing = TrafficLightDetector.objects.filter(pk=db_id, status='active').first()
|
||||
if not existing:
|
||||
parts = code.split('_', 2)
|
||||
if len(parts) == 3:
|
||||
old_key = f"{parts[0]}-{parts[2]}"
|
||||
existing = TrafficLightDetector.objects.filter(intersection=intersection, code=old_key, status='active').first()
|
||||
|
||||
selected_model_id = request.POST.get(f"model_detector_{code}")
|
||||
db_model = None
|
||||
if selected_model_id:
|
||||
db_model = TrafficLightDetectorModel.objects.filter(pk=selected_model_id).first()
|
||||
if not db_model:
|
||||
db_model = resolve_detector_model(dd["model_name"])
|
||||
|
||||
custom_name = request.POST.get(f"name_detector_{code}", dd["name"])
|
||||
if existing:
|
||||
existing.code = code
|
||||
if pole:
|
||||
existing.pole = pole
|
||||
if db_model:
|
||||
existing.model = db_model
|
||||
existing.name = custom_name
|
||||
existing.name_fr = custom_name
|
||||
existing.name_nl = custom_name
|
||||
existing.phases = dd.get("phases", "")
|
||||
existing.updated_by = request.user
|
||||
existing.save()
|
||||
else:
|
||||
TrafficLightDetector.objects.create(
|
||||
intersection=intersection,
|
||||
pole=pole,
|
||||
code=code,
|
||||
name=custom_name,
|
||||
name_fr=custom_name,
|
||||
name_nl=custom_name,
|
||||
model=db_model,
|
||||
phases=dd.get("phases", ""),
|
||||
status='active',
|
||||
validation_status='validated',
|
||||
updated_by=request.user
|
||||
)
|
||||
|
||||
messages.success(request, "Les assets existants ont été mis à jour et les nouveaux ont été créés sans archiver l'existant.")
|
||||
|
||||
# C. SELECTIVE OR INDIVIDUAL INTEGRATION
|
||||
else:
|
||||
# We iterate over each asset category, checking the user choices from the form post parameters
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue