feat: add continuous GPS tracking, pulsing location marker, and distance calculation to inspection maps
This commit is contained in:
parent
97ecd8d9fb
commit
c8349a3171
3 changed files with 255 additions and 31 deletions
|
|
@ -290,7 +290,7 @@
|
|||
'match', ['coalesce', ['get', 'structure_type'], ''],
|
||||
['massif drainant', 'tranchee drainante', 'drain', 'collecteur en beton poreux', 'reseau egout separatif', 'oed'], '#8d6e63',
|
||||
['noue', 'jardin de pluie', 'bande filtrante', 'evc'], '#009688',
|
||||
['filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'avaloir de rail', 'trop plein', 'ta'], '#0277bd',
|
||||
['filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'avaloir de rail', 'trop plein', 'ta'], '#455a64',
|
||||
'#0288d1'
|
||||
],
|
||||
'line-width': 2.5
|
||||
|
|
@ -343,11 +343,13 @@
|
|||
['==', ['coalesce', ['get', 'status'], ''], 'inactive'], '#343a40',
|
||||
'#198754'
|
||||
],
|
||||
['match', ['coalesce', ['get', 'model_name'], ''], ['roaddrain', 'drain'], true, false], '#37474f',
|
||||
[
|
||||
'match', ['coalesce', ['get', 'structure_type'], ''],
|
||||
['massif drainant', 'tranchee drainante', 'oed'], '#8d6e63',
|
||||
['jardin de pluie', 'noue', 'evc'], '#009688',
|
||||
['bassin orage', 'citerne', 'ta'], '#0277bd',
|
||||
['avaloir', 'avaloir de rail', 'bouche injection', 'filet eau', 'caniveau infiltrant', 'gargouille de trottoir', 'puits infiltration', 'drain', 'regard', 'vanne regulation', 'vanne isolation'], '#37474f',
|
||||
'#0288d1'
|
||||
]
|
||||
],
|
||||
|
|
@ -731,51 +733,99 @@
|
|||
}, 400);
|
||||
}
|
||||
|
||||
let gpsWatchId = null;
|
||||
let isFirstGpsFix = true;
|
||||
|
||||
function setupGeolocation() {
|
||||
const gpsBtn = document.getElementById('btn-gps-locate');
|
||||
if (gpsBtn) {
|
||||
gpsBtn.addEventListener('click', locateUser);
|
||||
gpsBtn.addEventListener('click', function() {
|
||||
if (window.currentGpsPosition && map) {
|
||||
map.flyTo({
|
||||
center: [window.currentGpsPosition.lng, window.currentGpsPosition.lat],
|
||||
zoom: 18,
|
||||
duration: 800
|
||||
});
|
||||
} else {
|
||||
startGpsTracking(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const hasPreselect = (window.PRESELECT_LON !== null && window.PRESELECT_LAT !== null && typeof window.PRESELECT_LON !== 'undefined' && typeof window.PRESELECT_LAT !== 'undefined');
|
||||
if (!hasPreselect) {
|
||||
locateUser();
|
||||
startGpsTracking(!hasPreselect);
|
||||
|
||||
window.addEventListener('beforeunload', stopGpsTracking);
|
||||
}
|
||||
|
||||
function stopGpsTracking() {
|
||||
if (gpsWatchId !== null && navigator.geolocation) {
|
||||
navigator.geolocation.clearWatch(gpsWatchId);
|
||||
gpsWatchId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function locateUser() {
|
||||
startGpsTracking(true);
|
||||
}
|
||||
|
||||
function startGpsTracking(shouldFlyTo = false) {
|
||||
if (!navigator.geolocation) return;
|
||||
|
||||
const i18n = window.INSPECTION_MAP_I18N || {};
|
||||
const statusText = document.getElementById('gps-status-text');
|
||||
if (statusText) statusText.textContent = i18n.geolocation || (typeof gettext !== 'undefined' ? gettext('Géolocalisation...') : 'Géolocalisation...');
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
if (gpsWatchId !== null) {
|
||||
navigator.geolocation.clearWatch(gpsWatchId);
|
||||
gpsWatchId = null;
|
||||
}
|
||||
|
||||
gpsWatchId = navigator.geolocation.watchPosition(
|
||||
function(position) {
|
||||
const lng = position.coords.longitude;
|
||||
const lat = position.coords.latitude;
|
||||
const accuracy = position.coords.accuracy;
|
||||
|
||||
window.currentGpsPosition = { lng, lat, accuracy, timestamp: Date.now() };
|
||||
|
||||
try {
|
||||
window.dispatchEvent(new CustomEvent('loko:gps-update', {
|
||||
detail: window.currentGpsPosition
|
||||
}));
|
||||
} catch(e) {}
|
||||
|
||||
if (typeof maplibregl !== 'undefined' && maplibregl.Marker && map) {
|
||||
if (userMarker) {
|
||||
userMarker.setLngLat([lng, lat]);
|
||||
} else {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'user-gps-dot';
|
||||
el.className = 'user-gps-dot-container';
|
||||
el.innerHTML = '<div class="user-gps-dot-pulse"></div><div class="user-gps-dot"></div>';
|
||||
|
||||
userMarker = new maplibregl.Marker({ element: el })
|
||||
userMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
|
||||
.setLngLat([lng, lat])
|
||||
.addTo(map);
|
||||
}
|
||||
|
||||
map.flyTo({ center: [lng, lat], zoom: 17 });
|
||||
if (shouldFlyTo || isFirstGpsFix) {
|
||||
map.flyTo({ center: [lng, lat], zoom: 18, duration: 1000 });
|
||||
isFirstGpsFix = false;
|
||||
shouldFlyTo = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = i18n.positionUpdated || (typeof gettext !== 'undefined' ? gettext('Position actualisée') : 'Position actualisée');
|
||||
}
|
||||
if (statusText) statusText.textContent = i18n.positionUpdated || (typeof gettext !== 'undefined' ? gettext('Position actualisée') : 'Position actualisée');
|
||||
},
|
||||
function(err) {
|
||||
console.warn('GPS error:', err.message);
|
||||
if (statusText) statusText.textContent = i18n.gpsUnavailable || (typeof gettext !== 'undefined' ? gettext('GPS indisponible') : 'GPS indisponible');
|
||||
console.warn('GPS watchPosition error:', err.message);
|
||||
if (statusText) {
|
||||
statusText.textContent = i18n.gpsUnavailable || (typeof gettext !== 'undefined' ? gettext('GPS indisponible') : 'GPS indisponible');
|
||||
}
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 5000 }
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 2000 }
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,8 +95,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- Conteneur Carte Agrandie -->
|
||||
<div id="giep_modal_map_container" class="rounded-3 border overflow-hidden" style="height: 480px; max-height: 68vh; position: relative;">
|
||||
<div id="giep_modal_map_container" class="rounded-3 border overflow-hidden position-relative" style="height: 480px; max-height: 68vh;">
|
||||
<div id="giepModalMap" style="width: 100%; height: 100%;"></div>
|
||||
<!-- Bouton Recentrer sur ma position GPS -->
|
||||
<button type="button" id="giep_modal_recenter_user_btn" class="btn btn-light btn-sm shadow rounded-circle border position-absolute d-flex align-items-center justify-content-center" style="bottom: 16px; right: 16px; z-index: 10; width: 44px; height: 44px;" title="{% translate 'Ma position GPS' %}">
|
||||
<i class="bi bi-crosshair text-primary fs-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2 px-3 bg-light">
|
||||
|
|
@ -153,6 +157,39 @@
|
|||
.btn-seg:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.user-gps-dot-container {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.user-gps-dot-pulse {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(111, 66, 193, 0.35);
|
||||
animation: gpsPulse 2s infinite ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.user-gps-dot {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #6f42c1;
|
||||
border: 2.5px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(111, 66, 193, 0.65), 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
z-index: 2;
|
||||
}
|
||||
@keyframes gpsPulse {
|
||||
0% { transform: scale(0.5); opacity: 1; }
|
||||
70% { transform: scale(1.6); opacity: 0; }
|
||||
100% { transform: scale(1.6); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
@ -460,6 +497,18 @@
|
|||
|
||||
let currentMapPopup = null;
|
||||
|
||||
function computeDistanceMeters(lat1, lon1, lat2, lon2) {
|
||||
if (!lat1 || !lon1 || !lat2 || !lon2) return null;
|
||||
const R = 6371000;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
function showAssetPopup(asset, lngLat) {
|
||||
if (!modalMapInstance) return;
|
||||
if (currentMapPopup) {
|
||||
|
|
@ -474,6 +523,18 @@
|
|||
else if (devType === 'ta') badgeHtml = '<span class="badge bg-teal-subtle text-teal text-uppercase me-1">TA</span>';
|
||||
else badgeHtml = '<span class="badge bg-secondary-subtle text-secondary text-uppercase me-1">Équipement</span>';
|
||||
|
||||
let distanceBadgeHtml = '';
|
||||
if (window.currentGpsPosition && lngLat) {
|
||||
const d = computeDistanceMeters(
|
||||
window.currentGpsPosition.lat, window.currentGpsPosition.lng,
|
||||
lngLat.lat, lngLat.lng
|
||||
);
|
||||
if (d !== null && !isNaN(d)) {
|
||||
const distStr = d < 1000 ? `${Math.round(d)} m` : `${(d / 1000).toFixed(1)} km`;
|
||||
distanceBadgeHtml = `<div class="mb-2"><span class="badge bg-light text-primary border extra-small d-inline-flex align-items-center gap-1 shadow-2xs"><i class="bi bi-geo-alt-fill text-danger"></i>~ ${distStr}</span></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const popupNode = document.createElement('div');
|
||||
popupNode.className = 'p-1';
|
||||
popupNode.innerHTML = `
|
||||
|
|
@ -486,6 +547,7 @@
|
|||
${badgeHtml}
|
||||
<span>${asset.structure_type || ''}</span>
|
||||
</div>
|
||||
${distanceBadgeHtml}
|
||||
<button type="button" class="btn btn-primary btn-sm w-100 rounded-pill py-1.5 px-3 fw-semibold select-popup-btn">
|
||||
<i class="bi bi-check-circle me-1"></i>{% filter escapejs %}{% translate "Sélectionner" %}{% endfilter %}
|
||||
</button>
|
||||
|
|
@ -1055,10 +1117,14 @@
|
|||
|
||||
if (matchText && matchType && asset.rawFeature && typeof maplibregl !== 'undefined') {
|
||||
const geom = asset.rawFeature.geometry;
|
||||
let color = '#0277bd'; // Bleu GIEP (naturerwiasset)
|
||||
let color = '#0277bd'; // Bleu GIEP par défaut
|
||||
const descStr = ((asset.structure_type || '') + ' ' + (asset.name || '')).toLowerCase();
|
||||
const isPointAccess = descStr.includes('avaloir') || descStr.includes('injection') || descStr.includes('regard') || descStr.includes('grille') || descStr.includes('caniveau') || descStr.includes('filet eau');
|
||||
|
||||
if (devType === 'evc') color = '#2e7d32'; // Vert EVC
|
||||
if (devType === 'oed') color = '#0277bd'; // Bleu OED
|
||||
if (devType === 'ta') color = '#00796b'; // Turquoise TA
|
||||
else if (devType === 'oed') color = '#8d6e63'; // Brun OED
|
||||
else if (devType === 'ta') color = '#00796b'; // Turquoise TA
|
||||
else if (isPointAccess) color = '#37474f'; // Gris ardoise (avaloir, regard, bouche d'injection)
|
||||
|
||||
if (geom) {
|
||||
const featureClone = {
|
||||
|
|
@ -1204,9 +1270,65 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Cadrer la carte sur les assets
|
||||
// Cadrer la carte sur les assets et la position GPS
|
||||
if (hasCoords && bounds && !bounds.isEmpty()) {
|
||||
modalMapInstance.fitBounds(bounds, { padding: 50, maxZoom: 18, duration: 300 });
|
||||
if (window.currentGpsPosition) {
|
||||
const userLng = window.currentGpsPosition.lng;
|
||||
const userLat = window.currentGpsPosition.lat;
|
||||
const centerLngLat = bounds.getCenter();
|
||||
const distToCenter = computeDistanceMeters(userLat, userLng, centerLngLat.lat, centerLngLat.lng);
|
||||
if (distToCenter !== null && distToCenter < 1200) {
|
||||
bounds.extend([userLng, userLat]);
|
||||
}
|
||||
}
|
||||
modalMapInstance.fitBounds(bounds, { padding: 50, maxZoom: 18, duration: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
let modalMapUserMarker = null;
|
||||
let modalGpsWatchId = null;
|
||||
|
||||
function updateModalMapGpsPosition(pos) {
|
||||
if (!pos || !modalMapInstance) return;
|
||||
const lng = pos.lng || pos.coords?.longitude;
|
||||
const lat = pos.lat || pos.coords?.latitude;
|
||||
if (!lng || !lat) return;
|
||||
|
||||
if (modalMapUserMarker) {
|
||||
modalMapUserMarker.setLngLat([lng, lat]);
|
||||
} else if (typeof maplibregl !== 'undefined') {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'user-gps-dot-container';
|
||||
el.innerHTML = '<div class="user-gps-dot-pulse"></div><div class="user-gps-dot"></div>';
|
||||
modalMapUserMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
|
||||
.setLngLat([lng, lat])
|
||||
.addTo(modalMapInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function startModalGpsTracking() {
|
||||
if (!navigator.geolocation) return;
|
||||
if (modalGpsWatchId !== null) return;
|
||||
modalGpsWatchId = navigator.geolocation.watchPosition(
|
||||
(position) => {
|
||||
const pos = {
|
||||
lng: position.coords.longitude,
|
||||
lat: position.coords.latitude,
|
||||
accuracy: position.coords.accuracy,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
window.currentGpsPosition = pos;
|
||||
updateModalMapGpsPosition(pos);
|
||||
},
|
||||
(err) => console.warn('Modal GPS watchPosition error:', err.message),
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 2000 }
|
||||
);
|
||||
}
|
||||
|
||||
function stopModalGpsTracking() {
|
||||
if (modalGpsWatchId !== null && navigator.geolocation) {
|
||||
navigator.geolocation.clearWatch(modalGpsWatchId);
|
||||
modalGpsWatchId = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1220,30 +1342,27 @@
|
|||
modalMapInstance = new maplibregl.Map({
|
||||
container: 'giepModalMap',
|
||||
style: style,
|
||||
center: [4.3517, 50.8503],
|
||||
zoom: 15,
|
||||
center: (window.currentGpsPosition) ? [window.currentGpsPosition.lng, window.currentGpsPosition.lat] : [4.3517, 50.8503],
|
||||
zoom: 16,
|
||||
antialias: true
|
||||
});
|
||||
|
||||
// Contrôles de navigation et de géolocalisation
|
||||
// Contrôles de navigation
|
||||
modalMapInstance.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
|
||||
const geolocate = new maplibregl.GeolocateControl({
|
||||
positionOptions: { enableHighAccuracy: true },
|
||||
trackUserLocation: true,
|
||||
showUserLocation: true
|
||||
});
|
||||
modalMapInstance.addControl(geolocate, 'top-right');
|
||||
|
||||
modalMapInstance.on('load', () => {
|
||||
updateModalMapGpsPosition(window.currentGpsPosition);
|
||||
renderModalMapFeatures(true);
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
modalMapInstance.resize();
|
||||
updateModalMapGpsPosition(window.currentGpsPosition);
|
||||
renderModalMapFeatures(true);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
startModalGpsTracking();
|
||||
}
|
||||
|
||||
function openMapModal(idx) {
|
||||
|
|
@ -2031,10 +2150,40 @@
|
|||
});
|
||||
modalEl.addEventListener('hidden.bs.modal', (e) => {
|
||||
e.stopPropagation();
|
||||
stopModalGpsTracking();
|
||||
toggleParentGlobalSections(true);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('loko:gps-update', (e) => {
|
||||
updateModalMapGpsPosition(e.detail);
|
||||
});
|
||||
|
||||
const recenterBtn = rootModal.querySelector('#giep_modal_recenter_user_btn') || document.getElementById('giep_modal_recenter_user_btn');
|
||||
if (recenterBtn) {
|
||||
recenterBtn.addEventListener('click', () => {
|
||||
if (window.currentGpsPosition && modalMapInstance) {
|
||||
modalMapInstance.flyTo({
|
||||
center: [window.currentGpsPosition.lng, window.currentGpsPosition.lat],
|
||||
zoom: 18,
|
||||
duration: 800
|
||||
});
|
||||
} else if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(pos => {
|
||||
const lng = pos.coords.longitude;
|
||||
const lat = pos.coords.latitude;
|
||||
window.currentGpsPosition = { lng, lat, accuracy: pos.coords.accuracy, timestamp: Date.now() };
|
||||
updateModalMapGpsPosition(window.currentGpsPosition);
|
||||
if (modalMapInstance) {
|
||||
modalMapInstance.flyTo({ center: [lng, lat], zoom: 18, duration: 800 });
|
||||
}
|
||||
}, err => {
|
||||
console.warn('GPS error:', err);
|
||||
}, { enableHighAccuracy: true, timeout: 8000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const searchInput = rootModal.querySelector('#giep_modal_search_input, .giep-modal-search-input');
|
||||
const typeFilter = rootModal.querySelector('#giep_modal_type_filter, .giep-modal-type-filter');
|
||||
|
||||
|
|
|
|||
|
|
@ -61,13 +61,38 @@
|
|||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.user-gps-dot-container {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.user-gps-dot-pulse {
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(111, 66, 193, 0.35);
|
||||
animation: gpsPulse 2s infinite ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.user-gps-dot {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #0d6efd;
|
||||
border: 2px solid #ffffff;
|
||||
background-color: #6f42c1;
|
||||
border: 2.5px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px rgba(13, 110, 253, 0.8);
|
||||
box-shadow: 0 2px 8px rgba(111, 66, 193, 0.65), 0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
z-index: 2;
|
||||
}
|
||||
@keyframes gpsPulse {
|
||||
0% { transform: scale(0.5); opacity: 1; }
|
||||
70% { transform: scale(1.6); opacity: 0; }
|
||||
100% { transform: scale(1.6); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Bottom Sheet Modal Styling for Mobile */
|
||||
|
|
|
|||
Loading…
Reference in a new issue