feat(trafficlights): add cable geometry copy, duplication modal and node editing hints in cross plan import
- Add explanatory guidelines for cable node operations (double-click segment to insert node, double-click node to delete, drag to reposition) - Add floating reminder pill on map during cable editing mode - Add "Copier tracé..." dropdown on cable cards to import trace from other cables - Add "Appliquer aux autres câbles..." modal to duplicate active trace across multiple cables at once - Add direct map jump button on cable rows in comparative table - Include db_coords in compared_cables context to preserve pre-existing geometries - Add tests and update translations (fr, nl, en)
This commit is contained in:
parent
1d99cdd09b
commit
244b16b3d3
9 changed files with 747 additions and 57 deletions
Binary file not shown.
|
|
@ -8588,6 +8588,87 @@ msgstr "Archive existing (do not recreate)"
|
||||||
msgid "Archiver l'existant"
|
msgid "Archiver l'existant"
|
||||||
msgstr "Archive existing"
|
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é)"
|
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
#~ msgstr "Missing from Excel file (Will be kept or archived)"
|
#~ msgstr "Missing from Excel file (Will be kept or archived)"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -8639,6 +8639,87 @@ msgstr "Archiver l'existant (Ne pas recréer)"
|
||||||
msgid "Archiver l'existant"
|
msgid "Archiver l'existant"
|
||||||
msgstr "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é)"
|
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
#~ msgstr "Absent du fichier Excel (Sera conservé ou archivé)"
|
#~ msgstr "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -8587,6 +8587,87 @@ msgstr "Bestaande archiveren (niet opnieuw aanmaken)"
|
||||||
msgid "Archiver l'existant"
|
msgid "Archiver l'existant"
|
||||||
msgstr "Bestaande archiveren"
|
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é)"
|
#~ msgid "Absent du fichier Excel (Sera conservé ou archivé)"
|
||||||
#~ msgstr "Ontbreekt in het Excel-bestand (Wordt bewaard of gearchiveerd)"
|
#~ msgstr "Ontbreekt in het Excel-bestand (Wordt bewaard of gearchiveerd)"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -661,6 +661,11 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span>
|
<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if item.status != 'db_only' %}
|
||||||
|
<button type="button" class="btn btn-xs btn-outline-primary ms-1 show-cable-on-map-btn" data-code="{{ item.code }}" title="{% translate 'Voir et éditer sur la carte' %}">
|
||||||
|
<i class="bi bi-geo-alt"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><span class="text-secondary small">{{ item.deduced_model_name|default:"—" }}</span></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><span class="text-secondary small">{{ item.db_model_name|default:"—" }}</span></td>
|
||||||
|
|
@ -874,13 +879,19 @@
|
||||||
<!-- Map Cables Sub-pane -->
|
<!-- Map Cables Sub-pane -->
|
||||||
<div class="tab-pane fade h-100" id="map-cables-pane" role="tabpanel" aria-labelledby="map-cables-tab">
|
<div class="tab-pane fade h-100" id="map-cables-pane" role="tabpanel" aria-labelledby="map-cables-tab">
|
||||||
<div class="d-flex flex-column h-100 align-items-start justify-content-start">
|
<div class="d-flex flex-column h-100 align-items-start justify-content-start">
|
||||||
<div class="mb-2 p-2 bg-light rounded-3 border w-100">
|
<div class="mb-2 p-2.5 bg-light rounded-3 border w-100 shadow-sm" style="font-size: 0.78rem;">
|
||||||
<div class="font-weight-semibold text-success mb-1">
|
<div class="d-flex align-items-center justify-content-between mb-1.5 pb-1 border-bottom">
|
||||||
<i class="bi bi-bezier"></i> {% translate "Tracé des câbles" %}
|
<span class="font-weight-semibold text-success d-flex align-items-center gap-1">
|
||||||
|
<i class="bi bi-bezier"></i> {% translate "Aide au tracé des câbles" %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-secondary mb-0 small">
|
<ul class="list-unstyled mb-0 text-secondary d-flex flex-column gap-1" style="line-height: 1.35;">
|
||||||
{% translate "Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer la ligne du câble." %}
|
<li><i class="bi bi-plus-circle text-primary me-1"></i><strong>{% translate "Tracer" %} :</strong> {% translate "Double-clic sur la carte pour ajouter un point." %}</li>
|
||||||
</p>
|
<li><i class="bi bi-node-plus text-success me-1"></i><strong>{% translate "Ajouter un nœud" %} :</strong> {% translate "Double-clic sur un segment du câble." %}</li>
|
||||||
|
<li><i class="bi bi-node-minus text-danger me-1"></i><strong>{% translate "Supprimer un nœud" %} :</strong> {% translate "Double-clic sur un nœud (poignée orange)." %}</li>
|
||||||
|
<li><i class="bi bi-arrows-move text-warning me-1"></i><strong>{% translate "Déplacer" %} :</strong> {% translate "Glisser-déposer les poignées pour ajuster." %}</li>
|
||||||
|
<li><i class="bi bi-files text-info me-1"></i><strong>{% translate "Copier tracé" %} :</strong> {% translate "Copiez le tracé d'un autre câble suivant le même trajet (ex: 48 et tel.)." %}</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-group list-group-flush overflow-auto flex-grow-1 w-100 align-self-stretch">
|
<div class="list-group list-group-flush overflow-auto flex-grow-1 w-100 align-self-stretch">
|
||||||
|
|
@ -889,12 +900,23 @@
|
||||||
<div class="list-group-item py-2 px-1 border-bottom d-flex flex-column gap-1 cable-config-item align-self-stretch" data-code="{{ item.code }}">
|
<div class="list-group-item py-2 px-1 border-bottom d-flex flex-column gap-1 cable-config-item align-self-stretch" data-code="{{ item.code }}">
|
||||||
<div class="d-flex align-items-center justify-content-between w-100">
|
<div class="d-flex align-items-center justify-content-between w-100">
|
||||||
<strong class="text-dark font-monospace">{{ item.name }}</strong>
|
<strong class="text-dark font-monospace">{{ item.name }}</strong>
|
||||||
|
<span class="badge cable-geom-status-badge font-monospace" id="badge-cable-status-{{ item.code }}" style="font-size: 0.72rem;">
|
||||||
|
<span class="text-muted">…</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small font-monospace">{{ item.deduced_model_name }}</div>
|
<div class="text-muted small font-monospace">{{ item.deduced_model_name }}</div>
|
||||||
<div class="mt-1 align-self-start">
|
<div class="mt-1 d-flex align-items-center gap-1 flex-wrap">
|
||||||
<button type="button" class="btn btn-xs btn-outline-success py-0.5 px-2 edit-cable-btn rounded-2" data-code="{{ item.code }}">
|
<button type="button" class="btn btn-xs btn-outline-success py-0.5 px-2 edit-cable-btn rounded-2" data-code="{{ item.code }}">
|
||||||
<i class="bi bi-pencil-square"></i> {% translate "Tracer / Éditer" %}
|
<i class="bi bi-pencil-square"></i> {% translate "Tracer / Éditer" %}
|
||||||
</button>
|
</button>
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button type="button" class="btn btn-xs btn-outline-secondary dropdown-toggle py-0.5 px-2 rounded-2 copy-cable-btn" data-bs-toggle="dropdown" aria-expanded="false" title="{% translate "Copier le tracé d'un autre câble" %}">
|
||||||
|
<i class="bi bi-files"></i> {% translate "Copier tracé…" %}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm copy-cable-menu py-1" data-target-code="{{ item.code }}" style="font-size: 0.8rem; min-width: 220px;">
|
||||||
|
<!-- Populated dynamically by JS -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -914,10 +936,17 @@
|
||||||
<!-- Mode indicator centered at the top -->
|
<!-- Mode indicator centered at the top -->
|
||||||
<div class="position-absolute top-0 start-50 translate-middle-x m-3 z-3 bg-white py-1 px-3 rounded-pill shadow border d-flex align-items-center gap-2" style="white-space: nowrap;">
|
<div class="position-absolute top-0 start-50 translate-middle-x m-3 z-3 bg-white py-1 px-3 rounded-pill shadow border d-flex align-items-center gap-2" style="white-space: nowrap;">
|
||||||
<span class="small text-muted font-monospace font-weight-semibold mb-0" id="map-mode-indicator">{% translate "Mode: Lecture" %}</span>
|
<span class="small text-muted font-monospace font-weight-semibold mb-0" id="map-mode-indicator">{% translate "Mode: Lecture" %}</span>
|
||||||
|
<button type="button" id="btn-duplicate-trace" class="btn btn-xs btn-outline-primary rounded-pill py-0.5 px-2" style="display: none; font-size: 0.75rem;">
|
||||||
|
<i class="bi bi-copy"></i> {% translate "Appliquer aux autres câbles…" %}
|
||||||
|
</button>
|
||||||
<button type="button" id="btn-clear-trace" class="btn btn-xs btn-outline-danger rounded-pill py-0.5 px-2" style="display: none; font-size: 0.75rem;">
|
<button type="button" id="btn-clear-trace" class="btn btn-xs btn-outline-danger rounded-pill py-0.5 px-2" style="display: none; font-size: 0.75rem;">
|
||||||
<i class="bi bi-trash"></i> {% translate "Effacer" %}
|
<i class="bi bi-trash"></i> {% translate "Effacer" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="cable-edit-tip" class="position-absolute start-50 translate-middle-x z-2 px-3 py-1 bg-white bg-opacity-95 rounded-pill shadow-sm border small text-muted text-center" style="top: 56px; display: none; font-size: 0.75rem; white-space: nowrap;">
|
||||||
|
<i class="bi bi-info-circle text-primary me-1"></i>
|
||||||
|
<span>{% translate "Double-clic sur un segment pour ajouter un nœud • Double-clic sur un nœud pour le supprimer • Glisser pour déplacer" %}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="map" class="w-100 h-100"></div>
|
<div id="map" class="w-100 h-100"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -926,6 +955,46 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Modal: Appliquer le tracé d'un câble à d'autres câbles -->
|
||||||
|
<div class="modal fade" id="duplicateCableModal" tabindex="-1" aria-labelledby="duplicateCableModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content rounded-4 border-0 shadow">
|
||||||
|
<div class="modal-header border-bottom py-2.5 px-3">
|
||||||
|
<h6 class="modal-title font-weight-semibold text-dark d-flex align-items-center gap-2 mb-0" id="duplicateCableModalLabel">
|
||||||
|
<i class="bi bi-copy text-primary"></i> {% translate "Appliquer le tracé à d'autres câbles" %}
|
||||||
|
</h6>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-3">
|
||||||
|
<p class="text-secondary small mb-3">
|
||||||
|
{% translate "Copier le tracé de" %} <strong id="modal-source-cable-name" class="text-dark"></strong> <span id="modal-source-cable-info" class="badge bg-light text-secondary border"></span> {% translate "vers les câbles sélectionnés ci-dessous :" %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="p-2.5 bg-light rounded-3 border mb-2">
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="modal-select-all-cables">
|
||||||
|
<label class="form-check-label font-weight-semibold small text-dark cursor-pointer" for="modal-select-all-cables">
|
||||||
|
{% translate "Tout sélectionner" %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-target-cables-list" class="d-flex flex-column gap-1.5 overflow-auto pe-1" style="max-height: 240px;">
|
||||||
|
<!-- Dynamically generated list of cables -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-top py-2 px-3 d-flex justify-content-between">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary rounded-2" data-bs-dismiss="modal">
|
||||||
|
{% translate "Annuler" %}
|
||||||
|
</button>
|
||||||
|
<button type="button" id="btn-confirm-duplicate-cable" class="btn btn-sm btn-primary rounded-2 px-3 d-flex align-items-center gap-1.5">
|
||||||
|
<i class="bi bi-check-lg"></i> {% translate "Copier le tracé" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1084,30 +1153,47 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const tableView = document.getElementById('table-view');
|
const tableView = document.getElementById('table-view');
|
||||||
const mapView = document.getElementById('map-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 => {
|
document.querySelectorAll('.show-map-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', function(e) {
|
btn.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetTab = this.dataset.targetTab;
|
switchToMapView(this.dataset.targetTab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
tableView.classList.remove('d-block');
|
document.querySelectorAll('.show-cable-on-map-btn').forEach(btn => {
|
||||||
tableView.classList.add('d-none');
|
btn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
mapView.classList.remove('d-none');
|
e.stopPropagation();
|
||||||
mapView.classList.add('d-block');
|
switchToMapView('cables', this.dataset.code);
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1178,7 +1264,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
name: "{{ item.name|escapejs }}",
|
name: "{{ item.name|escapejs }}",
|
||||||
status: "{{ item.status|escapejs }}",
|
status: "{{ item.status|escapejs }}",
|
||||||
geom_modified: {{ item.geom_modified|yesno:"true,false" }},
|
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 %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
|
|
@ -1213,6 +1303,87 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const placedPoles = {};
|
const placedPoles = {};
|
||||||
let layerSwitcher = null;
|
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 = `<i class="bi bi-bezier2"></i> ${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 = `<i class="bi bi-bezier2"></i> ${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 = `<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1"><i class="bi bi-arrows-move"></i> {% translate "Tracé modifié" %} (${len} m)</span> <button type="button" class="btn btn-xs btn-outline-primary ms-1 show-cable-on-map-btn" data-code="${code}" title="{% translate 'Voir et éditer sur la carte' %}"><i class="bi bi-geo-alt"></i></button>`;
|
||||||
|
} else {
|
||||||
|
geomCell.innerHTML = `<span class="badge bg-light text-muted border">{% translate "Non tracé" %}</span> <button type="button" class="btn btn-xs btn-outline-primary ms-1 show-cable-on-map-btn" data-code="${code}" title="{% translate 'Tracer sur la carte' %}"><i class="bi bi-geo-alt"></i></button>`;
|
||||||
|
}
|
||||||
|
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) {
|
function markCableAsModified(code) {
|
||||||
const selectAction = document.querySelector(`select[name="action_cable_${code}"]`);
|
const selectAction = document.querySelector(`select[name="action_cable_${code}"]`);
|
||||||
if (selectAction) {
|
if (selectAction) {
|
||||||
|
|
@ -1225,10 +1396,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const cb = document.querySelector(`input[name="selected_cable_${code}"]`);
|
const cb = document.querySelector(`input[name="selected_cable_${code}"]`);
|
||||||
if (cb) cb.checked = true;
|
if (cb) cb.checked = true;
|
||||||
|
|
||||||
const geomCell = document.getElementById(`geom_cell_cable_${code}`);
|
updateTableCableGeomCell(code);
|
||||||
if (geomCell) {
|
updateCableBadge(code);
|
||||||
geomCell.innerHTML = `<span class="badge bg-warning text-dark d-inline-flex align-items-center gap-1"><i class="bi bi-arrows-move"></i> {% translate "Tracé modifié" %}</span>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]);
|
const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]);
|
||||||
|
|
@ -1718,8 +1887,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
deactivateCableEditingMode();
|
deactivateCableEditingMode();
|
||||||
|
|
||||||
activeEditCable = code;
|
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-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');
|
const mapPane = document.getElementById('map-cables-pane');
|
||||||
mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light'));
|
mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light'));
|
||||||
|
|
@ -1728,25 +1901,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
let coords = cableGeometries[code];
|
let coords = cableGeometries[code];
|
||||||
if (!coords || coords.length === 0) {
|
if (!coords || coords.length === 0) {
|
||||||
const cableInfo = cablesList.find(c => c.code === code);
|
const defaultCoords = getCableCoords(code);
|
||||||
const connected = cableInfo.connected_poles || [];
|
coords = defaultCoords && defaultCoords.length > 0 ? defaultCoords.map(pt => [pt[0], pt[1]]) : [];
|
||||||
|
|
||||||
coords = [];
|
|
||||||
connected.forEach(pCode => {
|
|
||||||
if (placedPoles[pCode]) {
|
|
||||||
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cableGeometries[code] = coords;
|
cableGeometries[code] = coords;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawCableHandles(code);
|
drawCableHandles(code);
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
|
updateCableBadge(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deactivateCableEditingMode() {
|
function deactivateCableEditingMode() {
|
||||||
activeEditCable = null;
|
activeEditCable = null;
|
||||||
document.getElementById('btn-clear-trace').style.display = 'none';
|
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" %}`;
|
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Lecture" %}`;
|
||||||
|
|
||||||
cableHandles.forEach(h => h.remove());
|
cableHandles.forEach(h => h.remove());
|
||||||
|
|
@ -1795,6 +1964,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(code);
|
markCableAsModified(code);
|
||||||
|
populateCopyCableMenus();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener('dblclick', function(e) {
|
el.addEventListener('dblclick', function(e) {
|
||||||
|
|
@ -1804,6 +1974,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(code);
|
markCableAsModified(code);
|
||||||
|
populateCopyCableMenus();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1861,6 +2032,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(activeEditCable);
|
markCableAsModified(activeEditCable);
|
||||||
|
populateCopyCableMenus();
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('dblclick', function(e) {
|
map.on('dblclick', function(e) {
|
||||||
|
|
@ -1884,6 +2056,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(activeEditCable);
|
markCableAsModified(activeEditCable);
|
||||||
|
populateCopyCableMenus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hover mouse styles for active cable line
|
// Hover mouse styles for active cable line
|
||||||
|
|
@ -1907,20 +2080,192 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
redrawCablesOnMap();
|
redrawCablesOnMap();
|
||||||
saveCablesToForm();
|
saveCablesToForm();
|
||||||
markCableAsModified(activeEditCable);
|
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 = `
|
||||||
|
<button type="button" class="dropdown-item py-1.5 px-3 d-flex align-items-center justify-content-between copy-from-cable-btn" data-source="${other.code}" data-target="${targetCode}">
|
||||||
|
<span class="font-monospace text-truncate me-2">${other.name}</span>
|
||||||
|
<span class="badge bg-light text-secondary border font-monospace" style="font-size: 0.7rem;">${coords.length} pts (${len} m)</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
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 = `<i class="bi bi-check-circle-fill text-success me-1"></i><strong>{% translate "Tracé copié de" %} ${sourceName} {% translate "vers" %} ${targetName}</strong>`;
|
||||||
|
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 = `<div class="text-center text-muted small py-3">{% translate "Aucun autre câble disponible dans ce carrefour." %}</div>`;
|
||||||
|
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
|
||||||
|
? `<span class="badge bg-light text-secondary border font-monospace ms-auto" style="font-size: 0.7rem;">${cCoords.length} pts (${cLen} m)</span>`
|
||||||
|
: `<span class="badge bg-light text-muted border font-monospace ms-auto" style="font-size: 0.7rem;">{% translate "Non tracé" %}</span>`;
|
||||||
|
|
||||||
|
const itemDiv = document.createElement('div');
|
||||||
|
itemDiv.className = 'form-check d-flex align-items-center p-2 rounded border bg-white';
|
||||||
|
itemDiv.innerHTML = `
|
||||||
|
<input class="form-check-input modal-cable-target-cb me-2" type="checkbox" value="${c.code}" id="modal_cb_target_${c.code}">
|
||||||
|
<label class="form-check-label small text-dark d-flex align-items-center w-100 cursor-pointer mb-0" for="modal_cb_target_${c.code}">
|
||||||
|
<span class="font-monospace font-weight-semibold me-2">${c.name}</span>
|
||||||
|
<span class="text-muted small">${c.deduced_model_name ? '(' + c.deduced_model_name + ')' : ''}</span>
|
||||||
|
${geomBadge}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
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 = `<i class="bi bi-check-circle-fill text-success me-1"></i><strong>${appliedCount} {% translate "câble(s) mis à jour avec ce tracé !" %}</strong>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (activeEditCable) tipEl.innerHTML = origContent;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function saveCablesToForm() {
|
function saveCablesToForm() {
|
||||||
cablesList.forEach(c => {
|
cablesList.forEach(c => {
|
||||||
let coords = cableGeometries[c.code];
|
let coords = cableGeometries[c.code];
|
||||||
if (!coords || coords.length === 0) {
|
if (!coords || coords.length === 0) {
|
||||||
coords = [];
|
coords = getCableCoords(c.code);
|
||||||
const connected = c.connected_poles || [];
|
|
||||||
connected.forEach(pCode => {
|
|
||||||
if (placedPoles[pCode]) {
|
|
||||||
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const input = document.getElementById(`geom_cable_${c.code}`);
|
const input = document.getElementById(`geom_cable_${c.code}`);
|
||||||
if (input && coords && coords.length >= 2) {
|
if (input && coords && coords.length >= 2) {
|
||||||
|
|
@ -1953,13 +2298,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
let coords = cableGeometries[c.code];
|
let coords = cableGeometries[c.code];
|
||||||
if (!coords || coords.length === 0) {
|
if (!coords || coords.length === 0) {
|
||||||
coords = [];
|
coords = getCableCoords(c.code);
|
||||||
const connected = c.connected_poles || [];
|
|
||||||
connected.forEach(pCode => {
|
|
||||||
if (placedPoles[pCode]) {
|
|
||||||
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coords && coords.length >= 2) {
|
if (coords && coords.length >= 2) {
|
||||||
|
|
@ -1987,6 +2326,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
map.triggerRepaint();
|
map.triggerRepaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial cable badges and copy menus update
|
||||||
|
cablesList.forEach(c => {
|
||||||
|
updateCableBadge(c.code);
|
||||||
|
});
|
||||||
|
populateCopyCableMenus();
|
||||||
|
|
||||||
// ── Georeferenced Plans & DXF Background Loader ──
|
// ── Georeferenced Plans & DXF Background Loader ──
|
||||||
const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData');
|
const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData');
|
||||||
let georeferencedDocuments = [];
|
let georeferencedDocuments = [];
|
||||||
|
|
|
||||||
|
|
@ -3325,6 +3325,88 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
if os.path.exists(tmp_path):
|
if os.path.exists(tmp_path):
|
||||||
os.remove(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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4401,6 +4401,15 @@ def import_cross_plan(request, intersection_id):
|
||||||
geom_modified = True
|
geom_modified = True
|
||||||
details_diff.append(_("Nouveau tracé déduit des poteaux"))
|
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'
|
status = 'modified' if (is_diff or geom_modified) else 'identical'
|
||||||
compared_cables.append({
|
compared_cables.append({
|
||||||
'code': code,
|
'code': code,
|
||||||
|
|
@ -4414,6 +4423,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
'has_db_geom': has_geom,
|
'has_db_geom': has_geom,
|
||||||
'geom_modified': geom_modified,
|
'geom_modified': geom_modified,
|
||||||
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
||||||
|
'db_coords': db_coords,
|
||||||
})
|
})
|
||||||
existing_cables.pop(code)
|
existing_cables.pop(code)
|
||||||
else:
|
else:
|
||||||
|
|
@ -4432,9 +4442,18 @@ def import_cross_plan(request, intersection_id):
|
||||||
'has_db_geom': False,
|
'has_db_geom': False,
|
||||||
'geom_modified': False,
|
'geom_modified': False,
|
||||||
'db_geom_length': None,
|
'db_geom_length': None,
|
||||||
|
'db_coords': [],
|
||||||
})
|
})
|
||||||
for code, ec in existing_cables.items():
|
for code, ec in existing_cables.items():
|
||||||
has_geom = ec.geom is not None
|
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({
|
compared_cables.append({
|
||||||
'code': code,
|
'code': code,
|
||||||
'name': ec.name_fr or code,
|
'name': ec.name_fr or code,
|
||||||
|
|
@ -4446,6 +4465,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
'has_db_geom': has_geom,
|
'has_db_geom': has_geom,
|
||||||
'geom_modified': False,
|
'geom_modified': False,
|
||||||
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
'db_geom_length': round(ec.geom.length, 1) if has_geom else None,
|
||||||
|
'db_coords': db_coords,
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── 3. LANTERNS COMPARISON ──
|
# ── 3. LANTERNS COMPARISON ──
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue