feat: implement interactive mini-map and fullscreen positioning mode for precise sign placement

This commit is contained in:
kdeterme 2026-08-23 12:10:29 +02:00
parent d931827c84
commit d138ee98c4
3 changed files with 229 additions and 17 deletions

View file

@ -1669,8 +1669,184 @@
const modal = bootstrap.Modal.getOrCreateInstance(modalEl); const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
let miniMap = null;
let miniMarker = null;
let fullScreenPlacementMarker = null;
let isMapPlacementActive = false;
function updateNewSignCoords(lat, lon, updateMini = false) {
const inputLat = document.getElementById('new_sign_lat');
const inputLon = document.getElementById('new_sign_lon');
const coordsDisplay = document.getElementById('new_sign_gps_coords');
const fLat = parseFloat(lat);
const fLon = parseFloat(lon);
if (isNaN(fLat) || isNaN(fLon)) return;
if (inputLat) inputLat.value = fLat.toFixed(6);
if (inputLon) inputLon.value = fLon.toFixed(6);
if (coordsDisplay) {
coordsDisplay.textContent = `Lat: ${fLat.toFixed(5)}, Lon: ${fLon.toFixed(5)}`;
}
if (updateMini && miniMarker && miniMap) {
miniMarker.setLngLat([fLon, fLat]);
miniMap.setCenter([fLon, fLat]);
}
}
function initOrUpdateMiniMap(lat, lon) {
const container = document.getElementById('new_sign_mini_map');
if (!container || typeof maplibregl === 'undefined') return;
const fLat = parseFloat(lat);
const fLon = parseFloat(lon);
if (isNaN(fLat) || isNaN(fLon)) return;
if (!miniMap) {
const styleUrl = window.MAP_STYLE_LIGHT || 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
miniMap = new maplibregl.Map({
container: 'new_sign_mini_map',
style: styleUrl,
center: [fLon, fLat],
zoom: 18.5,
attributionControl: false
});
miniMarker = new maplibregl.Marker({ draggable: true, color: '#dc3545' })
.setLngLat([fLon, fLat])
.addTo(miniMap);
miniMarker.on('dragend', function() {
const lngLat = miniMarker.getLngLat();
updateNewSignCoords(lngLat.lat, lngLat.lng, false);
});
miniMap.on('click', function(e) {
miniMarker.setLngLat(e.lngLat);
updateNewSignCoords(e.lngLat.lat, e.lngLat.lng, false);
});
} else {
miniMap.setCenter([fLon, fLat]);
if (miniMarker) miniMarker.setLngLat([fLon, fLat]);
setTimeout(() => miniMap.resize(), 150);
}
}
modalEl.addEventListener('shown.bs.modal', function() {
const inputLat = document.getElementById('new_sign_lat');
const inputLon = document.getElementById('new_sign_lon');
const lat = inputLat ? parseFloat(inputLat.value) : null;
const lon = inputLon ? parseFloat(inputLon.value) : null;
if (lat !== null && lon !== null && !isNaN(lat) && !isNaN(lon)) {
initOrUpdateMiniMap(lat, lon);
}
if (miniMap) {
setTimeout(() => miniMap.resize(), 200);
}
});
// Bouton Recentrer sur le GPS de l'appareil
const btnResetGps = document.getElementById('btn_sign_reset_gps');
if (btnResetGps) {
btnResetGps.addEventListener('click', function() {
let lat = null, lon = null;
if (typeof userMarker !== 'undefined' && userMarker) {
const lngLat = userMarker.getLngLat();
lat = lngLat.lat;
lon = lngLat.lng;
} else if (typeof map !== 'undefined' && map) {
const center = map.getCenter();
lat = center.lat;
lon = center.lng;
}
if (lat !== null && lon !== null) {
updateNewSignCoords(lat, lon, true);
}
});
}
// Mode Positionnement Plein Écran sur la grande carte
const btnAdjustFullscreen = document.getElementById('btn_sign_adjust_fullscreen');
const placementBar = document.getElementById('map-placement-bar');
const btnConfirmPlacement = document.getElementById('btn-confirm-map-placement');
const btnCancelPlacement = document.getElementById('btn-cancel-map-placement');
function onMapPlacementClick(e) {
if (!isMapPlacementActive || !fullScreenPlacementMarker) return;
fullScreenPlacementMarker.setLngLat(e.lngLat);
}
if (btnAdjustFullscreen) {
btnAdjustFullscreen.addEventListener('click', function() {
const inputLat = document.getElementById('new_sign_lat');
const inputLon = document.getElementById('new_sign_lon');
let curLat = inputLat ? parseFloat(inputLat.value) : null;
let curLon = inputLon ? parseFloat(inputLon.value) : null;
if (curLat === null || curLon === null || isNaN(curLat) || isNaN(curLon)) {
if (typeof map !== 'undefined' && map) {
const center = map.getCenter();
curLat = center.lat;
curLon = center.lng;
}
}
modal.hide();
if (typeof map !== 'undefined' && map && curLat !== null && curLon !== null) {
isMapPlacementActive = true;
if (placementBar) placementBar.classList.remove('d-none');
if (fullScreenPlacementMarker) {
fullScreenPlacementMarker.remove();
}
fullScreenPlacementMarker = new maplibregl.Marker({ draggable: true, color: '#dc3545' })
.setLngLat([curLon, curLat])
.addTo(map);
map.flyTo({ center: [curLon, curLat], zoom: 19 });
map.on('click', onMapPlacementClick);
}
});
}
if (btnConfirmPlacement) {
btnConfirmPlacement.addEventListener('click', function() {
if (fullScreenPlacementMarker) {
const lngLat = fullScreenPlacementMarker.getLngLat();
updateNewSignCoords(lngLat.lat, lngLat.lng, true);
fullScreenPlacementMarker.remove();
fullScreenPlacementMarker = null;
}
isMapPlacementActive = false;
if (placementBar) placementBar.classList.add('d-none');
if (typeof map !== 'undefined' && map) {
map.off('click', onMapPlacementClick);
}
modal.show();
});
}
if (btnCancelPlacement) {
btnCancelPlacement.addEventListener('click', function() {
if (fullScreenPlacementMarker) {
fullScreenPlacementMarker.remove();
fullScreenPlacementMarker = null;
}
isMapPlacementActive = false;
if (placementBar) placementBar.classList.add('d-none');
if (typeof map !== 'undefined' && map) {
map.off('click', onMapPlacementClick);
}
modal.show();
});
}
btnAdd.addEventListener('click', function() { btnAdd.addEventListener('click', function() {
// Déterminer la position GPS // Déterminer la position GPS par défaut
let lat = null, lon = null; let lat = null, lon = null;
if (typeof userMarker !== 'undefined' && userMarker) { if (typeof userMarker !== 'undefined' && userMarker) {
const lngLat = userMarker.getLngLat(); const lngLat = userMarker.getLngLat();
@ -1682,16 +1858,8 @@
lon = center.lng; lon = center.lng;
} }
const inputLat = document.getElementById('new_sign_lat'); if (lat !== null && lon !== null) {
const inputLon = document.getElementById('new_sign_lon'); updateNewSignCoords(lat, lon, false);
const coordsDisplay = document.getElementById('new_sign_gps_coords');
if (inputLat && inputLon && lat !== null && lon !== null) {
inputLat.value = lat.toFixed(6);
inputLon.value = lon.toFixed(6);
if (coordsDisplay) {
coordsDisplay.textContent = `Lat: ${lat.toFixed(5)}, Lon: ${lon.toFixed(5)}`;
}
} }
// Réinitialiser la liste des panneaux et l'aperçu photo // Réinitialiser la liste des panneaux et l'aperçu photo

View file

@ -16,14 +16,34 @@
<form id="formQuickAddSign" enctype="multipart/form-data"> <form id="formQuickAddSign" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<!-- Coordonnées GPS --> <!-- Coordonnées et Localisation exacte -->
<div class="card bg-light border-0 rounded-3 p-2.5 mb-3"> <div class="card border rounded-3 p-2.5 mb-3 bg-white shadow-xs">
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between mb-2">
<span class="small fw-semibold text-secondary d-flex align-items-center gap-1"> <span class="small fw-bold text-dark d-flex align-items-center gap-1">
<i class="bi bi-geo-alt-fill text-danger"></i>{% translate "Position d'implantation" %} <i class="bi bi-geo-alt-fill text-danger"></i>{% translate "Emplacement exact du mât" %}
</span> </span>
<span class="badge bg-white text-dark border extra-small" id="new_sign_gps_coords">Lat: -, Lon: -</span> <span class="badge bg-light text-dark border extra-small" id="new_sign_gps_coords">Lat: -, Lon: -</span>
</div> </div>
<!-- Mini-carte interactive de précision -->
<div class="position-relative mb-2 rounded-3 overflow-hidden border shadow-xs" style="height: 160px; background: #e9ecef;">
<div id="new_sign_mini_map" style="width: 100%; height: 100%;"></div>
<div class="position-absolute top-0 end-0 m-1.5 d-flex flex-column gap-1" style="z-index: 5;">
<button type="button" id="btn_sign_adjust_fullscreen" class="btn btn-sm btn-white bg-white shadow-sm border py-1 px-2 d-flex align-items-center gap-1 extra-small" title="{% translate 'Ajuster sur la grande carte' %}">
<i class="bi bi-arrows-fullscreen text-primary"></i>
<span class="fw-semibold">{% translate "Grande carte" %}</span>
</button>
<button type="button" id="btn_sign_reset_gps" class="btn btn-sm btn-white bg-white shadow-sm border py-1 px-2 d-flex align-items-center gap-1 extra-small" title="{% translate 'Recentrer sur ma position GPS' %}">
<i class="bi bi-crosshair text-primary"></i>
<span>{% translate "Mon GPS" %}</span>
</button>
</div>
</div>
<div class="d-flex align-items-center justify-content-between text-muted" style="font-size: 0.75rem;">
<span><i class="bi bi-info-circle me-1"></i>{% translate "Glissez le repère rouge ou touchez la carte pour placer le mât au mètre près." %}</span>
</div>
<input type="hidden" name="lat" id="new_sign_lat" value=""> <input type="hidden" name="lat" id="new_sign_lat" value="">
<input type="hidden" name="lon" id="new_sign_lon" value=""> <input type="hidden" name="lon" id="new_sign_lon" value="">
</div> </div>

View file

@ -234,6 +234,30 @@
<!-- Carte MapLibre --> <!-- Carte MapLibre -->
<div id="inspection-map"></div> <div id="inspection-map"></div>
<!-- Bandeau de positionnement précis sur la grande carte -->
<div id="map-placement-bar" class="position-absolute top-0 start-50 translate-middle-x mt-3 d-none shadow-lg" style="z-index: 1050; width: 92%; max-width: 520px;">
<div class="card border-0 bg-dark bg-opacity-95 text-white rounded-4 p-2.5 shadow">
<div class="d-flex align-items-center justify-content-between gap-2">
<div class="d-flex align-items-center gap-2 overflow-hidden">
<div class="spinner-grow spinner-grow-sm text-danger flex-shrink-0" role="status"></div>
<div class="text-truncate">
<div class="fw-bold small text-white text-truncate">{% translate "Emplacement exact du mât" %}</div>
<div class="text-white-50 extra-small text-truncate">{% translate "Glissez le repère rouge ou touchez la carte" %}</div>
</div>
</div>
<div class="d-flex align-items-center gap-1.5 flex-shrink-0">
<button type="button" class="btn btn-sm btn-outline-light py-1 px-2.5" id="btn-cancel-map-placement">
{% translate "Annuler" %}
</button>
<button type="button" class="btn btn-sm btn-success fw-bold py-1 px-3 d-flex align-items-center gap-1 shadow-sm" id="btn-confirm-map-placement">
<i class="bi bi-check-lg"></i>
<span>{% translate "Valider" %}</span>
</button>
</div>
</div>
</div>
</div>
<!-- Actions Flottantes (Bouton Ajout Panneau & GPS) --> <!-- Actions Flottantes (Bouton Ajout Panneau & GPS) -->
<div class="map-floating-actions"> <div class="map-floating-actions">
<button id="btn-add-quick-sign" class="btn btn-primary btn-lg shadow rounded-circle d-none align-items-center justify-content-center border-0" title="{% translate 'Ajouter un panneau de signalisation' %}" style="width: 52px; height: 52px;"> <button id="btn-add-quick-sign" class="btn btn-primary btn-lg shadow rounded-circle d-none align-items-center justify-content-center border-0" title="{% translate 'Ajouter un panneau de signalisation' %}" style="width: 52px; height: 52px;">