feat(B04·B05): 하단 지도 · 평면 노선 편집 · 배수유역도 바탕 MapLibre 적용 — 공용 GL 부품 · TM 주인 카메라 · 처음 = 켬 · WebGL 못 쓰면 옛 캔버스 (PLAN 589-2 · 요구 448)
- 공용 부품 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>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
/* =============================================================================
|
||||
* B04_PreProcess_UI_MapGl.ts
|
||||
* TM 주인 화면의 GL 바탕(PLAN 589-2 · 요구 448) — B04 하단 지도 · B05 평면 노선 편집 · 배수유역도.
|
||||
* 공용 부품 `ui_template_gl_map`(MapLibre) 을 지도 칸 맨 밑에 깔고, 화면(`ViewState` · 돌림)을
|
||||
* 따라감 — 카메라 = 화면 가운데 1차 맞춤(`logic/…_MapGl_Frame` · 닮음꼴) + 남는 늘림 = 지도 칸 CSS 행렬 · 위 그림(노선 · 관 · 측점 · 고르기)은
|
||||
* 위 캔버스 그대로 · 누름 · 끌기 판정 그대로.
|
||||
* 처음 = 켬 · 끔 = 옛 캔버스(취향 `aislo:pref:map.canvas` = "1" · 세 화면 하나) · WebGL 못 씀 · 잃음 =
|
||||
* 저절로 옛 캔버스 + 알림(취향은 안 바꿈).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ExpressionSpecification, FilterSpecification } from "maplibre-gl";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createGlMap,
|
||||
GL_FONT,
|
||||
satelliteTileSource,
|
||||
type GlMap,
|
||||
type GlRaster,
|
||||
type GlSpec,
|
||||
} from "@ui/ui_template_gl_map";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
import type { VWorldMeta } from "./B04_PreProcess_Api_Fetch";
|
||||
import type { Normalizer } from "./B04_PreProcess_UI_MapRender";
|
||||
import { glCameraOf } from "./logic/B04_PreProcess_UI_MapGl_Frame";
|
||||
|
||||
/** 옛 캔버스로 그림("1") — 처음 = GL. */
|
||||
const CANVAS_PREF_KEY = "aislo:pref:map.canvas";
|
||||
export const GL_TOGGLE_TITLE = "그래픽 카드 그림(바탕 겹을 GPU 로 · 끔 = 옛 캔버스 그림)";
|
||||
|
||||
export interface TmGl<K extends string> {
|
||||
gl: GlMap<K>;
|
||||
/** 지금 GL 로 그리나. */
|
||||
on: () => boolean;
|
||||
/** 켬끔 단추 — 취향을 적고 바꿈. */
|
||||
toggle: () => void;
|
||||
/** 화면을 따라감 — `toScreen` = 정규화 → 화면 px(돌림 포함 · 캔버스와 같은 식). */
|
||||
follow: (
|
||||
meta: VWorldMeta | null,
|
||||
normalizer: Normalizer | null,
|
||||
toScreen: (nx: number, ny: number) => [number, number],
|
||||
width: number,
|
||||
height: number,
|
||||
) => void;
|
||||
/** 화면 px → 지도 칸 안 px(칸 행렬 · 둘레 여유를 되돌림) — 칸 자르기(clip-path)용. */
|
||||
local: (x: number, y: number) => [number, number];
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
/** 지도 칸 둘레 여유(px · CSS `.ui-gl-map` inset) — 칸 행렬(늘림 0.4 %)이 화면 귀퉁이를 비우지 않게. */
|
||||
const GL_MARGIN_PX = 8;
|
||||
|
||||
/** 그림 · 선 겹을 GL 로(위성 조각 = `satelliteData`) — `parent` 맨 앞에 지도 칸 · `onChange` = 켬끔이 바뀜(다시 그림 · 처음 켬은 안 부름). */
|
||||
export function createTmGl<K extends string>(opts: {
|
||||
parent: HTMLElement;
|
||||
specs: () => GlSpec<K>[];
|
||||
themeNames: readonly string[];
|
||||
onChange: (on: boolean) => void;
|
||||
}): TmGl<K> {
|
||||
let active = false;
|
||||
/** 켜려는 중 · 켜 둠 — 못 켬 알림은 이때만. */
|
||||
let wanted = false;
|
||||
let size = "";
|
||||
let css: [number, number, number, number] = [1, 0, 0, 1];
|
||||
/** 처음 켬이 끝남 — 그 전(만드는 중)에는 `onChange` 를 안 부름(부르는 쪽 `on()` 으로 읽음). */
|
||||
let started = false;
|
||||
const gl = createGlMap<K>({
|
||||
specs: opts.specs,
|
||||
themeNames: opts.themeNames,
|
||||
onFail: (why) => {
|
||||
if (!wanted) return;
|
||||
showToast(`그래픽 카드 그림을 못 써 옛 그림으로 — ${why}`, "info");
|
||||
use(false);
|
||||
},
|
||||
});
|
||||
function use(want: boolean): void {
|
||||
wanted = want;
|
||||
active = want && gl.enable(opts.parent);
|
||||
if (!active) gl.enable(null);
|
||||
size = "";
|
||||
if (started) opts.onChange(active);
|
||||
}
|
||||
use(readByKey(CANVAS_PREF_KEY) !== "1");
|
||||
started = true;
|
||||
return {
|
||||
gl,
|
||||
on: () => active,
|
||||
toggle() {
|
||||
writeByKey(CANVAS_PREF_KEY, active ? "1" : "0");
|
||||
use(!active);
|
||||
},
|
||||
follow(meta, normalizer, toScreen, width, height) {
|
||||
if (!active || !meta || !normalizer || width <= 0 || height <= 0) return;
|
||||
const container = gl.container();
|
||||
const now = `${width}x${height}`;
|
||||
if (container && now !== size) {
|
||||
// 칸 = 화면(캔버스 정수 px) + 둘레 여유 — 가운데가 캔버스 가운데와 같게
|
||||
size = now;
|
||||
container.style.width = `${width + 2 * GL_MARGIN_PX}px`;
|
||||
container.style.height = `${height + 2 * GL_MARGIN_PX}px`;
|
||||
gl.resize();
|
||||
}
|
||||
const camera = glCameraOf(meta, normalizer, toScreen, width, height);
|
||||
if (!camera) return;
|
||||
gl.camera(camera.center, camera.zoom, camera.bearing);
|
||||
css = camera.css;
|
||||
if (!container) return;
|
||||
container.style.transform = `matrix(${css.join(", ")}, 0, 0)`;
|
||||
// 검증 창구 — 캔버스 자리(정규화 → 화면) 대 MapLibre 자리를 화면에서 잼
|
||||
Object.assign(container, {
|
||||
glCheck: { map: gl.map(), normalizer, toScreen, css, width, height },
|
||||
});
|
||||
},
|
||||
local(x, y) {
|
||||
const [a, b, c, d] = css;
|
||||
const det = a * d - b * c || 1;
|
||||
const [w, h] = size.split("x").map(Number);
|
||||
const dx = x - w / 2;
|
||||
const dy = y - h / 2;
|
||||
return [
|
||||
(d * dx - c * dy) / det + w / 2 + GL_MARGIN_PX,
|
||||
(a * dy - b * dx) / det + h / 2 + GL_MARGIN_PX,
|
||||
];
|
||||
},
|
||||
dispose: () => gl.enable(null),
|
||||
};
|
||||
}
|
||||
|
||||
const NOT_POINT = ["!=", ["geometry-type"], "Point"] as unknown as FilterSpecification;
|
||||
const IS_POINT = ["==", ["geometry-type"], "Point"] as unknown as FilterSpecification;
|
||||
|
||||
/** 위성 조각 자료(`setData("satellite", …)` · 프로젝트마다). */
|
||||
export const satelliteData = (projectId: string): GlRaster => ({
|
||||
raster: satelliteTileSource(projectId),
|
||||
});
|
||||
|
||||
/** 위성 · 그림 겹(조각 · 한 장). */
|
||||
export const rasterSpec = <K extends string>(key: K, source: string = key): GlSpec<K> =>
|
||||
({
|
||||
key,
|
||||
id: `${key}`,
|
||||
type: "raster",
|
||||
source,
|
||||
paint: { "raster-opacity": 1, "raster-fade-duration": 0 },
|
||||
}) as GlSpec<K>;
|
||||
|
||||
/** 선 겹(캔버스 `drawPreparedLayer` 꼴) — 선 + 점(점 = 지름 4 동그라미 · x = 표고점 X 글자). */
|
||||
export function lineSpecs<K extends string>(
|
||||
key: K,
|
||||
color: string | ExpressionSpecification,
|
||||
width: number,
|
||||
opts: {
|
||||
source?: string;
|
||||
marker?: "dot" | "x";
|
||||
filter?: unknown;
|
||||
opacity?: number;
|
||||
dash?: number[];
|
||||
} = {},
|
||||
): GlSpec<K>[] {
|
||||
const source = opts.source ?? key;
|
||||
const lineFilter = (
|
||||
opts.filter ? ["all", NOT_POINT, opts.filter] : NOT_POINT
|
||||
) as FilterSpecification;
|
||||
const specs: GlSpec<K>[] = [
|
||||
{
|
||||
key,
|
||||
id: `${key}-line`,
|
||||
type: "line",
|
||||
source,
|
||||
filter: lineFilter,
|
||||
layout: { "line-join": "round", "line-cap": "round" },
|
||||
paint: {
|
||||
"line-color": color,
|
||||
"line-width": width,
|
||||
...(opts.opacity !== undefined ? { "line-opacity": opts.opacity } : {}),
|
||||
...(opts.dash ? { "line-dasharray": opts.dash.map((px) => px / width) } : {}),
|
||||
},
|
||||
} as GlSpec<K>,
|
||||
];
|
||||
if (opts.marker === "x")
|
||||
specs.push({
|
||||
key,
|
||||
id: `${key}-point`,
|
||||
type: "symbol",
|
||||
source,
|
||||
filter: IS_POINT,
|
||||
layout: {
|
||||
"text-field": "×",
|
||||
"text-font": [GL_FONT],
|
||||
"text-size": 16,
|
||||
"text-allow-overlap": true,
|
||||
"text-ignore-placement": true,
|
||||
},
|
||||
paint: { "text-color": color },
|
||||
} as GlSpec<K>);
|
||||
else if (opts.marker === "dot")
|
||||
specs.push({
|
||||
key,
|
||||
id: `${key}-point`,
|
||||
type: "circle",
|
||||
source,
|
||||
filter: IS_POINT,
|
||||
paint: { "circle-radius": 2, "circle-color": color },
|
||||
} as GlSpec<K>);
|
||||
return specs;
|
||||
}
|
||||
|
||||
/** 지도 칸이 켰을 때 쓰는 바탕 그림(위성 한 장 · 백지도 · 혼합)의 네 귀퉁이 — 메타 상자 = 메르카토르 그림 네모. */
|
||||
export const metaCorners = (
|
||||
meta: VWorldMeta,
|
||||
): [[number, number], [number, number], [number, number], [number, number]] => [
|
||||
[meta.lon_min, meta.lat_max],
|
||||
[meta.lon_max, meta.lat_max],
|
||||
[meta.lon_max, meta.lat_min],
|
||||
[meta.lon_min, meta.lat_min],
|
||||
];
|
||||
Reference in New Issue
Block a user