|
|
@ -288,6 +288,7 @@ let routeMapInstance = null |
|
|
let routeMapAttached = false |
|
|
let routeMapAttached = false |
|
|
let pendingStyleHandler = null |
|
|
let pendingStyleHandler = null |
|
|
let pendingFitRoute = false |
|
|
let pendingFitRoute = false |
|
|
|
|
|
let styleRebuildHandler = null |
|
|
|
|
|
|
|
|
const routeForm = reactive({ name: '', altitude: 50, speed: 10, turnMode: 'auto' }) |
|
|
const routeForm = reactive({ name: '', altitude: 50, speed: 10, turnMode: 'auto' }) |
|
|
const routePoints = ref([]) |
|
|
const routePoints = ref([]) |
|
|
@ -711,6 +712,34 @@ async function waitForMapHostSize(el, tries = 40) { |
|
|
return el.clientWidth > 0 && el.clientHeight > 0 |
|
|
return el.clientWidth > 0 && el.clientHeight > 0 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function waitForMapStyleReady(map, tries = 40) { |
|
|
|
|
|
if (!map) return false |
|
|
|
|
|
for (let i = 0; i < tries; i++) { |
|
|
|
|
|
try { |
|
|
|
|
|
if (map.isStyleLoaded?.() && map.loaded?.() !== false) { |
|
|
|
|
|
// 再等一帧,避开 setProjection 后瞬时仍 isStyleLoaded 的窗口 |
|
|
|
|
|
await new Promise((r) => requestAnimationFrame(r)) |
|
|
|
|
|
if (map.isStyleLoaded?.()) return true |
|
|
|
|
|
} |
|
|
|
|
|
} catch (_) { |
|
|
|
|
|
/* ignore */ |
|
|
|
|
|
} |
|
|
|
|
|
await new Promise((r) => { |
|
|
|
|
|
const onIdle = () => { |
|
|
|
|
|
map.off?.('idle', onIdle) |
|
|
|
|
|
r() |
|
|
|
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
map.once?.('idle', onIdle) |
|
|
|
|
|
setTimeout(r, 50) |
|
|
|
|
|
} catch (_) { |
|
|
|
|
|
setTimeout(r, 50) |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
return !!map.isStyleLoaded?.() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function scheduleRouteSync({ fit = false } = {}) { |
|
|
function scheduleRouteSync({ fit = false } = {}) { |
|
|
const map = routeMapInstance |
|
|
const map = routeMapInstance |
|
|
if (!map) return |
|
|
if (!map) return |
|
|
@ -721,47 +750,18 @@ function scheduleRouteSync({ fit = false } = {}) { |
|
|
} catch (_) { |
|
|
} catch (_) { |
|
|
/* ignore */ |
|
|
/* ignore */ |
|
|
} |
|
|
} |
|
|
|
|
|
// projection/style 变更后 source 可能被清掉,先重建 |
|
|
|
|
|
if (!ensureTaskLayers(map)) return |
|
|
syncRouteLayer({ fit: shouldFit }) |
|
|
syncRouteLayer({ fit: shouldFit }) |
|
|
} |
|
|
} |
|
|
// 双 rAF:等 hidden 解除后的布局/合成,再 resize + fit |
|
|
|
|
|
requestAnimationFrame(() => { |
|
|
requestAnimationFrame(() => { |
|
|
requestAnimationFrame(() => { |
|
|
requestAnimationFrame(() => { |
|
|
run(fit) |
|
|
run(fit) |
|
|
if (!fit) return |
|
|
if (!fit) return |
|
|
// 再补一帧:ResizeObserver/样式生效后二次 fit,避免首帧 0 尺寸 |
|
|
|
|
|
requestAnimationFrame(() => run(true)) |
|
|
requestAnimationFrame(() => run(true)) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function onRouteMapClick(e) { |
|
|
|
|
|
if (!routeMapAttached || !routeMapInstance) return |
|
|
|
|
|
const hit = routeMapInstance.queryRenderedFeatures(e.point, { |
|
|
|
|
|
layers: [WP_CIRCLE, WP_LABEL].filter((id) => routeMapInstance.getLayer(id)), |
|
|
|
|
|
}) |
|
|
|
|
|
if (hit[0]?.properties?.index != null) { |
|
|
|
|
|
openWaypointEditor(Number(hit[0].properties.index)) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if (!validRouteDefaults()) return |
|
|
|
|
|
const { lng, lat } = e.lngLat |
|
|
|
|
|
routePoints.value.push({ |
|
|
|
|
|
longitude: lng, |
|
|
|
|
|
latitude: lat, |
|
|
|
|
|
altitude: Number(routeForm.altitude), |
|
|
|
|
|
speed: Number(routeForm.speed), |
|
|
|
|
|
turnMode: routeForm.turnMode, |
|
|
|
|
|
}) |
|
|
|
|
|
routeCursor.value = formatCoord(lng, lat) |
|
|
|
|
|
routeFormError.value = '' |
|
|
|
|
|
syncRouteLayer() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onRouteMapMove(e) { |
|
|
|
|
|
if (!routeMapAttached) return |
|
|
|
|
|
routeCursor.value = formatCoord(e.lngLat.lng, e.lngLat.lat) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function attachRouteMap() { |
|
|
async function attachRouteMap() { |
|
|
await nextTick() |
|
|
await nextTick() |
|
|
const host = routeMapEl.value |
|
|
const host = routeMapEl.value |
|
|
@ -774,7 +774,6 @@ async function attachRouteMap() { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 编辑器从 display:none 打开时,先等到宿主有尺寸再搬地图 |
|
|
|
|
|
await waitForMapHostSize(host) |
|
|
await waitForMapHostSize(host) |
|
|
|
|
|
|
|
|
routeMapInstance = map |
|
|
routeMapInstance = map |
|
|
@ -783,34 +782,65 @@ async function attachRouteMap() { |
|
|
routeMapAttached = true |
|
|
routeMapAttached = true |
|
|
routeMapReady.value = false |
|
|
routeMapReady.value = false |
|
|
|
|
|
|
|
|
|
|
|
// setProjection(mercator) 可能异步重建 style,先等稳定再挂图层 |
|
|
|
|
|
await waitForMapStyleReady(map) |
|
|
|
|
|
|
|
|
const boot = () => { |
|
|
const boot = () => { |
|
|
pendingStyleHandler = null |
|
|
|
|
|
if (!routeMapAttached || routeMapInstance !== map) return |
|
|
if (!routeMapAttached || routeMapInstance !== map) return |
|
|
clearTaskLayers(map) |
|
|
clearTaskLayers(map) |
|
|
if (!ensureTaskLayers(map)) return |
|
|
if (!ensureTaskLayers(map)) return |
|
|
|
|
|
map.off('click', onRouteMapClick) |
|
|
|
|
|
map.off('mousemove', onRouteMapMove) |
|
|
map.on('click', onRouteMapClick) |
|
|
map.on('click', onRouteMapClick) |
|
|
map.on('mousemove', onRouteMapMove) |
|
|
map.on('mousemove', onRouteMapMove) |
|
|
const c = map.getCenter() |
|
|
const c = map.getCenter() |
|
|
routeCursor.value = formatCoord(c.lng, c.lat) |
|
|
routeCursor.value = formatCoord(c.lng, c.lat) |
|
|
routeMapReady.value = true |
|
|
routeMapReady.value = true |
|
|
const shouldFit = pendingFitRoute |
|
|
const shouldFit = pendingFitRoute |
|
|
pendingFitRoute = false |
|
|
|
|
|
scheduleRouteSync({ fit: shouldFit }) |
|
|
|
|
|
|
|
|
// pendingFitRoute 只在首次 boot 消费;style 重建时仍按当前航点 fit |
|
|
|
|
|
if (shouldFit) pendingFitRoute = false |
|
|
|
|
|
scheduleRouteSync({ fit: shouldFit || routePoints.value.length > 0 }) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (pendingStyleHandler) { |
|
|
|
|
|
map.off('load', pendingStyleHandler) |
|
|
|
|
|
map.off('style.load', pendingStyleHandler) |
|
|
|
|
|
pendingStyleHandler = null |
|
|
|
|
|
} |
|
|
|
|
|
if (styleRebuildHandler) { |
|
|
|
|
|
map.off('style.load', styleRebuildHandler) |
|
|
|
|
|
styleRebuildHandler = null |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// style 后续再变(底图切换/projection)时重建航线层 |
|
|
|
|
|
styleRebuildHandler = () => { |
|
|
|
|
|
if (!routeMapAttached || routeMapInstance !== map) return |
|
|
|
|
|
boot() |
|
|
} |
|
|
} |
|
|
|
|
|
map.on('style.load', styleRebuildHandler) |
|
|
|
|
|
|
|
|
if (map.isStyleLoaded()) { |
|
|
if (map.isStyleLoaded()) { |
|
|
boot() |
|
|
boot() |
|
|
} else { |
|
|
} else { |
|
|
pendingStyleHandler = boot |
|
|
|
|
|
|
|
|
pendingStyleHandler = () => { |
|
|
|
|
|
pendingStyleHandler = null |
|
|
|
|
|
boot() |
|
|
|
|
|
} |
|
|
map.once('load', pendingStyleHandler) |
|
|
map.once('load', pendingStyleHandler) |
|
|
|
|
|
map.once('style.load', pendingStyleHandler) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function teardownRouteMap() { |
|
|
function teardownRouteMap() { |
|
|
if (routeMapInstance && pendingStyleHandler) { |
|
|
if (routeMapInstance && pendingStyleHandler) { |
|
|
routeMapInstance.off('load', pendingStyleHandler) |
|
|
routeMapInstance.off('load', pendingStyleHandler) |
|
|
|
|
|
routeMapInstance.off('style.load', pendingStyleHandler) |
|
|
pendingStyleHandler = null |
|
|
pendingStyleHandler = null |
|
|
} |
|
|
} |
|
|
|
|
|
if (routeMapInstance && styleRebuildHandler) { |
|
|
|
|
|
routeMapInstance.off('style.load', styleRebuildHandler) |
|
|
|
|
|
styleRebuildHandler = null |
|
|
|
|
|
} |
|
|
if (routeMapInstance) { |
|
|
if (routeMapInstance) { |
|
|
routeMapInstance.off('click', onRouteMapClick) |
|
|
routeMapInstance.off('click', onRouteMapClick) |
|
|
routeMapInstance.off('mousemove', onRouteMapMove) |
|
|
routeMapInstance.off('mousemove', onRouteMapMove) |
|
|
@ -818,7 +848,6 @@ function teardownRouteMap() { |
|
|
} |
|
|
} |
|
|
if (routeMapAttached) { |
|
|
if (routeMapAttached) { |
|
|
mapHelper.restoreMapContainer() |
|
|
mapHelper.restoreMapContainer() |
|
|
// 监控默认 globe + pitch 0;不要切 3D pitch 45 |
|
|
|
|
|
try { |
|
|
try { |
|
|
mapHelper.getMap()?.setProjection?.('globe') |
|
|
mapHelper.getMap()?.setProjection?.('globe') |
|
|
} catch (_) { |
|
|
} catch (_) { |
|
|
@ -831,6 +860,35 @@ function teardownRouteMap() { |
|
|
pendingFitRoute = false |
|
|
pendingFitRoute = false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onRouteMapClick(e) { |
|
|
|
|
|
if (!routeMapAttached || !routeMapInstance) return |
|
|
|
|
|
const hit = routeMapInstance.queryRenderedFeatures(e.point, { |
|
|
|
|
|
layers: [WP_CIRCLE, WP_LABEL].filter((id) => routeMapInstance.getLayer(id)), |
|
|
|
|
|
}) |
|
|
|
|
|
if (hit[0]?.properties?.index != null) { |
|
|
|
|
|
openWaypointEditor(Number(hit[0].properties.index)) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if (!validRouteDefaults()) return |
|
|
|
|
|
const { lng, lat } = e.lngLat |
|
|
|
|
|
routePoints.value.push({ |
|
|
|
|
|
longitude: lng, |
|
|
|
|
|
latitude: lat, |
|
|
|
|
|
altitude: Number(routeForm.altitude), |
|
|
|
|
|
speed: Number(routeForm.speed), |
|
|
|
|
|
turnMode: routeForm.turnMode, |
|
|
|
|
|
}) |
|
|
|
|
|
routeCursor.value = formatCoord(lng, lat) |
|
|
|
|
|
routeFormError.value = '' |
|
|
|
|
|
syncRouteLayer() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onRouteMapMove(e) { |
|
|
|
|
|
if (!routeMapAttached) return |
|
|
|
|
|
routeCursor.value = formatCoord(e.lngLat.lng, e.lngLat.lat) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function openRouteEditor() { |
|
|
function openRouteEditor() { |
|
|
resetRouteEditor() |
|
|
resetRouteEditor() |
|
|
routeEditorOpen.value = true |
|
|
routeEditorOpen.value = true |
|
|
|