- 서버: 지오빅데이터(KIGAM) 수치지질도 1:50,000 — WFS 암상 면(깨진 한글 칸은 기호별 점 조회로 채움) · 받은 범위 ∪ 노선으로 자름 · 저장본 · 노선 따라 1 m 표본 구간 · 키는 서버만(.env GEOBIGDATA_API_KEY · 쿼리 key) - 그림 조각 대리에 geology(키 쿼리 이름 칸 key_param · 옛 겹 serviceKey 그대로) - 화면(D03 · B05): 「제한·재해」에 「암상 지질도」 — 그림 조각 + 갈래 색 면 · 「기호 · 암상」 글 · 범례 · 노선 구간 표(참고 — 토사 · 암 판정 아님) - 켠 밖 겹마다 범례 맨 아래 출처 줄(브이월드 · 국토지리정보원 · 산림청 · 환경부 · 생활안전지도 공공누리 · 한국지질자원연구원 CC BY-NC) - 범례 칸 높이 한도 · 밀기(낮은 화면) - 시험: test_760_1 새로 · test_634_2 · test_553_4 겹 차례 표에 geology 한 줄 더함(새 겹 · 낡은 표) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
236 lines
9.4 KiB
TypeScript
236 lines
9.4 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 · 옛 캔버스는 받은 범위 한 장 그대로) · 암상 지질도(geology_map · 760-1 · 도형 = `map_geology`).
|
|
* 조각만 쓰는 겹(`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",
|
|
"geology_map",
|
|
];
|
|
|
|
/** 고정 소스 — 벡터 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"),
|
|
geology_map: wmt("geology"),
|
|
};
|
|
}
|
|
|
|
/** 토양형 코드(산림입지토양도 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" | "geology",
|
|
opacity: number,
|
|
source: string = key,
|
|
): Spec =>
|
|
({
|
|
key,
|
|
id: source === key ? key : source,
|
|
type: "raster",
|
|
source,
|
|
paint: { "raster-opacity": opacity, "raster-fade-duration": 0 },
|
|
}) as Spec;
|
|
|
|
/** 그림 겹(암상 지질도 · 산사태위험 · 생태자연도) — 아래 차례(급경사 위). */
|
|
export const forestRasterSpecs = (): Spec[] => [
|
|
raster("geology", 0.5, "geology_map"),
|
|
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;
|
|
}
|