feat: implement interactive mini-map and fullscreen positioning mode for precise sign placement
This commit is contained in:
parent
d931827c84
commit
d138ee98c4
3 changed files with 229 additions and 17 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue