/* ============================================================================= * D03_PlanAnalysis_UI_MapGl.ts * D03 GL 바탕(PLAN 586-1 · 586-2 · 624-2 · 요구 445 B · 448) — 바탕 겹 스물을 MapLibre(WebGL · BSD-3)로. * 겹 꼴 · 차례 = 공용 `A00_Common/map/map_layer_specs`(624-2 · B05 평면과 같은 꼴) · 여기는 D03 지도 세우기만: * 고정 소스(위성 조각 z6~19 · 등고선 615 줌별 벡터 조각 · 산림 조각 다섯 622-2) · 받는 동안 서클 · 임도망 고름 · 위성 밝기. * 글 = 화면 Noto Sans KR(OFL · 자체 보관)을 그 자리에서 그림 — 꼴에 글꼴 주소가 없으면 MapLibre 가 모든 글자를 화면 글꼴로. * 그은 선 · 재기 · 고르기 · 누름 = 위 캔버스 그대로(위에 겹침). * 카메라 주인 = D03 화면(`ViewState` · 돌림) 하나 — 여기는 따라가기만 · 조작은 안 받음. * 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 색 = 받는 쪽이 지물에 붙인 `_color`. * 그래픽 카드 · WebGL 이 없거나 잃으면 `onFail` — 부르는 쪽이 옛 캔버스로. * 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2). * ========================================================================== */ import type { Map as MapLibreMap } from "maplibre-gl"; import { contourTileSource, createGlMap, GL_FONT, satelliteTileSource, type GlImage, } from "@ui/ui_template_gl_map"; import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { forestSources, TRACKED_SOURCES } from "../A00_Common/map/map_forest_layers"; import { MAP_THEME_NAMES, mapLayerSpecs, pickFilter, type GlSource, } from "../A00_Common/map/map_layer_specs"; import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs"; import { SAT_LIGHT_DEFAULT } from "../A00_Common/map/map_sat_light"; export type { GlImage }; /** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */ export const glFont = (weight: string, text: string): Promise => document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null); export interface GlBase { /** 자료 담기(지도 없어도 · 켤 때 올림) — null = 비움. */ setData: (source: GlSource, data: GeoJsonCollection | GlImage | null) => void; setVisible: (key: PlanLayerKey, on: boolean) => void; /** 올린 임도망 고름(605-2) — 그 묶음 · 선만 굵게 강조 · null = 없앰. */ setRoadPick: (pick: RoadPick | null) => void; /** 위성 밝기 상한(0~1) — 바로 반영 · 지도를 다시 세워도 유지. */ setSatelliteLight: (level: number) => void; /** 켬 = 지도를 `parent` 맨 앞에 만듦(참 = 섬 · 거짓 = WebGL 못 씀) · null = 버림. */ enable: (parent: HTMLElement | null) => boolean; map: () => MapLibreMap | null; /** 화면을 따라감(경위도 가운데 · 줌 · 돌림 도) — 같은 화면에 바로 그림. */ camera: (center: [number, number], zoom: number, bearingDeg: number) => void; resize: () => void; } /** `track` = 받는 일을 진행 서클에 셈(등고 · 산림 조각 받는 동안). */ export function createGlBase( projectId: string, track: (job: Promise) => Promise, onFail: (why: string) => void, ): GlBase { let light = SAT_LIGHT_DEFAULT; let pick: RoadPick | null = null; const base = createGlMap({ specs: () => mapLayerSpecs(light, pick), fixed: { satellite: satelliteTileSource(projectId), contour: contourTileSource(projectId), ...forestSources(projectId), }, themeNames: MAP_THEME_NAMES, className: "d03-plan__gl", onFail, }); /** 등고 · 산림 조각 받는 동안 서클 하나 — 다 받거나 지도가 쉬면(`idle`) · 지도를 버리면 끝. */ let settle: (() => void) | null = null; const done = (): void => { settle?.(); settle = null; }; let watched: MapLibreMap | null = null; function watch(map: MapLibreMap): void { watched = map; map.on("sourcedataloading", (event) => { if (!TRACKED_SOURCES.includes(event.sourceId) || settle) return; void track(new Promise((resolve) => (settle = resolve))); }); map.on("sourcedata", (event) => { const loaded = (id: string) => !map.getSource(id) || map.isSourceLoaded(id); if (TRACKED_SOURCES.includes(event.sourceId) && TRACKED_SOURCES.every(loaded)) done(); }); map.on("idle", done); } return { ...base, enable(parent) { const on = base.enable(parent); const map = base.map(); if (!map) done(); else if (map !== watched) watch(map); return on; }, setRoadPick(next) { pick = next; const map = base.map(); for (const id of ["forestRoadFile-pick-halo", "forestRoadFile-pick"]) if (map?.getLayer(id)) map.setFilter(id, pickFilter(pick)); }, setSatelliteLight(next) { light = next; const map = base.map(); if (map?.getLayer("satellite")) map.setPaintProperty("satellite", "raster-brightness-max", next); }, }; }