From ae8a2cb9b6940247276938db7c6307df1e0fc055 Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Tue, 23 Jun 2026 16:47:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:[FL-182][=E9=AB=98=E7=A8=8B=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=A2=84=E8=A7=88=E4=BC=98=E5=8C=96]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- memory/2026-06-23.md | 22 +++++++ web/src/app/admin/elevation-records/page.tsx | 59 +++++++++++++---- .../elevation-preview-cesium-map.tsx | 65 +++++++++++++++---- web/src/lib/elevation-terrain.test.js | 20 +++++- web/src/lib/elevation-terrain.ts | 22 +++++++ 5 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 memory/2026-06-23.md diff --git a/memory/2026-06-23.md b/memory/2026-06-23.md new file mode 100644 index 0000000..a0c1886 --- /dev/null +++ b/memory/2026-06-23.md @@ -0,0 +1,22 @@ +# Work Log - 高程数据预览对比(FL-182) + +- 背景: + - 高程数据预览需要同时提供格栅和地形两种方式,便于在同一预览窗口内对比查看。 + +- 本次处理: + - 高程预览组件新增 `previewMode`,支持 `grid` 固定显示采样格网/点位、`terrain` 固定加载三维地形瓦片、`auto` 保持原有自动回退行为。 + - 高程数据预览弹窗改为左右两栏,对同一份预览数据同时渲染“格栅预览”和“地形预览”。 + - 抽离并测试预览模式判定逻辑,避免格栅模式误加载地形或地形模式误回退为格栅。 + +- 验证: + - 基线:`npm --workspace web exec eslint src/app/admin/elevation-records/page.tsx src/components/elevation-preview-cesium-map.tsx` 通过,存在既有 `react-hooks/exhaustive-deps` warning。 + - 基线:`npm --workspace web exec tsc --noEmit --pretty false` 通过。 + - 基线:`npm --workspace web exec node --experimental-strip-types src/lib/elevation-terrain.test.js` 通过,3 passed,存在既有 Node module type warning。 + - 修改后:`npm --workspace web exec eslint src/app/admin/elevation-records/page.tsx src/components/elevation-preview-cesium-map.tsx src/lib/elevation-terrain.ts src/lib/elevation-terrain.test.js` 通过,仍仅有上述既有 warning。 + - 修改后:`npm --workspace web exec tsc --noEmit --pretty false` 通过。 + - 修改后:`npm --workspace web exec node --experimental-strip-types src/lib/elevation-terrain.test.js` 通过,4 passed,存在既有 Node module type warning。 + - 修改后:`git diff --check` 通过。 + +- 风险与关注点: + - 改动仅影响高程数据预览前端展示和模式判定,不改变 `/api/v1/elevation/records/{id}/preview` 请求/响应字段。 + - 地形预览依赖已有地形瓦片状态;未 ready 时会显示椭球底面参考点和状态提示。 diff --git a/web/src/app/admin/elevation-records/page.tsx b/web/src/app/admin/elevation-records/page.tsx index 44a48dd..82ad567 100644 --- a/web/src/app/admin/elevation-records/page.tsx +++ b/web/src/app/admin/elevation-records/page.tsx @@ -1062,21 +1062,52 @@ export default function ElevationRecordsPage() { {previewData.record?.file_format} {previewData.total_points?.toLocaleString()} {previewData.sampled_points?.toLocaleString()} + {previewData.preview_mode === "terrain_grid" ? "格栅" : "点云"} + {previewData.record?.terrain_status} -
- +
+
+
+ 格栅预览 + {previewData.preview_mode === "terrain_grid" ? "格栅数据" : "点位数据"} +
+ +
+
+
+ 地形预览 + {previewData.record.terrain_status} +
+ +
)} diff --git a/web/src/components/elevation-preview-cesium-map.tsx b/web/src/components/elevation-preview-cesium-map.tsx index ef394e0..ddfd093 100644 --- a/web/src/components/elevation-preview-cesium-map.tsx +++ b/web/src/components/elevation-preview-cesium-map.tsx @@ -6,7 +6,13 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { getApiBaseUrl } from "@/lib/api"; import { withBasePath } from "@/lib/base-path"; import { reloadOnceOnChunkError } from "@/lib/chunk-error"; -import { getElevationTerrainLayerUrl, getElevationTerrainRenderState } from "@/lib/elevation-terrain"; +import { + getElevationTerrainLayerUrl, + getElevationTerrainRenderState, + shouldDrawElevationGridOverlay, + shouldUseElevationTerrainTiles, + type ElevationPreviewDisplayMode, +} from "@/lib/elevation-terrain"; import type { ElevationDatasetPreviewCell, ElevationDatasetPreviewPoint, ElevationDatasetSummary } from "@/types/auth"; type ElevationPreviewCesiumMapProps = { @@ -18,6 +24,8 @@ type ElevationPreviewCesiumMapProps = { points: ElevationDatasetPreviewPoint[]; cells?: ElevationDatasetPreviewCell[]; loading?: boolean; + previewMode?: ElevationPreviewDisplayMode; + height?: number | string; }; type CesiumNamespace = typeof import("cesium"); @@ -59,6 +67,8 @@ export function ElevationPreviewCesiumMap({ points, cells = [], loading = false, + previewMode = "auto", + height = 520, }: ElevationPreviewCesiumMapProps) { const containerRef = useRef(null); const viewerRef = useRef(null); @@ -72,6 +82,28 @@ export function ElevationPreviewCesiumMap({ () => (dataset ? getElevationTerrainRenderState(dataset) : "fallback"), [dataset], ); + const shouldLoadTerrain = useMemo( + () => shouldUseElevationTerrainTiles(previewMode, terrainRenderState), + [previewMode, terrainRenderState], + ); + const previewHint = useMemo(() => { + if (previewMode === "grid") { + return "格栅模式固定显示采样格网/点位,颜色由蓝到红表示高程由低到高。"; + } + if (previewMode === "terrain") { + return "地形模式加载真实三维瓦片,并保留少量参考点用于定位。"; + } + return "颜色由蓝到红表示高程由低到高;地形瓦片就绪时优先加载真实三维地形,失败时自动回退到现有色带/点位预览。"; + }, [previewMode]); + const terrainErrorMessage = useMemo(() => { + if (!terrainError) { + return ""; + } + if (previewMode === "terrain") { + return `地形瓦片加载失败,已切换到椭球底面参考点:${terrainError}`; + } + return `地形瓦片加载失败,已回退到抽样预览:${terrainError}`; + }, [previewMode, terrainError]); const safePoints = useMemo( () => @@ -227,7 +259,7 @@ export function ElevationPreviewCesiumMap({ // Remove all existing imagery layers first viewer.imageryLayers.removeAll(); - if (terrainRenderState !== "ready" || !dataset) { + if (!shouldLoadTerrain || !dataset) { viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider(); viewer.scene.globe.depthTestAgainstTerrain = false; return; @@ -293,7 +325,7 @@ export function ElevationPreviewCesiumMap({ viewer.imageryLayers.remove(addedImageryLayer, false); } }; - }, [accessToken, dataset, ready, terrainRenderState]); + }, [accessToken, dataset, ready, shouldLoadTerrain]); useEffect(() => { const viewer = viewerRef.current; @@ -309,8 +341,8 @@ export function ElevationPreviewCesiumMap({ } const positions: import("cesium").Cartesian3[] = []; - const shouldDrawFallbackOverlay = terrainRenderState !== "ready" || !!terrainError; - if (shouldDrawFallbackOverlay && safeCells.length > 0) { + const shouldDrawGridOverlay = shouldDrawElevationGridOverlay(previewMode, terrainRenderState, !!terrainError); + if (shouldDrawGridOverlay && safeCells.length > 0) { for (let index = 0; index < safeCells.length; index += 1) { const cell = safeCells[index]; const centerLon = (cell.min_longitude + cell.max_longitude) / 2; @@ -343,7 +375,7 @@ export function ElevationPreviewCesiumMap({ `, }); } - } else if (shouldDrawFallbackOverlay) { + } else if (shouldDrawGridOverlay) { for (let index = 0; index < safePoints.length; index += 1) { const point = safePoints[index]; const position = Cesium.Cartesian3.fromDegrees(point.longitude, point.latitude, point.altitude_m); @@ -372,7 +404,7 @@ export function ElevationPreviewCesiumMap({ } // When terrain is ready but no overlay is drawn, add sample reference points for visibility - if (!shouldDrawFallbackOverlay && (safeCells.length > 0 || safePoints.length > 0)) { + if (!shouldDrawGridOverlay && (safeCells.length > 0 || safePoints.length > 0)) { const referencePoints = safeCells.length > 0 ? safeCells.slice(0, 10).map(cell => ({ lon: (cell.min_longitude + cell.max_longitude) / 2, @@ -456,7 +488,7 @@ export function ElevationPreviewCesiumMap({ offset: new Cesium.HeadingPitchRange(0, -1.0, Math.max(1200, boundingSphere.radius * 2.0)), }); } - }, [altitudeRange.max, altitudeRange.min, dataset, ready, safeCells, safePoints, terrainError, terrainRenderState]); + }, [altitudeRange.max, altitudeRange.min, dataset, previewMode, ready, safeCells, safePoints, terrainError, terrainRenderState]); if (error) { return ( @@ -475,16 +507,27 @@ export function ElevationPreviewCesiumMap({ return (
- 颜色由蓝到红表示高程由低到高;地形瓦片就绪时优先加载真实三维地形(垂直夸张5倍以增强可见性),失败时自动回退到现有色带/点位预览。 + {previewHint}
+ {previewMode === "terrain" && terrainRenderState !== "ready" ? ( + + ) : null} {terrainError ? ( ) : null} -
+
{pointerInfo ?
{pointerInfo}
: null} {loading && (
diff --git a/web/src/lib/elevation-terrain.test.js b/web/src/lib/elevation-terrain.test.js index a785236..ec50454 100644 --- a/web/src/lib/elevation-terrain.test.js +++ b/web/src/lib/elevation-terrain.test.js @@ -1,7 +1,12 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { countLineTowersOutsideTerrainBounds, getElevationTerrainRenderState } from "./elevation-terrain.ts"; +import { + countLineTowersOutsideTerrainBounds, + getElevationTerrainRenderState, + shouldDrawElevationGridOverlay, + shouldUseElevationTerrainTiles, +} from "./elevation-terrain.ts"; test("getElevationTerrainRenderState reports ready only when terrain url is available", () => { assert.equal( @@ -33,3 +38,16 @@ test("countLineTowersOutsideTerrainBounds ignores towers without coordinates and assert.equal(countLineTowersOutsideTerrainBounds(towers, bounds), 2); assert.equal(countLineTowersOutsideTerrainBounds(towers, null), 0); }); + +test("elevation preview display mode separates grid and terrain rendering", () => { + assert.equal(shouldUseElevationTerrainTiles("grid", "ready"), false); + assert.equal(shouldUseElevationTerrainTiles("terrain", "ready"), true); + assert.equal(shouldUseElevationTerrainTiles("auto", "ready"), true); + assert.equal(shouldUseElevationTerrainTiles("terrain", "processing"), false); + + assert.equal(shouldDrawElevationGridOverlay("grid", "ready", false), true); + assert.equal(shouldDrawElevationGridOverlay("terrain", "failed", true), false); + assert.equal(shouldDrawElevationGridOverlay("auto", "ready", false), false); + assert.equal(shouldDrawElevationGridOverlay("auto", "ready", true), true); + assert.equal(shouldDrawElevationGridOverlay("auto", "processing", false), true); +}); diff --git a/web/src/lib/elevation-terrain.ts b/web/src/lib/elevation-terrain.ts index ba60f64..0f2aadb 100644 --- a/web/src/lib/elevation-terrain.ts +++ b/web/src/lib/elevation-terrain.ts @@ -1,6 +1,7 @@ import type { ElevationDatasetSummary, ElevationTerrainBounds, LineTowerSummary } from "@/types/auth"; export type ElevationTerrainRenderState = "ready" | "processing" | "failed" | "fallback"; +export type ElevationPreviewDisplayMode = "auto" | "grid" | "terrain"; export function getElevationTerrainRenderState(dataset: Pick< ElevationDatasetSummary, @@ -62,3 +63,24 @@ export function getElevationTerrainLayerUrl(dataset: Pick< } return `/api/v1/elevation/datasets/${dataset.id}/terrain`; } + +export function shouldUseElevationTerrainTiles( + previewMode: ElevationPreviewDisplayMode, + terrainRenderState: ElevationTerrainRenderState, +): boolean { + return previewMode !== "grid" && terrainRenderState === "ready"; +} + +export function shouldDrawElevationGridOverlay( + previewMode: ElevationPreviewDisplayMode, + terrainRenderState: ElevationTerrainRenderState, + hasTerrainError: boolean, +): boolean { + if (previewMode === "grid") { + return true; + } + if (previewMode === "terrain") { + return false; + } + return terrainRenderState !== "ready" || hasTerrainError; +}