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
+16 -24
View File
@@ -21,6 +21,8 @@
* GL 바탕(586-2 · 처음 = 켬 · 끝 단추 끔 = 옛 캔버스 · `aislo:pref:d03.canvas`) — 겹 스물 · 등고 글 · 지명을
* MapLibre 가 그림(`D03_PlanAnalysis_UI_MapGl`) · 캔버스 바탕 겹은 쉼(등고 간격 셈만) · 받기 · 범례는 같음 ·
* WebGL 못 씀 · 잃음 = 저절로 옛 캔버스 + 알림(취향은 안 바꿈).
* 받은 산림 · 환경 자료(622-2) = 국유림 임도 · 경제림 · 생태자연도(조각만 · 받기 없음) · 산림입지도 · 산사태위험(GL = 조각 ·
* 옛 캔버스 = 지금 받기 그대로) — 꼴 · 범례 = `D03_PlanAnalysis_UI_ForestLayers`.
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
@@ -58,7 +60,6 @@ import {
GROUPS,
LAYERS,
LINE_LAYERS,
SOIL_COLORS,
WATER_KEYS,
ZONE_COLORS,
glImage,
@@ -71,7 +72,8 @@ import {
} from "./D03_PlanAnalysis_UI_LayerDefs";
import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend";
import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint";
import { createGlBase, GL_FONT, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
import { createGlBase, glFont, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
import { soilColor, tileOnly, type TileOnlyKey } from "./D03_PlanAnalysis_UI_ForestLayers";
import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight";
import {
createSheetTiles,
@@ -89,17 +91,14 @@ import {
import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
const PREF_KEY = "aislo:pref:d03.layers";
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
const glFont = (weight: string, text: string): Promise<unknown> =>
document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null);
/** 옛 캔버스로 그림("1") — 처음 = GL. */
const CANVAS_PREF_KEY = "aislo:pref:d03.canvas";
/** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */
const NO_SATELLITE_NOTE = "옛 그림은 위성 바탕 없이 그림";
type DataKey = keyof Prepared;
/** 겹 → 받는 자료(임도 짐작 = 도로 자료). */
const dataKey = (key: Exclude<PlanLayerKey, "satellite">): DataKey =>
/** 겹 → 받는 자료(임도 짐작 = 도로 자료 · 조각만 쓰는 겹 = 없음). */
const dataKey = (key: Exclude<PlanLayerKey, TileOnlyKey>): DataKey =>
key === "forestRoad" ? "road" : key;
export interface PlanLayers {
@@ -197,6 +196,7 @@ export function createPlanLayers(opts: {
glOn = want && map.setGl(gl);
if (!glOn) map.setGl(null);
glButton.setAttribute("aria-pressed", String(glOn));
if (!glOn) void Promise.all(LAYERS.map((def) => ensure(def.key))); // 산사태위험 한 장(622-2)
sync();
map.redrawBase();
}
@@ -212,8 +212,9 @@ export function createPlanLayers(opts: {
legend.className = "d03-plan__legend";
legend.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이";
/** 받음 — 위성 = GL 조각(받을 것 없음 · 옛 캔버스 = 위성 없음). */
const loadedOf = (key: PlanLayerKey): boolean => key === "satellite" || !!data[dataKey(key)];
/** 받음 — 위성 · 산림 조각 = GL 조각(받을 것 없음 · 옛 캔버스 = 안 그림) · 산사태위험 = GL 이면 조각(622-2). */
const loadedOf = (key: PlanLayerKey): boolean =>
tileOnly(key) || (glOn && key === "landslide") || !!data[dataKey(key)];
function sync(): void {
for (const def of LAYERS) {
@@ -222,8 +223,8 @@ export function createPlanLayers(opts: {
button.disabled = !meta;
const title = !meta
? `${def.title} — 지형 받기 뒤 보임`
: def.key === "satellite" && !glOn
? `${def.title} — ${NO_SATELLITE_NOTE}`
: tileOnly(def.key) && !glOn
? `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`
: def.title;
button.title = title;
button.setAttribute("aria-label", title);
@@ -287,7 +288,7 @@ export function createPlanLayers(opts: {
/** 켜진 겹의 자료를 받음(한 번) — 실패 = 그 겹만 끔(기억은 안 바꿈 · 다음에 다시 받음) + 알림. */
function ensure(key: PlanLayerKey): Promise<void> {
if (!meta || !on[key] || loadedOf(key)) return Promise.resolve();
const slot = key === "satellite" ? key : dataKey(key);
const slot = tileOnly(key) ? "satellite" : dataKey(key);
const running = pending.get(slot);
if (running) return running;
const mine = areaTurn;
@@ -301,7 +302,7 @@ export function createPlanLayers(opts: {
if (mine !== areaTurn) return;
const text = error instanceof Error ? error.message : String(error);
for (const def of LAYERS)
if (def.key === key || (def.key !== "satellite" && dataKey(def.key) === slot))
if (def.key === key || (!tileOnly(def.key) && dataKey(def.key) === slot))
on[def.key] = false;
sync();
map.redraw();
@@ -543,21 +544,12 @@ export function createPlanLayers(opts: {
} else if (key === "soils") {
const soils = await fetchPlanSoils(projectId);
got.soils = {
kinds: soils.summary.map((soil, i) => ({
kinds: soils.summary.map((soil) => ({
layer: prepare(pick(soils, (props) => props["code"] === soil.code)),
color: SOIL_COLORS[i % SOIL_COLORS.length],
color: soilColor(soil.code), // GL 조각과 같은 무리 색(그림 = 조각 · 622-2)
})),
summary: soils.summary,
};
const colorOf = new Map(
soils.summary.map((soil, i) => [soil.code, got.soils!.kinds[i].color]),
);
put(
"soils",
glTagged(soils, (props) =>
themeColor(colorOf.get(props["code"] as never) ?? "", "#facc15"),
),
);
} else if (key === "faults" || key === "lineaments" || key === "zeroOrder") {
const kind = LINE_LAYERS[key].kind;
// 0차곡 첫 셈 = 몇 초 — 늦으면 알림