Tracé GPS réel (Traccar)
@@ -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('
' + String(p.t || '').replace(/[<>&]/g, '') + '
').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)