|
|
|
@ -152,10 +152,8 @@ |
|
|
|
</div> |
|
|
|
<div class="route-form-error">{{ routeFormError }}</div> |
|
|
|
</aside> |
|
|
|
<div ref="routeMap" class="route-planner-map" @click="addWaypointFromMap" @mousemove="updateRouteCursor"> |
|
|
|
<div class="route-map-base"></div> |
|
|
|
<svg class="route-drawing" viewBox="0 0 1000 600" preserveAspectRatio="none"><polyline :points="routePolyline" /><g><g v-for="(point, index) in routePoints" :key="index" @click.stop="openWaypointEditor(index)"><circle :cx="point.x" :cy="point.y" r="10" :fill="index === 0 ? '#35a66a' : index === routePoints.length - 1 ? '#c56c28' : '#2d82da'" /><text :x="point.x" :y="point.y">{{ index + 1 }}</text></g></g></svg> |
|
|
|
<div class="route-map-status"><span><i></i>地图已加载</span><span>{{ routeCursor }}</span></div> |
|
|
|
<div ref="routeMapEl" class="route-planner-map"> |
|
|
|
<div class="route-map-status"><span><i></i>{{ routeMapReady ? '地图已加载' : '地图加载中…' }}</span><span>{{ routeCursor }}</span></div> |
|
|
|
<div class="route-map-legend"><span><i class="start"></i>起点</span><span><i></i>航点</span><span><i class="end"></i>终点</span></div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -215,12 +213,18 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup> |
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue' |
|
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue' |
|
|
|
import { useRoute, useRouter } from 'vue-router' |
|
|
|
import * as maptalks from 'maptalks' |
|
|
|
import { useUiStore } from '@/stores/modules/uiStore' |
|
|
|
import request from '@/utils/http' |
|
|
|
import { useDeviceStore } from '@/stores/modules/devicesStore' |
|
|
|
import * as urls from '@/config/urls' |
|
|
|
import commonRefs from '@/utils/commonRefs' |
|
|
|
import mapHelper from '@/core/mapHelper' |
|
|
|
|
|
|
|
const ROUTE_LAYER_ID = 'tasks-route-editor' |
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute() |
|
|
|
const router = useRouter() |
|
|
|
@ -237,8 +241,13 @@ const routeSearch = ref('') |
|
|
|
const routeEditorOpen = ref(false) |
|
|
|
const routeSaving = ref(false) |
|
|
|
const routeFormError = ref('') |
|
|
|
const routeCursor = ref('119.820000, 30.310000') |
|
|
|
const routeMap = ref(null) |
|
|
|
const routeCursor = ref('--') |
|
|
|
const routeMapEl = ref(null) |
|
|
|
const routeMapReady = ref(false) |
|
|
|
let routeMapInstance = null |
|
|
|
let routeLayer = null |
|
|
|
let routeMapAttached = false |
|
|
|
|
|
|
|
const routeForm = reactive({ name: '', altitude: 50, speed: 10, turnMode: 'auto' }) |
|
|
|
const routePoints = ref([]) |
|
|
|
const waypointEditorOpen = ref(false) |
|
|
|
@ -355,6 +364,10 @@ async function load() { |
|
|
|
|
|
|
|
onMounted(load) |
|
|
|
|
|
|
|
onUnmounted(() => { |
|
|
|
teardownRouteMap() |
|
|
|
}) |
|
|
|
|
|
|
|
const filteredPlans = computed(() => { |
|
|
|
const q = search.value.trim().toLowerCase() |
|
|
|
return plans.value.filter((p) => { |
|
|
|
@ -380,22 +393,167 @@ const filteredRoutes = computed(() => { |
|
|
|
return routes.value.filter((r) => r.name.toLowerCase().includes(q)) |
|
|
|
}) |
|
|
|
|
|
|
|
const routePolyline = computed(() => routePoints.value.map((point) => `${point.x},${point.y}`).join(' ')) |
|
|
|
const routeSummary = computed(() => `${routePoints.value.length} 个航点 · ${routeDistance(routePoints.value).toFixed(1)} km`) |
|
|
|
|
|
|
|
function formatCoord(lon, lat) { |
|
|
|
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return '--' |
|
|
|
return `${lon.toFixed(6)}, ${lat.toFixed(6)}` |
|
|
|
} |
|
|
|
|
|
|
|
function resetRouteEditor() { |
|
|
|
Object.assign(routeForm, { name: '', altitude: 50, speed: 10, turnMode: 'auto' }) |
|
|
|
routePoints.value = [] |
|
|
|
routeFormError.value = '' |
|
|
|
routeCursor.value = '119.820000, 30.310000' |
|
|
|
routeCursor.value = '--' |
|
|
|
} |
|
|
|
|
|
|
|
function syncRouteLayer() { |
|
|
|
if (!routeLayer) return |
|
|
|
routeLayer.clear() |
|
|
|
|
|
|
|
const coords = routePoints.value |
|
|
|
.map((p) => [Number(p.longitude), Number(p.latitude)]) |
|
|
|
.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat)) |
|
|
|
|
|
|
|
if (coords.length >= 2) { |
|
|
|
new maptalks.LineString(coords, { |
|
|
|
symbol: { |
|
|
|
lineColor: '#2d82da', |
|
|
|
lineWidth: 3, |
|
|
|
lineOpacity: 0.95, |
|
|
|
}, |
|
|
|
}).addTo(routeLayer) |
|
|
|
} |
|
|
|
|
|
|
|
coords.forEach((c, i) => { |
|
|
|
const isStart = i === 0 |
|
|
|
const isEnd = i === coords.length - 1 && coords.length > 1 |
|
|
|
const color = isStart ? '#35a66a' : isEnd ? '#c56c28' : '#2d82da' |
|
|
|
const marker = new maptalks.Marker(c, { |
|
|
|
cursor: 'pointer', |
|
|
|
properties: { index: i }, |
|
|
|
symbol: [ |
|
|
|
{ |
|
|
|
markerType: 'ellipse', |
|
|
|
markerFill: color, |
|
|
|
markerLineColor: '#fff', |
|
|
|
markerLineWidth: 2, |
|
|
|
markerWidth: 18, |
|
|
|
markerHeight: 18, |
|
|
|
}, |
|
|
|
{ |
|
|
|
textName: String(i + 1), |
|
|
|
textFill: '#fff', |
|
|
|
textSize: 11, |
|
|
|
textWeight: 'bold', |
|
|
|
textDy: 1, |
|
|
|
}, |
|
|
|
], |
|
|
|
}) |
|
|
|
marker.on('click', (e) => { |
|
|
|
if (e?.domEvent) { |
|
|
|
e.domEvent.stopPropagation?.() |
|
|
|
e.domEvent.preventDefault?.() |
|
|
|
} |
|
|
|
openWaypointEditor(i) |
|
|
|
}) |
|
|
|
marker.addTo(routeLayer) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function onRouteMapClick(e) { |
|
|
|
if (!routeMapAttached) return |
|
|
|
// 点击已有航点时由 marker 处理 |
|
|
|
if (e?.target && e.target !== routeMapInstance) return |
|
|
|
if (!validRouteDefaults()) return |
|
|
|
|
|
|
|
const coord = e?.coordinate |
|
|
|
if (!coord) return |
|
|
|
const longitude = Number(coord.x) |
|
|
|
const latitude = Number(coord.y) |
|
|
|
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return |
|
|
|
|
|
|
|
routePoints.value.push({ |
|
|
|
longitude, |
|
|
|
latitude, |
|
|
|
altitude: Number(routeForm.altitude), |
|
|
|
speed: Number(routeForm.speed), |
|
|
|
turnMode: routeForm.turnMode, |
|
|
|
}) |
|
|
|
routeCursor.value = formatCoord(longitude, latitude) |
|
|
|
routeFormError.value = '' |
|
|
|
syncRouteLayer() |
|
|
|
} |
|
|
|
|
|
|
|
function onRouteMapMove(e) { |
|
|
|
if (!routeMapAttached) return |
|
|
|
const coord = e?.coordinate |
|
|
|
if (!coord) return |
|
|
|
routeCursor.value = formatCoord(Number(coord.x), Number(coord.y)) |
|
|
|
} |
|
|
|
|
|
|
|
async function attachRouteMap() { |
|
|
|
await nextTick() |
|
|
|
const host = routeMapEl.value |
|
|
|
if (!host) return |
|
|
|
|
|
|
|
const map = await commonRefs.getRef('map') |
|
|
|
if (!map) { |
|
|
|
routeMapReady.value = false |
|
|
|
ui.toast('地图未初始化(缺少天地图 token)') |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
routeMapInstance = map |
|
|
|
mapHelper.replaceMapContainer(host) |
|
|
|
mapHelper.toggleMapMode({ is2D: true }) |
|
|
|
routeMapAttached = true |
|
|
|
routeMapReady.value = true |
|
|
|
|
|
|
|
const existing = map.getLayer(ROUTE_LAYER_ID) |
|
|
|
if (existing) existing.remove() |
|
|
|
routeLayer = new maptalks.VectorLayer(ROUTE_LAYER_ID, [], { zIndex: 140, forceRenderOnMoving: true }) |
|
|
|
routeLayer.addTo(map) |
|
|
|
|
|
|
|
map.on('click', onRouteMapClick) |
|
|
|
map.on('mousemove', onRouteMapMove) |
|
|
|
|
|
|
|
const center = map.getCenter() |
|
|
|
if (center) routeCursor.value = formatCoord(center.x, center.y) |
|
|
|
|
|
|
|
// 容器迁移后尺寸刷新 |
|
|
|
requestAnimationFrame(() => { |
|
|
|
map.checkSize?.() |
|
|
|
syncRouteLayer() |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function teardownRouteMap() { |
|
|
|
if (routeMapInstance) { |
|
|
|
routeMapInstance.off('click', onRouteMapClick) |
|
|
|
routeMapInstance.off('mousemove', onRouteMapMove) |
|
|
|
const layer = routeMapInstance.getLayer(ROUTE_LAYER_ID) |
|
|
|
if (layer) layer.remove() |
|
|
|
} |
|
|
|
routeLayer = null |
|
|
|
|
|
|
|
if (routeMapAttached) { |
|
|
|
mapHelper.restoreMapContainer() |
|
|
|
routeMapAttached = false |
|
|
|
} |
|
|
|
|
|
|
|
routeMapInstance = null |
|
|
|
routeMapReady.value = false |
|
|
|
} |
|
|
|
|
|
|
|
function openRouteEditor() { |
|
|
|
resetRouteEditor() |
|
|
|
routeEditorOpen.value = true |
|
|
|
attachRouteMap() |
|
|
|
} |
|
|
|
|
|
|
|
function closeRouteEditor() { |
|
|
|
teardownRouteMap() |
|
|
|
routeEditorOpen.value = false |
|
|
|
waypointEditorOpen.value = false |
|
|
|
} |
|
|
|
@ -412,18 +570,6 @@ function routeDistance(points) { |
|
|
|
}, 0) |
|
|
|
} |
|
|
|
|
|
|
|
function mapPoint(event) { |
|
|
|
const rect = routeMap.value.getBoundingClientRect() |
|
|
|
const x = Math.max(0, Math.min(1000, (event.clientX - rect.left) / rect.width * 1000)) |
|
|
|
const y = Math.max(0, Math.min(600, (event.clientY - rect.top) / rect.height * 600)) |
|
|
|
return { x, y, longitude: 119.8 + x / 1000 * 0.04, latitude: 30.33 - y / 600 * 0.04 } |
|
|
|
} |
|
|
|
|
|
|
|
function updateRouteCursor(event) { |
|
|
|
const point = mapPoint(event) |
|
|
|
routeCursor.value = `${point.longitude.toFixed(6)}, ${point.latitude.toFixed(6)}` |
|
|
|
} |
|
|
|
|
|
|
|
function validRouteDefaults() { |
|
|
|
if (routeForm.altitude < 20 || routeForm.altitude > 120) { |
|
|
|
routeFormError.value = '默认高度应在 20~120 米之间。' |
|
|
|
@ -436,32 +582,40 @@ function validRouteDefaults() { |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
function addWaypointFromMap(event) { |
|
|
|
if (!validRouteDefaults()) return |
|
|
|
const point = mapPoint(event) |
|
|
|
routePoints.value.push({ ...point, altitude: Number(routeForm.altitude), speed: Number(routeForm.speed), turnMode: routeForm.turnMode }) |
|
|
|
routeFormError.value = '' |
|
|
|
} |
|
|
|
|
|
|
|
function undoRoutePoint() { |
|
|
|
routePoints.value.pop() |
|
|
|
syncRouteLayer() |
|
|
|
} |
|
|
|
|
|
|
|
function clearRoutePoints() { |
|
|
|
routePoints.value = [] |
|
|
|
syncRouteLayer() |
|
|
|
} |
|
|
|
|
|
|
|
function applyRouteDefaults() { |
|
|
|
if (!validRouteDefaults()) return |
|
|
|
routePoints.value = routePoints.value.map((point) => ({ ...point, altitude: Number(routeForm.altitude), speed: Number(routeForm.speed), turnMode: routeForm.turnMode })) |
|
|
|
routePoints.value = routePoints.value.map((point) => ({ |
|
|
|
longitude: point.longitude, |
|
|
|
latitude: point.latitude, |
|
|
|
altitude: Number(routeForm.altitude), |
|
|
|
speed: Number(routeForm.speed), |
|
|
|
turnMode: routeForm.turnMode, |
|
|
|
})) |
|
|
|
routeFormError.value = '' |
|
|
|
syncRouteLayer() |
|
|
|
} |
|
|
|
|
|
|
|
function openWaypointEditor(index) { |
|
|
|
const point = routePoints.value[index] |
|
|
|
if (!point) return |
|
|
|
editingWaypointIndex.value = index |
|
|
|
Object.assign(waypointForm, point) |
|
|
|
Object.assign(waypointForm, { |
|
|
|
longitude: point.longitude, |
|
|
|
latitude: point.latitude, |
|
|
|
altitude: point.altitude, |
|
|
|
speed: point.speed, |
|
|
|
turnMode: point.turnMode, |
|
|
|
}) |
|
|
|
waypointFormError.value = '' |
|
|
|
waypointEditorOpen.value = true |
|
|
|
} |
|
|
|
@ -476,6 +630,10 @@ function saveWaypoint() { |
|
|
|
waypointFormError.value = '请输入有效的经纬度。' |
|
|
|
return |
|
|
|
} |
|
|
|
if (waypointForm.longitude < -180 || waypointForm.longitude > 180 || waypointForm.latitude < -90 || waypointForm.latitude > 90) { |
|
|
|
waypointFormError.value = '经纬度超出有效范围。' |
|
|
|
return |
|
|
|
} |
|
|
|
if (waypointForm.altitude < 20 || waypointForm.altitude > 120) { |
|
|
|
waypointFormError.value = '高度应在 20~120 米之间。' |
|
|
|
return |
|
|
|
@ -484,20 +642,23 @@ function saveWaypoint() { |
|
|
|
waypointFormError.value = '速度应在 1~20 m/s 之间。' |
|
|
|
return |
|
|
|
} |
|
|
|
const x = (waypointForm.longitude - 119.8) / 0.04 * 1000 |
|
|
|
const y = (30.33 - waypointForm.latitude) / 0.04 * 600 |
|
|
|
if (x < 0 || x > 1000 || y < 0 || y > 600) { |
|
|
|
waypointFormError.value = '经纬度超出当前地图显示范围。' |
|
|
|
return |
|
|
|
routePoints.value[editingWaypointIndex.value] = { |
|
|
|
longitude: Number(waypointForm.longitude), |
|
|
|
latitude: Number(waypointForm.latitude), |
|
|
|
altitude: Number(waypointForm.altitude), |
|
|
|
speed: Number(waypointForm.speed), |
|
|
|
turnMode: waypointForm.turnMode, |
|
|
|
} |
|
|
|
routePoints.value[editingWaypointIndex.value] = { ...waypointForm, x, y } |
|
|
|
syncRouteLayer() |
|
|
|
closeWaypointEditor() |
|
|
|
} |
|
|
|
|
|
|
|
function removeWaypoint(index) { |
|
|
|
routePoints.value.splice(index, 1) |
|
|
|
syncRouteLayer() |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function submitRoute() { |
|
|
|
const name = routeForm.name.trim() |
|
|
|
if (!name) { |
|
|
|
|