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>
This commit is contained in:
@@ -1,27 +1,33 @@
|
||||
/* =============================================================================
|
||||
* map_layer_bar.ts
|
||||
* 공용 지도 겹 줄(PLAN 569-2 · 586-2 · 624-2 · 요구 530) — 겹 단추 한 줄(공용 지도 도구줄 `createMapTools` · 묶음 사이 나눔선) +
|
||||
* 끝에 그래픽 카드 단추(GL ↔ 옛 캔버스).
|
||||
* 보일 겹 · 처음 켬 · 취향 열쇠 = 페이지 표(`map_layer_list` `MAP_LAYER_PAGES`) · 누르면 켬끔 뒤집고 취향에 적고 `onToggle`.
|
||||
* 막힘 = 지형을 아직 안 받음(`sync({ ready: false })`) · 조각만 쓰는 겹은 옛 캔버스에서 「안 보임」 글.
|
||||
* 공용 지도 겹 줄(PLAN 569-2 · 586-2 · 624-2 · 634-2 · 요구 530 · 546) — 겹 단추 한 줄(공용 지도 도구줄 `createMapTools`):
|
||||
* 단독 단추(표 묶음 없음) | 묶음 단추(여럿 고르기 메뉴 · 켠 수 배지 · 골라도 안 닫힘) | 그래픽 카드 단추(GL ↔ 옛 캔버스).
|
||||
* 보일 겹 · 묶음 · 호버제목 · 처음 켬 · 취향 열쇠 = 표(`map_layer_list`) · 누르면 켬끔 뒤집고 취향에 적고 `onToggle`.
|
||||
* 막힘 = 지형을 아직 안 받음(`ready: false`) · 조각만 쓰는 겹은 옛 캔버스에서 · 페이지 까닭(`why`) — 「제목 — 까닭」.
|
||||
* 받기 · 그리기 · 범례는 페이지 몫 — 켬끔 값(`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 { createPickMenu, type PickMenu } from "@ui/ui_template_context_menu";
|
||||
import { createMapTools, mapToolTip } from "@ui/ui_template_map_tools";
|
||||
import { writeByKey } from "../b_page_state";
|
||||
import { tileOnly } from "./map_forest_layers";
|
||||
import {
|
||||
LAYER_GROUPS,
|
||||
MAP_LAYER_PAGES,
|
||||
pageGroups,
|
||||
pageLayers,
|
||||
readLayerPref,
|
||||
type LayerGroup,
|
||||
type MapLayerKey,
|
||||
type MapPage,
|
||||
} from "./map_layer_list";
|
||||
|
||||
/** 그래픽 카드 단추 호버(표 밖 · 그룹1 끝 단독). */
|
||||
const GL_TITLE = "그래픽 카드";
|
||||
|
||||
export interface MapLayerBar {
|
||||
/** 도구줄 감싼 것(자리는 페이지 CSS). */
|
||||
/** 도구줄 감싼 것(자리는 페이지 CSS · 한 행 = `createMapToolRow`). */
|
||||
root: HTMLElement;
|
||||
/** 도구줄 자체(폭 재기). */
|
||||
bar: HTMLElement;
|
||||
@@ -30,8 +36,12 @@ export interface MapLayerBar {
|
||||
/** 뒤집고 적고 `onToggle`. */
|
||||
toggle: (key: MapLayerKey) => void;
|
||||
save: () => void;
|
||||
/** 단추 눌림 · 막힘 · 글을 켬끔 값에 맞춤. */
|
||||
sync: (state: { ready: boolean; glOn: boolean }) => void;
|
||||
/** 단추 · 메뉴 줄의 눌림 · 막힘 · 글을 켬끔 값에 맞춤 · `why` = 페이지 막힘 까닭(겹마다). */
|
||||
sync: (state: {
|
||||
ready: boolean;
|
||||
glOn: boolean;
|
||||
why?: Partial<Record<MapLayerKey, string>>;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function createMapLayerBar(opts: {
|
||||
@@ -49,10 +59,12 @@ export function createMapLayerBar(opts: {
|
||||
save();
|
||||
opts.onToggle(key);
|
||||
};
|
||||
const defs = pageGroups(page);
|
||||
const defs = pageLayers(page);
|
||||
const buttons = new Map<MapLayerKey, HTMLButtonElement>();
|
||||
const groups = defs.map((group) =>
|
||||
group.map((def) => {
|
||||
const menus = new Map<LayerGroup, PickMenu>();
|
||||
const singles: HTMLElement[] = [];
|
||||
for (const def of defs) {
|
||||
if (!def.group) {
|
||||
const button = createIconButton({
|
||||
icon: def.icon,
|
||||
title: def.title,
|
||||
@@ -61,33 +73,58 @@ export function createMapLayerBar(opts: {
|
||||
onClick: () => toggle(def.key),
|
||||
});
|
||||
button.dataset.layer = def.key;
|
||||
button.dataset.tip = def.title;
|
||||
buttons.set(def.key, button);
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
singles.push(button);
|
||||
continue;
|
||||
}
|
||||
if (menus.has(def.group)) continue;
|
||||
const group = def.group;
|
||||
const menu = createPickMenu({
|
||||
multi: true,
|
||||
icon: LAYER_GROUPS[group].icon,
|
||||
title: LAYER_GROUPS[group].name,
|
||||
items: defs
|
||||
.filter((one) => one.group === group)
|
||||
.map((one) => ({ value: one.key, label: one.title, icon: one.icon })),
|
||||
onPick: (key) => key && toggle(key as MapLayerKey),
|
||||
});
|
||||
menu.root.dataset.layerGroup = group;
|
||||
menus.set(group, menu);
|
||||
}
|
||||
for (const def of defs)
|
||||
if (def.group) menus.get(def.group)!.setItem(def.key, { checked: on[def.key] });
|
||||
const glButton = createIconButton({
|
||||
icon: "gauge",
|
||||
title: "그래픽 카드 그림(바탕 겹 · 글을 GPU 로 · 끔 = 옛 캔버스 그림)",
|
||||
title: GL_TITLE,
|
||||
size: 32,
|
||||
pressed: opts.glOn,
|
||||
onClick: opts.onGl,
|
||||
});
|
||||
glButton.dataset.layer = "gl";
|
||||
groups.push([glButton]);
|
||||
const tools = createMapTools({ label: "지도 겹", groups });
|
||||
const tools = createMapTools({
|
||||
label: "지도 겹",
|
||||
groups: [singles, [...menus.values()].map((menu) => menu.root), [glButton]],
|
||||
});
|
||||
|
||||
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);
|
||||
function sync({ ready, glOn, why = {} }: Parameters<MapLayerBar["sync"]>[0]): void {
|
||||
for (const def of defs) {
|
||||
const reason = !ready
|
||||
? "지형 받기 뒤 보임"
|
||||
: (why[def.key] ??
|
||||
(tileOnly(def.key) && !glOn ? "옛 그림(그래픽 카드 끔)엔 안 보임" : null));
|
||||
const button = buttons.get(def.key);
|
||||
if (button) {
|
||||
button.setAttribute("aria-pressed", String(on[def.key]));
|
||||
button.disabled = !!reason;
|
||||
mapToolTip(button, reason);
|
||||
continue;
|
||||
}
|
||||
menus.get(def.group!)!.setItem(def.key, {
|
||||
checked: on[def.key],
|
||||
disabled: !!reason,
|
||||
title: reason ? `${def.title} — ${reason}` : def.title,
|
||||
});
|
||||
}
|
||||
glButton.setAttribute("aria-pressed", String(glOn));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { GlImage } from "@ui/ui_template_gl_map";
|
||||
import type { GeoJsonCollection, PlanRaster } from "./map_layer_api";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { GeoJsonCollection, PlanBand, PlanRaster } from "./map_layer_api";
|
||||
|
||||
/** 도로 중 소로 · 미분류 구분(옛 임도 짐작 · 겹 걷음 · 페이지 자료 셈만 씀). */
|
||||
export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]);
|
||||
@@ -37,6 +38,20 @@ export function glTagged(
|
||||
};
|
||||
}
|
||||
|
||||
/** 띠 GL 자료(581-1 · 634-2) — 도로 300 m 먼저 · 계류 100 m 위 · `_band` = 겹 가름(계류 = `bands` · 도로 = `roadBands`). */
|
||||
export function bandCollection(info: { stream: PlanBand; road: PlanBand }): GeoJsonCollection {
|
||||
const band = (name: "stream" | "road", fallback: string): GeoJsonCollection =>
|
||||
glTagged(info[name].collection, () => themeColor(`--draw-map-band-${name}`, fallback), {
|
||||
_band: name,
|
||||
});
|
||||
return {
|
||||
features: [
|
||||
...(band("road", "#facc15").features ?? []),
|
||||
...(band("stream", "#22d3ee").features ?? []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** GL 그림 자리 — 서버 경위도 귀퉁이(왼쪽 아래부터 돎) → 왼쪽 위부터 · 없으면 null. */
|
||||
export function glImage(raster: PlanRaster, url = raster.image): GlImage | null {
|
||||
const box = raster.box;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* =============================================================================
|
||||
* map_layer_list.ts
|
||||
* 공용 지도 겹 목록(PLAN 624-2 · 요구 530 · 결정 A② · A⑧ · A⑨) — 예정노선(D03) · B05 평면이 같이 씀.
|
||||
* 겹 = 열쇠 · 아이콘 · 이름 · 처음 값(`on` = 예정노선) · 단추 묶음(바탕 · 물 · 길 · 구역 · 사이 나눔선).
|
||||
* 페이지 표(`MAP_LAYER_PAGES`) = 보일 겹(숨길 겹) · 처음 켬(없으면 겹의 `on`) · 켬끔 취향 열쇠.
|
||||
* 겹 = 요구 546 그룹1 표 한 줄 — 열쇠 · 아이콘 · 호버제목 · 아이콘 묶음(`LAYER_GROUPS` · 없음 = 단독) · 보일 페이지 · 처음 값(`on` = 예정노선).
|
||||
* 페이지 표(`MAP_LAYER_PAGES`) = 처음 켬(없으면 겹의 `on`) · 켬끔 취향 열쇠.
|
||||
* 선 겹 꼴 · 보호구역 색 · 산사태위험 범례 = GL 꼴(`map_layer_specs`) · 범례(`map_legend`) · 옛 캔버스가 같이 읽음.
|
||||
* 쓰는 법 = 겹 줄 `createMapLayerBar({ page })`(`map_layer_bar`) · 새 겹 = `GROUPS` 한 줄 + 꼴 한 벌.
|
||||
* 쓰는 법 = 겹 줄 `createMapLayerBar({ page })`(`map_layer_bar`) · 새 겹 = `LAYERS` 한 줄 + 꼴 한 벌.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { IconName } from "@ui/ui_template_icons";
|
||||
@@ -32,7 +32,8 @@ export type MapLayerKey =
|
||||
| "faults"
|
||||
| "lineaments"
|
||||
| "zeroOrder"
|
||||
| "bands"
|
||||
| "bands" // 계류 100 m 띠(자료 = 띠 둘 한 벌 · `_band` 로 가름)
|
||||
| "roadBands" // 도로 300 m 띠(같은 자료 · 「도로」 묶음)
|
||||
| "building"
|
||||
| "place";
|
||||
|
||||
@@ -42,130 +43,81 @@ export interface RoadPick {
|
||||
line?: number;
|
||||
}
|
||||
|
||||
/** 지도가 있는 페이지. */
|
||||
export type MapPage = "d03" | "b05";
|
||||
|
||||
/** 아이콘 묶음(요구 546 표 「아이콘 그룹명」) — 묶음 단추 = 아이콘 + ▾ + 켠 수 · 여럿 고르기 메뉴. */
|
||||
export type LayerGroup = "water" | "road" | "land" | "limit";
|
||||
export const LAYER_GROUPS: Record<LayerGroup, { name: string; icon: IconName }> = {
|
||||
water: { name: "수계망도", icon: "streams" },
|
||||
road: { name: "도로", icon: "signpost" },
|
||||
land: { name: "토지·건물", icon: "parcelHouse" },
|
||||
limit: { name: "제한·재해", icon: "ban" },
|
||||
};
|
||||
|
||||
export interface LayerDef {
|
||||
key: MapLayerKey;
|
||||
icon: IconName;
|
||||
/** 호버제목(요구 546 표) — 마우스 글 · 메뉴 줄 글. */
|
||||
title: string;
|
||||
/** 아이콘 묶음 — null = 단독 단추. */
|
||||
group: LayerGroup | null;
|
||||
/** 보일 페이지(표 「사용」). */
|
||||
pages: readonly MapPage[];
|
||||
/** 예정노선 처음 값 — 다른 페이지는 `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: "nationalRoad",
|
||||
icon: "route",
|
||||
title:
|
||||
"임도망도(산림청 전국 국가임도 · 간선 · 작업 · 산불 색 · 지방 · 민간임도는 「임도(업로드)」로)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "forestRoadFile",
|
||||
icon: "map",
|
||||
title:
|
||||
"임도(업로드)(산림청 시군구 SHP 등 사용자가 올린 지방 · 민간임도 = 밝은 분홍 실선 · 왼쪽 「위치」 칸에서 올리고 묶음 고름 · 지움)",
|
||||
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 },
|
||||
],
|
||||
const BOTH: readonly MapPage[] = ["d03", "b05"];
|
||||
const row = (
|
||||
key: MapLayerKey,
|
||||
icon: IconName,
|
||||
title: string,
|
||||
group: LayerGroup | null,
|
||||
on = false,
|
||||
): LayerDef => ({ key, icon, title, group, pages: BOTH, on });
|
||||
|
||||
/** 그룹1 표(요구 546 · 차례 = 단추 차례) — 도로 300 m 띠는 표 밖 한 줄(「도로」 묶음 · 기능 남김). */
|
||||
export const LAYERS: readonly LayerDef[] = [
|
||||
row("satellite", "satellite", "위성사진", null, true),
|
||||
row("hillshade", "sun", "음영기복도", null),
|
||||
row("contour", "mountain", "등고선", null, true),
|
||||
row("slope", "angle", "급경사", null),
|
||||
row("stream", "droplet", "세류", "water", true),
|
||||
row("water", "ripple", "하천", "water"),
|
||||
row("zeroOrder", "arrowsJoin", "0차곡", "water"),
|
||||
row("bands", "borderOuter", "계류100m", "water"),
|
||||
row("road", "road", "도로", "road"),
|
||||
row("nationalRoad", "route", "임도망도", "road"),
|
||||
row("forestRoadFile", "map", "임도(업로드)", "road"),
|
||||
row("roadBands", "roadBand", "도로300m", "road"),
|
||||
row("cadastral", "borderAll", "지적도", "land"),
|
||||
row("building", "building", "건물", "land"),
|
||||
row("place", "mapPin", "지명", "land"),
|
||||
row("zones", "shield", "보호규제구역", "limit"),
|
||||
row("soils", "leaf", "산림입지도", "limit"),
|
||||
row("ecoForest", "trees", "국유림·사유림", "limit"),
|
||||
row("ecology", "plant", "생태자연도", "limit"),
|
||||
row("landslide", "alert", "산사태위험지도", "limit"),
|
||||
row("landslideZones", "alertOctagon", "산사태취약지역", "limit"),
|
||||
row("faults", "bolt", "단층", "limit"),
|
||||
row("lineaments", "lineament", "지질구조선", "limit"),
|
||||
];
|
||||
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",
|
||||
},
|
||||
/** 페이지 표 — 처음 켬(없음 = 겹의 `on`) · 켬끔 취향 열쇠. */
|
||||
export const MAP_LAYER_PAGES: Record<MapPage, { on?: readonly MapLayerKey[]; pref: string }> = {
|
||||
d03: { pref: "aislo:pref:d03.layers" },
|
||||
// A② 처음 = 지금 그대로(등고선 · 하천)
|
||||
b05: { 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 const pageLayers = (page: MapPage): LayerDef[] =>
|
||||
LAYERS.filter((def) => def.pages.includes(page));
|
||||
|
||||
/** 켬끔 취향 — 없거나 깨지면 페이지 처음 값 · 숨긴 겹 = 끔. */
|
||||
/** 켬끔 취향 — 없거나 깨지면 페이지 처음 값 · 안 보일 겹 = 끔. */
|
||||
export function readLayerPref(page: MapPage): Record<MapLayerKey, boolean> {
|
||||
const { hide, on: first, pref } = MAP_LAYER_PAGES[page];
|
||||
const { 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>;
|
||||
@@ -176,7 +128,7 @@ export function readLayerPref(page: MapPage): Record<MapLayerKey, boolean> {
|
||||
} catch {
|
||||
// 깨진 값 = 처음 값
|
||||
}
|
||||
for (const key of hide) on[key] = false;
|
||||
for (const def of LAYERS) if (!def.pages.includes(page)) on[def.key] = false;
|
||||
return on;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,19 +71,25 @@ function haloLine(
|
||||
}
|
||||
|
||||
/** 면 겹 — 채움 + 테두리(캔버스 `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,
|
||||
{
|
||||
@@ -91,6 +97,7 @@ function fillEdge(
|
||||
id: `${key}-edge`,
|
||||
type: "line",
|
||||
source,
|
||||
...only,
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": edge.color,
|
||||
@@ -131,11 +138,9 @@ export function mapLayerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
...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],
|
||||
}),
|
||||
// 띠 = 한 자료(`_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",
|
||||
|
||||
@@ -151,17 +151,26 @@ export function legendRows(
|
||||
: [{ label: "산림입지도 없음(받은 범위 안)", color: "transparent", off: true }],
|
||||
);
|
||||
}
|
||||
if (on.bands && data.bands) {
|
||||
// 계류 100 m 띠 · 도로 300 m 띠 = 겹 둘(634-2 · 자료 한 벌)
|
||||
if ((on.bands || on.roadBands) && data.bands) {
|
||||
const { stream, road } = data.bands.info;
|
||||
rows.push([
|
||||
{
|
||||
label: `계류 ${stream.dist_m} m 띠 ${hectare(stream.area_m2)} (${(stream.ratio * 100).toFixed(1)} %)`,
|
||||
color: "var(--draw-map-band-stream)",
|
||||
},
|
||||
{
|
||||
label: `도로 ${road.dist_m} m 띠 ${hectare(road.area_m2)} (${(road.ratio * 100).toFixed(1)} %)`,
|
||||
color: "var(--draw-map-band-road)",
|
||||
},
|
||||
...(on.bands
|
||||
? [
|
||||
{
|
||||
label: `계류 ${stream.dist_m} m 띠 ${hectare(stream.area_m2)} (${(stream.ratio * 100).toFixed(1)} %)`,
|
||||
color: "var(--draw-map-band-stream)",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(on.roadBands
|
||||
? [
|
||||
{
|
||||
label: `도로 ${road.dist_m} m 띠 ${hectare(road.area_m2)} (${(road.ratio * 100).toFixed(1)} %)`,
|
||||
color: "var(--draw-map-band-road)",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}
|
||||
const lines = LINE_KEYS.flatMap((key) => {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* =============================================================================
|
||||
* map_terrain_chip.ts
|
||||
* 공용 지도 지형 칩(PLAN 624-2 · 요구 530 · 결정 A④ · A⑤ · D2) — 도구줄 끝 분할 고르기 하나: 수치지형도 · 라이다.
|
||||
* 이름 = 예정노선 「분석 지형」 · B05 「설계 지형」(바꾸면 뒤 단계 모두 다시 셈 · 확인창은 B05 몫).
|
||||
* 없는 원천 = 막힘 + 까닭 글(D03 라이다 = D 프로젝트에 LAS 올린 뒤 · 624-4).
|
||||
* 마우스 글 = 도구줄 표 호버제목(「수치지형도/라이다」 · 634-2) · 확인창은 B05 몫.
|
||||
* 없는 원천 = 막힘 + 「제목 — 까닭」(D03 라이다 = D 프로젝트에 LAS 올린 뒤 · 624-4).
|
||||
* 화면 칩만 — 바꾸는 일(서버 원천 인자 624-1 · B05 설계 지형 바꿈 626-1)은 `onChange` 로 페이지가 붙임.
|
||||
* 쓰는 법 = `createTerrainChip({ page: "d03", value: "sheet", have: ["sheet"] })` → `{ root, set, setHave }` · root 를 도구줄 `terrain` 칸에.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import type { MapPage } from "./map_layer_list";
|
||||
import { mapToolTitle } from "./map_toolbar";
|
||||
|
||||
/** 서버 겹 길 `terrain=` 값과 같음(624-1 · 빈 값 = 설계 지형). */
|
||||
export type TerrainSource = "sheet" | "lidar";
|
||||
@@ -18,14 +19,12 @@ export const TERRAIN_NAMES: Record<TerrainSource, string> = {
|
||||
lidar: "라이다",
|
||||
};
|
||||
|
||||
const CAPTION: Record<MapPage, string> = {
|
||||
d03: "분석 지형 — 급경사 · 음영 · 0차곡 · 긋기 높이에 쓰는 지형",
|
||||
b05: "설계 지형 — 바꾸면 종단 · 횡단 · 배수 · 토공을 모두 다시 셈",
|
||||
};
|
||||
/** 막힘 까닭(마우스 글 = 「수치지형도/라이다 — 까닭」 · 요구 546). */
|
||||
const MISSING: Record<TerrainSource, string> = {
|
||||
sheet: "수치지형도 — 이 프로젝트 범위에 도엽 지형 없음",
|
||||
lidar: "라이다 — 올린 LAS 없음",
|
||||
sheet: "이 프로젝트 범위에 도엽 지형 없음",
|
||||
lidar: "올린 LAS 없음",
|
||||
};
|
||||
const TITLE = mapToolTitle("terrain");
|
||||
|
||||
export interface TerrainChip {
|
||||
root: HTMLElement;
|
||||
@@ -46,19 +45,19 @@ export function createTerrainChip(opts: {
|
||||
options: sources.map((value) => ({ value, label: TERRAIN_NAMES[value] })),
|
||||
value: opts.value,
|
||||
size: 32,
|
||||
ariaLabel: CAPTION[opts.page],
|
||||
ariaLabel: TITLE,
|
||||
onChange: (value) => opts.onChange?.(value as TerrainSource),
|
||||
});
|
||||
const root = segment.root;
|
||||
root.dataset.act = "terrain";
|
||||
root.title = CAPTION[opts.page];
|
||||
root.title = TITLE;
|
||||
const setHave = (have: readonly TerrainSource[]): void => {
|
||||
for (const value of sources) {
|
||||
const item = root.querySelector<HTMLButtonElement>(`[data-value="${value}"]`)!;
|
||||
item.disabled = !have.includes(value);
|
||||
item.title = item.disabled
|
||||
? MISSING[value]
|
||||
: `${CAPTION[opts.page]} · ${TERRAIN_NAMES[value]}`;
|
||||
// 열림 = 글 없음(칩 글 「수치지형도/라이다」 그대로) · 막힘 = 「제목 — 까닭」
|
||||
if (item.disabled) item.title = `${TITLE} — ${TERRAIN_NAMES[value]} ${MISSING[value]}`;
|
||||
else item.removeAttribute("title");
|
||||
}
|
||||
};
|
||||
setHave(opts.have);
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
/* =============================================================================
|
||||
* map_toolbar.ts
|
||||
* 공용 지도 도구줄 표(PLAN 624-2 · 요구 530 「상단 아이콘 셋 같게 · 필요 없는 것 숨김」) — 예정노선(D03) · B05 평면이 같이 씀.
|
||||
* 한 줄 = 열쇠(`ui_template_map_tools` 단추 이름 · 아이콘 · 마우스 글은 거기) · 할 일 · 보일 페이지.
|
||||
* 묶음 = 나눔선 · 차례 = 도구줄 차례 · 단추 아닌 칸(간격 고르기 · 추천 노선 보기 · 지형 칩)은 이름을 여기 둠.
|
||||
* 누름 동작 · 페이지 문구는 페이지 몫 — 만든 요소를 `parts` 로 넘기면 그 자리에 · 안 넘긴 단추는 기본 단추(돌리기 둘 = 공용 돌림이 `data-act` 로 찾음).
|
||||
* 거리 재기 = 단추 자리만 같고 동작은 페이지 몫(결정 A⑩) · 지형 칩 = `map_terrain_chip`.
|
||||
* 공용 지도 도구줄 표(PLAN 624-2 · 634-2 · 요구 530 · 546) — 예정노선(D03) · B05 평면이 같이 씀.
|
||||
* 한 줄 = 요구 546 그룹2 표 한 줄 — 열쇠(`ui_template_map_tools` 단추 이름 · 아이콘) · 호버제목(`title`) · 할 일 · 보일 페이지.
|
||||
* 묶음 = 나눔선 · 차례 = 도구줄 차례 · 단추 아닌 칸(간격 고르기 · 추천 노선 보기 · 지형 칩)은 페이지가 만든 요소.
|
||||
* 마우스 글 = 이 표 `title` 하나(페이지 덮어쓰기 없음) · 막힘이면 페이지가 `mapToolTip(단추, 까닭)` → 「제목 — 까닭」.
|
||||
* 누름 동작은 페이지 몫 — 만든 요소를 `parts` 로 넘기면 그 자리에 · 안 넘긴 단추는 기본 단추(돌리기 둘 = 공용 돌림이 `data-act` 로 찾음).
|
||||
* [?] 도움말 단추 = `createMapTools` 의 `hint`(맨 앞 · 글 = 이 표 첫 줄).
|
||||
* 쓰는 법 = `createMapTools({ label, hint, groups: mapToolGroups("d03", { undo: undoButton, terrain: chip.root }) })`.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MapToolAct } from "@ui/ui_template_map_tools";
|
||||
import { mapToolButton, mapToolTip, type MapToolAct } from "@ui/ui_template_map_tools";
|
||||
import type { MapPage } from "./map_layer_list";
|
||||
|
||||
export type MapToolKey = MapToolAct | "snap-step" | "recommend" | "terrain";
|
||||
export type MapToolKey = MapToolAct | "hint-toggle" | "snap-step" | "recommend" | "terrain";
|
||||
|
||||
export interface MapToolRow {
|
||||
key: MapToolKey;
|
||||
/** 단추 아닌 칸 이름(단추는 `MAP_TOOLS` 이름). */
|
||||
name?: string;
|
||||
/** 호버제목(요구 546 표). */
|
||||
title: string;
|
||||
does: string;
|
||||
pages: readonly MapPage[];
|
||||
}
|
||||
@@ -25,59 +26,85 @@ const BOTH: readonly MapPage[] = ["d03", "b05"];
|
||||
const B05: readonly MapPage[] = ["b05"];
|
||||
|
||||
export const MAP_TOOLBAR: readonly (readonly MapToolRow[])[] = [
|
||||
[{ key: "hint-toggle", title: "도움말", does: "조작 도움말 펼치기 · 접기(`hint`)", pages: BOTH }],
|
||||
[
|
||||
{ key: "undo", does: "편집 한 걸음 되돌림", pages: BOTH },
|
||||
{ key: "redo", does: "되돌린 걸음 다시", pages: BOTH },
|
||||
{ key: "undo", title: "되돌리기", does: "편집 한 걸음 되돌림(Ctrl+Z)", pages: BOTH },
|
||||
{ key: "redo", title: "다시하기", does: "되돌린 걸음 다시(Ctrl+Y)", pages: BOTH },
|
||||
],
|
||||
[
|
||||
{ key: "view3d", does: "3D 큰 창(동작 = 페이지 몫)", pages: BOTH },
|
||||
{ key: "rotate-ccw", does: "지도 반시계로 돌림(Ctrl+휠도)", pages: BOTH },
|
||||
{ key: "rotate-cw", does: "지도 시계로 돌림", pages: BOTH },
|
||||
{ key: "view3d", title: "3D", does: "3D 큰 창(동작 = 페이지 몫)", pages: BOTH },
|
||||
{ key: "rotate-ccw", title: "좌 회전", does: "지도 반시계로 돌림(Ctrl+휠도)", pages: BOTH },
|
||||
{ key: "rotate-cw", title: "우 회전", does: "지도 시계로 돌림", pages: BOTH },
|
||||
],
|
||||
[
|
||||
{ key: "contour-minor", does: "라이다 1 m 등고선 켬끔(라이다일 때만 열림)", pages: B05 },
|
||||
{ key: "stations", does: "측점 눈금 · 번호 켬끔", pages: BOTH },
|
||||
{ key: "slopes", does: "구간 기울기 글 켬끔", pages: BOTH },
|
||||
{ key: "footprint", does: "도로 끝 · 비탈 끝(절/성토선) 켬끔", pages: B05 },
|
||||
{ key: "measure", does: "거리 재기(동작 = 페이지 몫 · A⑩)", pages: BOTH },
|
||||
{
|
||||
key: "contour-minor",
|
||||
title: "등고선 1m",
|
||||
does: "라이다 1 m 등고선 켬끔(라이다일 때만 열림)",
|
||||
pages: B05,
|
||||
},
|
||||
{ key: "stations", title: "측점", does: "측점 눈금 · 번호 켬끔", pages: BOTH },
|
||||
{ key: "slopes", title: "길이·기울기", does: "구간 길이 · 기울기 글 켬끔", pages: BOTH },
|
||||
{ key: "footprint", title: "절성토선", does: "도로 끝 · 비탈 끝 켬끔", pages: B05 },
|
||||
{ key: "measure", title: "길이측정", does: "거리 재기(동작 = 페이지 몫 · A⑩)", pages: BOTH },
|
||||
],
|
||||
[
|
||||
{ key: "station-snap", does: "곡선 가운데(MC)를 간격 배수에 맞춤", pages: B05 },
|
||||
{ key: "snap-step", name: "측점 스냅 간격", does: "스냅 간격 고르기", pages: B05 },
|
||||
{
|
||||
key: "station-snap",
|
||||
title: "스냅",
|
||||
does: "곡선 가운데(MC)를 간격 배수에 맞춤",
|
||||
pages: B05,
|
||||
},
|
||||
{ key: "snap-step", title: "스냅 간격", does: "스냅 간격 고르기(고르기 메뉴)", pages: B05 },
|
||||
],
|
||||
[
|
||||
{
|
||||
key: "expected-draw",
|
||||
does: "예정노선 변경 켬끔 — 예정노선 페이지 긋기 그대로 · 고친 예정노선 따라 평면선 다시 추적(626-3)",
|
||||
title: "예정노선 변경",
|
||||
does: "예정노선 변경 켬끔(626-3 · 걷음 = 635-3)",
|
||||
pages: B05,
|
||||
},
|
||||
{
|
||||
key: "expected-original",
|
||||
title: "예정노선 원본",
|
||||
does: "예정노선(원본) 옅은 점선 켬끔(626-3 · 걷음 = 635-3)",
|
||||
pages: B05,
|
||||
},
|
||||
{ key: "expected-original", does: "예정노선(원본) 옅은 점선 켬끔(626-3)", pages: B05 },
|
||||
],
|
||||
[
|
||||
{
|
||||
key: "recommend",
|
||||
name: "추천 노선 보기",
|
||||
does: "노선 추천 저장본 선 하나만 보기 · 끔(626-1 · 고르기 = 페이지 몫)",
|
||||
title: "추천노선",
|
||||
does: "노선 추천 저장본 선 하나만 보기 · 끔(626-1 · 예정노선 = 636-2)",
|
||||
pages: B05,
|
||||
},
|
||||
],
|
||||
[{ key: "terrain", name: "지형 칩", does: "지형 원천 보기 · 고르기", pages: BOTH }],
|
||||
[{ key: "terrain", title: "수치지형도/라이다", does: "지형 원천 보기 · 고르기", pages: BOTH }],
|
||||
];
|
||||
|
||||
const isAct = (key: MapToolKey): key is MapToolAct =>
|
||||
key !== "snap-step" && key !== "recommend" && key !== "terrain";
|
||||
const ROWS = MAP_TOOLBAR.flat();
|
||||
|
||||
/** 그 페이지 도구줄 묶음 — 넘긴 요소 · 기본 단추(단추 열쇠만) · 빈 묶음 뺌. */
|
||||
/** 표 호버제목 — 페이지 칸(지형 칩 막힘 글 등)이 씀. */
|
||||
export const mapToolTitle = (key: MapToolKey): string =>
|
||||
ROWS.find((row) => row.key === key)?.title ?? key;
|
||||
|
||||
const isAct = (key: MapToolKey): key is MapToolAct =>
|
||||
key !== "hint-toggle" && key !== "snap-step" && key !== "recommend" && key !== "terrain";
|
||||
|
||||
/** 그 페이지 도구줄 묶음 — 넘긴 요소 · 기본 단추(단추 열쇠만) · 마우스 글 = 표 · 빈 묶음 뺌. */
|
||||
export function mapToolGroups(
|
||||
page: MapPage,
|
||||
parts: Partial<Record<MapToolKey, HTMLElement>> = {},
|
||||
): (MapToolAct | HTMLElement)[][] {
|
||||
): HTMLElement[][] {
|
||||
return MAP_TOOLBAR.map((group) =>
|
||||
group
|
||||
.filter((row) => row.pages.includes(page))
|
||||
.flatMap((row): (MapToolAct | HTMLElement)[] => {
|
||||
const made = parts[row.key];
|
||||
return made ? [made] : isAct(row.key) ? [row.key] : [];
|
||||
.flatMap((row): HTMLElement[] => {
|
||||
const made = parts[row.key] ?? (isAct(row.key) ? mapToolButton(row.key) : null);
|
||||
if (!made) return [];
|
||||
made.dataset.tip = row.title;
|
||||
mapToolTip(made);
|
||||
return [made];
|
||||
}),
|
||||
).filter((group) => group.length);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user