loko/streetup/assets/templates/assets/trafficlights_import_cross_plan.html
2026-07-22 14:48:40 +02:00

2076 lines
112 KiB
HTML

{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block content %}
<!-- MapLibre & CSS/JS requirements -->
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
<script src="{% static 'common/address_search_control.js' %}"></script>
<div class="container-fluid py-4" style="background-color: #f8fafc; min-height: 100vh;">
<!-- Header/Breadcrumbs -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-2" style="background-color: transparent; padding: 0;">
<li class="breadcrumb-item"><a href="{% url 'assets:trafficlights_index' %}">{% translate "Thématiques" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'assets:trafficlights_intersections_detail' intersection.id %}">{{ intersection.code }}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% translate "Importation Plan de croix" %}</li>
</ol>
</nav>
<div class="d-flex align-items-center justify-content-between">
<h2 class="h3 font-weight-bold text-dark mb-0">
<i class="bi bi-file-earmark-excel text-success me-2"></i>
{% translate "Importation d'un plan de croix (Excel)" %}
</h2>
<a href="{% url 'assets:trafficlights_intersections_detail' intersection.id %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> {% translate "Retour au carrefour" %}
</a>
</div>
</div>
</div>
<!-- Step 1: Select/Upload Plan -->
{% if step == 'select' %}
<div class="row">
<div class="col-md-6 mx-auto">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header text-white py-3" style="background: linear-gradient(135deg, #10b981, #059669);">
<h5 class="card-title mb-0 font-weight-semibold">
<i class="bi bi-file-earmark-arrow-up me-2"></i> {% translate "Sélectionner / Téléverser le plan de croix (.xlsx)" %}
</h5>
</div>
<div class="card-body p-4" style="background-color: #ffffff;">
{% if error %}
<div class="alert alert-danger rounded-3 d-flex align-items-center mb-4" role="alert">
<i class="bi bi-exclamation-triangle-fill fs-5 me-2"></i>
<div>{{ error }}</div>
</div>
{% endif %}
<form method="post" enctype="multipart/form-data" id="select-import-form">
{% csrf_token %}
<!-- Select Existing Cross Plans -->
{% if cross_plan_choices %}
<div class="mb-4">
<label class="form-label font-weight-semibold text-secondary d-flex justify-content-between align-items-center">
<span>{% translate "Utiliser des plans de croix déjà rattachés" %}</span>
<div class="form-check form-check-inline m-0">
<input class="form-check-input" type="checkbox" id="select_all_existing_plans">
<label class="form-check-label small text-muted font-weight-medium" for="select_all_existing_plans">
{% translate "Tout sélectionner" %}
</label>
</div>
</label>
<div class="border rounded-3 p-3 bg-light overflow-auto" style="max-height: 250px;">
{% for cp in cross_plan_choices %}
<div class="form-check py-1 border-bottom last-border-0">
<input class="form-check-input existing-plan-checkbox" type="checkbox" name="existing_cross_plans" value="{{ cp.id }}" id="cp_{{ cp.id }}">
<label class="form-check-label text-dark small font-weight-medium d-block cursor-pointer" for="cp_{{ cp.id }}">
<i class="bi bi-file-earmark-excel-fill text-success me-1"></i>
{{ cp.title }} <span class="text-muted font-weight-normal">({{ cp.filename }})</span>
</label>
</div>
{% endfor %}
</div>
<div class="form-text mt-2 text-muted">
{% translate "Sélectionnez un ou plusieurs plans de croix déjà associés à ce carrefour, ou téléversez-en de nouveaux ci-dessous." %}
</div>
</div>
<div class="text-center my-3 text-muted font-weight-medium">
— {% translate "OU" %} —
</div>
{% endif %}
<!-- Upload Excel File -->
<div class="mb-4">
<label for="cross_plan_file_upload" class="form-label font-weight-semibold text-secondary">
{% translate "Fichier Excel du plan de croix" %}
</label>
<input type="file" name="cross_plan_file_upload" id="cross_plan_file_upload" class="form-control form-control-lg rounded-3" accept=".xlsx" multiple required>
<div class="form-text mt-2 text-muted">
{% translate "Téléversez un ou plusieurs classeurs Excel de plans de croix (contenant l'onglet 'kruisjesplan' ou 'Plan croix')." %}
</div>
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-success btn-lg rounded-3 shadow-sm font-weight-semibold">
<i class="bi bi-search me-1"></i> {% translate "Analyser et comparer les assets" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('cross_plan_file_upload');
const checkboxes = document.querySelectorAll('.existing-plan-checkbox');
const selectAllCheckbox = document.getElementById('select_all_existing_plans');
const form = document.getElementById('select-import-form');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
checkboxes.forEach(cb => {
cb.checked = selectAllCheckbox.checked;
});
});
}
if (fileInput && form) {
fileInput.removeAttribute('required');
form.addEventListener('submit', function(e) {
const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
if (!fileInput.value && !anyChecked) {
e.preventDefault();
alert("{% translate 'Veuillez téléverser un fichier Excel ou sélectionner au moins un plan existant.' %}");
}
});
}
});
</script>
{% endif %}
<!-- Step 2: Compare & Edit Actions -->
{% if step == 'preview' %}
<form method="post" action="{% url 'assets:integrate_cross_plan' intersection.id %}" id="combined-import-form">
{% csrf_token %}
<input type="hidden" name="cross_plan_filename" value="{{ cross_plan_filename }}">
<input type="hidden" name="global_action" id="global_action_input" value="apply">
<!-- 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="">
{% endif %}
{% endfor %}
<!-- Hidden input fields for cables geometries -->
{% for item in cables %}
{% if item.status != 'db_only' %}
<input type="hidden" name="geom_cable_{{ item.code }}" id="geom_cable_{{ item.code }}" value="">
{% endif %}
{% endfor %}
<!-- A. STANDARD COMPARATIVE TABLE VIEW (Default Visible) -->
<div id="table-view" class="d-block">
<div class="row">
<!-- Summary Card -->
<div class="col-12 mb-4">
<div class="card border-0 shadow-sm rounded-4 p-3 d-flex flex-row align-items-center justify-content-between" style="background-color: #ffffff;">
<div>
<h5 class="mb-1 text-dark font-weight-semibold">{% translate "Aperçu de la comparaison des assets" %}</h5>
<p class="text-muted mb-0 small">
{% if uploaded_filenames %}
{% translate "Fichiers :" %} <strong>{{ uploaded_filenames|join:", " }}</strong>
{% else %}
{% translate "Fichier :" %} <code>{{ cross_plan_filename }}</code>
{% endif %}
| {% translate "Carrefour :" %} <strong>{{ intersection.code }}</strong>
</p>
</div>
<div class="d-flex gap-2">
<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)" %}
</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" %}
</button>
</div>
</div>
</div>
<!-- Main Interactive Table Grid -->
<div class="col-12">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden mb-4">
<!-- Tab Headers -->
<div class="card-header bg-light border-bottom p-0">
<ul class="nav nav-tabs border-0" id="comparisonTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active px-4 py-3 border-0 rounded-0" id="poles-tab" data-bs-toggle="tab" data-bs-target="#poles-pane" type="button" role="tab" aria-controls="poles-pane" aria-selected="true">
<i class="bi bi-cone-striped me-2"></i> {% translate "Poteaux" %} <span class="badge bg-secondary ms-1">{{ poles|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 py-3 border-0 rounded-0" id="lanterns-tab" data-bs-toggle="tab" data-bs-target="#lanterns-pane" type="button" role="tab" aria-controls="lanterns-pane" aria-selected="false">
<i class="bi bi-traffic-light me-2"></i> {% translate "Lanternes" %} <span class="badge bg-secondary ms-1">{{ lanterns|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 py-3 border-0 rounded-0" id="detectors-tab" data-bs-toggle="tab" data-bs-target="#detectors-pane" type="button" role="tab" aria-controls="detectors-pane" aria-selected="false">
<i class="bi bi-broadcast-pin me-2"></i> {% translate "Détecteurs & BP" %} <span class="badge bg-secondary ms-1">{{ detectors|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 py-3 border-0 rounded-0" id="cables-tab" data-bs-toggle="tab" data-bs-target="#cables-pane" type="button" role="tab" aria-controls="cables-pane" aria-selected="false">
<i class="bi bi-bezier2 me-2"></i> {% translate "Câbles" %} <span class="badge bg-secondary ms-1">{{ cables|length }}</span>
</button>
</li>
</ul>
</div>
<!-- Tab Contents -->
<div class="card-body p-0">
<div class="tab-content" id="comparisonTabsContent">
<!-- 1. POLES PANEL -->
<div class="tab-pane fade show active" id="poles-pane" role="tabpanel" aria-labelledby="poles-tab">
<div class="p-3 bg-light border-bottom d-flex align-items-center justify-content-between">
<span class="text-secondary small">
<i class="bi bi-info-circle"></i> {% translate "Vous pouvez géolocaliser les poteaux en les plaçant sur le plan de la carte." %}
</span>
<button type="button" class="btn btn-outline-primary btn-sm rounded-3 show-map-btn" data-target-tab="poles">
<i class="bi bi-map-fill me-1"></i> {% translate "Positionner les poteaux sur la carte" %}
</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4" style="width: 40px;"><input type="checkbox" id="select-all-poles" class="form-check-input" checked></th>
<th>{% translate "Code Asset" %}</th>
<th>{% translate "Nom" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Modèle Déduit (Excel)" %}</th>
<th>{% translate "Modèle Actuel (DB)" %}</th>
<th>{% translate "Décision" %}</th>
<th class="pe-4">{% translate "Forcer le Modèle" %}</th>
</tr>
</thead>
<tbody>
{% for item in poles %}
<tr class="{% if item.status == 'new' %}table-success-light{% elif item.status == 'modified' %}table-warning-light{% elif item.status == 'db_only' %}table-danger-light{% endif %}">
<td class="ps-4"><input type="checkbox" name="{% if item.status == 'db_only' %}selected_pole_db_{{ item.code }}{% else %}selected_pole_{{ item.code }}{% endif %}" class="form-check-input pole-checkbox" checked></td>
<td class="font-weight-medium"><code>{{ item.code }}</code><input type="hidden" name="db_id_pole_{{ item.code }}" value="{{ item.db_id|default:'' }}"></td>
<td>
<input type="text" name="name_pole_{{ item.code }}" class="form-control form-control-sm" value="{{ item.name }}">
</td>
<td>
{% if item.status == 'new' %}
<span class="badge bg-success">{% translate "Nouveau" %}</span>
{% elif item.status == 'modified' %}
<span class="badge bg-warning text-dark">{% translate "Modifié" %}</span>
{% elif item.status == 'identical' %}
<span class="badge bg-light text-secondary border">{% translate "Identique" %}</span>
{% elif item.status == 'db_only' %}
<span class="badge bg-danger">{% translate "DB uniquement" %}</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>
{% if item.status == 'db_only' %}
<select name="action_pole_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="keep">{% translate "Conserver en DB" %}</option>
<option value="archive" selected>{% translate "Archiver" %}</option>
</select>
{% else %}
<select name="action_pole_{{ item.code }}" class="form-select form-select-sm rounded-2">
{% if item.status == 'new' %}
<option value="add" selected>{% translate "Créer l'asset" %}</option>
<option value="keep">{% translate "Ignorer" %}</option>
{% elif item.status == 'modified' %}
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
<option value="keep">{% translate "Conserver existant" %}</option>
{% elif item.status == 'identical' %}
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
{% endif %}
</select>
{% endif %}
</td>
<td class="pe-4">
{% if item.status != 'db_only' %}
<select name="model_pole_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="">-- {% translate "Laisser par défaut" %} --</option>
{% for m in pole_models %}
<option value="{{ m.id }}">{{ m.name_fr }}</option>
{% endfor %}
</select>
{% else %}
<span class="text-muted small">—</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">{% translate "Aucun poteau détecté." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 2. LANTERNS PANEL -->
<div class="tab-pane fade" id="lanterns-pane" role="tabpanel" aria-labelledby="lanterns-tab">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4" style="width: 40px;"><input type="checkbox" id="select-all-lanterns" class="form-check-input" checked></th>
<th>{% translate "Code Asset" %}</th>
<th>{% translate "Nom" %}</th>
<th>{% translate "Poteau" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Modèle Déduit (Excel)" %}</th>
<th>{% translate "Modèle Actuel (DB)" %}</th>
<th>{% translate "Décision" %}</th>
<th class="pe-4">{% translate "Forcer le Modèle" %}</th>
</tr>
</thead>
<tbody>
{% for item in lanterns %}
<tr class="{% if item.status == 'new' %}table-success-light{% elif item.status == 'modified' %}table-warning-light{% elif item.status == 'db_only' %}table-danger-light{% endif %}">
<td class="ps-4"><input type="checkbox" name="{% if item.status == 'db_only' %}selected_lantern_db_{{ item.code }}{% else %}selected_lantern_{{ item.code }}{% endif %}" class="form-check-input lantern-checkbox" checked></td>
<td class="font-weight-medium"><code>{{ item.code }}</code><input type="hidden" name="db_id_lantern_{{ item.code }}" value="{{ item.db_id|default:'' }}"></td>
<td>
<input type="text" name="name_lantern_{{ item.code }}" class="form-control form-control-sm" value="{{ item.name }}">
</td>
<td><code>{{ item.pole_code }}</code></td>
<td>
{% if item.status == 'new' %}
<span class="badge bg-success">{% translate "Nouveau" %}</span>
{% elif item.status == 'modified' %}
<span class="badge bg-warning text-dark">{% translate "Modifié" %}</span>
{% elif item.status == 'identical' %}
<span class="badge bg-light text-secondary border">{% translate "Identique" %}</span>
{% elif item.status == 'db_only' %}
<span class="badge bg-danger">{% translate "DB uniquement" %}</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>
{% if item.status == 'db_only' %}
<select name="action_lantern_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="keep">{% translate "Conserver en DB" %}</option>
<option value="archive" selected>{% translate "Archiver" %}</option>
</select>
{% else %}
<select name="action_lantern_{{ item.code }}" class="form-select form-select-sm rounded-2">
{% if item.status == 'new' %}
<option value="add" selected>{% translate "Créer l'asset" %}</option>
<option value="keep">{% translate "Ignorer" %}</option>
{% elif item.status == 'modified' %}
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
<option value="keep">{% translate "Conserver existant" %}</option>
{% elif item.status == 'identical' %}
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
{% endif %}
</select>
{% endif %}
</td>
<td class="pe-4">
{% if item.status != 'db_only' %}
<select name="model_lantern_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="">-- {% translate "Laisser par défaut" %} --</option>
{% for m in lantern_models %}
<option value="{{ m.id }}">{{ m.name_fr }}</option>
{% endfor %}
</select>
{% else %}
<span class="text-muted small">—</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-4 text-muted">{% translate "Aucune lanterne détectée." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 3. DETECTORS PANEL -->
<div class="tab-pane fade" id="detectors-pane" role="tabpanel" aria-labelledby="detectors-tab">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4" style="width: 40px;"><input type="checkbox" id="select-all-detectors" class="form-check-input" checked></th>
<th>{% translate "Code Asset" %}</th>
<th>{% translate "Nom" %}</th>
<th>{% translate "Poteau" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Modèle Déduit (Excel)" %}</th>
<th>{% translate "Modèle Actuel (DB)" %}</th>
<th>{% translate "Décision" %}</th>
<th class="pe-4">{% translate "Forcer le Modèle" %}</th>
</tr>
</thead>
<tbody>
{% for item in detectors %}
<tr class="{% if item.status == 'new' %}table-success-light{% elif item.status == 'modified' %}table-warning-light{% elif item.status == 'db_only' %}table-danger-light{% endif %}">
<td class="ps-4"><input type="checkbox" name="{% if item.status == 'db_only' %}selected_detector_db_{{ item.code }}{% else %}selected_detector_{{ item.code }}{% endif %}" class="form-check-input detector-checkbox" checked></td>
<td class="font-weight-medium"><code>{{ item.code }}</code><input type="hidden" name="db_id_detector_{{ item.code }}" value="{{ item.db_id|default:'' }}"></td>
<td>
<input type="text" name="name_detector_{{ item.code }}" class="form-control form-control-sm" value="{{ item.name }}">
</td>
<td><code>{{ item.pole_code|default:"—" }}</code></td>
<td>
{% if item.status == 'new' %}
<span class="badge bg-success">{% translate "Nouveau" %}</span>
{% elif item.status == 'modified' %}
<span class="badge bg-warning text-dark">{% translate "Modifié" %}</span>
{% elif item.status == 'identical' %}
<span class="badge bg-light text-secondary border">{% translate "Identique" %}</span>
{% elif item.status == 'db_only' %}
<span class="badge bg-danger">{% translate "DB uniquement" %}</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>
{% if item.status == 'db_only' %}
<select name="action_detector_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="keep">{% translate "Conserver en DB" %}</option>
<option value="archive" selected>{% translate "Archiver" %}</option>
</select>
{% else %}
<select name="action_detector_{{ item.code }}" class="form-select form-select-sm rounded-2">
{% if item.status == 'new' %}
<option value="add" selected>{% translate "Créer l'asset" %}</option>
<option value="keep">{% translate "Ignorer" %}</option>
{% elif item.status == 'modified' %}
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
<option value="keep">{% translate "Conserver existant" %}</option>
{% elif item.status == 'identical' %}
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
{% endif %}
</select>
{% endif %}
</td>
<td class="pe-4">
{% if item.status != 'db_only' %}
<select name="model_detector_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="">-- {% translate "Laisser par défaut" %} --</option>
{% for m in detector_models %}
<option value="{{ m.id }}">{{ m.name_fr }}</option>
{% endfor %}
</select>
{% else %}
<span class="text-muted small">—</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center py-4 text-muted">{% translate "Aucun détecteur détecté." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- 4. CABLES PANEL -->
<div class="tab-pane fade" id="cables-pane" role="tabpanel" aria-labelledby="cables-tab">
<div class="p-3 bg-light border-bottom d-flex align-items-center justify-content-between">
<span class="text-secondary small">
<i class="bi bi-info-circle"></i> {% translate "Vous pouvez tracer précisément les câbles d'interconnexion le long des tranchées de la carte." %}
</span>
<button type="button" class="btn btn-outline-success btn-sm rounded-3 show-map-btn" data-target-tab="cables">
<i class="bi bi-bezier2 me-1"></i> {% translate "Tracer les câbles sur la carte" %}
</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-4" style="width: 40px;"><input type="checkbox" id="select-all-cables" class="form-check-input" checked></th>
<th>{% translate "Code Asset" %}</th>
<th>{% translate "Nom" %}</th>
<th>{% translate "Statut" %}</th>
<th>{% translate "Modèle Déduit (Excel)" %}</th>
<th>{% translate "Modèle Actuel (DB)" %}</th>
<th>{% translate "Décision" %}</th>
<th class="pe-4">{% translate "Forcer le Modèle" %}</th>
</tr>
</thead>
<tbody>
{% for item in cables %}
<tr class="{% if item.status == 'new' %}table-success-light{% elif item.status == 'modified' %}table-warning-light{% elif item.status == 'db_only' %}table-danger-light{% endif %}">
<td class="ps-4"><input type="checkbox" name="{% if item.status == 'db_only' %}selected_cable_db_{{ item.code }}{% else %}selected_cable_{{ item.code }}{% endif %}" class="form-check-input cable-checkbox" checked></td>
<td class="font-weight-medium"><code>{{ item.code }}</code><input type="hidden" name="db_id_cable_{{ item.code }}" value="{{ item.db_id|default:'' }}"></td>
<td>
<input type="text" name="name_cable_{{ item.code }}" class="form-control form-control-sm" value="{{ item.name }}">
</td>
<td>
{% if item.status == 'new' %}
<span class="badge bg-success">{% translate "Nouveau" %}</span>
{% elif item.status == 'modified' %}
<span class="badge bg-warning text-dark">{% translate "Modifié" %}</span>
{% elif item.status == 'identical' %}
<span class="badge bg-light text-secondary border">{% translate "Identique" %}</span>
{% elif item.status == 'db_only' %}
<span class="badge bg-danger">{% translate "DB uniquement" %}</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>
{% if item.status == 'db_only' %}
<select name="action_cable_db_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="keep">{% translate "Conserver en DB" %}</option>
<option value="archive" selected>{% translate "Archiver" %}</option>
</select>
{% else %}
<select name="action_cable_{{ item.code }}" class="form-select form-select-sm rounded-2">
{% if item.status == 'new' %}
<option value="add" selected>{% translate "Créer l'asset" %}</option>
<option value="keep">{% translate "Ignorer" %}</option>
{% elif item.status == 'modified' %}
<option value="modify" selected>{% translate "Mettre à jour l'asset" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
<option value="keep">{% translate "Conserver existant" %}</option>
{% elif item.status == 'identical' %}
<option value="keep" selected>{% translate "Ne rien faire (Identique)" %}</option>
<option value="replace">{% translate "Remplacer (Archiver + Créer)" %}</option>
{% endif %}
</select>
{% endif %}
</td>
<td class="pe-4">
{% if item.status != 'db_only' %}
<select name="model_cable_{{ item.code }}" class="form-select form-select-sm rounded-2">
<option value="">-- {% translate "Laisser par défaut" %} --</option>
{% for m in cable_models %}
<option value="{{ m.id }}">{{ m.name_fr }}</option>
{% endfor %}
</select>
{% else %}
<span class="text-muted small">—</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">{% translate "Aucun câble détecté." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- B. SPLIT SCREEN WORKSPACE VIEW (Default Hidden) -->
<div id="map-view" class="d-none">
<div class="row">
<!-- Left Panel (Sidebar for map elements) -->
<div class="col-lg-4 col-md-5 mb-3 d-flex flex-column" style="height: calc(100vh - 120px);">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden h-100 d-flex flex-column" style="background-color: #ffffff;">
<!-- Header & Switch back button + placed count display -->
<div class="p-3 border-bottom bg-light d-flex flex-column gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm w-100 hide-map-btn rounded-3">
<i class="bi bi-arrow-left me-1"></i> {% translate "Retour au tableau comparatif" %}
</button>
<div class="d-flex align-items-center justify-content-between text-muted small px-1">
<span>{% translate "Poteaux positionnés :" %}</span>
<strong id="placed-poles-count" class="text-primary font-monospace">0 / 0</strong>
</div>
</div>
<!-- Tabs Header -->
<div class="card-header bg-white py-2 border-bottom">
<ul class="nav nav-tabs card-header-tabs" id="mapTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active font-weight-bold px-2 py-1.5" id="map-poles-tab" data-bs-toggle="tab" data-bs-target="#map-poles-pane" type="button" role="tab" aria-controls="map-poles-pane" aria-selected="true" style="font-size: 0.82rem;">
{% translate "Poteaux" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link font-weight-bold px-2 py-1.5" id="map-cables-tab" data-bs-toggle="tab" data-bs-target="#map-cables-pane" type="button" role="tab" aria-controls="map-cables-pane" aria-selected="false" style="font-size: 0.82rem;">
{% translate "Câbles" %}
</button>
</li>
</ul>
</div>
<!-- Scrollable Content without double scrollbars -->
<div class="card-body p-3 flex-grow-1 overflow-hidden d-flex flex-column" style="font-size: 0.82rem;">
<div class="tab-content flex-grow-1 overflow-hidden" id="mapTabsContent">
<!-- Map Poles Sub-pane -->
<div class="tab-pane fade show active h-100" id="map-poles-pane" role="tabpanel" aria-labelledby="map-poles-tab">
<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="font-weight-semibold text-primary mb-1">
<i class="bi bi-info-circle-fill"></i> {% translate "Aide au placement" %}
</div>
<p class="text-secondary mb-0 small">
{% translate "Cliquez sur 'Placer sur la carte' pour un poteau, puis cliquez sur la carte pour le positionner." %}
</p>
</div>
<div class="list-group list-group-flush overflow-auto flex-grow-1 w-100 align-self-stretch">
{% for item in poles %}
{% if item.status != 'db_only' %}
<div class="list-group-item py-2 px-1 border-bottom d-flex flex-column gap-1 pole-config-item align-self-stretch" data-code="{{ item.code }}">
<div class="d-flex align-items-center justify-content-between w-100">
<strong class="font-monospace text-dark">{{ item.short_code }}</strong>
<span class="badge bg-danger text-white georef-status-badge" id="georef_badge_{{ item.code }}" style="font-size: 0.7rem;">{% translate "À placer" %}</span>
</div>
<div class="text-muted small font-monospace">{{ item.deduced_model_name|default:item.db_model_name }}</div>
<div class="small text-muted">
<span id="meta_lanterns_{{ item.code }}">0 Lanternes</span> |
<span id="meta_detectors_{{ item.code }}">0 Détecteurs</span>
</div>
<div class="mt-1 align-self-start">
<button type="button" class="btn btn-xs btn-outline-primary py-0.5 px-2 start-placement-btn rounded-2" data-code="{{ item.code }}">
<i class="bi bi-geo-alt-fill"></i> {% translate "Placer sur la carte" %}
</button>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<!-- Map Cables Sub-pane -->
<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="mb-2 p-2 bg-light rounded-3 border w-100">
<div class="font-weight-semibold text-success mb-1">
<i class="bi bi-bezier"></i> {% translate "Tracé des câbles" %}
</div>
<p class="text-secondary mb-0 small">
{% translate "Cliquez sur 'Tracer / Éditer' puis double-cliquez sur la carte pour tracer la ligne du câble." %}
</p>
</div>
<div class="list-group list-group-flush overflow-auto flex-grow-1 w-100 align-self-stretch">
{% for item in cables %}
{% if item.status != 'db_only' %}
<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">
<strong class="text-dark font-monospace">{{ item.name }}</strong>
</div>
<div class="text-muted small font-monospace">{{ item.deduced_model_name }}</div>
<div class="mt-1 align-self-start">
<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" %}
</button>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Panel (Interactive Map) -->
<div class="col-lg-8 col-md-7 mb-3" style="height: calc(100vh - 120px);">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden h-100 position-relative">
<!-- 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;">
<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-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" %}
</button>
</div>
<div id="map" class="w-100 h-100"></div>
</div>
</div>
</div>
</div>
</form>
<!-- Georeferenced Plans Modal -->
<div class="modal fade" id="georefPlanModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded-3 border-0 shadow-lg">
<div class="modal-header bg-light border-bottom-0 py-3">
<h5 class="modal-title font-weight-bold text-dark"><i class="bi bi-geo-alt-fill text-primary me-2"></i>{% translate "Plans géoréférencés" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body py-3">
<!-- Nav Tabs -->
<ul class="nav nav-tabs nav-fill mb-3" id="georefPlanTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active font-weight-bold" id="tab-image-plan" data-bs-toggle="tab" data-bs-target="#panel-image-plan" type="button" role="tab" aria-controls="panel-image-plan" aria-selected="true">
<i class="bi bi-file-earmark-image me-1"></i>{% translate "Plan image" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link font-weight-bold" id="tab-vector-plan" data-bs-toggle="tab" data-bs-target="#panel-vector-plan" type="button" role="tab" aria-controls="panel-vector-plan" aria-selected="false">
<i class="bi bi-file-earmark-code me-1"></i>{% translate "Plan CAO" %}
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content pt-2" id="georefPlanTabContent">
<!-- Tab 1: Image Plan -->
<div class="tab-pane fade show active" id="panel-image-plan" role="tabpanel" aria-labelledby="tab-image-plan">
<div id="image-plan-selection-container">
<div id="image-plans-list" class="d-flex flex-column gap-3 mb-2">
<!-- Dynamically populated list of images with switches and sliders -->
</div>
<div id="no-georef-images-alert" class="text-center py-4" style="display: none;">
<div class="text-muted mb-3"><i class="bi bi-info-circle" style="font-size: 2.5rem;"></i></div>
<p class="text-dark font-weight-semibold mb-1">{% translate "Aucun plan d'image géoréférencé disponible" %}</p>
<p class="text-muted small">{% translate "Géoréférencez d'abord un plan PDF ou image dans l'onglet Documents de la fiche." %}</p>
</div>
</div>
</div>
<!-- Tab 2: Vector Plan (DXF) -->
<div class="tab-pane fade" id="panel-vector-plan" role="tabpanel" aria-labelledby="tab-vector-plan">
<div id="dxf-selection-container">
{% if not dxf_files %}
<div class="text-center py-4">
<div class="text-muted mb-3"><i class="bi bi-exclamation-triangle" style="font-size: 2.5rem;"></i></div>
<p class="text-dark font-weight-semibold mb-1">{% translate "Aucun fichier DXF disponible pour ce carrefour" %}</p>
<p class="text-muted small">{% translate "Veuillez d'abord en importer un." %}</p>
</div>
{% else %}
<!-- File Selection -->
<div class="mb-3">
<label class="form-label font-weight-semibold text-muted small uppercase">{% translate "Sélectionner le plan DXF" %}</label>
{% if dxf_files|length == 1 %}
<div class="p-3 bg-light border rounded-3 font-weight-semibold text-dark mb-3">
<i class="bi bi-file-earmark-check-fill text-success me-2"></i>{{ dxf_files.0 }}
<input type="hidden" id="select-dxf-file" value="{{ dxf_files.0 }}">
</div>
{% else %}
<select id="select-dxf-file" class="form-select rounded-3 p-3 mb-3">
<option value="">{% translate "-- Sélectionner un plan CAO (DXF) --" %}</option>
{% for f in dxf_files %}
<option value="{{ f }}">{{ f }}</option>
{% endfor %}
</select>
{% endif %}
</div>
<!-- Layers selection checklist -->
<div id="dxf-detail-layers-section" style="display: none;" class="mb-3">
<label class="form-label font-weight-semibold text-muted small uppercase d-flex justify-content-between align-items-center mb-2">
<span>{% translate "Couches vectorielles CAO" %}</span>
<button type="button" id="dxf-detail-toggle-all-layers" class="btn btn-link btn-sm p-0 text-decoration-none" style="font-size: 0.75rem;">
{% translate "Décocher tout" %}
</button>
</label>
<div id="dxf-detail-layers-spinner" class="text-center py-4" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<div class="text-muted small mt-2">{% translate "Lecture des couches du plan..." %}</div>
</div>
<div id="dxf-detail-layers-list" class="border rounded-3 p-3 bg-white" style="max-height: 250px; overflow-y: auto;">
<!-- Checkboxes will be populated dynamically -->
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="modal-footer border-top-0 pt-0 pb-4 justify-content-between px-4">
<button type="button" id="btn-disable-all-plans" class="btn btn-outline-danger px-3 rounded-3">
<i class="bi bi-eye-slash-fill me-1"></i>{% translate "Désactiver tout" %}
</button>
<button type="button" class="btn btn-secondary px-4 rounded-3 font-weight-semibold shadow-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if step == 'preview' %}
<!-- Embedded JSON Data for JS Map Initialization -->
{{ existing_poles_geojson|json_script:"existing_poles_geojson" }}
{{ intersection.lon|json_script:"intersection_lon" }}
{{ intersection.lat|json_script:"intersection_lat" }}
{{ dxf_poles|json_script:"dxf_poles" }}
{{ dxf_files|json_script:"dxf_files" }}
{{ georeferenced_documents_json|json_script:"georeferencedDocumentsData" }}
{% endif %}
<style>
.table-success-light { background-color: #f0fdf4 !important; }
.table-warning-light { background-color: #fffbeb !important; }
.table-danger-light { background-color: #fef2f2 !important; }
.nav-tabs .nav-link {
color: #64748b;
font-weight: 500;
background-color: transparent;
transition: background-color 0.15s, color 0.15s;
}
.nav-tabs .nav-link:hover {
background-color: #f1f5f9;
color: #334155;
}
.nav-tabs .nav-link.active {
color: #2563eb !important;
background-color: #ffffff !important;
border-bottom: 2px solid #2563eb !important;
}
.pole-marker {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: transform 0.15s ease;
}
.pole-marker:hover { transform: scale(1.1); }
.btn-xs { padding: 2px 6px; font-size: 0.75rem; }
.cable-handle:hover { transform: scale(1.3); }
/* Pull top-right controls to the top and leave margin below orthophotos button */
#map .maplibregl-ctrl-top-right {
top: 10px !important;
}
.layer-switcher-ctrl {
margin-bottom: 15px !important;
}
</style>
<script>
function triggerGlobalAction(actionType) {
if (actionType === 'replace_all') {
if (confirm("{% translate 'Voulez-vous vraiment archiver TOUS les assets actifs existants de ce carrefour et recréer les nouveaux à partir du fichier Excel ? Cette action est irréversible.' %}")) {
document.getElementById('global_action_input').value = 'replace_all';
document.getElementById('combined-import-form').submit();
}
}
}
</script>
{% if step == 'preview' %}
<script>
document.addEventListener('DOMContentLoaded', () => {
// 1. Select all checkboxes in tables
const bindSelectAll = (headerId, checkboxClass) => {
const headerCheckbox = document.getElementById(headerId);
if (!headerCheckbox) return;
headerCheckbox.addEventListener('change', () => {
const checkboxes = document.querySelectorAll('.' + checkboxClass);
checkboxes.forEach(cb => {
cb.checked = headerCheckbox.checked;
});
});
};
bindSelectAll('select-all-poles', 'pole-checkbox');
bindSelectAll('select-all-lanterns', 'lantern-checkbox');
bindSelectAll('select-all-detectors', 'detector-checkbox');
bindSelectAll('select-all-cables', 'cable-checkbox');
// 2. Interactive Switch Logic between Table View and Map View
const tableView = document.getElementById('table-view');
const mapView = document.getElementById('map-view');
document.querySelectorAll('.show-map-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const targetTab = this.dataset.targetTab;
tableView.classList.remove('d-block');
tableView.classList.add('d-none');
mapView.classList.remove('d-none');
mapView.classList.add('d-block');
// Trigger MapLibre GL canvas resize recalculation
if (map) {
map.resize();
}
// Activate corresponding tab in map sidebar
if (targetTab === 'poles') {
const tabEl = document.getElementById('map-poles-tab');
if (tabEl) new bootstrap.Tab(tabEl).show();
} else if (targetTab === 'cables') {
const tabEl = document.getElementById('map-cables-tab');
if (tabEl) new bootstrap.Tab(tabEl).show();
}
});
});
document.querySelectorAll('.hide-map-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
mapView.classList.remove('d-block');
mapView.classList.add('d-none');
tableView.classList.remove('d-none');
tableView.classList.add('d-block');
});
});
// 3. MapLibre GL interactive workspace integration
const intersectionId = "{{ intersection.id }}";
window.dxfFile = "{{ dxf_file|escapejs }}";
const intersectionLon = JSON.parse(document.getElementById('intersection_lon').textContent);
const intersectionLat = JSON.parse(document.getElementById('intersection_lat').textContent);
const existingPolesGeojson = JSON.parse(document.getElementById('existing_poles_geojson').textContent);
const initialDxfPoles = JSON.parse(document.getElementById('dxf_poles').textContent);
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const polesList = [
{% for item in poles %}
{
code: "{{ item.code|escapejs }}",
short_code: "{{ item.short_code|escapejs }}",
status: "{{ item.status|escapejs }}"
},
{% endfor %}
];
const lanternsList = [
{% for item in lanterns %}
{
code: "{{ item.code|escapejs }}",
pole_code: "{{ item.pole_code|escapejs }}"
},
{% endfor %}
];
const detectorsList = [
{% for item in detectors %}
{
code: "{{ item.code|escapejs }}",
pole_code: "{{ item.pole_code|escapejs }}"
},
{% endfor %}
];
const cablesList = [
{% for item in cables %}
{
code: "{{ item.code|escapejs }}",
name: "{{ item.name|escapejs }}",
connected_poles: {{ item.connected_poles|safe|default:"[]" }}
},
{% endfor %}
];
// Compute metadata counts for sidebar listing
polesList.forEach(p => {
const lanterns = lanternsList.filter(l => l.pole_code === p.code).length;
const detectors = detectorsList.filter(d => d.pole_code === p.code).length;
const ml = document.getElementById(`meta_lanterns_${p.code}`);
const md = document.getElementById(`meta_detectors_${p.code}`);
if (ml) ml.textContent = `${lanterns} Lanternes`;
if (md) md.textContent = `${detectors} Détecteurs`;
});
let center = [4.3508, 50.8465]; // default Brussels
if (intersectionLon !== null && intersectionLat !== null) {
center = [parseFloat(intersectionLon), parseFloat(intersectionLat)];
}
const map = new maplibregl.Map({
style: window.MAP_STYLE_LIGHT || 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
center: center,
zoom: 17,
container: 'map'
});
let activePlacementPole = null;
let activeEditCable = null;
const placedPoles = {};
const cableGeometries = {};
let cableHandles = [];
const snapCandidates = initialDxfPoles.map(dp => [dp.lon, dp.lat, dp.x_3812, dp.y_3812]);
// AddressSearchControl initialization
const AUTOCOMPLETE_STREET_URL = "{% url 'common:autocomplete_street' %}";
if (typeof AddressSearchControl !== 'undefined') {
const searchCtrl = new AddressSearchControl({
autocompleteUrl: AUTOCOMPLETE_STREET_URL,
placeholder: "{% translate 'Rechercher une adresse…' %}"
});
map.addControl(searchCtrl, 'top-left');
}
// Custom Plans Control in Top Right (first, at the top)
class CustomPlansControl {
onAdd(map) {
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group';
const button = document.createElement('button');
button.type = 'button';
button.id = 'btn-load-georef-plan';
button.className = 'btn btn-xs btn-light bg-white border shadow-sm d-flex align-items-center gap-1';
button.style.border = 'none';
button.style.padding = '8px 12px';
button.style.height = 'auto';
button.style.width = 'auto';
button.style.fontWeight = 'bold';
button.style.fontSize = '0.75rem';
button.innerHTML = '<i class="bi bi-geo-alt-fill text-primary"></i> Plans';
// Toggle bootstrap modal natively
button.setAttribute('data-bs-toggle', 'modal');
button.setAttribute('data-bs-target', '#georefPlanModal');
this._container.appendChild(button);
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}
map.addControl(new CustomPlansControl(), 'top-right');
// --- Contrôle photo aérienne (LayerSwitcherControl) --- (second, below Plans button)
if (typeof LayerSwitcherControl !== 'undefined') {
const layerSwitcher = new LayerSwitcherControl({
layers: [
{
id: 'ortho',
name: 'Orthophotos',
icon: 'satellite',
group: 'ortho',
beforeLayer: 'existing-poles-layer',
layer: {
type: 'raster',
paint: { 'raster-opacity': 1 }
},
sublayers: [
{
id: 'urbis',
name: window.MAP_ORTHO_WMS_NAME || 'Urbis (Bruxelles)',
default: true,
source: {
type: 'raster',
tiles: [
window.MAP_ORTHO_WMS_URL || 'https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}'
],
tileSize: 256,
attribution: window.MAP_ORTHO_WMS_ATTRIBUTION || '© Brussels UrbIS'
}
},
{
id: 'vlaanderen',
name: 'Vlaanderen 2023',
source: {
type: 'raster',
tiles: [
'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}'
],
tileSize: 256,
attribution: '© Digitaal Vlaanderen - Orthofoto 2023'
}
}
]
}
]
});
map.addControl(layerSwitcher, 'top-right');
}
// Classic Zoom Control with Bearing/Compass in Top Right (below Orthophotos control)
map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'top-right');
map.on('load', function() {
// Load active poles as read-only features
map.addSource('existing-poles', {
type: 'geojson',
data: existingPolesGeojson
});
map.addLayer({
id: 'existing-poles-layer',
type: 'circle',
source: 'existing-poles',
paint: {
'circle-radius': 6,
'circle-color': '#6b7280',
'circle-stroke-width': 1.5,
'circle-stroke-color': '#ffffff'
}
});
// Add DXF snapping points source and layer
const dxfPolesFeatures = initialDxfPoles.map(dp => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [dp.lon, dp.lat]
},
properties: {
id: dp.id,
label: dp.label
}
}));
map.addSource('dxf-snap-points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: dxfPolesFeatures
}
});
map.addLayer({
id: 'dxf-snap-layer',
type: 'circle',
source: 'dxf-snap-points',
paint: {
'circle-radius': 5,
'circle-color': '#e2e8f0',
'circle-stroke-width': 1.5,
'circle-stroke-color': '#94a3b8'
}
});
// Add Cables source and layer for tracing representation
map.addSource('cable-lines-source', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
map.addLayer({
id: 'cable-lines-layer',
type: 'line',
source: 'cable-lines-source',
paint: {
'line-color': '#10b981',
'line-width': 1.8,
'line-dasharray': [1.5, 1.5]
}
});
map.addLayer({
id: 'cable-lines-active-layer',
type: 'line',
source: 'cable-lines-source',
filter: ['==', 'active', true],
paint: {
'line-color': '#f59e0b',
'line-width': 2.8
}
});
if (window.dxfFile && window.dxfFile !== "") {
loadDxfBackground(window.dxfFile);
}
updatePolesCounter();
});
function getSnappedCoords(lon, lat) {
let bestDistance = Infinity;
let bestCoords = [lon, lat];
let bestLambert = [null, null];
const threshold = 0.00005; // approx 5m
snapCandidates.forEach(pt => {
const d = Math.sqrt((pt[0] - lon) ** 2 + (pt[1] - lat) ** 2);
if (d < threshold && d < bestDistance) {
bestDistance = d;
bestCoords = [pt[0], pt[1]];
bestLambert = [pt[2], pt[3]];
}
});
return { coords: bestCoords, lambert: bestLambert };
}
function updatePolesCounter() {
const total = polesList.filter(p => p.status !== 'db_only').length;
const placed = Object.keys(placedPoles).length;
document.getElementById('placed-poles-count').textContent = `${placed} / ${total}`;
}
function markPoleAsGeoreferenced(code, lon, lat, x_3812, y_3812) {
const badge = document.getElementById(`georef_badge_${code}`);
if (badge) {
badge.classList.remove('bg-danger');
badge.classList.add('bg-success');
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 : "";
redrawCablesOnMap();
updatePolesCounter();
saveCablesToForm();
}
map.on('click', function(e) {
if (!activePlacementPole) return;
const originalCoords = [e.lngLat.lng, e.lngLat.lat];
const snapped = getSnappedCoords(originalCoords[0], originalCoords[1]);
placePoleMarker(activePlacementPole, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1]);
});
function placePoleMarker(code, lon, lat, x_3812, y_3812) {
if (placedPoles[code]) {
placedPoles[code].marker.remove();
}
const el = document.createElement('div');
el.className = 'pole-marker shadow-sm';
el.style.backgroundColor = '#2563eb';
el.style.color = '#ffffff';
el.style.padding = '3px 8px';
el.style.borderRadius = '20px';
el.style.fontWeight = 'bold';
el.style.fontSize = '12px';
el.style.border = '2px solid #ffffff';
el.style.cursor = 'move';
const short_code = polesList.find(p => p.code === code)?.short_code || code;
el.textContent = short_code;
const marker = new maplibregl.Marker({
element: el,
draggable: true
})
.setLngLat([lon, lat])
.addTo(map);
placedPoles[code] = {
lon: lon,
lat: lat,
x_3812: x_3812,
y_3812: y_3812,
marker: marker
};
marker.on('dragend', function() {
const lngLat = marker.getLngLat();
const snapped = getSnappedCoords(lngLat.lng, lngLat.lat);
marker.setLngLat(snapped.coords);
placedPoles[code].lon = snapped.coords[0];
placedPoles[code].lat = snapped.coords[1];
placedPoles[code].x_3812 = snapped.lambert[0];
placedPoles[code].y_3812 = snapped.lambert[1];
markPoleAsGeoreferenced(code, snapped.coords[0], snapped.coords[1], snapped.lambert[0], snapped.lambert[1]);
});
markPoleAsGeoreferenced(code, lon, lat, x_3812, y_3812);
const mapPane = document.getElementById('map-poles-pane');
mapPane.querySelectorAll('.pole-config-item').forEach(item => item.classList.remove('bg-light'));
const listItem = mapPane.querySelector(`.pole-config-item[data-code="${code}"]`);
if (listItem) {
listItem.classList.add('bg-light');
}
const unplaced = polesList.filter(p => p.status !== 'db_only' && !placedPoles[p.code]);
if (unplaced.length > 0) {
activatePlacementMode(unplaced[0].code);
} else {
deactivatePlacementMode();
}
}
function activatePlacementMode(code) {
deactivateCableEditingMode();
activePlacementPole = code;
map.getCanvas().style.cursor = 'crosshair';
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Placement de" %} ${code.split('_').pop()}`;
document.querySelectorAll('.start-placement-btn').forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
});
const activeBtn = document.querySelector(`.start-placement-btn[data-code="${code}"]`);
if (activeBtn) {
activeBtn.classList.remove('btn-outline-primary');
activeBtn.classList.add('btn-primary');
}
}
function deactivatePlacementMode() {
activePlacementPole = null;
map.getCanvas().style.cursor = '';
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Lecture" %}`;
document.querySelectorAll('.start-placement-btn').forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
});
}
document.querySelectorAll('.start-placement-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const code = this.dataset.code;
if (activePlacementPole === code) {
deactivatePlacementMode();
} else {
activatePlacementMode(code);
}
});
});
// Cable editor helpers
function activateCableEditingMode(code) {
deactivatePlacementMode();
deactivateCableEditingMode();
activeEditCable = code;
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Tracé" %} ${cablesList.find(c => c.code === code).name}`;
document.getElementById('btn-clear-trace').style.display = 'block';
const mapPane = document.getElementById('map-cables-pane');
mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light'));
const item = mapPane.querySelector(`.cable-config-item[data-code="${code}"]`);
if (item) item.classList.add('bg-light');
let coords = cableGeometries[code];
if (!coords || coords.length === 0) {
const cableInfo = cablesList.find(c => c.code === code);
const connected = cableInfo.connected_poles || [];
coords = [];
connected.forEach(pCode => {
if (placedPoles[pCode]) {
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
}
});
cableGeometries[code] = coords;
}
drawCableHandles(code);
redrawCablesOnMap();
}
function deactivateCableEditingMode() {
activeEditCable = null;
document.getElementById('btn-clear-trace').style.display = 'none';
document.getElementById('map-mode-indicator').textContent = `{% translate "Mode: Lecture" %}`;
cableHandles.forEach(h => h.remove());
cableHandles = [];
const mapPane = document.getElementById('map-cables-pane');
mapPane.querySelectorAll('.cable-config-item').forEach(item => item.classList.remove('bg-light'));
redrawCablesOnMap();
}
function drawCableHandles(code) {
cableHandles.forEach(h => h.remove());
cableHandles = [];
const coords = cableGeometries[code] || [];
coords.forEach((pt, index) => {
const el = document.createElement('div');
el.className = 'cable-handle shadow';
el.style.width = '12px';
el.style.height = '12px';
el.style.backgroundColor = '#f59e0b';
el.style.border = '2px solid #ffffff';
el.style.borderRadius = '50%';
el.style.cursor = 'pointer';
const marker = new maplibregl.Marker({
element: el,
draggable: true
})
.setLngLat(pt)
.addTo(map);
cableHandles.push(marker);
marker.on('drag', function() {
const lngLat = marker.getLngLat();
cableGeometries[code][index] = [lngLat.lng, lngLat.lat];
redrawCablesOnMap();
});
marker.on('dragend', function() {
const lngLat = marker.getLngLat();
const snapped = getSnappedCoords(lngLat.lng, lngLat.lat);
marker.setLngLat(snapped.coords);
cableGeometries[code][index] = snapped.coords;
redrawCablesOnMap();
saveCablesToForm();
});
el.addEventListener('dblclick', function(e) {
e.stopPropagation();
cableGeometries[code].splice(index, 1);
drawCableHandles(code);
redrawCablesOnMap();
saveCablesToForm();
});
});
}
function getDistanceToSegment(pt, p1, p2) {
const x = pt[0], y = pt[1];
const x1 = p1[0], y1 = p1[1];
const x2 = p2[0], y2 = p2[1];
const dx = x2 - x1;
const dy = y2 - y1;
if (dx === 0 && dy === 0) {
return Math.sqrt((x - x1) ** 2 + (y - y1) ** 2);
}
let t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
t = Math.max(0, Math.min(1, t)); // clamp to segment
const px = x1 + t * dx;
const py = y1 + t * dy;
return Math.sqrt((x - px) ** 2 + (y - py) ** 2);
}
let dblClickLayerHandled = false;
// Double-click on segment to insert intermediate point
map.on('dblclick', 'cable-lines-active-layer', function(e) {
if (!activeEditCable) return;
dblClickLayerHandled = true;
e.preventDefault();
const clickPoint = [e.lngLat.lng, e.lngLat.lat];
const snapped = getSnappedCoords(clickPoint[0], clickPoint[1]);
const coords = cableGeometries[activeEditCable];
if (!coords || coords.length < 2) return;
let minDistance = Infinity;
let insertIndex = coords.length;
for (let i = 0; i < coords.length - 1; i++) {
const p1 = coords[i];
const p2 = coords[i+1];
const d = getDistanceToSegment(clickPoint, p1, p2);
if (d < minDistance) {
minDistance = d;
insertIndex = i + 1;
}
}
coords.splice(insertIndex, 0, snapped.coords);
drawCableHandles(activeEditCable);
redrawCablesOnMap();
saveCablesToForm();
});
map.on('dblclick', function(e) {
if (!activeEditCable) return;
e.preventDefault();
if (dblClickLayerHandled) {
dblClickLayerHandled = false;
return;
}
const coords = [e.lngLat.lng, e.lngLat.lat];
const snapped = getSnappedCoords(coords[0], coords[1]);
if (!cableGeometries[activeEditCable]) {
cableGeometries[activeEditCable] = [];
}
cableGeometries[activeEditCable].push(snapped.coords);
drawCableHandles(activeEditCable);
redrawCablesOnMap();
saveCablesToForm();
});
// Hover mouse styles for active cable line
map.on('mouseenter', 'cable-lines-active-layer', function() {
if (activeEditCable) {
map.getCanvas().style.cursor = 'pointer';
}
});
map.on('mouseleave', 'cable-lines-active-layer', function() {
if (activeEditCable) {
map.getCanvas().style.cursor = 'crosshair';
} else {
map.getCanvas().style.cursor = '';
}
});
document.getElementById('btn-clear-trace').addEventListener('click', function() {
if (activeEditCable) {
cableGeometries[activeEditCable] = [];
drawCableHandles(activeEditCable);
redrawCablesOnMap();
saveCablesToForm();
}
});
function saveCablesToForm() {
cablesList.forEach(c => {
let coords = cableGeometries[c.code];
if (!coords || coords.length === 0) {
coords = [];
const connected = c.connected_poles || [];
connected.forEach(pCode => {
if (placedPoles[pCode]) {
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
}
});
}
const input = document.getElementById(`geom_cable_${c.code}`);
if (input && coords && coords.length >= 2) {
input.value = JSON.stringify(coords);
} else if (input) {
input.value = "";
}
});
}
document.querySelectorAll('.edit-cable-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const code = this.dataset.code;
if (activeEditCable === code) {
deactivateCableEditingMode();
} else {
activateCableEditingMode(code);
}
});
});
function redrawCablesOnMap() {
if (!map.getSource('cable-lines-source')) return;
const features = [];
cablesList.forEach(c => {
// Only draw/show the cable if it is actively being edited!
if (c.code !== activeEditCable) return;
let coords = cableGeometries[c.code];
if (!coords || coords.length === 0) {
coords = [];
const connected = c.connected_poles || [];
connected.forEach(pCode => {
if (placedPoles[pCode]) {
coords.push([placedPoles[pCode].lon, placedPoles[pCode].lat]);
}
});
}
if (coords && coords.length >= 2) {
// Force a new array reference to bypass MapLibre's internal cache
const freshCoords = coords.map(pt => [pt[0], pt[1]]);
features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: freshCoords
},
properties: {
code: c.code,
active: true
}
});
}
});
map.getSource('cable-lines-source').setData({
type: 'FeatureCollection',
features: features
});
map.triggerRepaint();
}
// ── Georeferenced Plans & DXF Background Loader ──
const georeferencedDocumentsDataEl = document.getElementById('georeferencedDocumentsData');
let georeferencedDocuments = [];
if (georeferencedDocumentsDataEl) {
try {
const parsed = JSON.parse(georeferencedDocumentsDataEl.textContent);
if (Array.isArray(parsed)) {
georeferencedDocuments = parsed;
}
} catch (e) {
console.error("Error parsing georeferenced documents data:", e);
}
}
function showGeorefImage(doc, active, opacity) {
const srcId = `georef-image-overlay-${doc.id}`;
const lyrId = `georef-image-overlay-layer-${doc.id}`;
if (!active) {
if (map.getLayer(lyrId)) {
map.setLayoutProperty(lyrId, 'visibility', 'none');
}
return;
}
if (map.getSource(srcId)) {
if (map.getLayer(lyrId)) {
map.setLayoutProperty(lyrId, 'visibility', 'visible');
map.setPaintProperty(lyrId, 'raster-opacity', opacity);
}
} else {
map.addSource(srcId, {
type: 'image',
url: doc.url,
coordinates: doc.coords
});
let beforeId = undefined;
if (map.getLayer('dxf-layout-fills')) {
beforeId = 'dxf-layout-fills';
} else if (map.getLayer('dxf-layout-lines')) {
beforeId = 'dxf-layout-lines';
} else if (map.getLayer('existing-poles-layer')) {
beforeId = 'existing-poles-layer';
}
map.addLayer({
'id': lyrId,
'type': 'raster',
'source': srcId,
'layout': { 'visibility': 'visible' },
'paint': { 'raster-opacity': opacity }
}, beforeId);
}
// Zoom/fitBounds to document
const coords = doc.coords;
const bounds = coords.reduce((acc, coord) => {
return acc.extend(coord);
}, new maplibregl.LngLatBounds(coords[0], coords[0]));
map.fitBounds(bounds, { padding: 40, maxZoom: 19 });
}
let fullDxfGeoJson = null;
function fetchDxfGeoJson(file) {
const spinner = document.getElementById('dxf-detail-layers-spinner');
if (spinner) spinner.style.display = 'block';
const url = `/assets/trafficlights/intersections/${intersectionId}/dxf-layout-geojson/?file=${encodeURIComponent(file)}&detail=true&layers=all`;
fetch(url)
.then(response => response.json())
.then(data => {
fullDxfGeoJson = data;
applyDxfVectorFilters();
if (spinner) spinner.style.display = 'none';
})
.catch(err => {
console.error("Error fetching DXF GeoJSON:", err);
if (spinner) spinner.style.display = 'none';
});
}
function applyDxfVectorFilters() {
if (!fullDxfGeoJson) return;
const selectedLayers = [];
document.querySelectorAll('.dxf-vector-layer-toggle:checked').forEach(cb => {
selectedLayers.push(cb.dataset.layer);
});
const filteredFeatures = fullDxfGeoJson.features.filter(f => {
const layerName = f.properties && (f.properties.Layer || f.properties.layer);
return selectedLayers.includes(layerName);
});
const filteredData = {
...fullDxfGeoJson,
features: filteredFeatures
};
ensureDxfSourcesAndLayers();
if (map.getSource('dxf-layout')) {
map.getSource('dxf-layout').setData(filteredData);
['dxf-layout-fills', 'dxf-layout-lines', 'dxf-layout-symbols'].forEach(lId => {
if (map.getLayer(lId)) {
map.setLayoutProperty(lId, 'visibility', 'visible');
}
});
}
}
function ensureDxfSourcesAndLayers() {
if (!map.getSource('dxf-layout')) {
map.addSource('dxf-layout', {
type: 'geojson',
data: { type: "FeatureCollection", features: [] }
});
const beforeId = map.getLayer('existing-poles-layer') ? 'existing-poles-layer' : undefined;
map.addLayer({
'id': 'dxf-layout-fills',
'type': 'fill',
'source': 'dxf-layout',
'layout': {
'visibility': 'visible'
},
'paint': {
'fill-color': ['coalesce', ['get', 'color'], '#94a3b8'],
'fill-opacity': 0.1
}
}, beforeId);
map.addLayer({
'id': 'dxf-layout-lines',
'type': 'line',
'source': 'dxf-layout',
'layout': {
'visibility': 'visible'
},
'paint': {
'line-color': ['coalesce', ['get', 'color'], '#475569'],
'line-width': 1.2,
'line-opacity': 0.7
}
}, beforeId);
map.addLayer({
'id': 'dxf-layout-symbols',
'type': 'symbol',
'source': 'dxf-layout',
'filter': ['has', 'Text'],
'layout': {
'visibility': 'visible',
'text-field': ['get', 'Text'],
'text-font': ['Noto Sans Regular'],
'text-size': [
'interpolate',
['linear'],
['zoom'],
17, ['coalesce', ['*', ['get', 'text_height'], 6], 10],
21, ['coalesce', ['*', ['get', 'text_height'], 24], 14]
],
'text-rotate': ['coalesce', ['*', ['get', 'text_angle'], -1], 0],
'text-rotation-alignment': 'map',
'text-anchor': ['coalesce', ['get', 'text_anchor'], 'center'],
'text-allow-overlap': true,
'text-ignore-placement': true
},
'paint': {
'text-color': ['coalesce', ['get', 'color'], '#000000'],
'text-halo-color': '#ffffff',
'text-halo-width': 2
}
}, beforeId);
}
}
function updateButtonActiveState() {
const btn = document.getElementById('btn-load-georef-plan');
if (!btn) return;
let anyImageActive = false;
document.querySelectorAll('.image-plan-toggle').forEach(cb => {
if (cb.checked) anyImageActive = true;
});
let anyVectorActive = false;
document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => {
if (cb.checked) anyVectorActive = true;
});
if (anyImageActive || anyVectorActive) {
btn.classList.remove('btn-light', 'bg-white', 'border');
btn.classList.add('btn-primary');
} else {
btn.classList.remove('btn-primary');
btn.classList.add('btn-light', 'bg-white', 'border');
}
}
const noGeorefImagesAlert = document.getElementById('no-georef-images-alert');
const imagePlansList = document.getElementById('image-plans-list');
if (imagePlansList) {
imagePlansList.innerHTML = '';
if (georeferencedDocuments.length === 0) {
if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'block';
} else {
if (noGeorefImagesAlert) noGeorefImagesAlert.style.display = 'none';
georeferencedDocuments.forEach(doc => {
const item = document.createElement('div');
item.className = 'image-plan-item p-3 bg-light border rounded-3 d-flex flex-column gap-2';
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<span class="text-dark font-weight-bold" style="font-size: 0.9rem;">
<i class="bi ${doc.is_dxf ? 'bi-file-earmark-code text-success' : 'bi-file-earmark-image text-primary'} me-2"></i>${doc.title}
</span>
<div class="form-check form-switch">
<input class="form-check-input image-plan-toggle" type="checkbox" role="switch" id="switch-georef-img-${doc.id}" data-id="${doc.id}">
</div>
</div>
<div class="image-plan-opacity-container" id="opacity-container-${doc.id}" style="display: none;">
<label class="form-label font-weight-semibold text-muted small uppercase d-flex justify-content-between mb-1">
<span>Opacité</span>
<span class="opacity-val text-primary font-weight-bold">95%</span>
</label>
<input type="range" class="form-range image-plan-opacity-slider" min="0" max="100" value="95" id="range-georef-img-opacity-${doc.id}" data-id="${doc.id}">
</div>
`;
imagePlansList.appendChild(item);
const toggle = item.querySelector('.image-plan-toggle');
const opacityContainer = item.querySelector('.image-plan-opacity-container');
const slider = item.querySelector('.image-plan-opacity-slider');
const opacityVal = item.querySelector('.opacity-val');
toggle.addEventListener('change', () => {
const active = toggle.checked;
if (active) {
opacityContainer.style.display = 'block';
} else {
opacityContainer.style.display = 'none';
}
const opacity = parseFloat(slider.value) / 100.0;
showGeorefImage(doc, active, opacity);
updateButtonActiveState();
});
slider.addEventListener('input', () => {
const val = slider.value;
opacityVal.textContent = val + '%';
const opacity = parseFloat(val) / 100.0;
showGeorefImage(doc, toggle.checked, opacity);
});
});
}
}
const dxfFileSelect = document.getElementById('select-dxf-file');
let detailLayersFetched = {};
let detailLayersResponseData = {};
function fetchDetailLayers() {
if (!dxfFileSelect) return;
const file = dxfFileSelect.value;
if (!file) {
const section = document.getElementById('dxf-detail-layers-section');
if (section) section.style.display = 'none';
return;
}
if (detailLayersFetched[file]) {
renderDetailLayers(file);
fetchDxfGeoJson(file);
return;
}
const spinner = document.getElementById('dxf-detail-layers-spinner');
const list = document.getElementById('dxf-detail-layers-list');
const section = document.getElementById('dxf-detail-layers-section');
if (section) section.style.display = 'block';
if (spinner) spinner.style.display = 'block';
if (list) list.style.display = 'none';
fetch(`/assets/trafficlights/intersections/${intersectionId}/dxf-layers/?file=${encodeURIComponent(file)}`)
.then(response => response.json())
.then(data => {
detailLayersFetched[file] = data.layers || [];
detailLayersResponseData[file] = data;
renderDetailLayers(file);
fetchDxfGeoJson(file);
if (spinner) spinner.style.display = 'none';
if (list) list.style.display = 'block';
})
.catch(err => {
console.error("Error fetching detail layers:", err);
if (spinner) {
spinner.innerHTML = `<div class="text-danger small"><i class="bi bi-exclamation-triangle-fill"></i> Erreur de chargement.</div>`;
}
});
}
function renderDetailLayers(file) {
const container = document.getElementById('dxf-detail-layers-list');
if (!container) return;
container.innerHTML = '';
const layers = detailLayersFetched[file] || [];
const responseData = detailLayersResponseData[file] || {};
const selectedLayers = responseData.selected_layers || [];
layers.forEach(layer => {
const isChecked = selectedLayers.includes(layer.name) || selectedLayers.length === 0;
const div = document.createElement('div');
div.className = 'form-check py-1 d-flex align-items-center justify-content-between';
div.innerHTML = `
<div class="d-flex align-items-center">
<span class="color-indicator me-2" style="display: inline-block; width: 14px; height: 14px; border-radius: 3px; background-color: ${layer.color}; border: 1px solid rgba(0,0,0,0.1);"></span>
<label class="form-check-label text-dark font-weight-medium small" for="dxf-detail-layer-${layer.name}">
${layer.name}
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input dxf-vector-layer-toggle" type="checkbox" role="switch"
id="dxf-detail-layer-${layer.name}" data-layer="${layer.name}" ${isChecked ? 'checked' : ''}>
</div>
`;
container.appendChild(div);
const toggle = div.querySelector('.dxf-vector-layer-toggle');
toggle.addEventListener('change', () => {
applyDxfVectorFilters();
updateButtonActiveState();
});
});
}
if (dxfFileSelect) {
dxfFileSelect.addEventListener('change', () => {
fetchDetailLayers();
});
// Auto-load if there is a default select value or single dxf
if (dxfFileSelect.value) {
fetchDetailLayers();
}
}
const btnToggleAllLayers = document.getElementById('dxf-detail-toggle-all-layers');
if (btnToggleAllLayers) {
btnToggleAllLayers.addEventListener('click', () => {
const checkboxes = document.querySelectorAll('.dxf-vector-layer-toggle');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => {
cb.checked = !allChecked;
});
btnToggleAllLayers.textContent = allChecked ? "Tout cocher" : "Décocher tout";
applyDxfVectorFilters();
updateButtonActiveState();
});
}
const btnDisableAllPlans = document.getElementById('btn-disable-all-plans');
if (btnDisableAllPlans) {
btnDisableAllPlans.addEventListener('click', () => {
document.querySelectorAll('.image-plan-toggle').forEach(cb => {
if (cb.checked) {
cb.checked = false;
cb.dispatchEvent(new Event('change'));
}
});
document.querySelectorAll('.dxf-vector-layer-toggle').forEach(cb => {
if (cb.checked) {
cb.checked = false;
}
});
applyDxfVectorFilters();
const modalEl = document.getElementById('georefPlanModal');
if (modalEl) {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
}
updateButtonActiveState();
});
}
// GeoJSON Client-side Vector Layers Loading
const geojsonFileInput = document.getElementById('geojson-file-input');
if (geojsonFileInput) {
geojsonFileInput.addEventListener('change', function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const geojson = JSON.parse(e.target.result);
const sourceId = `user-vector-source-${Date.now()}`;
map.addSource(sourceId, { type: 'geojson', data: geojson });
map.addLayer({
id: `${sourceId}-lines`,
type: 'line',
source: sourceId,
filter: ['in', '$type', 'LineString', 'Polygon', 'MultiLineString', 'MultiPolygon'],
paint: { 'line-color': '#3b82f6', 'line-width': 2, 'line-opacity': 0.8 }
});
map.addLayer({
id: `${sourceId}-points`,
type: 'circle',
source: sourceId,
filter: ['==', '$type', 'Point'],
paint: {
'circle-radius': 5,
'circle-color': '#3b82f6',
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff'
}
});
const coordinates = [];
const extractCoords = (geom) => {
if (!geom) return;
if (geom.type === 'Point') coordinates.push(geom.coordinates);
else if (geom.type === 'LineString' || geom.type === 'MultiPoint') coordinates.push(...geom.coordinates);
else if (geom.type === 'Polygon' || geom.type === 'MultiLineString') geom.coordinates.forEach(c => coordinates.push(...c));
};
if (geojson.features) {
geojson.features.forEach(f => extractCoords(f.geometry));
} else {
extractCoords(geojson.geometry);
}
if (coordinates.length > 0) {
const bounds = coordinates.reduce((b, coord) => {
return [
[Math.min(b[0][0], coord[0]), Math.min(b[0][1], coord[1])],
[Math.max(b[1][0], coord[0]), Math.max(b[1][1], coord[1])]
];
}, [[coordinates[0][0], coordinates[0][1]], [coordinates[0][0], coordinates[0][1]]]);
map.fitBounds(bounds, { padding: 40 });
}
alert(`{% translate "Couche vectorielle chargée avec succès !" %}`);
} catch (err) {
alert("Fichier GeoJSON invalide: " + err.message);
}
};
reader.readAsText(file);
});
}
const form = document.getElementById('combined-import-form');
if (form) {
form.addEventListener('submit', function() {
saveCablesToForm();
});
}
});
</script>
{% endif %}
{% endblock %}