feat(D03 지도 겹): 받은 산림 · 환경 자료 겹 다섯 화면 — 국유림 임도 · 산림입지도 · 경제림 · 생태자연도 · 산사태 = 서버 줌별 조각 고정 소스 · 범례 · 받는 동안 서클 (PLAN 622-2 · 요구 529)

- 공용 부품 tileSource(벡터 pbf · 그림 png) 덧 · 새 파일 UI_ForestLayers(꼴 · 소스 · 범례 · 토양 무리 색)
- 산림입지도 그림 = forest_soil 조각(z10 이상) · 브이월드 받기 · 요약 그대로 · 범례 · 옛 캔버스 색 = 같은 토양 무리 색
- 산사태위험 GL = 조각 그림(받기 없음) · 옛 캔버스 = 받은 범위 한 장 그대로(끌 때 받음)
- 새 겹 셋(국유림 임도 · 경제림 · 생태자연도) 처음 끔 · 옛 캔버스엔 안 보임 · 색 토큰 --draw-map-* 덧 · 아이콘 trees · plant 덧
- UI_Layers 688 → 680 줄(GL 글꼴 미리 받기 = UI_MapGl 로 · 브이월드 토양 GL 꼬리표 뺌)
- 지킴 시험 test_622_2_forest_layers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 19:08:01 +09:00
co-authored by Claude Opus 5.5
parent 2648c92b70
commit 57ff92262b
9 changed files with 348 additions and 37 deletions
+25 -8
View File
@@ -10,6 +10,7 @@
* 카메라 주인 = D03 화면(`ViewState` · 돌림) 하나 — 여기는 따라가기만 · 조작은 안 받음.
* 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 색 = 받는 쪽이 지물에 붙인 `_color`.
* 등고선 = 줌별 벡터 조각(615 · 위성처럼 고정 소스 · 가는 선 = 모두 · 굵은 선 · 글 = `k` 1) · 받는 동안 서클.
* 받은 산림 · 환경 자료(622-2) = 같은 꼴 고정 소스 다섯(`D03_PlanAnalysis_UI_ForestLayers`) · 받는 동안 서클.
* 그래픽 카드 · WebGL 이 없거나 잃으면 `onFail` — 부르는 쪽이 옛 캔버스로.
* 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2) · 여기는 겹 꼴만.
* ========================================================================== */
@@ -26,6 +27,13 @@ import {
import { themeColor } from "@ui/ui_template_palette";
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs";
import {
forestAreaSpecs,
forestRasterSpecs,
forestRoadSpecs,
forestSources,
TRACKED_SOURCES,
} from "./D03_PlanAnalysis_UI_ForestLayers";
import { SAT_LIGHT_DEFAULT } from "./D03_PlanAnalysis_UI_SatLight";
export { GL_FONT, type GlImage };
@@ -34,6 +42,9 @@ export { GL_FONT, type GlImage };
export type GlSource = Exclude<PlanLayerKey, "satellite" | "forestRoad">;
const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT];
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
export const glFont = (weight: string, text: string): Promise<unknown> =>
document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null);
export interface GlBase {
/** 자료 담기(지도 없어도 · 켤 때 올림) — null = 비움. */
@@ -129,7 +140,7 @@ function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
const place = themeColor("--draw-map-place", "#f8fafc");
const landslideZone = themeColor("--draw-map-landslide-zone", "#e11d48");
const raster = (
key: GlSource | "satellite",
key: "satellite" | "hillshade" | "slope",
opacity: number,
extra: Record<string, number> = {},
): GlSpec =>
@@ -147,8 +158,8 @@ function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
raster("satellite", 1, { "raster-brightness-max": light, "raster-saturation": SAT_SATURATION }),
raster("hillshade", 1), // 위성 위 = 「곱하기」와 같은 검은 그늘 그림(받는 쪽이 바꿈)
raster("slope", 0.55),
raster("landslide", 0.6),
...fillEdge("soils", "soils", COLOR, 0.35, { color: COLOR, width: 1.4 }),
...forestRasterSpecs(), // 산사태위험 · 생태자연도 = 조각 그림(622-2)
...forestAreaSpecs(), // 산림입지도 · 경제림 = 벡터 조각
...fillEdge("zones", "zones", COLOR, 0.22, { color: COLOR, width: 1.6 }),
...fillEdge("bands", "bands", COLOR, ["case", ["==", ["get", "_band"], "road"], 0.16, 0.18], {
color: COLOR,
@@ -241,6 +252,7 @@ function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
[],
{ color: HALO, width: 4 },
),
...forestRoadSpecs(), // 국유림 임도 = 벡터 조각
// 고른 묶음 · 선 강조(605-2) — 같은 자료에서 걸러 한 겹 더 굵게
...["-pick-halo", "-pick"].map(
(tail, i) =>
@@ -348,7 +360,7 @@ const THEME_NAMES = [
/** 위성 채도(−1~1) — 조금 연하게. */
const SAT_SATURATION = -0.3;
/** `track` = 받는 일을 진행 서클에 셈(등고 조각 받는 동안). */
/** `track` = 받는 일을 진행 서클에 셈(등고 · 산림 조각 받는 동안). */
export function createGlBase(
projectId: string,
track: <T>(job: Promise<T>) => Promise<T>,
@@ -358,12 +370,16 @@ export function createGlBase(
let pick: RoadPick | null = null;
const base = createGlMap<PlanLayerKey>({
specs: () => layerSpecs(light, pick),
fixed: { satellite: satelliteTileSource(projectId), contour: contourTileSource(projectId) },
fixed: {
satellite: satelliteTileSource(projectId),
contour: contourTileSource(projectId),
...forestSources(projectId),
},
themeNames: THEME_NAMES,
className: "d03-plan__gl",
onFail,
});
/** 등고 조각 받는 동안 서클 하나 — 다 받거나 지도가 쉬면(`idle`) · 지도를 버리면 끝. */
/** 등고 · 산림 조각 받는 동안 서클 하나 — 다 받거나 지도가 쉬면(`idle`) · 지도를 버리면 끝. */
let settle: (() => void) | null = null;
const done = (): void => {
settle?.();
@@ -373,11 +389,12 @@ export function createGlBase(
function watch(map: MapLibreMap): void {
watched = map;
map.on("sourcedataloading", (event) => {
if (event.sourceId !== "contour" || settle) return;
if (!TRACKED_SOURCES.includes(event.sourceId) || settle) return;
void track(new Promise<void>((resolve) => (settle = resolve)));
});
map.on("sourcedata", (event) => {
if (event.sourceId === "contour" && map.isSourceLoaded("contour")) done();
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);
}