- map_layer_list = 겹 스물셋 · 처음 값 · 페이지 표(d03 · b05 보일 겹 · 처음 켬 · 취향 열쇠 — A② · A⑨) - map_layer_specs = GL 겹 꼴 · 차례(D03 UI_MapGl 에서 옮김 · 선 겹 꼴은 LINE_LAYERS 한 벌) - map_layer_bar = 겹 단추 줄 · 켬끔 · 취향 · 그래픽 카드 단추 / map_legend = 범례 칸 · 조각 겹 범례 - map_forest_layers = D03_UI_ForestLayers 옮김(이름만) - D03 UI_Layers 683 → 639 줄 · UI_MapGl 423 → 115 줄 · UI_LayerDefs 는 캔버스 도움 + 옛 이름 다시 내놓음 - 시험 길 고침(553-4 · 581-2 · 584-1 · 584-3 · 606-1 · 615 · 622-2) — 옮긴 파일 자리 · 553-4 겹 목록은 622-2 때 낡은 것(국유림 임도 · 경제림 · 생태자연도 빠짐) 같이 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
318 lines
11 KiB
TypeScript
318 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, 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). */
|
|
function fillEdge(
|
|
key: MapLayerKey,
|
|
source: GlSource,
|
|
fill: string | ExpressionSpecification,
|
|
opacity: number | ExpressionSpecification,
|
|
edge: { color: string | ExpressionSpecification; width: number; dash?: number[] },
|
|
): GlSpec[] {
|
|
return [
|
|
{
|
|
key,
|
|
id: key,
|
|
type: "fill",
|
|
source,
|
|
paint: { "fill-color": fill, "fill-opacity": opacity },
|
|
} as GlSpec,
|
|
{
|
|
key,
|
|
id: `${key}-edge`,
|
|
type: "line",
|
|
source,
|
|
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 }),
|
|
...fillEdge("bands", "bands", COLOR, ["case", ["==", ["get", "_band"], "road"], 0.16, 0.18], {
|
|
color: COLOR,
|
|
width: 1.2,
|
|
dash: [6, 4],
|
|
}),
|
|
{
|
|
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,
|
|
}),
|
|
...haloLine(
|
|
"forestRoad",
|
|
"road",
|
|
themeColor("--draw-map-forest-road", "#f59e0b"),
|
|
2,
|
|
[7, 4],
|
|
{ color: HALO, width: 3.6 },
|
|
[
|
|
"in",
|
|
["get", "도로구분"],
|
|
["literal", ["소로", "미분류"]],
|
|
] as unknown as FilterSpecification,
|
|
),
|
|
...haloLine(
|
|
"forestRoadFile",
|
|
"forestRoadFile",
|
|
themeColor("--draw-map-forest-road-file", "#ff2fd0"),
|
|
2.4,
|
|
[],
|
|
{ color: HALO, width: 4 },
|
|
),
|
|
...forestRoadSpecs(), // 국유림 임도 = 벡터 조각
|
|
// 고른 묶음 · 선 강조(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,
|
|
{
|
|
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;
|