- 공용 부품 ui_template_gl_map(지도 세우기 · 자료 · 켬끔 · 색 다시 · 카메라) — D03 GL 껍데기를 올림 · D03 은 겹 꼴만(바뀜 없음) - TM 주인 따라가기(B04_PreProcess_UI_MapGl · logic/_MapGl_Frame) — 화면 가운데 1차 맞춤 닮음꼴 카메라 + 남는 늘림(웹 메르카토르 가로세로 0.43 %)은 지도 칸 CSS 행렬 · 칸 둘레 여유 8 px - B04 하단 지도 — 위성 조각(줌인 선명) · 백지도 · 혼합 · 도엽 · 지적 · 행정 · 등고선 겹 GL · 등고 글 · 배수유역 · 계획선 · 관 = 위 캔버스 - B05 배수유역도 — 위성 · 세부유역 채움(짝홀 다각형) · 분수령 · 도엽 GL · 번호 · 상류 세류 · 관 · 강도 = 위 캔버스 - B05 평면 노선 편집 — 바탕칠 · LAS · 도엽 등고선(간격 거름 · 1 m 선) · 하천(LAS 상자 자름) GL · 노선 둘레 띠 = 지도 칸 clip-path · 고른 등고선 · 글 · 노선 · 꼭짓점 = 위 캔버스 - 켬끔 칸(GPU · 세 화면 한 취향 aislo:pref:map.canvas) · WebGL 못 씀 · 잃음 = 옛 캔버스 + 알림 · 누름 · 끌기 판정 손 안 댐 - B04 층 단추 글 = MapLayers gisLayerLabel(지도 본체 700 줄) - 지킴 시험 test_589_2_gl_frame(카메라 대 캔버스 0.5 px · 칸 행렬 늘이기만 · 되돌림 1 mm · 상자 자르기 · 짝홀 다각형) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
206 lines
8.2 KiB
TypeScript
206 lines
8.2 KiB
TypeScript
/* =============================================================================
|
|
* 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 ⑮).
|
|
* 고른 등고선 · 등고 글 · 노선 · 꼭짓점 · 곡선 · 측점 · 재기 · 끌기 = 위 캔버스 그대로 · 집기 판정 그대로.
|
|
* ========================================================================== */
|
|
|
|
import type { FilterSpecification } from "maplibre-gl";
|
|
import { createIconButton } from "@ui/ui_template_elements";
|
|
import { themeColor } from "@ui/ui_template_palette";
|
|
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import { createTmGl, GL_TOGGLE_TITLE, 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 { contourBandRect } 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;
|
|
dispose: () => void;
|
|
}
|
|
|
|
const BY_CANVAS = new WeakMap<HTMLCanvasElement, RouteEditGl>();
|
|
/** 그 캔버스의 GL 바탕(편집 뼈대가 만듦) — 없으면 옛 캔버스 그대로. */
|
|
export const routeGlOf = (canvas: HTMLCanvasElement): RouteEditGl | undefined =>
|
|
BY_CANVAS.get(canvas);
|
|
|
|
/** `canvas` = 편집 캔버스(칸 = 그 부모) · `after` = 켬끔 단추를 그 뒤에(도구줄) · `host` = 떠날 때 버림. */
|
|
export function createRouteEditGl(
|
|
canvas: HTMLCanvasElement,
|
|
after: HTMLElement,
|
|
host?: { onDispose: (fn: () => void) => void },
|
|
): RouteEditGl {
|
|
const wrap = canvas.parentElement!;
|
|
let redraw: (() => void) | null = null;
|
|
let shown: [RouteEditContours | null, ReadonlyArray<PreparedLayer> | 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;
|
|
const button = createIconButton({
|
|
icon: "gauge",
|
|
title: GL_TOGGLE_TITLE,
|
|
size: 32,
|
|
onClick: () => tm.toggle(),
|
|
});
|
|
button.dataset.act = "gl";
|
|
after.after(button);
|
|
const tm = createTmGl<Key>({
|
|
parent: wrap,
|
|
specs: () => [
|
|
...lineSpecs<Key>("sheets", themeColor("--map-sheet-stream", "#2563eb"), 1.2, {
|
|
marker: "dot",
|
|
}),
|
|
...lineSpecs<Key>(
|
|
"minor",
|
|
themeColor("--map-contour-minor", "rgba(165, 180, 252, 0.45)"),
|
|
0.5,
|
|
),
|
|
...lineSpecs<Key>("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 => {
|
|
button.setAttribute("aria-pressed", String(on));
|
|
// 바탕칠 = 칸(지도 칸은 띠로 잘림 · 캔버스는 투명)
|
|
wrap.style.background = on ? "var(--draw-surface)" : "";
|
|
};
|
|
mark(tm.on());
|
|
|
|
/** 바탕 한 벌을 바꿈(받은 뒤 · 띠 따라 다시 받은 뒤). */
|
|
function setBackground(
|
|
nextMeta: VWorldMeta,
|
|
contours: RouteEditContours | null,
|
|
otherSheets: ReadonlyArray<PreparedLayer>,
|
|
): 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 (!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 { 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", true);
|
|
gl.setVisible("contour", true);
|
|
gl.setVisible("minor", minor);
|
|
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,
|
|
];
|
|
tm.follow(
|
|
meta,
|
|
normalizer,
|
|
(nx, ny) => turn(...normalizedToScreen(view, nx, ny)),
|
|
view.width,
|
|
view.height,
|
|
);
|
|
// 노선 둘레 띠만(캔버스 자르기와 같은 네모 · 돌림 · 칸 안 px)
|
|
const line = (scene.plannedLine.length ? scene.plannedLine : scene.planned) as Vertex[];
|
|
const band = scene.hasMeta ? contourBandRect(line, scene.toScreen, bandM) : null;
|
|
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;
|
|
},
|
|
dispose: tm.dispose,
|
|
};
|
|
BY_CANVAS.set(canvas, api);
|
|
host?.onDispose(api.dispose);
|
|
return api;
|
|
}
|