feat(job-map): itinéraire routier OSRM tracé DANS la carte MapLibre
Le module carte du job (JobMapModule, réutilisé volet Tournées + détail ticket) avait un repère + tracé GPS Traccar mais AUCUN itinéraire in-app (seulement un lien Google externe). Ajout d'un toggle « Itinéraire routier » : trace le trajet routier RÉEL via l'OSRM auto-hébergé (roster.osrmRoute → geometry) sur la carte MapLibre + affiche km/min, et cadre la vue sur le trajet. Départ = position live du tech assigné (traccarLive), sinon géoloc du navigateur ; destination = coords du SERVICE (GPS de l'adresse de service, désormais fiables). Lien « Navigation » Google conservé pour la voix sur téléphone. Réutilise l'infra existante (OSRM /roster/osrm-route, MapLibre/OSM auto-hébergé, ZÉRO Mapbox). Vérifié : build spa OK, 0 erreur HMR. Rendu live (fiche/ticket) = SSO côté prod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
12b9da3e36
commit
08a68016ec
|
|
@ -16,6 +16,14 @@
|
|||
:label="show ? 'Masquer la carte' : 'Voir la carte / tracé GPS'" @click="show = !show" />
|
||||
<div v-show="show">
|
||||
<div ref="mapEl" class="jd-map"></div>
|
||||
<!-- Itinéraire routier RÉEL (OSRM auto-hébergé) tracé DANS la carte MapLibre — départ = position live du tech
|
||||
assigné, sinon géoloc du navigateur. Le lien Google reste dispo pour la navigation vocale sur le téléphone. -->
|
||||
<div v-if="hasCoords" class="jd-track-row">
|
||||
<q-toggle v-model="showRoute" dense size="sm" color="primary" />
|
||||
<span class="text-caption text-weight-medium">Itinéraire routier</span>
|
||||
<span class="text-caption text-grey-6">{{ routeMsg }}</span>
|
||||
<a :href="navUrl" target="_blank" rel="noopener" class="jd-geolink q-ml-sm"><q-icon name="navigation" size="13px" /> Navigation</a>
|
||||
</div>
|
||||
<div v-if="canTrack" class="jd-track-row">
|
||||
<q-toggle v-model="showTrack" dense size="sm" color="warning" />
|
||||
<span class="text-caption text-weight-medium">Tracé GPS réel (Traccar)</span>
|
||||
|
|
@ -45,11 +53,13 @@ const props = defineProps({
|
|||
const mapEl = ref(null); let _map = null
|
||||
const show = ref(false)
|
||||
const showTrack = ref(false); const trackMsg = ref('')
|
||||
const showRoute = ref(false); const routeMsg = ref('')
|
||||
|
||||
const laNum = computed(() => +props.lat); const loNum = computed(() => +props.lon)
|
||||
const hasCoords = computed(() => isFinite(laNum.value) && isFinite(loNum.value) && (laNum.value !== 0 || loNum.value !== 0))
|
||||
const mapsUrl = computed(() => `https://www.google.com/maps/search/?api=1&query=${props.lat},${props.lon}`)
|
||||
const streetViewUrl = computed(() => `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${props.lat},${props.lon}`)
|
||||
const navUrl = computed(() => `https://www.google.com/maps/dir/?api=1&destination=${props.lat},${props.lon}&travelmode=driving`) // navigation vocale (téléphone)
|
||||
|
||||
async function initMap () {
|
||||
if (!mapEl.value) return
|
||||
|
|
@ -67,9 +77,14 @@ async function initMap () {
|
|||
_map.on('click', 'jd-near', (e) => { const p = e.features[0].properties; new mapboxgl.Popup({ offset: 10 }).setLngLat(e.features[0].geometry.coordinates).setHTML('<div style="font-size:11px;color:#475569">' + String(p.t || '').replace(/[<>&]/g, '') + '</div>').addTo(_map) })
|
||||
}
|
||||
if (hasLL) new mapboxgl.Marker({ color: '#1976d2' }).setLngLat([loNum.value, laNum.value]).addTo(_map)
|
||||
// Itinéraire routier (OSRM) — sous le tracé GPS réel
|
||||
_map.addSource('jd-route', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
_map.addLayer({ id: 'jd-route-halo', type: 'line', source: 'jd-route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#2563eb', 'line-width': 9, 'line-opacity': 0.2 } })
|
||||
_map.addLayer({ id: 'jd-route-l', type: 'line', source: 'jd-route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#2563eb', 'line-width': 4, 'line-opacity': 0.85 } })
|
||||
_map.addSource('jd-track', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
_map.addLayer({ id: 'jd-track-halo', type: 'line', source: 'jd-track', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#ff6f00', 'line-width': 8, 'line-opacity': 0.22 } })
|
||||
_map.addLayer({ id: 'jd-track-l', type: 'line', source: 'jd-track', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#ef6c00', 'line-width': 3, 'line-opacity': 0.78 } })
|
||||
if (showRoute.value) loadRoute()
|
||||
if (showTrack.value) loadTrack()
|
||||
})
|
||||
}
|
||||
|
|
@ -86,9 +101,33 @@ async function loadTrack () {
|
|||
} catch (e) { trackMsg.value = 'Tracé indisponible' }
|
||||
}
|
||||
|
||||
// Géoloc navigateur (repli si pas de tech assigné / pas de GPS live).
|
||||
function browserGeo () { return new Promise(res => { if (!navigator.geolocation) return res(null); navigator.geolocation.getCurrentPosition(p => res([p.coords.longitude, p.coords.latitude]), () => res(null), { enableHighAccuracy: true, timeout: 8000 }) }) }
|
||||
// Itinéraire routier RÉEL via l'OSRM auto-hébergé (roster.osrmRoute) → tracé DANS la carte MapLibre + km/min.
|
||||
// Départ : position live du tech assigné (Traccar), sinon la position du navigateur. Destination = coords du service.
|
||||
async function loadRoute () {
|
||||
if (!_map || !hasCoords.value) { routeMsg.value = ''; return }
|
||||
routeMsg.value = 'calcul…'
|
||||
let start = null, from = ''
|
||||
if (props.techId) {
|
||||
try { const p = await roster.techLivePosition(props.techId); const la = +(p && (p.lat != null ? p.lat : p.latitude)); const lo = +(p && (p.lon != null ? p.lon : p.longitude)); if (isFinite(la) && isFinite(lo) && (la || lo)) { start = [lo, la]; from = '· du technicien' } } catch (e) {}
|
||||
}
|
||||
if (!start) { const g = await browserGeo(); if (g) { start = g; from = '· de ma position' } }
|
||||
if (!start) { routeMsg.value = 'position de départ indisponible (autorise la géolocalisation)'; return }
|
||||
try {
|
||||
const r = await roster.osrmRoute([start, [loNum.value, laNum.value]])
|
||||
const src = _map && _map.getSource('jd-route')
|
||||
if (r && r.geometry && src) {
|
||||
src.setData({ type: 'Feature', geometry: r.geometry, properties: {} })
|
||||
routeMsg.value = `${r.km} km · ${r.mins} min ${from}`
|
||||
try { const b = new window.mapboxgl.LngLatBounds(); (r.geometry.coordinates || []).forEach(c => b.extend(c)); if (!b.isEmpty()) _map.fitBounds(b, { padding: 40, maxZoom: 15 }) } catch (e) {}
|
||||
} else routeMsg.value = 'itinéraire indisponible'
|
||||
} catch (e) { routeMsg.value = 'itinéraire indisponible' }
|
||||
}
|
||||
function destroy () { if (_map) { try { _map.remove() } catch (e) {} _map = null } }
|
||||
watch(show, (on) => { if (on) nextTick(() => setTimeout(initMap, 200)); else destroy() })
|
||||
watch(showTrack, (on) => { if (!_map) return; if (on) loadTrack(); else { const s = _map.getSource('jd-track'); if (s) s.setData({ type: 'FeatureCollection', features: [] }); trackMsg.value = '' } })
|
||||
watch(showRoute, (on) => { if (!_map) return; if (on) loadRoute(); else { const s = _map.getSource('jd-route'); if (s) s.setData({ type: 'FeatureCollection', features: [] }); routeMsg.value = ''; if (hasCoords.value) _map.flyTo({ center: [loNum.value, laNum.value], zoom: 14 }) } })
|
||||
onBeforeUnmount(destroy)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user