diff --git a/meshmap_frontend/src/components/NodeTrajectoryMap.vue b/meshmap_frontend/src/components/NodeTrajectoryMap.vue
index 27761d1..864dee8 100644
--- a/meshmap_frontend/src/components/NodeTrajectoryMap.vue
+++ b/meshmap_frontend/src/components/NodeTrajectoryMap.vue
@@ -40,15 +40,58 @@ function applyTileLayer() {
}).addTo(map)
}
+function pointStyle(isStart: boolean, isEnd: boolean): L.CircleMarkerOptions {
+ if (isStart) {
+ return { radius: 7, color: '#7f9183', fillColor: '#9aaa95', fillOpacity: 0.88, weight: 2 }
+ }
+ if (isEnd) {
+ return { radius: 7, color: '#b4877f', fillColor: '#c59b93', fillOpacity: 0.88, weight: 2 }
+ }
+ return { radius: 3, color: '#7d8f9a', fillColor: '#9ab3c2', fillOpacity: 0.72, weight: 1 }
+}
+
+function buildPositionPopupHTML(position: PositionRecord, isStart: boolean, isEnd: boolean): string {
+ const title = isStart ? '起点' : isEnd ? '终点' : '轨迹点'
+ const latitude = position.latitude == null ? '-' : position.latitude.toFixed(6)
+ const longitude = position.longitude == null ? '-' : position.longitude.toFixed(6)
+ const altitude = position.altitude == null ? '-' : `${position.altitude} m`
+ const time = new Date(position.created_at).toLocaleString()
+ return `
+
+ `
+}
+
+function escapeHTML(value: string): string {
+ return value.replace(/[&<>'"]/g, (char) => {
+ const entities: Record = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ "'": ''',
+ '"': '"',
+ }
+ return entities[char]
+ })
+}
+
function renderTrajectory() {
if (!map || !layer) {
return
}
- layer.clearLayers()
- const points = [...props.positions]
+ const trajectoryLayer = layer
+ trajectoryLayer.clearLayers()
+ const positions = [...props.positions]
.filter((position) => position.latitude != null && position.longitude != null)
.reverse()
- .map((position) => [position.latitude as number, position.longitude as number] as L.LatLngTuple)
+ const points = positions.map((position) => [position.latitude as number, position.longitude as number] as L.LatLngTuple)
if (points.length === 0) {
map.setView([0, 0], 2)
@@ -56,10 +99,16 @@ function renderTrajectory() {
}
if (points.length > 1) {
- L.polyline(points, { color: '#7d8f9a', weight: 4, opacity: 0.78 }).addTo(layer)
+ L.polyline(points, { color: '#7d8f9a', weight: 4, opacity: 0.78 }).addTo(trajectoryLayer)
}
- L.circleMarker(points[0], { radius: 6, color: '#7f9183', fillColor: '#9aaa95', fillOpacity: 0.88 }).bindPopup('起点').addTo(layer)
- L.circleMarker(points[points.length - 1], { radius: 6, color: '#b4877f', fillColor: '#c59b93', fillOpacity: 0.88 }).bindPopup('终点').addTo(layer)
+
+ positions.forEach((position, index) => {
+ const isStart = index === 0
+ const isEnd = index === positions.length - 1
+ L.circleMarker(points[index], pointStyle(isStart, isEnd))
+ .bindPopup(buildPositionPopupHTML(position, isStart, isEnd), { maxWidth: 280, className: 'trajectory-point-popup' })
+ .addTo(trajectoryLayer)
+ })
map.fitBounds(L.latLngBounds(points), { padding: [24, 24], maxZoom: 14 })
}