refactor(공용 지도): 겹 목록 · GL 겹 꼴 · 겹 줄 · 범례 · 산림 조각 겹을 새 공용 폴더 A00_Common/map 으로 — D03 은 부르기만 · 겉모습 · 동작 그대로 (PLAN 624-2 · 요구 530 · 결정 A⑧)

- 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>
This commit is contained in:
2026-10-31 20:19:01 +09:00
co-authored by Claude Opus 5.5
parent 689727fc98
commit b96479fd7e
16 changed files with 763 additions and 615 deletions
@@ -1,24 +1,24 @@
/* ============================================================================= /* =============================================================================
* D03_PlanAnalysis_UI_ForestLayers.ts * map_forest_layers.ts
* D03 받은 산림 · 환경 자료 겹(PLAN 622-2 · 요구 529) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴). * 공용 지도 — 받은 산림 · 환경 자료 겹(PLAN 622-2 · 624-2 · 요구 529 · 530) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴).
* 벡터 = 국유림 임도(forest_road · 간선 · 작업 · 산불 색 · 줌 따라 굵기) · 산림입지도(forest_soil · 토양 무리 색 · * 벡터 = 국유림 임도(forest_road · 간선 · 작업 · 산불 색 · 줌 따라 굵기) · 산림입지도(forest_soil · 토양 무리 색 ·
* 반투명 · z10 아래 숨김) · 경제림(eco_forest · 국유 · 사유 테두리) / * 반투명 · z10 아래 숨김) · 경제림(eco_forest · 국유 · 사유 테두리) /
* 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로). * 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로).
* 조각만 쓰는 겹(`TILE_ONLY`) = 받기 없음 · 옛 캔버스(그래픽 카드 끔)엔 안 보임. * 조각만 쓰는 겹(`TILE_ONLY`) = 받기 없음 · 옛 캔버스(그래픽 카드 끔)엔 안 보임.
* 토양 무리 색(`soilColor`) = 조각(`code` = SLTP_CD 두 자리) · 브이월드 요약(기호 「B₁」) 같은 색 — 범례 · 옛 캔버스도 이것. * 토양 무리 색(`soilColor`) = 조각(`code` = SLTP_CD 두 자리) · 브이월드 요약(기호 「B₁」) 같은 색 — 범례 · 옛 캔버스도 이것.
* 부르는 곳 = `D03_PlanAnalysis_UI_MapGl`(꼴 · 소스) · `UI_Layers`(받기 거름) · `UI_LayerLegend`(범례). * 부르는 곳 = `map_layer_specs`(꼴) · D03 `UI_MapGl`(소스) · `UI_Layers`(받기 거름) · `map_legend`(범례).
* ========================================================================== */ * ========================================================================== */
import type { ExpressionSpecification, SourceSpecification } from "maplibre-gl"; import type { ExpressionSpecification, SourceSpecification } from "maplibre-gl";
import type { LegendItem } from "@ui/ui_template_flow_legend"; import type { LegendItem } from "@ui/ui_template_flow_legend";
import { tileSource, type GlSpec } from "@ui/ui_template_gl_map"; import { tileSource, type GlSpec } from "@ui/ui_template_gl_map";
import { themeColor } from "@ui/ui_template_palette"; import { themeColor } from "@ui/ui_template_palette";
import type { PlanLayerKey } from "./D03_PlanAnalysis_UI_LayerDefs"; import type { MapLayerKey } from "./map_layer_list";
/** 조각만 쓰는 겹 — 받을 자료 없음(위성 포함). */ /** 조각만 쓰는 겹 — 받을 자료 없음(위성 포함). */
const TILE_ONLY = ["satellite", "nationalRoad", "ecoForest", "ecology"] as const; const TILE_ONLY = ["satellite", "nationalRoad", "ecoForest", "ecology"] as const;
export type TileOnlyKey = (typeof TILE_ONLY)[number]; export type TileOnlyKey = (typeof TILE_ONLY)[number];
export const tileOnly = (key: PlanLayerKey): key is TileOnlyKey => export const tileOnly = (key: MapLayerKey): key is TileOnlyKey =>
(TILE_ONLY as readonly string[]).includes(key); (TILE_ONLY as readonly string[]).includes(key);
/** 받는 동안 서클에 세는 조각 소스(등고선 포함). */ /** 받는 동안 서클에 세는 조각 소스(등고선 포함). */
@@ -89,7 +89,7 @@ const ROUND = { "line-join": "round", "line-cap": "round" } as const;
const zoomWidth = (z8: number, z12: number, z15: number): ExpressionSpecification => const zoomWidth = (z8: number, z12: number, z15: number): ExpressionSpecification =>
["interpolate", ["linear"], ["zoom"], 8, z8, 12, z12, 15, z15] as ExpressionSpecification; ["interpolate", ["linear"], ["zoom"], 8, z8, 12, z12, 15, z15] as ExpressionSpecification;
type Spec = GlSpec<PlanLayerKey>; type Spec = GlSpec<MapLayerKey>;
const raster = (key: "landslide" | "ecology", opacity: number): Spec => const raster = (key: "landslide" | "ecology", opacity: number): Spec =>
({ ({
key, key,
@@ -172,7 +172,7 @@ export function forestRoadSpecs(): Spec[] {
} }
/** 범례 — 켠 조각 겹(산사태위험 = 범례 자리 `LANDSLIDE_LEGEND` 따로). */ /** 범례 — 켠 조각 겹(산사태위험 = 범례 자리 `LANDSLIDE_LEGEND` 따로). */
export function forestLegendRows(on: Record<PlanLayerKey, boolean>): LegendItem[][] { export function forestLegendRows(on: Record<MapLayerKey, boolean>): LegendItem[][] {
const item = (label: string, color: string, kind?: LegendItem["kind"]): LegendItem => ({ const item = (label: string, color: string, kind?: LegendItem["kind"]): LegendItem => ({
label, label,
color: `var(${color})`, color: `var(${color})`,
+96
View File
@@ -0,0 +1,96 @@
/* =============================================================================
* map_layer_bar.ts
* 공용 지도 겹 줄(PLAN 569-2 · 586-2 · 624-2 · 요구 530) — 겹 단추 한 줄(공용 지도 도구줄 `createMapTools` · 묶음 사이 나눔선) +
* 끝에 그래픽 카드 단추(GL ↔ 옛 캔버스).
* 보일 겹 · 처음 켬 · 취향 열쇠 = 페이지 표(`map_layer_list` `MAP_LAYER_PAGES`) · 누르면 켬끔 뒤집고 취향에 적고 `onToggle`.
* 막힘 = 지형을 아직 안 받음(`sync({ ready: false })`) · 조각만 쓰는 겹은 옛 캔버스에서 「안 보임」 글.
* 받기 · 그리기 · 범례는 페이지 몫 — 켬끔 값(`on`)은 같은 객체를 같이 씀.
* 쓰는 법 = `createMapLayerBar({ page: "d03", glOn, onToggle, onGl })` → `{ root, bar, on, toggle, save, sync }`.
* ========================================================================== */
import { createIconButton } from "@ui/ui_template_button";
import { createMapTools } from "@ui/ui_template_map_tools";
import { writeByKey } from "../b_page_state";
import { tileOnly } from "./map_forest_layers";
import {
MAP_LAYER_PAGES,
pageGroups,
readLayerPref,
type MapLayerKey,
type MapPage,
} from "./map_layer_list";
export interface MapLayerBar {
/** 도구줄 감싼 것(자리는 페이지 CSS). */
root: HTMLElement;
/** 도구줄 자체(폭 재기). */
bar: HTMLElement;
/** 켬끔 — 페이지가 바꾸면 `save` · 받기 실패로 끄는 것은 안 적음. */
on: Record<MapLayerKey, boolean>;
/** 뒤집고 적고 `onToggle`. */
toggle: (key: MapLayerKey) => void;
save: () => void;
/** 단추 눌림 · 막힘 · 글을 켬끔 값에 맞춤. */
sync: (state: { ready: boolean; glOn: boolean }) => void;
}
export function createMapLayerBar(opts: {
page: MapPage;
glOn: boolean;
onToggle: (key: MapLayerKey) => void;
/** 그래픽 카드 단추 누름. */
onGl: () => void;
}): MapLayerBar {
const { page } = opts;
const on = readLayerPref(page);
const save = (): void => writeByKey(MAP_LAYER_PAGES[page].pref, JSON.stringify(on));
const toggle = (key: MapLayerKey): void => {
on[key] = !on[key];
save();
opts.onToggle(key);
};
const defs = pageGroups(page);
const buttons = new Map<MapLayerKey, HTMLButtonElement>();
const groups = defs.map((group) =>
group.map((def) => {
const button = createIconButton({
icon: def.icon,
title: def.title,
size: 32,
pressed: on[def.key],
onClick: () => toggle(def.key),
});
button.dataset.layer = def.key;
buttons.set(def.key, button);
return button;
}),
);
const glButton = createIconButton({
icon: "gauge",
title: "그래픽 카드 그림(바탕 겹 · 글을 GPU 로 · 끔 = 옛 캔버스 그림)",
size: 32,
pressed: opts.glOn,
onClick: opts.onGl,
});
glButton.dataset.layer = "gl";
groups.push([glButton]);
const tools = createMapTools({ label: "지도 겹", groups });
function sync({ ready, glOn }: { ready: boolean; glOn: boolean }): void {
for (const def of defs.flat()) {
const button = buttons.get(def.key)!;
button.setAttribute("aria-pressed", String(on[def.key]));
button.disabled = !ready;
const title = !ready
? `${def.title} — 지형 받기 뒤 보임`
: tileOnly(def.key) && !glOn
? `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`
: def.title;
button.title = title;
button.setAttribute("aria-label", title);
}
glButton.setAttribute("aria-pressed", String(glOn));
}
return { root: tools.root, bar: tools.bar, on, toggle, save, sync };
}
+221
View File
@@ -0,0 +1,221 @@
/* =============================================================================
* map_layer_list.ts
* 공용 지도 겹 목록(PLAN 624-2 · 요구 530 · 결정 A② · A⑧ · A⑨) — 예정노선(D03) · B05 평면이 같이 씀.
* 겹 = 열쇠 · 아이콘 · 이름 · 처음 값(`on` = 예정노선) · 단추 묶음(바탕 · 물 · 길 · 구역 · 사이 나눔선).
* 페이지 표(`MAP_LAYER_PAGES`) = 보일 겹(숨길 겹) · 처음 켬(없으면 겹의 `on`) · 켬끔 취향 열쇠.
* 선 겹 꼴 · 보호구역 색 · 산사태위험 범례 = GL 꼴(`map_layer_specs`) · 범례(`map_legend`) · 옛 캔버스가 같이 읽음.
* 쓰는 법 = 겹 줄 `createMapLayerBar({ page })`(`map_layer_bar`) · 새 겹 = `GROUPS` 한 줄 + 꼴 한 벌.
* ========================================================================== */
import type { IconName } from "@ui/ui_template_icons";
import type { LegendItem } from "@ui/ui_template_flow_legend";
import { readByKey } from "../b_page_state";
export type MapLayerKey =
| "satellite"
| "hillshade"
| "contour"
| "slope"
| "stream"
| "water"
| "road"
| "forestRoad"
| "forestRoadFile"
| "nationalRoad"
| "ecoForest"
| "ecology"
| "cadastral"
| "zones"
| "soils"
| "landslide"
| "landslideZones"
| "faults"
| "lineaments"
| "zeroOrder"
| "bands"
| "building"
| "place";
/** 올린 임도망 고름(605-2) — 묶음 하나 또는 그 안의 선 하나(지도 강조). */
export interface RoadPick {
bundle: string;
line?: number;
}
export interface LayerDef {
key: MapLayerKey;
icon: IconName;
title: string;
/** 예정노선 처음 값 — 다른 페이지는 `MAP_LAYER_PAGES[page].on`. */
on: boolean;
}
/** 단추 묶음(사이 나눔선) — 바탕 · 물 · 길 · 구역. 「지금」 겹 처음 값 = 끔(`_분석_440-C` 1-4). */
export const GROUPS: readonly (readonly LayerDef[])[] = [
[
{ key: "satellite", icon: "satellite", title: "위성사진", on: true },
{ key: "hillshade", icon: "sun", title: "음영기복(2 m 지형 · 산줄기 · 골)", on: false },
{ key: "contour", icon: "mountain", title: "도엽 등고선(굵은 선 = 계곡선 25 m)", on: true },
{ key: "slope", icon: "angle", title: "급경사 색(기울기 % 범위 · 범례 ⚙ 로 바꿈)", on: false },
],
[
{ key: "stream", icon: "droplet", title: "세류선(가는 파랑) · 하천(굵은 파랑)", on: true },
{ key: "water", icon: "ripple", title: "물 면(도엽 실폭하천 · 저수지 등)", on: false },
{ key: "road", icon: "road", title: "기존 도로(도엽 도로중심선)", on: false },
{
key: "forestRoad",
icon: "route",
title: "기존 임도 짐작(도엽 도로 소로 · 미분류 = 주황 점선 · 정식 임도망도 아님)",
on: false,
},
{
key: "forestRoadFile",
icon: "map",
title:
"올린 임도망도(산림청 시군구 SHP = 밝은 분홍 실선 · 왼쪽 「위치」 칸에서 올리고 묶음 고름 · 지움)",
on: false,
},
{
key: "nationalRoad",
icon: "flag",
title:
"국유림 임도(산림청 전국 국가임도 · 간선 · 작업 · 산불 색 · 지방 · 민간임도 없음 · 참고용)",
on: false,
},
],
[
{ key: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false },
{
key: "zones",
icon: "shield",
title: "보호 · 규제 구역(브이월드 여덟 종 · 참고용)",
on: false,
},
{
key: "soils",
icon: "leaf",
title: "산림입지도(토양형 · 산림청 산림입지토양도 1:25,000 · 줌 10 이상 · 참고용)",
on: false,
},
{
key: "ecoForest",
icon: "trees",
title: "경제림육성단지(산림청 2014 · 국유 · 사유 테두리 · 참고용)",
on: false,
},
{
key: "ecology",
icon: "plant",
title: "생태자연도(환경부 · 1~3 등급 · 별도관리 · 참고용)",
on: false,
},
{
key: "landslide",
icon: "alert",
title: "산사태위험지도(1~5 등급 · 행안부 생활안전지도 · 참고용)",
on: false,
},
{
key: "landslideZones",
icon: "alertOctagon",
title: "산사태취약지역(시도 지정 현황 · 점 + 지번 필지 테두리 · 참고용)",
on: false,
},
{ key: "faults", icon: "bolt", title: "단층(브이월드 지질 · 참고용)", on: false },
{
key: "lineaments",
icon: "lineDashed",
title: "지질구조선(브이월드 지질 · 참고용)",
on: false,
},
{
key: "zeroOrder",
icon: "arrowsJoin",
title: "0차곡(지형 추정 골짜기 머리 · 위 집수 0.1~1 ha · 곡두 통과 피함)",
on: false,
},
{
key: "bands",
icon: "borderOuter",
title: "계류 100 m · 도로 300 m 띠(타당성평가)",
on: false,
},
{ key: "building", icon: "building", title: "건물(도엽 · 하류 민가 · 인가)", on: false },
{ key: "place", icon: "mapPin", title: "지명(도엽 계곡 · 령 · 부락)", on: false },
],
];
export const LAYERS: readonly LayerDef[] = GROUPS.flat();
/** 지도가 있는 페이지. */
export type MapPage = "d03" | "b05";
/** 페이지 표 — 숨길 겹 · 처음 켬(없음 = 겹의 `on`) · 켬끔 취향 열쇠. */
export const MAP_LAYER_PAGES: Record<
MapPage,
{ hide: readonly MapLayerKey[]; on?: readonly MapLayerKey[]; pref: string }
> = {
d03: { hide: [], pref: "aislo:pref:d03.layers" },
// A② 처음 = 지금 그대로(등고선 · 하천) · A⑨ 띠(타당성) · 올린 임도망도 숨김
b05: {
hide: ["bands", "forestRoadFile"],
on: ["contour", "stream"],
pref: "aislo:pref:b05.layers",
},
};
/** 그 페이지에 보일 겹 묶음(빈 묶음 뺌). */
export const pageGroups = (page: MapPage): LayerDef[][] =>
GROUPS.map((group) =>
group.filter((def) => !MAP_LAYER_PAGES[page].hide.includes(def.key)),
).filter((group) => group.length);
/** 켬끔 취향 — 없거나 깨지면 페이지 처음 값 · 숨긴 겹 = 끔. */
export function readLayerPref(page: MapPage): Record<MapLayerKey, boolean> {
const { hide, on: first, pref } = MAP_LAYER_PAGES[page];
const on = Object.fromEntries(
LAYERS.map((def) => [def.key, first ? first.includes(def.key) : def.on]),
) as Record<MapLayerKey, boolean>;
try {
const saved = JSON.parse(readByKey(pref) ?? "{}") as Partial<Record<MapLayerKey, unknown>>;
for (const def of LAYERS)
if (typeof saved[def.key] === "boolean") on[def.key] = saved[def.key] as boolean;
} catch {
// 깨진 값 = 처음 값
}
for (const key of hide) on[key] = false;
return on;
}
/** 보호구역 종류 → 그림 색 값(서버 `ZONE_KINDS` 열쇠). */
export const ZONE_COLORS: Record<string, string> = {
forest: "--draw-map-zone-forest",
baekdu: "--draw-map-zone-baekdu",
water: "--draw-map-zone-water",
park: "--draw-map-zone-park",
heritage: "--draw-map-zone-heritage",
wetland: "--draw-map-zone-wetland",
wildlife: "--draw-map-zone-wildlife",
greenbelt: "--draw-map-zone-greenbelt",
};
/** 산사태위험지도 범례(1 등급 = 가장 위험 · 색 = WMS 그림 색). */
export const LANDSLIDE_LEGEND: LegendItem[] = [1, 2, 3, 4, 5].map((grade) => ({
label: `산사태 ${grade}등급`,
color: `var(--draw-map-landslide-grade-${grade})`,
}));
/** 선 겹 — 이름 · 그림 꼴(서버 `LINE_KINDS` 열쇠 = `kind` · 없으면 0차곡). 새 선 겹 = 여기 한 줄. */
export type LineLayerKey = "faults" | "lineaments" | "zeroOrder";
export const LINE_LAYERS: Record<
LineLayerKey,
{ label: string; kind?: string; color: string; width: number; dash: number[] }
> = {
faults: { label: "단층", kind: "faults", color: "--draw-map-fault", width: 2.4, dash: [] },
lineaments: {
label: "지질구조선",
kind: "lineaments",
color: "--draw-map-lineament",
width: 1.8,
dash: [8, 4],
},
zeroOrder: { label: "0차곡", color: "--draw-map-zero-order", width: 1.8, dash: [4, 3] },
};
export const LINE_KEYS = Object.keys(LINE_LAYERS) as LineLayerKey[];
+317
View File
@@ -0,0 +1,317 @@
/* =============================================================================
* 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;
+36
View File
@@ -0,0 +1,36 @@
/* =============================================================================
* map_legend.ts
* 공용 지도 범례(PLAN 569-2 · 622-2 · 624-2 · 요구 530) — 켠 겹 범례 칸 하나(줄마다 `createLegend`) + 페이지가 붙이는 덧 줄.
* 조각 겹 범례(산사태위험 · 국유림 임도 · 경제림 · 생태자연도) = 받을 자료 없음 → 켬끔만 보고 여기서 만듦(`tileLegendRows`).
* 받은 자료 범례(수 · 넓이 · 급경사 범위)는 받는 페이지 몫 — 줄을 만들어 `show` 에 넘김.
* 쓰는 법 = `const legend = createMapLegend("d03-plan__legend")` → `legend.show(rows, [덧 줄])` · 줄 · 덧 없으면 숨김.
* ========================================================================== */
import { createLegend, type LegendItem } from "@ui/ui_template_flow_legend";
import { forestLegendRows } from "./map_forest_layers";
import { LANDSLIDE_LEGEND, type MapLayerKey } from "./map_layer_list";
export interface MapLegend {
root: HTMLElement;
show: (rows: LegendItem[][], tail?: HTMLElement[]) => void;
}
/** 켠 조각 겹 범례 — 산사태위험 다음 산림 셋. */
export const tileLegendRows = (on: Record<MapLayerKey, boolean>): LegendItem[][] => [
...(on.landslide ? [LANDSLIDE_LEGEND] : []),
...forestLegendRows(on),
];
/** `className` = 자리 · 꼴(페이지 CSS). */
export function createMapLegend(className: string): MapLegend {
const root = document.createElement("div");
root.className = className;
root.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이";
return {
root,
show(rows, tail = []) {
root.replaceChildren(...rows.map((row) => createLegend(row)), ...tail);
root.hidden = !rows.length && !tail.length;
},
};
}
+12 -187
View File
@@ -1,14 +1,12 @@
/* ============================================================================= /* =============================================================================
* D03_PlanAnalysis_UI_LayerDefs.ts * D03_PlanAnalysis_UI_LayerDefs.ts
* D03 바탕 겹 정의(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2) — 겹 단추 묶음 · 처음 값 · 보호구역 · 토양형 색 · * D03 바탕 겹 받기 · 옛 캔버스 도움(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2 · 624-2) — 임도 짐작 구분 · 물 면 ·
* 산사태위험지도 범례 · 선 겹(단층 · 지질구조선 · 0차곡) 꼴 · 켬끔 취향 읽기 · 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 · * 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 · GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림).
* GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림). * 겹 목록 · 처음 값 · 켬끔 취향 · 선 겹 꼴 · 보호구역 색 = 공용 `A00_Common/map/map_layer_list`(624-2).
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기 차례 · 범례). * 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기 차례 · 범례).
* ========================================================================== */ * ========================================================================== */
import type { IconName } from "@ui/ui_template_icons"; import type { MapLayerKey } from "../A00_Common/map/map_layer_list";
import type { LegendItem } from "@ui/ui_template_flow_legend";
import { readByKey } from "../A00_Common/b_page_state";
import { import {
drawPreparedLayer, drawPreparedLayer,
normalizedToScreen, normalizedToScreen,
@@ -20,172 +18,15 @@ import type { PlanRaster } from "./D03_PlanAnalysis_Api";
import type { ScreenMap } from "./D03_PlanAnalysis_UI_Map"; import type { ScreenMap } from "./D03_PlanAnalysis_UI_Map";
import type { GlImage } from "./D03_PlanAnalysis_UI_MapGl"; import type { GlImage } from "./D03_PlanAnalysis_UI_MapGl";
export type PlanLayerKey = /** 겹 목록 · 페이지 표 = 공용 `map_layer_list`(624-2) — 옛 이름으로 다시 내놓음(캔버스 · 조각 · 임도망 목록이 읽음). */
| "satellite" export type PlanLayerKey = MapLayerKey;
| "hillshade" export {
| "contour" LINE_KEYS,
| "slope" LINE_LAYERS,
| "stream" ZONE_COLORS,
| "water" type RoadPick,
| "road" } from "../A00_Common/map/map_layer_list";
| "forestRoad"
| "forestRoadFile"
| "nationalRoad"
| "ecoForest"
| "ecology"
| "cadastral"
| "zones"
| "soils"
| "landslide"
| "landslideZones"
| "faults"
| "lineaments"
| "zeroOrder"
| "bands"
| "building"
| "place";
/** 올린 임도망 고름(605-2) — 묶음 하나 또는 그 안의 선 하나(지도 강조). */
export interface RoadPick {
bundle: string;
line?: number;
}
export interface LayerDef {
key: PlanLayerKey;
icon: IconName;
title: string;
on: boolean;
}
/** 단추 묶음(사이 나눔선) — 바탕 · 물 · 길 · 구역. 「지금」 겹 처음 값 = 끔(`_분석_440-C` 1-4). */
export const GROUPS: readonly (readonly LayerDef[])[] = [
[
{ key: "satellite", icon: "satellite", title: "위성사진", on: true },
{ key: "hillshade", icon: "sun", title: "음영기복(2 m 지형 · 산줄기 · 골)", on: false },
{ key: "contour", icon: "mountain", title: "도엽 등고선(굵은 선 = 계곡선 25 m)", on: true },
{ key: "slope", icon: "angle", title: "급경사 색(기울기 % 범위 · 범례 ⚙ 로 바꿈)", on: false },
],
[
{ key: "stream", icon: "droplet", title: "세류선(가는 파랑) · 하천(굵은 파랑)", on: true },
{ key: "water", icon: "ripple", title: "물 면(도엽 실폭하천 · 저수지 등)", on: false },
{ key: "road", icon: "road", title: "기존 도로(도엽 도로중심선)", on: false },
{
key: "forestRoad",
icon: "route",
title: "기존 임도 짐작(도엽 도로 소로 · 미분류 = 주황 점선 · 정식 임도망도 아님)",
on: false,
},
{
key: "forestRoadFile",
icon: "map",
title:
"올린 임도망도(산림청 시군구 SHP = 밝은 분홍 실선 · 왼쪽 「위치」 칸에서 올리고 묶음 고름 · 지움)",
on: false,
},
{
key: "nationalRoad",
icon: "flag",
title:
"국유림 임도(산림청 전국 국가임도 · 간선 · 작업 · 산불 색 · 지방 · 민간임도 없음 · 참고용)",
on: false,
},
],
[
{ key: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false },
{
key: "zones",
icon: "shield",
title: "보호 · 규제 구역(브이월드 여덟 종 · 참고용)",
on: false,
},
{
key: "soils",
icon: "leaf",
title: "산림입지도(토양형 · 산림청 산림입지토양도 1:25,000 · 줌 10 이상 · 참고용)",
on: false,
},
{
key: "ecoForest",
icon: "trees",
title: "경제림육성단지(산림청 2014 · 국유 · 사유 테두리 · 참고용)",
on: false,
},
{
key: "ecology",
icon: "plant",
title: "생태자연도(환경부 · 1~3 등급 · 별도관리 · 참고용)",
on: false,
},
{
key: "landslide",
icon: "alert",
title: "산사태위험지도(1~5 등급 · 행안부 생활안전지도 · 참고용)",
on: false,
},
{
key: "landslideZones",
icon: "alertOctagon",
title: "산사태취약지역(시도 지정 현황 · 점 + 지번 필지 테두리 · 참고용)",
on: false,
},
{ key: "faults", icon: "bolt", title: "단층(브이월드 지질 · 참고용)", on: false },
{
key: "lineaments",
icon: "lineDashed",
title: "지질구조선(브이월드 지질 · 참고용)",
on: false,
},
{
key: "zeroOrder",
icon: "arrowsJoin",
title: "0차곡(지형 추정 골짜기 머리 · 위 집수 0.1~1 ha · 곡두 통과 피함)",
on: false,
},
{
key: "bands",
icon: "borderOuter",
title: "계류 100 m · 도로 300 m 띠(타당성평가)",
on: false,
},
{ key: "building", icon: "building", title: "건물(도엽 · 하류 민가 · 인가)", on: false },
{ key: "place", icon: "mapPin", title: "지명(도엽 계곡 · 령 · 부락)", on: false },
],
];
export const LAYERS: readonly LayerDef[] = GROUPS.flat();
/** 보호구역 종류 → 그림 색 값(서버 `ZONE_KINDS` 열쇠). */
export const ZONE_COLORS: Record<string, string> = {
forest: "--draw-map-zone-forest",
baekdu: "--draw-map-zone-baekdu",
water: "--draw-map-zone-water",
park: "--draw-map-zone-park",
heritage: "--draw-map-zone-heritage",
wetland: "--draw-map-zone-wetland",
wildlife: "--draw-map-zone-wildlife",
greenbelt: "--draw-map-zone-greenbelt",
};
/** 산사태위험지도 범례(1 등급 = 가장 위험 · 색 = WMS 그림 색). */
export const LANDSLIDE_LEGEND: LegendItem[] = [1, 2, 3, 4, 5].map((grade) => ({
label: `산사태 ${grade}등급`,
color: `var(--draw-map-landslide-grade-${grade})`,
}));
/** 선 겹 — 이름 · 그림 꼴(서버 `LINE_KINDS` 열쇠 = `kind` · 없으면 0차곡). 새 선 겹 = 여기 한 줄. */
export type LineLayerKey = "faults" | "lineaments" | "zeroOrder";
export const LINE_LAYERS: Record<
LineLayerKey,
{ label: string; kind?: string; color: string; width: number; dash: number[] }
> = {
faults: { label: "단층", kind: "faults", color: "--draw-map-fault", width: 2.4, dash: [] },
lineaments: {
label: "지질구조선",
kind: "lineaments",
color: "--draw-map-lineament",
width: 1.8,
dash: [8, 4],
},
zeroOrder: { label: "0차곡", color: "--draw-map-zero-order", width: 1.8, dash: [4, 3] },
};
export const LINE_KEYS = Object.keys(LINE_LAYERS) as LineLayerKey[];
/** 도로 중 기존 임도로 짐작하는 구분. */ /** 도로 중 기존 임도로 짐작하는 구분. */
export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]); export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]);
/** 물 면 = 도엽 수계(E) 면 셋. */ /** 물 면 = 도엽 수계(E) 면 셋. */
@@ -283,22 +124,6 @@ export function drawRaster(
context.drawImage(raster.image, 0, 0); context.drawImage(raster.image, 0, 0);
} }
/** 켬끔 취향(`key`) — 없거나 깨지면 처음 값. */
export function readPref(key: string): Record<PlanLayerKey, boolean> {
const on = Object.fromEntries(LAYERS.map((def) => [def.key, def.on])) as Record<
PlanLayerKey,
boolean
>;
try {
const saved = JSON.parse(readByKey(key) ?? "{}") as Partial<Record<PlanLayerKey, unknown>>;
for (const def of LAYERS)
if (typeof saved[def.key] === "boolean") on[def.key] = saved[def.key] as boolean;
} catch {
// 깨진 값 = 처음 값
}
return on;
}
/** GL 자료 — 지물마다 그릴 색(`_color`) · 덧 속성을 붙인 사본. */ /** GL 자료 — 지물마다 그릴 색(`_color`) · 덧 속성을 붙인 사본. */
export function glTagged( export function glTagged(
collection: GeoJsonCollection, collection: GeoJsonCollection,
@@ -2,6 +2,7 @@
* D03_PlanAnalysis_UI_LayerLegend.ts * D03_PlanAnalysis_UI_LayerLegend.ts
* D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-3) — 켠 겹의 받은 범위 안 수 · 넓이. * D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-3) — 켠 겹의 받은 범위 안 수 · 넓이.
* 급경사 = 고른 범위(603)마다 % · ° · 비율(1 % 칸 수로 바로 셈). * 급경사 = 고른 범위(603)마다 % · ° · 비율(1 % 칸 수로 바로 셈).
* 범례 칸 · 조각 겹 범례 = 공용 `A00_Common/map/map_legend`(624-2) · 여기는 받은 자료 줄만.
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기). * 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기).
* ========================================================================== */ * ========================================================================== */
@@ -16,15 +17,14 @@ import type {
PlanZones, PlanZones,
} from "./D03_PlanAnalysis_Api"; } from "./D03_PlanAnalysis_Api";
import { import {
LANDSLIDE_LEGEND,
LINE_KEYS, LINE_KEYS,
LINE_LAYERS, LINE_LAYERS,
ZONE_COLORS, ZONE_COLORS,
hectare,
type LineLayerKey, type LineLayerKey,
type PlanLayerKey, type MapLayerKey,
} from "./D03_PlanAnalysis_UI_LayerDefs"; } from "../A00_Common/map/map_layer_list";
import { forestLegendRows } from "./D03_PlanAnalysis_UI_ForestLayers"; import { tileLegendRows } from "../A00_Common/map/map_legend";
import { hectare } from "./D03_PlanAnalysis_UI_LayerDefs";
import { import {
SLOPE_MAX_PCT, SLOPE_MAX_PCT,
bandRatios, bandRatios,
@@ -111,13 +111,12 @@ function landslideZoneRow({ info }: Prepared["landslideZones"]): LegendItem[] {
} }
export function legendRows( export function legendRows(
on: Record<PlanLayerKey, boolean>, on: Record<MapLayerKey, boolean>,
data: Partial<Prepared>, data: Partial<Prepared>,
): LegendItem[][] { ): LegendItem[][] {
const rows: LegendItem[][] = []; const rows: LegendItem[][] = [];
if (on.slope && data.slope) rows.push(slopeRow(data.slope)); if (on.slope && data.slope) rows.push(slopeRow(data.slope));
if (on.landslide) rows.push(LANDSLIDE_LEGEND); // GL = 조각(받기 없음 · 622-2) rows.push(...tileLegendRows(on)); // 조각 겹 = 받기 없음(622-2 · 공용 범례)
rows.push(...forestLegendRows(on));
if (on.landslideZones && data.landslideZones) rows.push(landslideZoneRow(data.landslideZones)); if (on.landslideZones && data.landslideZones) rows.push(landslideZoneRow(data.landslideZones));
if (on.zones && data.zones) { if (on.zones && data.zones) {
const items = Object.entries(data.zones.summary) const items = Object.entries(data.zones.summary)
+33 -77
View File
@@ -1,7 +1,7 @@
/* ============================================================================= /* =============================================================================
* D03_PlanAnalysis_UI_Layers.ts * D03_PlanAnalysis_UI_Layers.ts
* D03 평면 바탕 겹 켜고 끔(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 요구 395-2 · 440-8 · 442 · 443) — 지도 왼쪽 위 아이콘 한 줄 * D03 평면 바탕 겹 켜고 끔(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 624-2 · 요구 395-2 · 440-8 · 442 · 443) — 지도 왼쪽 위 아이콘 한 줄
* (공용 지도 도구줄 `createMapTools` · 넘치면 가로 밀기 571-1 · 오른쪽 평면 도구줄과 따로). * (공용 겹 줄 `A00_Common/map/map_layer_bar` · 넘치면 가로 밀기 571-1 · 오른쪽 평면 도구줄과 따로).
* *
* 겹 = 바탕(위성 · 음영기복 · 도엽 등고선(계곡선 25 m 굵게 · 수치) · 급경사 색) · * 겹 = 바탕(위성 · 음영기복 · 도엽 등고선(계곡선 25 m 굵게 · 수치) · 급경사 색) ·
* 물 · 길(세류선 · 하천 · 물 면(실폭하천 · 저수지) · 기존 도로 · 기존 임도 짐작(소로 · 미분류) · * 물 · 길(세류선 · 하천 · 물 면(실폭하천 · 저수지) · 기존 도로 · 기존 임도 짐작(소로 · 미분류) ·
@@ -9,28 +9,29 @@
* 구역(지적도 · 보호 · 규제 구역 여덟 종 · 산림입지도(토양형) · 산사태위험지도 · 산사태취약지역(시도 표 점 · 지번 필지 테두리) · * 구역(지적도 · 보호 · 규제 구역 여덟 종 · 산림입지도(토양형) · 산사태위험지도 · 산사태취약지역(시도 표 점 · 지번 필지 테두리) ·
* 선 겹(단층 · 지질구조선 · 0차곡) · * 선 겹(단층 · 지질구조선 · 0차곡) ·
* 계류 100 m · 도로 300 m 띠 · 건물 · 지명). * 계류 100 m · 도로 300 m 띠 · 건물 · 지명).
* 처음 켬 = 위성 · 등고선 · 세류 · 고른 값 = 화면 취향(`aislo:pref:d03.layers`) 기억. * 겹 목록 · 처음 켬(위성 · 등고선 · 세류) · 취향(`aislo:pref:d03.layers`) = 공용 `map_layer_list` 페이지 표 「d03」.
* 받기 = 켤 때 · 받은 지형 범위 지문(`load(have)` · 페이지가 지형면을 다 읽은 뒤 넘김)이 바뀌면 차례 +1 · 켠 겹 모두 다시 · * 받기 = 켤 때 · 받은 지형 범위 지문(`load(have)` · 페이지가 지형면을 다 읽은 뒤 넘김)이 바뀌면 차례 +1 · 켠 겹 모두 다시 ·
* 옛 차례 답은 버림(475) · 도엽 선 겹(등고 · 세류 · 물 면 · 도로 · 건물 · 지명) = 보이는 도엽 조각(606 · `UI_SheetTiles`) · 지형을 안 받았으면 단추를 막음 · * 옛 차례 답은 버림(475) · 도엽 선 겹(등고 · 세류 · 물 면 · 도로 · 건물 · 지명) = 보이는 도엽 조각(606 · `UI_SheetTiles`) · 지형을 안 받았으면 단추를 막음 ·
* 받기 실패 = 그 겹만 꺼짐 + 알림 · 받는 일은 페이지 서클에 셈(`track`). * 받기 실패 = 그 겹만 꺼짐 + 알림 · 받는 일은 페이지 서클에 셈(`track`).
* 그리기 차례(아래 → 위) = 위성(GL 조각만 · 옛 캔버스 = 위성 없음 · 590-2) → 음영기복 → 급경사 → 산사태위험지도 → 산림입지도 → 보호구역 → 띠 → 지적 → 물 면 → * 그리기 차례(아래 → 위) = 위성(GL 조각만 · 옛 캔버스 = 위성 없음 · 590-2) → 음영기복 → 급경사 → 산사태위험지도 → 산림입지도 → 보호구역 → 띠 → 지적 → 물 면 →
* 건물 → 등고선 → 도로 → 임도 짐작 → 올린 임도망도 → 세류 → 선 겹 → 산사태취약지역 → 지명 → 그은 선. * 건물 → 등고선 → 도로 → 임도 짐작 → 올린 임도망도 → 세류 → 선 겹 → 산사태취약지역 → 지명 → 그은 선.
* 겹 모두 = 바탕 캔버스(`base` · 582) · 등고선 = 줌 단계 색인 · 글 폭 기억. * 겹 모두 = 바탕 캔버스(`base` · 582) · 등고선 = 줌 단계 색인 · 글 폭 기억.
* 범례 = 켠 겹의 받은 범위 안 수 · 넓이(왼쪽 아래 · `D03_PlanAnalysis_UI_LayerLegend`). * 범례 = 켠 겹의 받은 범위 안 수 · 넓이(왼쪽 아래 · 칸 = 공용 `map_legend` · 줄 = `D03_PlanAnalysis_UI_LayerLegend`).
* 급경사 = 서버 값 그림을 고른 범위대로 칠함 · 범례 ⚙ → 범위 모달(603 · `D03_PlanAnalysis_UI_SlopeBands`). * 급경사 = 서버 값 그림을 고른 범위대로 칠함 · 범례 ⚙ → 범위 모달(603 · `D03_PlanAnalysis_UI_SlopeBands`).
* GL 바탕(586-2 · 처음 = 켬 · 끝 단추 끔 = 옛 캔버스 · `aislo:pref:d03.canvas`) — 겹 스물 · 등고 글 · 지명을 * GL 바탕(586-2 · 처음 = 켬 · 끝 단추 끔 = 옛 캔버스 · `aislo:pref:d03.canvas`) — 겹 스물 · 등고 글 · 지명을
* MapLibre 가 그림(`D03_PlanAnalysis_UI_MapGl`) · 캔버스 바탕 겹은 쉼(등고 간격 셈만) · 받기 · 범례는 같음 · * MapLibre 가 그림(`D03_PlanAnalysis_UI_MapGl`) · 캔버스 바탕 겹은 쉼(등고 간격 셈만) · 받기 · 범례는 같음 ·
* WebGL 못 씀 · 잃음 = 저절로 옛 캔버스 + 알림(취향은 안 바꿈). * WebGL 못 씀 · 잃음 = 저절로 옛 캔버스 + 알림(취향은 안 바꿈).
* 받은 산림 · 환경 자료(622-2) = 국유림 임도 · 경제림 · 생태자연도(조각만 · 받기 없음) · 산림입지도 · 산사태위험(GL = 조각 · * 받은 산림 · 환경 자료(622-2) = 국유림 임도 · 경제림 · 생태자연도(조각만 · 받기 없음) · 산림입지도 · 산사태위험(GL = 조각 ·
* 옛 캔버스 = 지금 받기 그대로) — 꼴 · 범례 = `D03_PlanAnalysis_UI_ForestLayers`. * 옛 캔버스 = 지금 받기 그대로) — 꼴 · 범례 = 공용 `A00_Common/map/map_forest_layers`.
* ========================================================================== */ * ========================================================================== */
import { showToast } from "@ui/ui_template_elements"; import { showToast } from "@ui/ui_template_elements";
import { createLegend } from "@ui/ui_template_flow_legend";
import { createIconButton } from "@ui/ui_template_button";
import { createMapTools } from "@ui/ui_template_map_tools";
import { themeColor } from "@ui/ui_template_palette"; import { themeColor } from "@ui/ui_template_palette";
import { readByKey, writeByKey } from "../A00_Common/b_page_state"; import { readByKey, writeByKey } from "../A00_Common/b_page_state";
import { soilColor, tileOnly, type TileOnlyKey } from "../A00_Common/map/map_forest_layers";
import { createMapLayerBar } from "../A00_Common/map/map_layer_bar";
import { LAYERS, LINE_LAYERS, ZONE_COLORS, type RoadPick } from "../A00_Common/map/map_layer_list";
import { createMapLegend } from "../A00_Common/map/map_legend";
import { fetchVWorldMeta, type VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { fetchVWorldMeta, type VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import { import {
createNormalizer, createNormalizer,
@@ -57,23 +58,16 @@ import {
} from "./D03_PlanAnalysis_Api"; } from "./D03_PlanAnalysis_Api";
import { import {
FOREST_ROAD_KINDS, FOREST_ROAD_KINDS,
GROUPS,
LAYERS,
LINE_LAYERS,
WATER_KEYS, WATER_KEYS,
ZONE_COLORS,
glImage, glImage,
glTagged, glTagged,
loadImage, loadImage,
readPref,
shadeImage, shadeImage,
type PlanLayerKey, type PlanLayerKey,
type RoadPick,
} from "./D03_PlanAnalysis_UI_LayerDefs"; } from "./D03_PlanAnalysis_UI_LayerDefs";
import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend"; import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend";
import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint"; import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint";
import { createGlBase, glFont, type GlImage } from "./D03_PlanAnalysis_UI_MapGl"; import { createGlBase, glFont, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
import { soilColor, tileOnly, type TileOnlyKey } from "./D03_PlanAnalysis_UI_ForestLayers";
import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight"; import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight";
import { import {
createSheetTiles, createSheetTiles,
@@ -90,7 +84,6 @@ import {
} from "./D03_PlanAnalysis_UI_SlopeBands"; } from "./D03_PlanAnalysis_UI_SlopeBands";
import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map"; import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
const PREF_KEY = "aislo:pref:d03.layers";
/** 옛 캔버스로 그림("1") — 처음 = GL. */ /** 옛 캔버스로 그림("1") — 처음 = GL. */
const CANVAS_PREF_KEY = "aislo:pref:d03.canvas"; const CANVAS_PREF_KEY = "aislo:pref:d03.canvas";
/** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */ /** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */
@@ -137,7 +130,6 @@ export function createPlanLayers(opts: {
track: <T>(job: Promise<T>) => Promise<T>; track: <T>(job: Promise<T>) => Promise<T>;
}): PlanLayers { }): PlanLayers {
const { projectId, map, track } = opts; const { projectId, map, track } = opts;
const on = readPref(PREF_KEY);
const data: Partial<Prepared> = {}; const data: Partial<Prepared> = {};
const pending = new Map<DataKey | "satellite", Promise<void>>(); const pending = new Map<DataKey | "satellite", Promise<void>>();
let meta: VWorldMeta | null = null; let meta: VWorldMeta | null = null;
@@ -152,6 +144,22 @@ export function createPlanLayers(opts: {
useGl(false); useGl(false);
}); });
let glOn = readByKey(CANVAS_PREF_KEY) !== "1"; let glOn = readByKey(CANVAS_PREF_KEY) !== "1";
/** 겹 줄 · 켬끔 · 취향 = 공용 겹 목록(624-2 · 페이지 표 「d03」). */
const layerBar = createMapLayerBar({
page: "d03",
glOn,
onToggle: (key) => {
void ensure(key);
sync();
map.redrawBase();
},
onGl: () => {
writeByKey(CANVAS_PREF_KEY, glOn ? "1" : "0");
useGl(!glOn);
if (!glOn) showToast(NO_SATELLITE_NOTE, "info");
},
});
const { on, toggle } = layerBar;
/** 도엽 조각(606) — 조각이 오면 그 겹만 합친 자료로 다시 얹음. */ /** 도엽 조각(606) — 조각이 오면 그 겹만 합친 자료로 다시 얹음. */
const tiles = createSheetTiles({ const tiles = createSheetTiles({
projectId, projectId,
@@ -166,78 +174,34 @@ export function createPlanLayers(opts: {
}, },
}); });
const buttons = new Map<PlanLayerKey, HTMLButtonElement>();
const groups = GROUPS.map((group) =>
group.map((def) => {
const button = createIconButton({
icon: def.icon,
title: def.title,
size: 32,
pressed: on[def.key],
onClick: () => toggle(def.key),
});
button.dataset.layer = def.key;
buttons.set(def.key, button);
return button;
}),
);
const glButton = createIconButton({
icon: "gauge",
title: "그래픽 카드 그림(바탕 겹 · 글을 GPU 로 · 끔 = 옛 캔버스 그림)",
size: 32,
pressed: glOn,
onClick: () => {
writeByKey(CANVAS_PREF_KEY, glOn ? "1" : "0");
useGl(!glOn);
if (!glOn) showToast(NO_SATELLITE_NOTE, "info");
},
});
glButton.dataset.layer = "gl";
/** GL ↔ 옛 캔버스 — 못 켜면 캔버스(알림은 `onFail`). */ /** GL ↔ 옛 캔버스 — 못 켜면 캔버스(알림은 `onFail`). */
function useGl(want: boolean): void { function useGl(want: boolean): void {
glOn = want && map.setGl(gl); glOn = want && map.setGl(gl);
if (!glOn) map.setGl(null); if (!glOn) map.setGl(null);
glButton.setAttribute("aria-pressed", String(glOn));
if (!glOn) void Promise.all(LAYERS.map((def) => ensure(def.key))); // 산사태위험 한 장(622-2) if (!glOn) void Promise.all(LAYERS.map((def) => ensure(def.key))); // 산사태위험 한 장(622-2)
sync(); sync();
map.redrawBase(); map.redrawBase();
} }
groups.push([glButton]); const bar = layerBar.root;
const tools = createMapTools({ label: "지도 겹", groups });
const bar = tools.root;
bar.classList.add("d03-plan__layers"); bar.classList.add("d03-plan__layers");
// 571-1 — 오른쪽 평면 도구줄이 이 줄 오른쪽부터(CSS `--d03-layers-w`) // 571-1 — 오른쪽 평면 도구줄이 이 줄 오른쪽부터(CSS `--d03-layers-w`)
new ResizeObserver(() => new ResizeObserver(() =>
map.root.style.setProperty("--d03-layers-w", `${tools.bar.offsetWidth}px`), map.root.style.setProperty("--d03-layers-w", `${layerBar.bar.offsetWidth}px`),
).observe(tools.bar); ).observe(layerBar.bar);
const legend = document.createElement("div"); const legendBox = createMapLegend("d03-plan__legend");
legend.className = "d03-plan__legend"; const legend = legendBox.root;
legend.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이";
/** 받음 — 위성 · 산림 조각 = GL 조각(받을 것 없음 · 옛 캔버스 = 안 그림) · 산사태위험 = GL 이면 조각(622-2). */ /** 받음 — 위성 · 산림 조각 = GL 조각(받을 것 없음 · 옛 캔버스 = 안 그림) · 산사태위험 = GL 이면 조각(622-2). */
const loadedOf = (key: PlanLayerKey): boolean => const loadedOf = (key: PlanLayerKey): boolean =>
tileOnly(key) || (glOn && key === "landslide") || !!data[dataKey(key)]; tileOnly(key) || (glOn && key === "landslide") || !!data[dataKey(key)];
function sync(): void { function sync(): void {
for (const def of LAYERS) { layerBar.sync({ ready: !!meta, glOn });
const button = buttons.get(def.key)!;
button.setAttribute("aria-pressed", String(on[def.key]));
button.disabled = !meta;
const title = !meta
? `${def.title} — 지형 받기 뒤 보임`
: tileOnly(def.key) && !glOn
? `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`
: def.title;
button.title = title;
button.setAttribute("aria-label", title);
}
for (const def of LAYERS) gl.setVisible(def.key, on[def.key]); for (const def of LAYERS) gl.setVisible(def.key, on[def.key]);
gl.setData("hillshade", hillshade && (on.satellite ? hillshade.shade : hillshade.plain)); gl.setData("hillshade", hillshade && (on.satellite ? hillshade.shade : hillshade.plain));
const rows = meta ? legendRows(on, data) : [];
const light = meta && glOn && on.satellite; // 옛 캔버스 = 위성 없음 const light = meta && glOn && on.satellite; // 옛 캔버스 = 위성 없음
legend.replaceChildren(...rows.map((row) => createLegend(row)), ...(light ? [lightRow] : [])); legendBox.show(meta ? legendRows(on, data) : [], light ? [lightRow] : []);
if (meta && on.slope && data.slope) legend.firstElementChild?.append(gear); if (meta && on.slope && data.slope) legend.firstElementChild?.append(gear);
legend.hidden = !rows.length && !light;
} }
/** 위성 밝기(요구 475) — 위성 켠 동안 범례 맨 아래 한 줄 · 바로 반영. */ /** 위성 밝기(요구 475) — 위성 켠 동안 범례 맨 아래 한 줄 · 바로 반영. */
@@ -279,14 +243,6 @@ export function createPlanLayers(opts: {
sync(); sync();
useGl(glOn); useGl(glOn);
function toggle(key: PlanLayerKey): void {
on[key] = !on[key];
writeByKey(PREF_KEY, JSON.stringify(on));
void ensure(key);
sync();
map.redrawBase();
}
/** 켜진 겹의 자료를 받음(한 번) — 실패 = 그 겹만 끔(기억은 안 바꿈 · 다음에 다시 받음) + 알림. */ /** 켜진 겹의 자료를 받음(한 번) — 실패 = 그 겹만 끔(기억은 안 바꿈 · 다음에 다시 받음) + 알림. */
function ensure(key: PlanLayerKey): Promise<void> { function ensure(key: PlanLayerKey): Promise<void> {
if (!meta || !on[key] || loadedOf(key)) return Promise.resolve(); if (!meta || !on[key] || loadedOf(key)) return Promise.resolve();
@@ -366,7 +322,7 @@ export function createPlanLayers(opts: {
gl.setRoadPick(null); gl.setRoadPick(null);
} }
on.forestRoadFile = !!info; on.forestRoadFile = !!info;
writeByKey(PREF_KEY, JSON.stringify(on)); layerBar.save();
} }
async function deleteRoad(bundle: string): Promise<boolean> { async function deleteRoad(bundle: string): Promise<boolean> {
+16 -324
View File
@@ -1,47 +1,37 @@
/* ============================================================================= /* =============================================================================
* D03_PlanAnalysis_UI_MapGl.ts * D03_PlanAnalysis_UI_MapGl.ts
* D03 GL 바탕(PLAN 586-1 · 586-2 · 요구 445 B · 448) — 바탕 겹 스물을 MapLibre(WebGL · BSD-3)로: * D03 GL 바탕(PLAN 586-1 · 586-2 · 624-2 · 요구 445 B · 448) — 바탕 겹 스물을 MapLibre(WebGL · BSD-3)로.
* 위성 조각(서버 대리 · 서버 기억 z6~19) · 그림 겹(음영 · 급경사 · 산사태위험 = image) · * 겹 꼴 · 차례 = 공용 `A00_Common/map/map_layer_specs`(624-2 · B05 평면과 같은 꼴) · 여기는 D03 지도 세우기만:
* 면 겹(산림입지 · 보호구역 · 띠 · 물 면 · 건물 = fill) · 선 겹(지적 · 등고선 · 도로 · 임도 짐작 · 올린 임도망도 · * 고정 소스(위성 조각 z6~19 · 등고선 615 줌별 벡터 조각 · 산림 조각 다섯 622-2) · 받는 동안 서클 · 임도망 고름 · 위성 밝기.
* 세류 · 단층 · 지질구조선 · 0차곡 · 산사태취약 필지 = line · 점선 포함) · 점(산사태취약 · 지명 = circle) · * 글 = 화면 Noto Sans KR(OFL · 자체 보관)을 그 자리에서 그림 — 꼴에 글꼴 주소가 없으면 MapLibre 가 모든 글자를 화면 글꼴로.
* 글(등고 수치 · 지명 = symbol · 겹치면 뺌 · 글꼴 = 화면 Noto Sans KR(OFL · 자체 보관)을 그 자리에서 그림 —
* 꼴에 글꼴 주소가 없으면 MapLibre 가 모든 글자를 화면 글꼴로 그림 · 글꼴 조각 파일 없음).
* 그은 선 · 재기 · 고르기 · 누름 = 위 캔버스 그대로(위에 겹침). * 그은 선 · 재기 · 고르기 · 누름 = 위 캔버스 그대로(위에 겹침).
* 카메라 주인 = D03 화면(`ViewState` · 돌림) 하나 — 여기는 따라가기만 · 조작은 안 받음. * 카메라 주인 = D03 화면(`ViewState` · 돌림) 하나 — 여기는 따라가기만 · 조작은 안 받음.
* 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 색 = 받는 쪽이 지물에 붙인 `_color`. * 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 색 = 받는 쪽이 지물에 붙인 `_color`.
* 등고선 = 줌별 벡터 조각(615 · 위성처럼 고정 소스 · 가는 선 = 모두 · 굵은 선 · 글 = `k` 1) · 받는 동안 서클.
* 받은 산림 · 환경 자료(622-2) = 같은 꼴 고정 소스 다섯(`D03_PlanAnalysis_UI_ForestLayers`) · 받는 동안 서클.
* 그래픽 카드 · WebGL 이 없거나 잃으면 `onFail` — 부르는 쪽이 옛 캔버스로. * 그래픽 카드 · WebGL 이 없거나 잃으면 `onFail` — 부르는 쪽이 옛 캔버스로.
* 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2) · 여기는 겹 꼴만. * 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2).
* ========================================================================== */ * ========================================================================== */
import type { ExpressionSpecification, FilterSpecification, Map as MapLibreMap } from "maplibre-gl"; import type { Map as MapLibreMap } from "maplibre-gl";
import { import {
contourTileSource, contourTileSource,
createGlMap, createGlMap,
GL_FONT, GL_FONT,
satelliteTileSource, satelliteTileSource,
type GlImage, type GlImage,
type GlSpec as GlMapSpec,
} from "@ui/ui_template_gl_map"; } from "@ui/ui_template_gl_map";
import { themeColor } from "@ui/ui_template_palette";
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs"; import { forestSources, TRACKED_SOURCES } from "../A00_Common/map/map_forest_layers";
import { import {
forestAreaSpecs, MAP_THEME_NAMES,
forestRasterSpecs, mapLayerSpecs,
forestRoadSpecs, pickFilter,
forestSources, type GlSource,
TRACKED_SOURCES, } from "../A00_Common/map/map_layer_specs";
} from "./D03_PlanAnalysis_UI_ForestLayers"; import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs";
import { SAT_LIGHT_DEFAULT } from "./D03_PlanAnalysis_UI_SatLight"; import { SAT_LIGHT_DEFAULT } from "./D03_PlanAnalysis_UI_SatLight";
export { GL_FONT, type GlImage }; export type { GlImage };
/** 자료 소스(임도 짐작 = 도로 소스를 거름 · 위성 = 조각). */
export type GlSource = Exclude<PlanLayerKey, "satellite" | "forestRoad">;
const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT];
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */ /** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
export const glFont = (weight: string, text: string): Promise<unknown> => export const glFont = (weight: string, text: string): Promise<unknown> =>
document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null); document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null);
@@ -62,304 +52,6 @@ export interface GlBase {
resize: () => void; resize: () => void;
} }
type GlSpec = GlMapSpec<PlanLayerKey>;
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: PlanLayerKey,
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: PlanLayerKey,
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_PlanAnalysis_UI_Layers` 의 색 · 굵기 · 투명도 · 차례 그대로. */
function layerSpecs(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: "faults" | "lineaments" | "zeroOrder", width: number, pattern: number[]) =>
haloLine(key, key, themeColor(LINE_COLOR[key], "#f43f5e"), width, pattern);
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,
...lineKind("faults", 2.4, []),
...lineKind("lineaments", 1.8, [8, 4]),
...lineKind("zeroOrder", 1.8, [4, 3]),
// 산사태취약 — 필지 = 「이 필지 안 어딘가」 테두리만 · 점 = 지정 자리
...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,
];
}
/** 고른 묶음 · 선만 거름(고름 없음 = 아무것도 안 맞는 거름). */
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;
}
/** 선 겹 색 값(캔버스 `LINE_LAYERS` 와 같음). */
const LINE_COLOR = {
faults: "--draw-map-fault",
lineaments: "--draw-map-lineament",
zeroOrder: "--draw-map-zero-order",
} as const;
/** 겹 색 지문 — 밝음 · 어두움이 바뀌면 색을 다시. */
const THEME_NAMES = [
"--draw-map-contour",
"--draw-map-road",
"--draw-map-stream",
"--draw-map-parcel",
] as const;
/** 위성 채도(−1~1) — 조금 연하게. */
const SAT_SATURATION = -0.3;
/** `track` = 받는 일을 진행 서클에 셈(등고 · 산림 조각 받는 동안). */ /** `track` = 받는 일을 진행 서클에 셈(등고 · 산림 조각 받는 동안). */
export function createGlBase( export function createGlBase(
projectId: string, projectId: string,
@@ -369,13 +61,13 @@ export function createGlBase(
let light = SAT_LIGHT_DEFAULT; let light = SAT_LIGHT_DEFAULT;
let pick: RoadPick | null = null; let pick: RoadPick | null = null;
const base = createGlMap<PlanLayerKey>({ const base = createGlMap<PlanLayerKey>({
specs: () => layerSpecs(light, pick), specs: () => mapLayerSpecs(light, pick),
fixed: { fixed: {
satellite: satelliteTileSource(projectId), satellite: satelliteTileSource(projectId),
contour: contourTileSource(projectId), contour: contourTileSource(projectId),
...forestSources(projectId), ...forestSources(projectId),
}, },
themeNames: THEME_NAMES, themeNames: MAP_THEME_NAMES,
className: "d03-plan__gl", className: "d03-plan__gl",
onFail, onFail,
}); });
+8 -4
View File
@@ -58,10 +58,11 @@ def test_d03_parts():
paint = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts") # 580 — 그림 나눔 paint = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts") # 580 — 그림 나눔
assert "map.stations" in paint and "map.slopes" in paint assert "map.stations" in paint and "map.slopes" in paint
# 569-2 · 581-1 · 581-2 — 바탕 겹 단추(공용 지도 도구줄 한 줄 · 묶음 셋) · 처음 켬 셋 · 취향 기억 # 569-2 · 581-1 · 581-2 — 바탕 겹 단추(공용 지도 도구줄 한 줄 · 묶음 셋) · 처음 켬 셋 · 취향 기억
defs = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_LayerDefs.ts") defs = _src("A00_Common/map/map_layer_list.ts") # 624-2 공용 겹 목록
assert "createMapTools({" in layers and "createIconButton({" in layers bar = _src("A00_Common/map/map_layer_bar.ts")
assert "createToolbar(" not in layers assert "createMapTools({" in bar and "createIconButton({" in bar and "createMapLayerBar({" in layers
assert '"aislo:pref:d03.layers"' in layers and "writeByKey(PREF_KEY" in layers assert "createToolbar(" not in layers and "createToolbar(" not in bar
assert '"aislo:pref:d03.layers"' in defs and "writeByKey(MAP_LAYER_PAGES[page].pref" in bar
keys = re.findall(r'key: "(\w+)",\s+icon: "\w+",\s+title:\s+"[^"]+",\s+on: (true|false)', defs) keys = re.findall(r'key: "(\w+)",\s+icon: "\w+",\s+title:\s+"[^"]+",\s+on: (true|false)', defs)
assert keys == [ assert keys == [
("satellite", "true"), ("satellite", "true"),
@@ -73,9 +74,12 @@ def test_d03_parts():
("road", "false"), ("road", "false"),
("forestRoad", "false"), ("forestRoad", "false"),
("forestRoadFile", "false"), ("forestRoadFile", "false"),
("nationalRoad", "false"),
("cadastral", "false"), ("cadastral", "false"),
("zones", "false"), ("zones", "false"),
("soils", "false"), ("soils", "false"),
("ecoForest", "false"),
("ecology", "false"),
("landslide", "false"), ("landslide", "false"),
("landslideZones", "false"), ("landslideZones", "false"),
("faults", "false"), ("faults", "false"),
+1 -1
View File
@@ -85,7 +85,7 @@ def test_soils_fail_raises(tmp_path, monkeypatch) -> None:
def test_screen_def_and_colors() -> None: def test_screen_def_and_colors() -> None:
defs = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8") defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
entry = re.search(r'key: "soils",\s*icon: "(\w+)",[^}]*on: (\w+)', defs) entry = re.search(r'key: "soils",\s*icon: "(\w+)",[^}]*on: (\w+)', defs)
assert entry and entry.group(2) == "false" # 처음 값 끔 assert entry and entry.group(2) == "false" # 처음 값 끔
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8") icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
@@ -191,7 +191,7 @@ def test_zero_order_no_dem(tmp_path) -> None:
def test_screen_defs_icons_colors() -> None: def test_screen_defs_icons_colors() -> None:
defs = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8") defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8") icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
for key, color in ( for key, color in (
@@ -324,7 +324,7 @@ def test_zone_layer_saved_and_no_cadastral(tmp_path, monkeypatch) -> None:
def test_screen_defs() -> None: def test_screen_defs() -> None:
folder = ROOT / "D03_PlanAnalysis" folder = ROOT / "D03_PlanAnalysis"
defs = (folder / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8") defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8") icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
paint = (folder / "D03_PlanAnalysis_UI_LayerPaint.ts").read_text( paint = (folder / "D03_PlanAnalysis_UI_LayerPaint.ts").read_text(
+1 -1
View File
@@ -9,7 +9,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
read = lambda rel: (ROOT / rel).read_text(encoding="utf-8") # noqa: E731 read = lambda rel: (ROOT / rel).read_text(encoding="utf-8") # noqa: E731
MAPGL = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts") MAPGL = read("A00_Common/map/map_layer_specs.ts") # 624-2 GL 겹 꼴 = 공용
LIGHT = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts") LIGHT = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts")
LAYERS = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts") LAYERS = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")
+2 -1
View File
@@ -94,4 +94,5 @@ def test_screen_uses_fixed_vector_source() -> None:
assert "/plan/contour/{z}/{x}/{y}.pbf" in gl assert "/plan/contour/{z}/{x}/{y}.pbf" in gl
base = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_MapGl.ts").read_text(encoding="utf-8") base = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_MapGl.ts").read_text(encoding="utf-8")
assert "contour: contourTileSource(projectId)" in base assert "contour: contourTileSource(projectId)" in base
assert base.count('"source-layer": "contour"') == 3 specs = (ROOT / "A00_Common" / "map" / "map_layer_specs.ts").read_text(encoding="utf-8")
assert specs.count('"source-layer": "contour"') == 3 # 624-2 GL 겹 꼴 = 공용
+4 -3
View File
@@ -8,6 +8,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
D03 = ROOT / "D03_PlanAnalysis" D03 = ROOT / "D03_PlanAnalysis"
MAP = ROOT / "A00_Common" / "map" # 624-2 공용 지도 부품
def _read(path: Path) -> str: def _read(path: Path) -> str:
@@ -18,7 +19,7 @@ def test_fixed_sources_follow_plan_622_routes() -> None:
gl = _read(ROOT / "ui_template" / "ui_template_gl_map.ts") gl = _read(ROOT / "ui_template" / "ui_template_gl_map.ts")
assert "export function tileSource" in gl assert "export function tileSource" in gl
assert "/plan/${path}/{z}/{x}/{y}.${ext}" in gl assert "/plan/${path}/{z}/{x}/{y}.${ext}" in gl
forest = _read(D03 / "D03_PlanAnalysis_UI_ForestLayers.ts") forest = _read(MAP / "map_forest_layers.ts")
for name in ("forest_road", "forest_soil", "eco_forest"): for name in ("forest_road", "forest_soil", "eco_forest"):
assert f'vt("{name}")' in forest assert f'vt("{name}")' in forest
assert f'...vector("{name}")' in forest or f'"source-layer": "{name}"' in forest assert f'...vector("{name}")' in forest or f'"source-layer": "{name}"' in forest
@@ -33,7 +34,7 @@ def test_fixed_sources_follow_plan_622_routes() -> None:
def test_layer_list_five_off_by_default() -> None: def test_layer_list_five_off_by_default() -> None:
defs = _read(D03 / "D03_PlanAnalysis_UI_LayerDefs.ts") defs = _read(MAP / "map_layer_list.ts")
icons = _read(ROOT / "ui_template" / "ui_template_icons.ts") icons = _read(ROOT / "ui_template" / "ui_template_icons.ts")
for key in ("nationalRoad", "soils", "ecoForest", "ecology", "landslide"): for key in ("nationalRoad", "soils", "ecoForest", "ecology", "landslide"):
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs) entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
@@ -43,7 +44,7 @@ def test_layer_list_five_off_by_default() -> None:
def test_colors_are_draw_map_tokens() -> None: def test_colors_are_draw_map_tokens() -> None:
theme = _read(ROOT / "ui_template" / "ui_template_theme.css") theme = _read(ROOT / "ui_template" / "ui_template_theme.css")
forest = _read(D03 / "D03_PlanAnalysis_UI_ForestLayers.ts") forest = _read(MAP / "map_forest_layers.ts")
for token in set(re.findall(r'"(--draw-map-[\w-]+)"', forest)): for token in set(re.findall(r'"(--draw-map-[\w-]+)"', forest)):
assert f"{token}:" in theme, token assert f"{token}:" in theme, token
assert all(f"--draw-map-soil-{n}:" in theme for n in range(1, 7)) assert all(f"--draw-map-soil-{n}:" in theme for n in range(1, 7))