/* ============================================================================= * map_layer_specs.ts * 공용 지도 GL 겹 꼴(PLAN 586-2 · 624-2 · 요구 530) — 바탕 겹 스물의 MapLibre 꼴 · 차례(아래 → 위): * 위성 · 그림 겹(음영 · 급경사 · 산사태위험 · 생태자연도) · 면 겹(산림입지 · 경제림 · 보호구역 · 띠 · 물 면 · 건물) · * 선 겹(지적 · 등고선 · 도로 · 임도망도 · 임도(업로드) · 세류 · 단층 · 지질구조선 · 0차곡 · 산사태취약 필지) · * 점 · 글(산사태취약 · 임도망도 연도 · 종류 · 지명 · 등고 수치). * 자료 = 받는 쪽이 지물에 붙인 `_color` · 고정 소스(위성 · 등고 · 산림 조각) 이름 = `map_forest_layers` · `ui_template_gl_map`. * 카메라 · 지도 세우기는 페이지 몫(D03 `UI_MapGl.createGlBase` · B05 `createTmGl`) — 여기는 꼴만. * ========================================================================== */ import type { ExpressionSpecification, FilterSpecification } from "maplibre-gl"; import { GL_FONT, type GlSpec as GlMapSpec } from "@ui/ui_template_gl_map"; import { themeColor } from "@ui/ui_template_palette"; import { forestAreaSpecs, forestRasterSpecs, forestRoadLabelSpec, forestRoadSpecs, } from "./map_forest_layers"; import { LINE_KEYS, LINE_LAYERS, type LineLayerKey, type MapLayerKey, type RoadPick, } from "./map_layer_list"; /** 자료 소스(위성 = 조각 · 걷은 임도 짐작 = 소스 없음). */ export type GlSource = Exclude; const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT]; type GlSpec = GlMapSpec; const HALO = "rgba(0, 0, 0, 0.55)"; const ROUND = { "line-join": "round", "line-cap": "round" } as const; const COLOR: ExpressionSpecification = ["get", "_color"]; /** 등고 조각 굵은 선(서버가 줌마다 정함) — 가는 선은 모두라 굵은 선은 두 번 겹쳐 짙음(캔버스처럼). */ const BOLD = ["==", ["get", "k"], 1] as unknown as FilterSpecification; /** 캔버스 점선(px) → MapLibre 점선(선 굵기 배수). */ const dash = (px: number[], width: number): number[] => px.map((v) => v / width); /** 선 겹 — 검은 테두리 + 선(점선이면 둘 다 같은 px 무늬). */ function haloLine( key: MapLayerKey, source: GlSource, color: string, width: number, pattern: number[] = [], halo = { color: HALO, width: width + 1.6 }, filter?: FilterSpecification, ): GlSpec[] { const line = (id: string, paint: Record, w: number): GlSpec => ({ key, id, type: "line", source, ...(filter ? { filter } : {}), layout: ROUND, paint: { ...paint, "line-width": w, ...(pattern.length ? { "line-dasharray": dash(pattern, w) } : {}), }, }) as GlSpec; return [ line(`${key}-halo`, { "line-color": halo.color }, halo.width), line(key, { "line-color": color }, width), ]; } /** 면 겹 — 채움 + 테두리(캔버스 `fillLayer` 처럼 테두리 투명도 0.9). */ /** 띠 테두리 — 계류 · 도로 같은 꼴. */ const BAND_EDGE = { color: COLOR, width: 1.2, dash: [6, 4] }; function fillEdge( key: MapLayerKey, source: GlSource, fill: string | ExpressionSpecification, opacity: number | ExpressionSpecification, edge: { color: string | ExpressionSpecification; width: number; dash?: number[] }, filter?: ExpressionSpecification, ): GlSpec[] { const only = filter ? { filter } : {}; return [ { key, id: key, type: "fill", source, ...only, paint: { "fill-color": fill, "fill-opacity": opacity }, } as GlSpec, { key, id: `${key}-edge`, type: "line", source, ...only, layout: ROUND, paint: { "line-color": edge.color, "line-width": edge.width, "line-opacity": 0.9, ...(edge.dash ? { "line-dasharray": dash(edge.dash, edge.width) } : {}), }, } as GlSpec, ]; } /** 겹 차례(아래 → 위)와 꼴 — 옛 캔버스(D03 `UI_LayerPaint`)의 색 · 굵기 · 투명도 · 차례 그대로 · `light` = 위성 밝기 상한. */ export function mapLayerSpecs(light: number, pick: RoadPick | null): GlSpec[] { const contour = themeColor("--draw-map-contour", "#ffd68c"); const place = themeColor("--draw-map-place", "#f8fafc"); const landslideZone = themeColor("--draw-map-landslide-zone", "#e11d48"); const raster = ( key: "satellite" | "hillshade" | "slope", opacity: number, extra: Record = {}, ): GlSpec => ({ key, id: key, type: "raster", source: key, paint: { "raster-opacity": opacity, "raster-fade-duration": 0, ...extra }, }) as GlSpec; const lineKind = (key: LineLayerKey): GlSpec[] => { const { color, width, dash } = LINE_LAYERS[key]; return haloLine(key, key, themeColor(color, "#f43f5e"), width, dash); }; return [ // 위성 = 조금 어둡고 연하게(요구 475 · 노선 선 대비) — 밝기 상한은 사용자 조절 raster("satellite", 1, { "raster-brightness-max": light, "raster-saturation": SAT_SATURATION }), raster("hillshade", 1), // 위성 위 = 「곱하기」와 같은 검은 그늘 그림(받는 쪽이 바꿈) raster("slope", 0.55), ...forestRasterSpecs(), // 산사태위험 · 생태자연도 = 조각 그림(622-2) ...forestAreaSpecs(), // 산림입지도 · 경제림 = 벡터 조각 ...fillEdge("zones", "zones", COLOR, 0.22, { color: COLOR, width: 1.6 }), // 띠 = 한 자료(`_band`) · 겹 둘(634-2 · 도로 300 m 아래 · 계류 100 m 위 — 옛 한 겹 차례 그대로) ...fillEdge("roadBands", "bands", COLOR, 0.16, BAND_EDGE, ["==", ["get", "_band"], "road"]), ...fillEdge("bands", "bands", COLOR, 0.18, BAND_EDGE, ["==", ["get", "_band"], "stream"]), { key: "cadastral", id: "cadastral", type: "line", source: "cadastral", layout: ROUND, paint: { "line-color": themeColor("--draw-map-parcel", "#f0abfc"), "line-width": 0.8, "line-opacity": 0.8, }, } as GlSpec, ...((): GlSpec[] => { const water = themeColor("--draw-map-water", "#0ea5e9"); return fillEdge("water", "water", water, 0.45, { color: water, width: 1 }); })(), ...fillEdge("building", "building", themeColor("--draw-map-building", "#fb7185"), 0.7, { color: HALO, width: 0.6, }), { key: "contour", id: "contour-minor", type: "line", source: "contour", "source-layer": "contour", layout: ROUND, paint: { "line-color": contour, "line-width": 0.8, "line-opacity": 0.7 }, } as GlSpec, { key: "contour", id: "contour-index", type: "line", source: "contour", "source-layer": "contour", filter: BOLD, layout: ROUND, paint: { "line-color": contour, "line-width": 1.6, "line-opacity": 0.7 }, } as GlSpec, { key: "contour", id: "contour-label", type: "symbol", source: "contour", "source-layer": "contour", filter: BOLD, layout: { "symbol-placement": "line", "symbol-spacing": 320, "text-field": ["to-string", ["get", "h"]], "text-font": [GL_FONT], "text-size": 10, "text-max-angle": 30, "text-padding": 2, }, paint: { "text-color": contour, "text-halo-color": "rgba(0, 0, 0, 0.6)", "text-halo-width": 1.5, }, } as GlSpec, ...haloLine("road", "road", themeColor("--draw-map-road", "#f8fafc"), 1.6, [], { color: HALO, width: 3.2, }), ...forestRoadSpecs(), // 임도망도(전국 국가임도) = 벡터 조각 · 임도(업로드) 아래 ...haloLine( "forestRoadFile", "forestRoadFile", themeColor("--draw-map-forest-road-file", "#ff2fd0"), 2.4, [], { color: HALO, width: 4 }, ), // 고른 묶음 · 선 강조(605-2) — 같은 자료에서 걸러 한 겹 더 굵게 ...["-pick-halo", "-pick"].map( (tail, i) => ({ key: "forestRoadFile", id: `forestRoadFile${tail}`, type: "line", source: "forestRoadFile", filter: pickFilter(pick), layout: ROUND, paint: { "line-color": i ? themeColor("--draw-map-sketch", "#ffe94d") : HALO, "line-width": i ? 4.2 : 7.4, }, }) as GlSpec, ), { key: "stream", id: "stream", type: "line", source: "stream", layout: ROUND, paint: { "line-color": themeColor("--draw-map-stream", "#38bdf8"), "line-width": ["case", ["==", ["get", "구분"], "세류"], 1, 2.4], }, } as GlSpec, ...LINE_KEYS.flatMap(lineKind), // 산사태취약 — 필지 = 「이 필지 안 어딘가」 테두리만 · 점 = 지정 자리 ...haloLine( "landslideZones", "landslideZones", landslideZone, 1.8, [6, 3], { color: "rgba(255, 255, 255, 0.7)", width: 3.2 }, ["!=", ["geometry-type"], "Point"] as unknown as FilterSpecification, ), { key: "landslideZones", id: "landslideZones-point", type: "circle", source: "landslideZones", filter: ["==", ["geometry-type"], "Point"], paint: { "circle-radius": 5, "circle-color": landslideZone, "circle-stroke-color": "#ffffff", "circle-stroke-width": 1.5, }, } as GlSpec, forestRoadLabelSpec(), // 임도망도 글 — 지명 아래(겹치면 지명이 앞섬) { key: "place", id: "place-dot", type: "circle", source: "place", paint: { "circle-radius": 2.5, "circle-color": place }, } as GlSpec, { key: "place", id: "place", type: "symbol", source: "place", layout: { "text-field": ["get", "name"], "text-font": GL_FONT_BOLD, "text-size": 11, "text-anchor": "bottom", "text-offset": [0, -0.36], "text-padding": 2, }, paint: { "text-color": place, "text-halo-color": "rgba(0, 0, 0, 0.7)", "text-halo-width": 1.5, }, } as GlSpec, ]; } /** 고른 묶음 · 선만 거름(고름 없음 = 아무것도 안 맞는 거름). */ export function pickFilter(pick: RoadPick | null): FilterSpecification { const all: unknown[] = ["all", ["==", ["get", "__bundle"], pick?.bundle ?? ""]]; if (pick?.line !== undefined) all.push(["==", ["get", "__line"], pick.line]); return all as unknown as FilterSpecification; } /** 겹 색 지문 — 밝음 · 어두움이 바뀌면 색을 다시. */ export const MAP_THEME_NAMES = [ "--draw-map-contour", "--draw-map-road", "--draw-map-stream", "--draw-map-parcel", ] as const; /** 위성 채도(−1~1) — 조금 연하게. */ const SAT_SATURATION = -0.3;