From 61733d064d73bbce975064ba59ddf80efe2e74de Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 11:40:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=A7=80=EB=8F=84):=20634-2=20=C2=B7=2063?= =?UTF-8?q?4-3=20=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=91=9C=20=EB=91=90=20?= =?UTF-8?q?=EC=9E=A5=20=C2=B7=20=EB=AC=B6=EC=9D=8C=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=C2=B7=20=ED=95=9C=20=ED=96=89=20=EB=B0=B0=EC=84=A0=20=C2=B7=20?= =?UTF-8?q?=ED=98=B8=EB=B2=84=20=3D=20=ED=91=9C(=EC=9A=94=EA=B5=AC=20546)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 겹 표(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 --- .../design/design_view_profile_3d_modal.ts | 5 +- A00_Common/map/map_layer_bar.ts | 97 +++++++--- A00_Common/map/map_layer_data.ts | 17 +- A00_Common/map/map_layer_list.ts | 182 +++++++----------- A00_Common/map/map_layer_specs.ts | 15 +- A00_Common/map/map_legend.ts | 27 ++- A00_Common/map/map_terrain_chip.ts | 25 ++- A00_Common/map/map_toolbar.ts | 93 +++++---- B05_Profile/B05_Profile_UI_RouteEdit.ts | 5 +- .../B05_Profile_UI_RouteEdit_Chrome.ts | 70 +++---- .../B05_Profile_UI_RouteEdit_Layers.ts | 42 ++-- .../B05_Profile_UI_RouteEdit_RecPreview.ts | 2 +- .../B05_Profile_UI_RouteEdit_SnapStep.ts | 20 +- .../B05_Profile_UI_Style_RouteEdit.css | 29 +-- .../D03_PlanAnalysis_UI_LayerPaint.ts | 25 +-- .../D03_PlanAnalysis_UI_Layers.ts | 29 +-- .../D03_PlanAnalysis_UI_LidarUpload.ts | 3 +- D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts | 10 +- .../D03_PlanAnalysis_UI_Style.css | 24 +-- resources/tester/test_346_2_footprint_draw.py | 3 +- .../tester/test_353_1_cut_fill_line_smooth.py | 5 +- resources/tester/test_390_4_snap_step_text.py | 11 +- resources/tester/test_553_4_de_parts.py | 35 +--- resources/tester/test_581_2_plan_soils.py | 5 +- resources/tester/test_584_1_plan_geo_lines.py | 5 +- .../test_584_3_plan_forest_road_zones.py | 5 +- resources/tester/test_622_2_forest_layers.py | 11 +- resources/tester/test_624_2_map_parts.py | 17 +- resources/tester/test_624_3_b05_map.py | 6 +- resources/tester/test_625_draw.py | 3 +- resources/tester/test_626_3_expected_edit.py | 9 +- resources/tester/test_634_2_hover_table.py | 111 +++++++++++ .../tester/test_b05_routeedit_initial.py | 7 +- ui_template/ui_template_icons.ts | 2 + 34 files changed, 527 insertions(+), 428 deletions(-) create mode 100644 resources/tester/test_634_2_hover_table.py diff --git a/A00_Common/design/design_view_profile_3d_modal.ts b/A00_Common/design/design_view_profile_3d_modal.ts index 4c91b0b63..13429296f 100644 --- a/A00_Common/design/design_view_profile_3d_modal.ts +++ b/A00_Common/design/design_view_profile_3d_modal.ts @@ -74,10 +74,7 @@ export function createViewer3dModal(deps: Viewer3dModalDeps) { } // 평면 도구줄 아이콘 단추(568-2) — 클래스는 가이드 촬영이 잡음. - const button = mapToolButton("view3d", { - title: "3D 화면을 큰 창으로 엽니다 — 닫으면 그리기를 멈춥니다", - onClick: open, - }); + const button = mapToolButton("view3d", { onClick: open }); // 마우스 글 = 공용 표(634-2) button.classList.add("b05-route__3d-open"); return { diff --git a/A00_Common/map/map_layer_bar.ts b/A00_Common/map/map_layer_bar.ts index b0c2f9db9..734fb45f4 100644 --- a/A00_Common/map/map_layer_bar.ts +++ b/A00_Common/map/map_layer_bar.ts @@ -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>; + }) => 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(); - const groups = defs.map((group) => - group.map((def) => { + const menus = new Map(); + 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[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)); } diff --git a/A00_Common/map/map_layer_data.ts b/A00_Common/map/map_layer_data.ts index 1b100fd93..e4115aa66 100644 --- a/A00_Common/map/map_layer_data.ts +++ b/A00_Common/map/map_layer_data.ts @@ -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; diff --git a/A00_Common/map/map_layer_list.ts b/A00_Common/map/map_layer_list.ts index 81654fa3c..580db287e 100644 --- a/A00_Common/map/map_layer_list.ts +++ b/A00_Common/map/map_layer_list.ts @@ -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 = { + 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 = { + 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 { - 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; @@ -176,7 +128,7 @@ export function readLayerPref(page: MapPage): Record { } 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; } diff --git a/A00_Common/map/map_layer_specs.ts b/A00_Common/map/map_layer_specs.ts index 6b433c29d..e8c7c3648 100644 --- a/A00_Common/map/map_layer_specs.ts +++ b/A00_Common/map/map_layer_specs.ts @@ -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", diff --git a/A00_Common/map/map_legend.ts b/A00_Common/map/map_legend.ts index 4ba5fd6cb..7ae0461d9 100644 --- a/A00_Common/map/map_legend.ts +++ b/A00_Common/map/map_legend.ts @@ -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) => { diff --git a/A00_Common/map/map_terrain_chip.ts b/A00_Common/map/map_terrain_chip.ts index b647f3c57..1b06308e7 100644 --- a/A00_Common/map/map_terrain_chip.ts +++ b/A00_Common/map/map_terrain_chip.ts @@ -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 = { lidar: "라이다", }; -const CAPTION: Record = { - d03: "분석 지형 — 급경사 · 음영 · 0차곡 · 긋기 높이에 쓰는 지형", - b05: "설계 지형 — 바꾸면 종단 · 횡단 · 배수 · 토공을 모두 다시 셈", -}; +/** 막힘 까닭(마우스 글 = 「수치지형도/라이다 — 까닭」 · 요구 546). */ const MISSING: Record = { - 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(`[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); diff --git a/A00_Common/map/map_toolbar.ts b/A00_Common/map/map_toolbar.ts index 25ba4acd1..5d4c388e6 100644 --- a/A00_Common/map/map_toolbar.ts +++ b/A00_Common/map/map_toolbar.ts @@ -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> = {}, -): (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); } diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 0fd8d47a5..e1b7d29bd 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -79,6 +79,7 @@ import { bindStationSnap, paintStationSnap } from "./B05_Profile_UI_RouteEdit_Sn import * as fit from "./logic/B05_Profile_RouteEdit_StationFit"; import { calcChecked } from "../A00_Common/design/design_calc_server"; import "./B05_Profile_UI_Style_RouteEdit.css"; +import { mapToolTip } from "@ui/ui_template_map_tools"; /** 등고선 눈금을 고를 때 쓸 라벨 예산 — B04 지도 기본값(350)보다 **넉넉히** 둔다 * (2026-09-12 사용자 지시 ⑦ 「좀더 촘촘히」). 편집 창은 노선 둘레 300m 띠만 보이므로 @@ -651,9 +652,7 @@ export async function mountRouteEditor( pickedContour = contourLock.reattach(contours?.layer ?? null); // 1m 선이 있는 자료(LAS)일 때만 단추를 연다 — 도엽에는 1m 선이 없다. minorContourButton.disabled = !contours?.minor; - minorContourButton.title = contours?.minor - ? "1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)" - : "LAS 등고선이 없어 1m 선이 없습니다"; + mapToolTip(minorContourButton, contours?.minor ? null : "LAS 등고선 없음"); }; const showStatus = (): void => { status.textContent = diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index b3366a1cd..792fb709c 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -1,5 +1,6 @@ import { createIconButton } from "@ui/ui_template_elements"; -import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools"; +import { createPickMenu, type PickMenu } from "@ui/ui_template_context_menu"; +import { createMapToolRow, createMapTools, mapToolButton } from "@ui/ui_template_map_tools"; import { mapToolGroups } from "../A00_Common/map/map_toolbar"; import { createSidePanels } from "@ui/ui_template_side_panels"; import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint"; @@ -20,9 +21,10 @@ import { bottomRowNarrow, infoLiftPx } from "./logic/B05_Profile_RouteEdit_InfoL * **배치**(352-1) — 지도가 칸 전체 · 머리 줄 · 테두리 없음. 도구는 3D 와 같은 떠 있는 유리 단추 * (지도 왼쪽 위 · ☰ 비킴), [?] 조작 설명은 도구줄 아래 2열, 상태·범례는 왼쪽 아래, 잰 값은 오른쪽 * 아래. 횡단 두 판은 지도 오른쪽을 덮는 판(아래 종단 판이 그 위). + * 한 행(634-3 · `createMapToolRow`) = 왼쪽 공용 겹 줄(`_Layers` · 끝 = 그래픽 카드 단추) · 오른쪽 도구줄 — 영역마다 가로 밀기. * 도구줄 차례 = 공용 표(624-3 · `A00_Common/map/map_toolbar` 「b05」) — [?] | ↶ ↷ | 3D ⟲ ⟳ | 1m 등고선 · 측점 · 기울기 · - * 절/성토선 · 거리 재기 | 측점 스냅 · 간격 | 예정노선 변경 · 원본(626-3) | 추천 노선 보기(626-1) | 설계 지형 칩(626-1). 아래 줄 = 공용 겹 줄(`_Layers` · - * 끝 = 그래픽 카드 단추) · 범례 = 상태 글판 위. 단추는 공용 지도 도구줄(568-2 · `ui_template_map_tools`) 아이콘 — 긴 설명은 마우스 글. + * 절/성토선 · 거리 재기 | 측점 스냅 · 간격(고르기 메뉴) | 예정노선 변경 · 원본(626-3) | 추천 노선 보기(626-1) | 설계 지형 칩(626-1). + * 범례 = 상태 글판 위. 마우스 글 = 공용 표 호버제목(634-2 · 페이지 덮어쓰기 없음). * * 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 · * 닫기 단추는 걷었다 · [취소] · [확인] · [노선 반영] · [저장 전으로]는 없앰(346-3 · 352-2 — 노선은 페이지 @@ -64,6 +66,8 @@ export interface RouteEditChrome { minorContourButton: HTMLButtonElement; /** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */ stationSnapButton: HTMLButtonElement; + /** 스냅 간격 고르기 메뉴(하나 · 634-3) — 누름 배선 = `onPick`(`_SnapStep`). */ + snapStep: PickMenu & { onPick: (fn: (value: string | null) => void) => void }; /** 예정노선 그리기 켬끔(626-3 · 배선 = `B05_Profile_UI_ExpectedDraw`). */ expectedDrawButton: HTMLButtonElement; /** 예정노선(원본 · 파일입력) 옅은 점선 켬끔(626-3 · 535-1). */ @@ -129,17 +133,19 @@ export function createRouteEditChrome( `; parent.append(overlay); - // 스냅 간격 고르기 — 단추 아님(도구줄에 요소로 끼움). - const extras = document.createElement("div"); - extras.innerHTML = ` - `; - const snapStep = extras.firstElementChild as HTMLElement; + // 스냅 간격 = 고르기 메뉴 하나(634-3 · 요구 546) — 누름 배선은 `_SnapStep`. + let pickSnapStep: (value: string | null) => void = () => {}; + const snapStep = createPickMenu({ + icon: "spacing", + title: "스냅 간격", + items: [ + { value: "10", label: "10 m" }, + { value: "station", label: "측점 No." }, + ], + onPick: (value) => pickSnapStep(value), + }); + snapStep.root.dataset.act = "snap-step"; + snapStep.root.classList.add("b05-routeedit__snap-step"); const terrain = createRouteTerrain(projectId); let gl: RouteEditGl | null = null; let redrawScene: (() => void) | null = null; @@ -152,37 +158,19 @@ export function createRouteEditChrome( const mapTools = createMapTools({ label: "평면 도구", hint: { lines: HINT_LINES, open: hintOpen, onToggle: (open) => (hintOpen = open) }, + // 마우스 글 = 공용 표 호버제목(634-2) · 막힘 까닭만 페이지(`mapToolTip`) groups: mapToolGroups("b05", { - undo: mapToolButton("undo", { title: "되돌리기 (Ctrl+Z)", disabled: true }), - redo: mapToolButton("redo", { title: "다시하기 (Ctrl+Y)", disabled: true }), + undo: mapToolButton("undo", { disabled: true }), + redo: mapToolButton("redo", { disabled: true }), ...(tools[0] ? { view3d: tools[0] } : {}), // 부른 쪽 단추([3D]) - "contour-minor": mapToolButton("contour-minor", { - title: "1m 등고선 — 1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)", - disabled: true, - }), - stations: mapToolButton("stations", { - title: "측점 — 측점 눈금 · 번호를 평면에 보이기 · 숨기기 (시점 · 종점 이름표는 늘 보임)", - }), - slopes: mapToolButton("slopes", { - title: "기울기 — 측점 사이 기울기(%)를 평면에 보이기 · 숨기기 (확대해야 보임)", - }), - footprint: mapToolButton("footprint", { - title: "절/성토선 — 도로 끝 · 비탈 끝(깎기 붉음 · 쌓기 청록)을 평면에 겹칩니다", - }), - measure: mapToolButton("measure", { - title: "거리 재기 — 노선 위 두 점을 눌러 거리·기울기를 잽니다 (Shift+클릭도 같음)", - }), - "station-snap": mapToolButton("station-snap", { - title: - "측점 스냅 — 켜면 고친 곡선의 가운데(MC)를 고른 간격 배수에 맞춥니다 — 일반측점(20 m)은 그대로 · MC 가 일반측점 위가 아니면 「No.N+10 (MC)」 특별측점을 더함 · 손대지 않은 곡선은 그 자리 유지 · 고정 교각점은 그대로 · 못 맞춘 곡선은 상태줄 「측점 밖 곡선」 · [저장] 때 저장", - }), - "snap-step": snapStep, + "contour-minor": mapToolButton("contour-minor", { disabled: true }), + "snap-step": snapStep.root, recommend: preview.root, terrain: terrain.root, }), }); mapTools.hint!.classList.add("b05-routeedit__hint"); // 가이드 촬영 · 엿보기 숨김이 잡음 - // 공용 지도 겹(624-3) — 도구줄 아래 줄 · 범례는 상태 글판 맨 위 + // 공용 지도 겹(624-3) — 한 행 왼쪽(634-3 · 도구줄은 오른쪽) · 범례는 상태 글판 맨 위 const wrap = overlay.querySelector(".b05-routeedit__canvas-wrap")!; const layers = createRouteEditLayers({ projectId, @@ -190,9 +178,8 @@ export function createRouteEditChrome( onGl: () => gl?.toggle(), redraw, }); - const toolRows = document.createElement("div"); - toolRows.className = "b05-routeedit__tools"; - toolRows.append(mapTools.root, layers.bar); + const toolRows = createMapToolRow(layers.bar, mapTools.root); + toolRows.classList.add("b05-routeedit__tools"); overlay.querySelector('[data-slot="map-tools"]')!.replaceWith(toolRows); // 횡단 미리보기 = 패널 양식(382-1 · 손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 자리는 지도 위 덮개 그대로 · @@ -292,6 +279,7 @@ export function createRouteEditChrome( measureButton, minorContourButton, stationSnapButton, + snapStep: Object.assign(snapStep, { onPick: (fn: typeof pickSnapStep) => (pickSnapStep = fn) }), expectedDrawButton: mapTools.button("expected-draw")!, expectedOriginalButton: mapTools.button("expected-original")!, terrain, diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Layers.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Layers.ts index a372b9143..5e0346958 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Layers.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Layers.ts @@ -1,7 +1,7 @@ /* ============================================================================= * B05_Profile_UI_RouteEdit_Layers.ts * 평면 노선 편집 지도 겹(PLAN 624-3 · 요구 530 · 결정 A② · A③ · A⑦ · A⑨) — 공용 겹 줄 · 범례(`A00_Common/map`)를 B05 평면에. - * 겹 목록 · 처음 켬 · 취향 = 공용 페이지 표 「b05」(띠 · 올린 임도망도 숨김 · 처음 = 등고선 · 하천만). + * 겹 목록 · 처음 켬 · 취향 = 공용 표(634-2 · 22 겹 모두 · 처음 = 등고선 · 하천만) · 임도(업로드) = 받은 자료 없으면 막힘 + 까닭. * 등고선 · 하천 = B05 자기 자료(LAS 5 m · 1 m · 도엽 · 집기 · 자물쇠 그대로 · `_Gl` 이 그림 · 노선 띠 300 m 자르기는 이 둘만) · * 켬끔은 GL 에서만(옛 캔버스 = 늘 보임). * 그 밖 겹 = GL 만(A⑦) · 띠로 안 자르는 둘째 GL 칸(맨 밑 · 켠 겹이 있을 때만 섬) · 꼴 = 공용 `mapLayerSpecs` · @@ -19,6 +19,7 @@ import type { Normalizer } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { glCameraOf } from "../B04_PreProcess/logic/B04_PreProcess_UI_MapGl_Frame"; import { forestSources, soilColor, tileOnly } from "../A00_Common/map/map_forest_layers"; import { + fetchPlanBands, fetchPlanCadastral, fetchPlanLandslideZones, fetchPlanLines, @@ -34,6 +35,7 @@ import { createMapLayerBar } from "../A00_Common/map/map_layer_bar"; import { FOREST_ROAD_KINDS, WATER_KEYS, + bandCollection, glImage, glTagged, loadImage, @@ -68,6 +70,9 @@ const SHEET_NAMES: Partial> = { building: ["건물"], place: ["지명"], }; +/** 겹 → 받는 자료 칸(임도 짐작 = 도로 · 도로 300 m 띠 = 띠 자료 · 634-3). */ +const slotOf = (key: MapLayerKey): MapLayerKey => + key === "forestRoad" ? "road" : key === "roadBands" ? "bands" : key; /** 도엽이 이보다 많으면 선 겹 숨김(D03 `FULL_MAX` 와 같음). */ const SHEET_MAX = 12; /** 지도 칸 둘레 여유(px · `.ui-gl-map` inset · B04 `createTmGl` 과 같음). */ @@ -173,24 +178,16 @@ export function createRouteEditLayers(opts: { const first = !meta && nextMeta; glOn = nextGl; meta = nextMeta; - layerBar.sync({ ready: !!meta, glOn }); - // 자기 겹(등고선 · 하천) = GL 에서만 켬끔 · 옛 캔버스는 늘 그림 - for (const key of OWN) { - const button = layerBar.root.querySelector(`[data-layer="${key}"]`); - if (!button || glOn) continue; - button.disabled = true; - button.title = `${LAYERS.find((def) => def.key === key)!.title} — 옛 그림(그래픽 카드 끔)엔 늘 보임`; - } - // 그 밖 겹 = GL 만(A⑦) + // 막힘 까닭(634-3 · 공용 sync 가 단추 · 메뉴 줄에 「제목 — 까닭」) — 자기 겹(등고선 · 하천) = GL 에서만 켬끔 · + // 옛 캔버스는 늘 그림 · 그 밖 겹 = GL 만(A⑦) · 임도(업로드) = 받은 자료 없음(546 공유 일감 전) + const why: Partial> = { forestRoadFile: "받은 임도 자료 없음" }; for (const def of LAYERS) { - if (isOwn(def.key)) continue; - gl.setVisible(def.key, on[def.key]); - const button = layerBar.root.querySelector(`[data-layer="${def.key}"]`); - if (button && meta && !glOn) { - button.disabled = true; - button.title = `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`; - } + if (!isOwn(def.key)) gl.setVisible(def.key, on[def.key]); + if (glOn || why[def.key]) continue; + if (isOwn(def.key)) why[def.key] = "옛 그림(그래픽 카드 끔)엔 늘 보임"; + else if (meta) why[def.key] = "옛 그림(그래픽 카드 끔)엔 안 보임"; } + layerBar.sync({ ready: !!meta, glOn, why }); if (hillshade) gl.setData("hillshade", on.satellite ? hillshade.shade : hillshade.plain); const shown = glOn ? on : ({} as Record); legendBox.show(meta ? legendRows(shown, data) : [], glOn && on.satellite ? [lightRow] : []); @@ -202,7 +199,7 @@ export function createRouteEditLayers(opts: { function ensure(key: MapLayerKey): Promise { if (!meta || !on[key] || isOwn(key) || tileOnly(key) || key === "landslide") return done(); if (loaded.has(key)) return done(); - const slot = key === "forestRoad" ? "road" : key; + const slot = slotOf(key); if (loaded.has(slot as MapLayerKey)) return done(); const running = pending.get(slot); if (running) return running; @@ -216,8 +213,7 @@ export function createRouteEditLayers(opts: { }) .catch((error: unknown) => { for (const def of LAYERS) - if (def.key === key || (def.key === "forestRoad" ? "road" : def.key) === slot) - on[def.key] = false; + if (def.key === key || slotOf(def.key) === slot) on[def.key] = false; sync(glOn, meta); opts.redraw(); const text = error instanceof Error ? error.message : String(error); @@ -287,6 +283,12 @@ export function createRouteEditLayers(opts: { .catch(() => null); return gl.setData("place", { features: places }); } + if (key === "bands" || key === "roadBands") { + // 계류 100 m · 도로 300 m 띠(634-3 · D03 과 같은 서버 길 · 자료 한 벌 · 겹 둘) + const info = await fetchPlanBands(projectId); + data.bands = { info }; + return gl.setData("bands", bandCollection(info)); + } if (key === "hillshade") { const raster = await fetchPlanRaster(projectId, "hillshade"); const image = await loadImage(raster.image); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts b/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts index f8d7e0013..a658eee70 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts @@ -43,7 +43,7 @@ export function createRecPreview(opts: { let turn = 0; const menu = createPickMenu({ icon: "compass", - title: "추천 노선 보기 — 노선 추천 저장본 선 하나만 겹쳐 봄(끔 = 안 보임 · 노선은 안 바뀜)", + title: "추천노선", // 마우스 글 = 공용 표 호버제목(634-2 · `mapToolGroups` 가 맞춤) items: MODES.map((mode) => ({ value: mode, label: LOGIC_LABEL[mode] })), offLabel: "끔", onPick: (mode) => void pick(mode as RouteInitialLogic | null), diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts b/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts index 7a0352930..a21ab3fa9 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts @@ -6,7 +6,7 @@ * 고른 값은 프로젝트별 화면 기억(pref `routeedit-snap-step`) · 스냅 켬이면 저장 노선에도 같이 * (`station_snap_step` · 390-5 · 서버 측점 셈이 MC 측점 값 자릿수로 씀 · 옛 노선 = 측점 No.). * 저장 노선이 스냅 켬이면 열 때 그 간격으로 고르기를 맞춤. 추천 · 서버 초기 노선은 측점 No. 그대로. - * 스냅 끔이면 고르기는 흐림(CSS) · 고를 수는 있음. + * 고르기 = 도구줄 고르기 메뉴 하나(634-3 · `chrome.snapStep`) · 스냅 끔이면 흐림(CSS) · 고를 수는 있음. * 상태줄 글은 고른 간격 따라(390-4) — 측점 No. = 「일반측점에」 · 10 m = 「10 m 배수에」. * 평면 눈금 = 일반측점(스냅이 옮기지 않음 · 280-4) + 곡선 측점(MC 에 측점 없을 때 · 252-2 · `ticks`). * ========================================================================== */ @@ -59,33 +59,35 @@ export type SnapStep = { /** 단추 · 고르기 배선 — `onChange(true)` = 단추 누름(켬끔 바꿈) · `onChange(false)` = 간격 바꿈. */ export function bindStationSnap( - chrome: Pick, + chrome: Pick, projectId: string, stationIntervalM: number, onChange: (toggle: boolean) => void, ): SnapStep { - const select = chrome.overlay.querySelector('[data-act="snap-step"]')!; + // 고르기 메뉴 하나(634-3 · 옛 날 → 고르기 메뉴 하나.""" import re chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8") - block = chrome[chrome.index('data-act="snap-step"') :] - block = block[: block.index("")] - assert re.findall(r'