/* ============================================================================= * B05_Profile_UI_RouteEdit_Gl.ts * 평면 노선 편집 GL 바탕(PLAN 589-2) — 바탕칠 · 등고선(LAS 5 m · 1 m 선 · 도엽) · 도엽 하천을 MapLibre 로. * TM 겹(LAS) · 도엽 모두 준비한 겹(정규화 · TM 자리)을 경위도로 되돌려 올림(`preparedToGeoJson`) — * 캔버스와 같은 자리 · 같은 솎기(간격 배수 · 1 m 선 켬 조건) · 노선 둘레 띠 자르기(지도 칸 clip-path) · * 하천 = LAS 등고선이 있는 데까지(자료를 상자로 자름 · 0-9 ⑮). * 고른 등고선 · 등고 글 · 노선 · 꼭짓점 · 곡선 · 측점 · 재기 · 끌기 = 위 캔버스 그대로 · 집기 판정 그대로. * 공용 지도 겹(624-3) = `_Layers` — 등고선 · 하천 켬끔은 그 겹 줄 · 그 밖 겹은 띠로 안 자르는 둘째 칸(같이 따라감) · * 켬끔 단추 = 겹 줄 끝 그래픽 카드 단추(`toggle`). * ========================================================================== */ import type { FilterSpecification } from "maplibre-gl"; import { themeColor } from "@ui/ui_template_palette"; import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { createTmGl, lineSpecs } from "../B04_PreProcess/B04_PreProcess_UI_MapGl"; import { createTmNormalizer, normalizedToScreen, type Normalizer, type PreparedLayer, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { lonLatOfNormFn, preparedToGeoJson, } from "../B04_PreProcess/logic/B04_PreProcess_UI_MapGl_Frame"; import type { RouteEditContours } from "./B05_Profile_UI_RouteEdit_Contour"; import type { RouteEditLayers } from "./B05_Profile_UI_RouteEdit_Layers"; import { sceneContourBand } from "./B05_Profile_UI_RouteEdit_Input"; import type { RouteEditScene } from "./B05_Profile_UI_RouteEdit_Render"; import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve"; type Key = "sheets" | "minor" | "contour"; /** 간격 배수만(높이 모르는 줄은 늘) — 캔버스 `drawPreparedLayer(everyM)` 와 같은 거름. */ const stepFilter = (step: number): FilterSpecification => (step > 0 ? ["any", ["!", ["has", "level"]], ["==", ["%", ["get", "level"], step], 0]] : ["literal", true]) as unknown as FilterSpecification; export interface RouteEditGl { /** 그리기마다 — 켜져 있으면 화면을 따라가고 참(캔버스는 바탕을 쉼) · `again` = 장면 다시 그리기(켬끔 뒤). */ follow: (scene: RouteEditScene, bandM: number, again: () => void) => boolean; /** GL ↔ 옛 캔버스(겹 줄 그래픽 카드 단추). */ toggle: () => void; /** 마지막 장면 다시 그리기(겹 켬끔 뒤). */ redraw: () => void; dispose: () => void; } const BY_CANVAS = new WeakMap(); /** 그 캔버스의 GL 바탕(편집 뼈대가 만듦) — 없으면 옛 캔버스 그대로. */ export const routeGlOf = (canvas: HTMLCanvasElement): RouteEditGl | undefined => BY_CANVAS.get(canvas); /** `canvas` = 편집 캔버스(칸 = 그 부모) · `layers` = 공용 지도 겹(624-3) · `host` = 떠날 때 버림. */ export function createRouteEditGl( canvas: HTMLCanvasElement, layers: RouteEditLayers, host?: { onDispose: (fn: () => void) => void }, ): RouteEditGl { const wrap = canvas.parentElement!; let redraw: (() => void) | null = null; let shown: [RouteEditContours | null, ReadonlyArray | null] = [null, null]; let step = 0; let meta: VWorldMeta | null = null; let normalizer: Normalizer | null = null; let toLonLat: ((nx: number, ny: number) => [number, number]) | null = null; let minorShown: PreparedLayer | null = null; let synced: [boolean, VWorldMeta | null] = [false, null]; const tm = createTmGl({ parent: wrap, specs: () => [ ...lineSpecs("sheets", themeColor("--map-sheet-stream", "#2563eb"), 1.2, { marker: "dot", }), ...lineSpecs( "minor", themeColor("--map-contour-minor", "rgba(165, 180, 252, 0.45)"), 0.5, ), ...lineSpecs("contour", themeColor("--map-contour-major", "#818cf8"), 0.8, { filter: stepFilter(step), }), ], themeNames: ["--draw-surface", "--map-contour-major", "--map-sheet-stream"], onChange: (on) => { mark(on); redraw?.(); }, }); const mark = (on: boolean): void => { synced = [on, meta]; layers.sync(on, meta); if (!on) layers.park(); // 바탕칠 = 칸(지도 칸은 띠로 잘림 · 캔버스는 투명) wrap.style.background = on ? "var(--draw-surface)" : ""; }; mark(tm.on()); /** 바탕 한 벌을 바꿈(받은 뒤 · 띠 따라 다시 받은 뒤). */ function setBackground( nextMeta: VWorldMeta, contours: RouteEditContours | null, otherSheets: ReadonlyArray, ): void { meta = nextMeta; normalizer = createTmNormalizer(nextMeta); toLonLat = lonLatOfNormFn(normalizer); minorShown = null; tm.gl.setData("minor", null); tm.gl.setData("contour", contours ? preparedToGeoJson(contours.layer, toLonLat) : null); // 하천 = LAS 등고선이 있는 데까지(정규화 상자) const features = contours?.source === "las" ? contours.layer.features : []; const box = features.length ? features.reduce( (got, feature) => [ Math.min(got[0], feature.minX), Math.min(got[1], feature.minY), Math.max(got[2], feature.maxX), Math.max(got[3], feature.maxY), ] as const, [Infinity, Infinity, -Infinity, -Infinity] as const, ) : undefined; tm.gl.setData("sheets", { features: otherSheets.flatMap((layer) => preparedToGeoJson(layer, toLonLat!, box).features), }); } const api: RouteEditGl = { follow(scene, bandM, again) { redraw = again; const container = tm.gl.container(); const at = scene.contours?.meta; if (at && (synced[0] !== tm.on() || synced[1] !== at)) { synced = [tm.on(), at]; // 겹 줄 · 범례 = 켬끔 · 범위가 바뀔 때만 layers.sync(...synced); } if (!tm.on() || !at || !container) return false; if (shown[0] !== scene.contours || shown[1] !== scene.otherSheets) { shown = [scene.contours, scene.otherSheets]; setBackground(at, scene.contours, scene.otherSheets); } if (!meta || !normalizer) return false; const own = layers.own(); const { view } = scene; const { gl } = tm; const minor = !!scene.minorContours && !!scene.contours && scene.contourStepM <= scene.contours.intervalM; if (minor && scene.minorContours !== minorShown && toLonLat) { minorShown = scene.minorContours; gl.setData("minor", preparedToGeoJson(scene.minorContours!, toLonLat)); } gl.setVisible("sheets", own.stream); gl.setVisible("contour", own.contour); gl.setVisible("minor", minor && own.contour); if (scene.contourStepM !== step) { step = scene.contourStepM; if (gl.map()?.getLayer("contour-line")) gl.map()!.setFilter("contour-line", [ "all", ["!=", ["geometry-type"], "Point"], stepFilter(step), ] as unknown as FilterSpecification); } // 캔버스 돌림(한가운데 축) — 같은 식으로 카메라 · 띠 귀퉁이 const cos = Math.cos(scene.rotationRad); const sin = Math.sin(scene.rotationRad); const cx = view.width / 2; const cy = view.height / 2; const turn = (x: number, y: number): [number, number] => [ cx + (x - cx) * cos - (y - cy) * sin, cy + (x - cx) * sin + (y - cy) * cos, ]; const toScreen = (nx: number, ny: number): [number, number] => turn(...normalizedToScreen(view, nx, ny)); tm.follow(meta, normalizer, toScreen, view.width, view.height); layers.follow(meta, normalizer, toScreen, view.width, view.height); // 노선 둘레 띠만(캔버스 자르기와 같은 네모 · 돌림 · 칸 안 px) const line = (scene.plannedLine.length ? scene.plannedLine : scene.planned) as Vertex[]; const band = sceneContourBand(scene, line, scene.toScreen, bandM); const corners: [number, number][] = band ? [ [band.x, band.y], [band.x + band.width, band.y], [band.x + band.width, band.y + band.height], [band.x, band.y + band.height], ] : []; container.style.clipPath = band ? `polygon(${corners .map(([x, y]) => tm .local(...turn(x, y)) .map((v) => `${v}px`) .join(" "), ) .join(", ")})` : ""; return true; }, toggle: () => tm.toggle(), redraw: () => redraw?.(), dispose: () => (layers.park(), tm.dispose()), }; BY_CANVAS.set(canvas, api); host?.onDispose(api.dispose); return api; }