77 MAP_HEIGHT ,
88 MAP_WIDTH ,
99 TILE_SIZE ,
10+ mapPixelToMapLatLng ,
1011 mapLatLngToWorld ,
1112 worldToMapLatLng ,
1213} from ' ./data/locations'
@@ -26,6 +27,8 @@ const bounds = L.latLngBounds([-MAP_HEIGHT, 0], [0, MAP_WIDTH])
2627const INITIAL_ZOOM = -3
2728const MIN_ZOOM = -3
2829const MARKER_FILTERS_STORAGE_KEY = 'nte-marker-filters'
30+ const NAVIGATION_WEBSOCKET_URL = import.meta.env.VITE_MAANTE_NAVI_WEBSOCKET_URL || 'ws://127.0.0.1:8765'
31+ const NAVIGATION_RECONNECT_DELAY = 2000
2932
3033function readStoredIds(key) {
3134 try {
@@ -59,6 +62,17 @@ const routePanelOpen = ref(false)
5962const activeRouteId = ref(null)
6063const isAddingSegment = ref(false)
6164const segmentMarkerIds = ref([])
65+ const navigationConnection = ref('disconnected')
66+ const navigationState = ref({
67+ position: null,
68+ angle: null,
69+ angleConfidence: 0,
70+ })
71+ const navigationConnectionLabel = computed(() => ({
72+ connected: 'CONNECTED',
73+ connecting: 'CONNECTING',
74+ disconnected: 'OFFLINE',
75+ })[navigationConnection.value])
6276
6377const emptyLocationForm = () => ({
6478 name: '',
@@ -82,6 +96,11 @@ const editorCategoryGroups = computed(() => [...new Set(editorCategories.value.m
8296let map
8397let markerLayer
8498let arrowLayer
99+ let navigationMarker
100+ let navigationSocket
101+ let navigationReconnectTimer
102+ let navigationClientStopped = false
103+ let navigationDisplayAngle = null
85104const markerLookup = new Map()
86105
87106const activeRoute = computed(() => routes.value.find((route) => route.id === activeRouteId.value) || null)
@@ -301,6 +320,102 @@ function renderRouteArrows() {
301320 activeRoute.value?.segments.forEach((segment, index) => drawMarkerPath(segment.markerIds, colors[index % colors.length]))
302321}
303322
323+ function createNavigationIcon() {
324+ return L.divIcon({
325+ className: 'navigation-arrow-shell',
326+ html: ` < div class = " navigation-arrow" >< img src= " ${publicAssetUrl('/images/map_webview_pointer.png')}" alt= " " >< / div> ` ,
327+ iconSize: [30, 35],
328+ iconAnchor: [15, 18],
329+ })
330+ }
331+
332+ function updateNavigationMarkerAngle(angle) {
333+ if (!Number.isFinite(angle)) return
334+ if (navigationDisplayAngle === null) {
335+ navigationDisplayAngle = angle
336+ } else {
337+ const delta = ((angle - navigationDisplayAngle + 540) % 360) - 180
338+ navigationDisplayAngle += delta
339+ }
340+ const image = navigationMarker?.getElement()?.querySelector('.navigation-arrow img')
341+ if (image) image.style.transform = ` rotate (${navigationDisplayAngle}deg)`
342+ }
343+
344+ function renderNavigationArrow() {
345+ if (!map || !navigationState.value.position) {
346+ navigationMarker?.setOpacity(0)
347+ return
348+ }
349+ if (!navigationMarker) {
350+ navigationMarker = L.marker(mapPixelToMapLatLng(navigationState.value.position), {
351+ icon: createNavigationIcon(),
352+ interactive: false,
353+ keyboard: false,
354+ zIndexOffset: 1000000,
355+ }).addTo(map)
356+ }
357+ navigationMarker.setLatLng(mapPixelToMapLatLng(navigationState.value.position))
358+ navigationMarker.setOpacity(1)
359+ const arrow = navigationMarker.getElement()?.querySelector('.navigation-arrow')
360+ if (arrow) {
361+ arrow.classList.toggle('navigation-arrow--angle-missing', navigationState.value.angle === null)
362+ }
363+ updateNavigationMarkerAngle(navigationState.value.angle)
364+ }
365+
366+ function handleNavigationMessage(event) {
367+ try {
368+ const payload = JSON.parse(event.data)
369+ if (payload.type !== 'maan-nav-state' || payload.version !== 1) return
370+ const pixelX = Number(payload.position?.pixelX)
371+ const pixelY = Number(payload.position?.pixelY)
372+ const sourceWidth = Number(payload.position?.sourceWidth)
373+ const sourceHeight = Number(payload.position?.sourceHeight)
374+ const angle = Number(payload.angle)
375+ navigationState.value = {
376+ position: Number.isFinite(pixelX) && Number.isFinite(pixelY)
377+ ? {
378+ pixelX,
379+ pixelY,
380+ sourceWidth: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : MAP_WIDTH,
381+ sourceHeight: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : MAP_HEIGHT,
382+ }
383+ : null,
384+ angle: payload.angle !== null && Number.isFinite(angle) ? angle : null,
385+ angleConfidence: Number(payload.angleConfidence) || 0,
386+ }
387+ renderNavigationArrow()
388+ } catch {
389+ // Ignore malformed messages and continue consuming the local stream.
390+ }
391+ }
392+
393+ function scheduleNavigationReconnect() {
394+ if (navigationClientStopped || navigationReconnectTimer) return
395+ navigationReconnectTimer = window.setTimeout(() => {
396+ navigationReconnectTimer = null
397+ connectNavigationSocket()
398+ }, NAVIGATION_RECONNECT_DELAY)
399+ }
400+
401+ function connectNavigationSocket() {
402+ if (navigationClientStopped || navigationSocket) return
403+ navigationConnection.value = 'connecting'
404+ const socket = new WebSocket(NAVIGATION_WEBSOCKET_URL)
405+ navigationSocket = socket
406+ socket.addEventListener('open', () => {
407+ if (navigationSocket === socket) navigationConnection.value = 'connected'
408+ })
409+ socket.addEventListener('message', handleNavigationMessage)
410+ socket.addEventListener('close', () => {
411+ if (navigationSocket !== socket) return
412+ navigationSocket = null
413+ navigationConnection.value = 'disconnected'
414+ scheduleNavigationReconnect()
415+ })
416+ socket.addEventListener('error', () => socket.close())
417+ }
418+
304419function focusSegment(segment) {
305420 if (!map) return
306421 const points = segment.markerIds.map((id) => locationLookup.value[id]).filter(Boolean).map(worldToMapLatLng)
@@ -577,10 +692,15 @@ onMounted(async () => {
577692 mapElement.value.dataset.minZoom = String(map.getMinZoom())
578693 mapElement.value.dataset.initialZoom = String(map.getZoom())
579694 renderMarkers()
695+ connectNavigationSocket()
580696 window.addEventListener('keydown', handleKeydown)
581697})
582698
583699onUnmounted(() => {
700+ navigationClientStopped = true
701+ if (navigationReconnectTimer) window.clearTimeout(navigationReconnectTimer)
702+ navigationSocket?.close()
703+ navigationMarker?.remove()
584704 window.removeEventListener('keydown', handleKeydown)
585705 map?.remove()
586706})
@@ -721,6 +841,9 @@ onUnmounted(() => {
721841 <div class="map-hud glass-panel">
722842 <button type="button" @click="resetView">重置视野</button>
723843 <span>LAT {{ coordinates.lat.toFixed(2) }}</span><span>LNG {{ coordinates.lng.toFixed(2) }}</span>
844+ <span class="navigation-status" :class="` navigation- status-- ${navigationConnection}` ">NAVI {{ navigationConnectionLabel }}</span>
845+ <span v-if="navigationState.position">POS {{ navigationState.position.pixelX.toFixed(0) }}, {{ navigationState.position.pixelY.toFixed(0) }}</span>
846+ <span v-if="navigationState.angle !== null">ANGLE {{ navigationState.angle.toFixed(1) }}°</span>
724847 </div>
725848 <div v-if="editorMode" class="editor-tip glass-panel">编辑模式:点击地图空白处添加点位</div>
726849 <div v-if="statusMessage" class="status-toast glass-panel">{{ statusMessage }}</div>
0 commit comments