Files
Aislo/A00_Common/map/map_layer_specs.ts
T
eomsangdonandClaude Opus 5.5 61733d064d feat(지도): 634-2 · 634-3 아이콘 표 두 장 · 묶음 메뉴 · 한 행 배선 · 호버 = 표(요구 546)
- 겹 표(map_layer_list) = 그룹1 22 줄 표 차례 · title = 호버제목 · group(수계망도 · 도로 · 토지·건물 · 제한·재해) · pages · 숨김 표 걷음 · 도로300m 한 줄(「도로」 묶음 · 기능 남김)
- 겹 줄(map_layer_bar) = 단독 넷 | 묶음 넷(여럿 고르기 메뉴 · 켠 수 배지 · 안 닫힘) | 그래픽 카드 · sync 가 단추 · 메뉴 줄 막힘 · 글을 맞춤(why = 페이지 까닭)
- 도구 표(map_toolbar) = 그룹2 17 줄 title · mapToolGroups 가 글을 표로 맞춤 · 기본 단추 생성 · 지형 칩 막힘 「수치지형도/라이다 — 까닭」
- 띠 = 한 자료 · 겹 둘(GL fillEdge 거름 · 옛 캔버스 · 범례 줄 따로) · B05 띠 받기(D03 과 같은 서버 길 · bandCollection 공용)
- D03 = 겹 줄 · 도구줄 한 행(createMapToolRow) · 222px 기댐 · 줄바꿈 CSS 걷음 · 측점 · 기울기 호버 덮어쓰기 걷음 · LAS 올리기 호버 걷음
- B05 = 세로 두 줄 → 한 행 · 호버 덮어쓰기 걷음(도구줄 · 1m 등고선 · 3D · 추천) · 겹 막힘 querySelector 덮어쓰기 → 공용 sync · 임도(업로드) 보임 + 「받은 임도 자료 없음」 막힘 · 스냅 간격 날 select → 고르기 메뉴(하나)
- 켬끔 기억 열쇠 · 측점 · 기울기 처음 값 · 그래픽 카드 취향 열쇠 둘 그대로
- 낡은 시험 고침(568_1 · 622_2 · 624_2 · 624_3 · 553_4 · 625_draw · b05_routeedit_initial · 626_3 · 390_4 · 346_2 · 353_1 · 581_2 · 584_1 · 584_3) — 표 꼴 row() · 호버 글 = 표 · 한 행 · 고르기 메뉴로 바뀐 글을 잡음 · 새 지킴 634_2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:40:26 +09:00

316 lines
11 KiB
TypeScript

/* =============================================================================
* 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<MapLayerKey, "satellite" | "forestRoad">;
const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT];
type GlSpec = GlMapSpec<MapLayerKey>;
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<string, unknown>, 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<string, number> = {},
): 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;