Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts
T
eomsangdonandClaude Opus 5.5 c6a1efad0c refactor(공용 지도): 겹 받기 · 범례 줄 · 급경사 범위 · 위성 밝기 · 겹 자료 도움을 A00_Common/map 으로 — D03 은 다시 내놓기 · 동작 그대로 (PLAN 624-3 · 요구 530)
- D03_Api 겹 받기 → map_layer_api.ts(D03_Api 가 다시 내놓음)
- UI_SlopeBands · logic SlopeBands · UI_SatLight → map_slope_bands · logic/map_slope_bands · map_sat_light
- 범례 줄 legendRows → map_legend.ts(받은 자료 꼴 LegendData · D03 LayerLegend 가 다시 내놓음)
- glImage · glTagged · shadeImage · loadImage · WATER_KEYS · FOREST_ROAD_KINDS · hectare → map_layer_data.ts
- 시험 경로 고침 — test_603(TS 자리) · test_606_1(밝기 파일) · test_584_3(범례 글 = 공용 파일도 읽음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:47:50 +09:00

116 lines
5.1 KiB
TypeScript

/* =============================================================================
* 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<unknown> =>
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: <T>(job: Promise<T>) => Promise<T>,
onFail: (why: string) => void,
): GlBase {
let light = SAT_LIGHT_DEFAULT;
let pick: RoadPick | null = null;
const base = createGlMap<PlanLayerKey>({
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<void>((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);
},
};
}