- 겹 단추줄 끝 [GL 바탕 시험] 켬끔(aislo:pref:d03.gl · 처음 = 지금 캔버스)
- 켬 = MapLibre(설치본 5.24 · BSD-3)가 위성 조각 · 급경사 그림 · 지적 · 등고선(주곡선 · 계곡선) · 도로 · 세류를 그림
· 받는 GeoJSON 그대로 · 등고 글 · 그 밖 겹 · 그은 선 · 재기 · 누름 = 캔버스 그대로
- 카메라 = D03 화면(가운데 · 확대 · 돌림)을 따라감 · 같은 화면에 바로 그림 · 캔버스 겹은 MapLibre 투영으로
- 위성 조각 대리 GET …/plan/tile/{z}/{x}/{y}(브이월드 Satellite z6~19 · 키 안 샘)
- 잼(headless 실 GPU · 검증_sub2_D · 겹 여섯): 줌아웃 화면 간격 중앙 20.9 → 4.2 ms · p90 29.2 → 12.5 · 끊김 5 → 0/30
· 끌기 20.8 → 4.2 ms · 끊김 2 → 0/40 · 그은 선 대 GL 바탕 0.02 px(돌림 30° 같음) · 재기 차 0 · 긋기 점 두 판 차 0.00001 m
- 줌인 = 위성 z19 조각으로 선명 · 급경사 그림 매끈 · 선 자리 두 판 같음
- 시험: test_586_tm_frame 조각 대리 범위 밖 400 · test_582 겹 수 낡은 셈(12 → 584 뒤 17) 고침
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
255 lines
8.7 KiB
TypeScript
255 lines
8.7 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_MapGl.ts
|
|
* D03 GL 바탕 시험(PLAN 586-1 · 요구 445 B) — 바탕 겹 여섯을 MapLibre(WebGL · BSD-3)로:
|
|
* 위성 조각(서버 대리 z6~19) · 급경사 그림 · 지적 · 등고선(주곡선 · 계곡선 25 m) · 도로 · 세류 · 하천.
|
|
* 그 밖 겹 · 등고 글 · 그은 선 · 재기 · 누름 = 지금 캔버스 그대로(위에 겹침).
|
|
* 카메라 = D03 화면(`ViewState` · 돌림)을 따라감 · 조작은 안 받음(누름은 위 캔버스).
|
|
* 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 지도가 없어도 자료는 담아 둠.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
Map as MapLibreMap,
|
|
type FilterSpecification,
|
|
type LayerSpecification,
|
|
type SourceSpecification,
|
|
} from "maplibre-gl";
|
|
import "maplibre-gl/dist/maplibre-gl.css";
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { themeColor } from "@ui/ui_template_palette";
|
|
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
|
|
|
/** GL 로 옮긴 겹. */
|
|
export type GlLayerKey = "satellite" | "slope" | "cadastral" | "contour" | "road" | "stream";
|
|
export const GL_KEYS: readonly GlLayerKey[] = [
|
|
"satellite",
|
|
"slope",
|
|
"cadastral",
|
|
"contour",
|
|
"road",
|
|
"stream",
|
|
];
|
|
|
|
/** 급경사 그림 — 주소 + 네 귀퉁이 경위도(왼쪽 위 · 오른쪽 위 · 오른쪽 아래 · 왼쪽 아래). */
|
|
export interface GlImage {
|
|
url: string;
|
|
corners: [[number, number], [number, number], [number, number], [number, number]];
|
|
}
|
|
|
|
export interface GlBase {
|
|
/** 자료 담기(지도 없어도 · 켤 때 올림) — null = 비움. */
|
|
setData: (
|
|
key: Exclude<GlLayerKey, "satellite">,
|
|
data: GeoJsonCollection | GlImage | null,
|
|
) => void;
|
|
setVisible: (key: GlLayerKey, on: boolean) => void;
|
|
/** 등고선 주곡선 간격(m) — 캔버스와 같은 「화면 안 가닥 수」 규칙. */
|
|
setContourStep: (step: number) => void;
|
|
/** 켬 = 지도를 `parent` 맨 앞에 만듦 · 끔 = 버림. */
|
|
enable: (parent: HTMLElement | null) => void;
|
|
map: () => MapLibreMap | null;
|
|
/** 화면을 따라감(경위도 가운데 · 줌 · 돌림 도) — 같은 화면에 바로 그림. */
|
|
camera: (center: [number, number], zoom: number, bearingDeg: number) => void;
|
|
resize: () => void;
|
|
}
|
|
|
|
const HALO = "rgba(0, 0, 0, 0.55)";
|
|
|
|
const LEVEL = ["to-number", ["get", "등고수치"], 0];
|
|
/** 주곡선 = 간격 배수(계곡선도 — 캔버스처럼 계곡선은 두 번 겹쳐 짙음). */
|
|
const minorFilter = (step: number): FilterSpecification =>
|
|
["==", ["%", LEVEL, step], 0] as unknown as FilterSpecification;
|
|
|
|
/** 겹 차례(아래 → 위)와 꼴 — 캔버스 `D03_PlanAnalysis_UI_Layers` 의 색 · 굵기 · 투명도 그대로. */
|
|
function layerSpecs(step: number): Array<LayerSpecification & { key: GlLayerKey }> {
|
|
const contour = themeColor("--draw-map-contour", "#ffd68c");
|
|
const road = themeColor("--draw-map-road", "#f8fafc");
|
|
const stream = themeColor("--draw-map-stream", "#38bdf8");
|
|
const parcel = themeColor("--draw-map-parcel", "#f0abfc");
|
|
const round = { "line-join": "round", "line-cap": "round" } as const;
|
|
return [
|
|
{ key: "satellite", id: "satellite", type: "raster", source: "satellite" },
|
|
{
|
|
key: "slope",
|
|
id: "slope",
|
|
type: "raster",
|
|
source: "slope",
|
|
paint: { "raster-opacity": 0.55, "raster-fade-duration": 0 },
|
|
},
|
|
{
|
|
key: "cadastral",
|
|
id: "cadastral",
|
|
type: "line",
|
|
source: "cadastral",
|
|
layout: round,
|
|
paint: { "line-color": parcel, "line-width": 0.8, "line-opacity": 0.8 },
|
|
},
|
|
{
|
|
key: "contour",
|
|
id: "contour-minor",
|
|
type: "line",
|
|
source: "contour",
|
|
filter: minorFilter(step),
|
|
layout: round,
|
|
paint: { "line-color": contour, "line-width": 0.8, "line-opacity": 0.7 },
|
|
},
|
|
{
|
|
key: "contour",
|
|
id: "contour-index",
|
|
type: "line",
|
|
source: "contour",
|
|
filter: ["==", ["%", LEVEL, 25], 0],
|
|
layout: round,
|
|
paint: { "line-color": contour, "line-width": 1.6, "line-opacity": 0.7 },
|
|
},
|
|
{
|
|
key: "road",
|
|
id: "road-halo",
|
|
type: "line",
|
|
source: "road",
|
|
layout: round,
|
|
paint: { "line-color": HALO, "line-width": 3.2 },
|
|
},
|
|
{
|
|
key: "road",
|
|
id: "road",
|
|
type: "line",
|
|
source: "road",
|
|
layout: round,
|
|
paint: { "line-color": road, "line-width": 1.6 },
|
|
},
|
|
{
|
|
key: "stream",
|
|
id: "stream",
|
|
type: "line",
|
|
source: "stream",
|
|
layout: round,
|
|
paint: {
|
|
"line-color": stream,
|
|
"line-width": ["case", ["==", ["get", "구분"], "세류"], 1, 2.4],
|
|
},
|
|
},
|
|
] as Array<LayerSpecification & { key: GlLayerKey }>;
|
|
}
|
|
|
|
/** 겹 색 지문 — 밝음 · 어두움이 바뀌면 색을 다시. */
|
|
const themeKey = (): string =>
|
|
["--draw-map-contour", "--draw-map-road", "--draw-map-stream", "--draw-map-parcel"]
|
|
.map((name) => themeColor(name, ""))
|
|
.join();
|
|
|
|
export function createGlBase(projectId: string): GlBase {
|
|
const data = new Map<GlLayerKey, GeoJsonCollection | GlImage | null>();
|
|
const visible = new Map<GlLayerKey, boolean>();
|
|
let step = 5;
|
|
let theme = "";
|
|
let map: MapLibreMap | null = null;
|
|
/** 첫 꼴이 섬(`isStyleLoaded` 는 조각 받는 동안 거짓이라 안 씀). */
|
|
let ready = false;
|
|
const applied = new Map<GlLayerKey, unknown>();
|
|
let container: HTMLDivElement | null = null;
|
|
|
|
const source = (key: GlLayerKey): SourceSpecification | null => {
|
|
if (key === "satellite")
|
|
return {
|
|
type: "raster",
|
|
tiles: [`${location.origin}${API_BASE_URL}/projects/${projectId}/plan/tile/{z}/{x}/{y}`],
|
|
tileSize: 256,
|
|
minzoom: 6,
|
|
maxzoom: 19,
|
|
};
|
|
const got = data.get(key);
|
|
if (!got) return null;
|
|
if ("url" in got) return { type: "image", url: got.url, coordinates: got.corners };
|
|
return { type: "geojson", data: got as never, tolerance: 0.375 };
|
|
};
|
|
|
|
/** 자료 · 켬끔 · 색을 지도에 맞춤(지도가 다 서면) — 자료는 바뀐 겹만 다시 올림. */
|
|
function apply(): void {
|
|
if (!map || !ready) return;
|
|
const now = themeKey();
|
|
const specs = layerSpecs(step);
|
|
const drop = (key?: GlLayerKey): void => {
|
|
for (const spec of specs)
|
|
if ((!key || spec.key === key) && map!.getLayer(spec.id)) map!.removeLayer(spec.id);
|
|
};
|
|
if (now !== theme) {
|
|
theme = now;
|
|
drop(); // 밝음 · 어두움 = 색 다시
|
|
}
|
|
for (const key of GL_KEYS) {
|
|
const got = key === "satellite" ? key : (data.get(key) ?? null);
|
|
if (applied.get(key) === got) continue;
|
|
applied.set(key, got);
|
|
if (map.getSource(key)) {
|
|
drop(key);
|
|
map.removeSource(key);
|
|
}
|
|
const want = source(key);
|
|
if (want) map.addSource(key, want);
|
|
}
|
|
specs.forEach(({ key, ...spec }, index) => {
|
|
if (!map!.getSource(key)) return;
|
|
if (!map!.getLayer(spec.id)) {
|
|
// 늦게 온 겹도 제 차례(위 겹 밑)에
|
|
const above = specs.slice(index + 1).find((next) => map!.getLayer(next.id));
|
|
map!.addLayer(spec as LayerSpecification, above?.id);
|
|
}
|
|
map!.setLayoutProperty(spec.id, "visibility", visible.get(key) ? "visible" : "none");
|
|
});
|
|
}
|
|
|
|
return {
|
|
setData(key, next) {
|
|
if (data.get(key) === next) return;
|
|
data.set(key, next);
|
|
apply();
|
|
},
|
|
setVisible(key, on) {
|
|
if (visible.get(key) === on) return;
|
|
visible.set(key, on);
|
|
apply();
|
|
},
|
|
setContourStep(next) {
|
|
if (next === step) return;
|
|
step = next;
|
|
if (map?.getLayer("contour-minor")) map.setFilter("contour-minor", minorFilter(step));
|
|
},
|
|
enable(parent) {
|
|
if (!parent === !map) return;
|
|
if (!parent) {
|
|
map?.remove();
|
|
container?.remove();
|
|
map = container = null;
|
|
ready = false;
|
|
theme = "";
|
|
applied.clear();
|
|
return;
|
|
}
|
|
container = document.createElement("div");
|
|
container.className = "d03-plan__gl";
|
|
parent.prepend(container);
|
|
map = new MapLibreMap({
|
|
container,
|
|
style: { version: 8, sources: {}, layers: [] },
|
|
interactive: false,
|
|
attributionControl: false,
|
|
fadeDuration: 0,
|
|
maxZoom: 24,
|
|
renderWorldCopies: false,
|
|
});
|
|
map.on("load", () => {
|
|
ready = true;
|
|
apply();
|
|
});
|
|
},
|
|
map: () => map,
|
|
camera(center, zoom, bearingDeg) {
|
|
if (!map) return;
|
|
if (ready && themeKey() !== theme) apply();
|
|
map.jumpTo({ center, zoom, bearing: bearingDeg });
|
|
if (ready) map.redraw(); // 캔버스와 같은 화면(다음 화면으로 미루지 않음)
|
|
},
|
|
resize: () => map?.resize(),
|
|
};
|
|
}
|