Files
Aislo/A00_Common/map/map_forest_layers.ts
T
eomsangdonandClaude Opus 5.5 a6ef53e8a3 feat(공용 지도 겹): 임도 짐작 걷음 · 그 자리 = 임도망도(전국 국가임도 · route 아이콘 · 범례 「임도망도 간선 · 작업 · 산불」 · 줌 14 부터 연도 · 종류 글) · 올린 임도망도 = 임도(업로드)(위치 칸 단추 · 목록 · 알림 · 서버 글) (PLAN 630 · 요구 532 · 534)
- 열쇠 그대로(nationalRoad · forestRoadFile) — 옛 켬끔 취향 이어짐 · forestRoad 열쇠 · 도로 소로 거름은 B05 자료 셈 이음으로 남김(겹 목록 · 꼴 · 범례만 걷음)
- 기존 도로(road) 겹 · 소스 · 범례 그대로
- 시험 553_4 겹 차례 · 584_3 범례 기대값을 새 이름으로 고침 · 622_2 에 630 · 534 지킴 덧

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

226 lines
9.2 KiB
TypeScript

/* =============================================================================
* map_forest_layers.ts
* 공용 지도 — 받은 산림 · 환경 자료 겹(PLAN 622-2 · 624-2 · 요구 529 · 530) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴).
* 벡터 = 임도망도(forest_road · 전국 국가임도 · 간선 · 작업 · 산불 색 · 줌 따라 굵기 · 줌 14 부터 연도 · 종류 글) · 산림입지도(forest_soil · 토양 무리 색 ·
* 반투명 · z10 아래 숨김) · 경제림(eco_forest · 국유 · 사유 테두리) /
* 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로).
* 조각만 쓰는 겹(`TILE_ONLY`) = 받기 없음 · 옛 캔버스(그래픽 카드 끔)엔 안 보임.
* 토양 무리 색(`soilColor`) = 조각(`code` = SLTP_CD 두 자리) · 브이월드 요약(기호 「B₁」) 같은 색 — 범례 · 옛 캔버스도 이것.
* 부르는 곳 = `map_layer_specs`(꼴) · D03 `UI_MapGl`(소스) · `UI_Layers`(받기 거름) · `map_legend`(범례).
* ========================================================================== */
import type { ExpressionSpecification, SourceSpecification } from "maplibre-gl";
import type { LegendItem } from "@ui/ui_template_flow_legend";
import { GL_FONT, tileSource, type GlSpec } from "@ui/ui_template_gl_map";
import { themeColor } from "@ui/ui_template_palette";
import type { MapLayerKey } from "./map_layer_list";
/** 조각만 쓰는 겹 — 받을 자료 없음(위성 포함). */
const TILE_ONLY = ["satellite", "nationalRoad", "ecoForest", "ecology"] as const;
export type TileOnlyKey = (typeof TILE_ONLY)[number];
export const tileOnly = (key: MapLayerKey): key is TileOnlyKey =>
(TILE_ONLY as readonly string[]).includes(key);
/** 받는 동안 서클에 세는 조각 소스(등고선 포함). */
export const TRACKED_SOURCES = [
"contour",
"forest_road",
"forest_soil",
"eco_forest",
"ecology",
"landslide",
];
/** 고정 소스 — 벡터 z8~15(서버 줌별 단순화) · 그림 z6~19. */
export function forestSources(projectId: string): Record<string, SourceSpecification> {
const vt = (name: string) => tileSource(projectId, `vt/${name}`, "pbf", 8, 15);
const wmt = (name: string) => tileSource(projectId, `wmt/${name}`, "png", 6, 19);
return {
forest_road: vt("forest_road"),
forest_soil: vt("forest_soil"),
eco_forest: vt("eco_forest"),
ecology: wmt("ecology"),
landslide: wmt("landslide"),
};
}
/** 토양형 코드(산림입지토양도 SLTP_CD) → 기호. 무리 = 갈색 · 적황색 · 암적색 · 회갈색 · 화산회 · 침식 · 미숙 · 그 밖(비산림). */
const SOIL_SYMBOLS: Record<string, string> = {
"01": "B1", "02": "B2", "03": "B3", "04": "B4",
"05": "rB1", "06": "rB2", "07": "R1", "08": "R2", "09": "Y",
"10": "DR1", "11": "DR2", "12": "DR3", "13": "DRb1", "14": "DRb2",
"15": "GrB1", "16": "GrB2", "17": "Va1", "18": "Va2", "19": "Va3", "20": "Va4",
"21": "Va-R1", "22": "Va-R2", "23": "Va-gr",
"24": "Er1", "25": "Er2", "26": "Er-c", "27": "Im", "28": "Li", "29": "Bs",
}; // prettier-ignore
/** 무리 첫 코드(차례 = `--draw-map-soil-1..6`). */
const SOIL_GROUP_START = [1, 5, 10, 15, 24, 30];
const soilToken = (group: number): string => `--draw-map-soil-${group + 1}`;
const SUB = /[₀-₉]/g;
/** 토양형(조각 두 자리 코드 · 브이월드 기호 「B₁」) → 무리 색 값 · 모름 = 「그 밖」. */
export function soilColor(code: string): string {
const plain = code.trim().replace(SUB, (c) => String(c.charCodeAt(0) - 0x2080));
const found = /^\d+$/.test(plain)
? Number(plain)
: Number(Object.keys(SOIL_SYMBOLS).find((key) => SOIL_SYMBOLS[key] === plain) ?? 99);
return soilToken(Math.max(SOIL_GROUP_START.filter((start) => found >= start).length - 1, 0));
}
/** 임도 사업 구분(`kind` 1 간선 · 2 작업 · 3 산불) 색 값. */
const ROAD_KINDS = [
{ kind: 1, label: "간선", color: "--draw-map-national-road-main" },
{ kind: 2, label: "작업", color: "--draw-map-national-road-work" },
{ kind: 3, label: "산불", color: "--draw-map-national-road-fire" },
];
const ECO_KINDS = [
{ own: "국유", color: "--draw-map-eco-national" },
{ own: "사유", color: "--draw-map-eco-private" },
];
/** 생태자연도 범례 = 환경부 WMS 그림 색(3등급 = 칠 없음). */
const ECOLOGY_LEGEND = [
{ label: "1등급", color: "--draw-map-ecology-1" },
{ label: "2등급", color: "--draw-map-ecology-2" },
{ label: "별도관리지역", color: "--draw-map-ecology-x" },
];
const HALO = "rgba(0, 0, 0, 0.55)";
const ROUND = { "line-join": "round", "line-cap": "round" } as const;
const zoomWidth = (z8: number, z12: number, z15: number): ExpressionSpecification =>
["interpolate", ["linear"], ["zoom"], 8, z8, 12, z12, 15, z15] as ExpressionSpecification;
type Spec = GlSpec<MapLayerKey>;
const raster = (key: "landslide" | "ecology", opacity: number): Spec =>
({
key,
id: key,
type: "raster",
source: key,
paint: { "raster-opacity": opacity, "raster-fade-duration": 0 },
}) as Spec;
/** 그림 겹(산사태위험 · 생태자연도) — 아래 차례(급경사 위). */
export const forestRasterSpecs = (): Spec[] => [raster("landslide", 0.6), raster("ecology", 0.55)];
/** 면 겹(산림입지도 · 경제림) — 보호구역 아래. */
export function forestAreaSpecs(): Spec[] {
const soilAt = (group: number): string => themeColor(soilToken(group), "#e879f9");
const soil = [
"step",
["to-number", ["get", "code"], 99],
soilAt(0),
...SOIL_GROUP_START.slice(1).flatMap((start, i) => [start, soilAt(i + 1)]),
] as ExpressionSpecification;
const eco = [
"match",
["get", "own"],
...ECO_KINDS.flatMap(({ own, color }) => [own, themeColor(color, "#34d399")]),
themeColor(ECO_KINDS[1].color, "#a78bfa"),
] as unknown as ExpressionSpecification;
const vector = (source: string) => ({ source, "source-layer": source });
return [
{
key: "soils",
id: "soils",
type: "fill",
...vector("forest_soil"),
minzoom: 10,
paint: { "fill-color": soil, "fill-opacity": 0.35 },
} as Spec,
{
key: "soils",
id: "soils-edge",
type: "line",
...vector("forest_soil"),
minzoom: 10,
layout: ROUND,
paint: { "line-color": soil, "line-width": 0.8, "line-opacity": 0.9 },
} as Spec,
{
key: "ecoForest",
id: "ecoForest",
type: "line",
...vector("eco_forest"),
layout: ROUND,
paint: { "line-color": eco, "line-width": zoomWidth(1, 1.8, 2.4) },
} as Spec,
];
}
/** 임도망도(전국 국가임도) 선 — 검은 테 + 사업 구분 색(도로 위 · 임도(업로드) 아래). */
export function forestRoadSpecs(): Spec[] {
const color = [
"match",
["get", "kind"],
...ROAD_KINDS.flatMap(({ kind, color }) => [kind, themeColor(color, "#ff6b00")]),
themeColor(ROAD_KINDS[0].color, "#ff6b00"),
] as unknown as ExpressionSpecification;
const line = (id: string, paint: Record<string, unknown>): Spec =>
({
key: "nationalRoad",
id,
type: "line",
source: "forest_road",
"source-layer": "forest_road",
layout: ROUND,
paint,
}) as Spec;
return [
line("nationalRoad-halo", { "line-color": HALO, "line-width": zoomWidth(2.2, 3.6, 5.6) }),
line("nationalRoad", { "line-color": color, "line-width": zoomWidth(1, 2, 3.6) }),
];
}
/** 임도망도 글(요구 534) — 줌 14 부터 선 따라 「개설 연도 · 종류」(연도 0 = 종류만) · 겹치면 뺌 · 꼴 = 지명 글. */
export function forestRoadLabelSpec(): Spec {
const kind = [
"match",
["get", "kind"],
...ROAD_KINDS.flatMap(({ kind, label }) => [kind, label]),
"",
] as unknown as ExpressionSpecification;
return {
key: "nationalRoad",
id: "nationalRoad-label",
type: "symbol",
source: "forest_road",
"source-layer": "forest_road",
minzoom: 14,
layout: {
"symbol-placement": "line",
"symbol-spacing": 300,
"text-field": [
"concat",
["case", [">", ["get", "year"], 0], ["concat", ["to-string", ["get", "year"]], " · "], ""],
kind,
],
"text-font": [GL_FONT],
"text-size": 11,
"text-max-angle": 30,
"text-padding": 2,
},
paint: {
"text-color": themeColor("--draw-map-place", "#f8fafc"),
"text-halo-color": "rgba(0, 0, 0, 0.7)",
"text-halo-width": 1.5,
},
} as unknown as Spec;
}
/** 범례 — 켠 조각 겹(산사태위험 = 범례 자리 `LANDSLIDE_LEGEND` 따로). */
export function forestLegendRows(on: Record<MapLayerKey, boolean>): LegendItem[][] {
const item = (label: string, color: string, kind?: LegendItem["kind"]): LegendItem => ({
label,
color: `var(${color})`,
...(kind ? { kind } : {}),
});
const rows: LegendItem[][] = [];
if (on.nationalRoad)
rows.push(ROAD_KINDS.map(({ label, color }) => item(`임도망도 ${label}`, color, "line")));
if (on.ecoForest)
rows.push(ECO_KINDS.map(({ own, color }) => item(`경제림 ${own}`, color, "line")));
if (on.ecology)
rows.push(ECOLOGY_LEGEND.map(({ label, color }) => item(`생태자연도 ${label}`, color)));
return rows;
}