Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_5
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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(
|
||||
<div class="b05-routeedit__busy" hidden><span></span></div>
|
||||
<aside class="b05-routeedit__side-wrap"></aside>`;
|
||||
parent.append(overlay);
|
||||
// 스냅 간격 고르기 — 단추 아님(도구줄에 요소로 끼움).
|
||||
const extras = document.createElement("div");
|
||||
extras.innerHTML = `
|
||||
<label class="b05-routeedit__terrain b05-routeedit__snap-step"
|
||||
title="측점 스냅 간격 — 곡선 가운데(MC)를 이 간격 배수에 맞춥니다(측점 No. = 측점 간격)">간격
|
||||
<select class="ui-select ui-select--sm" data-size="32" data-act="snap-step">
|
||||
<option value="10">10 m</option>
|
||||
<option value="station">측점 No.</option>
|
||||
</select>
|
||||
</label>`;
|
||||
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<HTMLElement>(".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,
|
||||
|
||||
@@ -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<Record<MapLayerKey, string[]>> = {
|
||||
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<HTMLButtonElement>(`[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<Record<MapLayerKey, string>> = { forestRoadFile: "받은 임도 자료 없음" };
|
||||
for (const def of LAYERS) {
|
||||
if (isOwn(def.key)) continue;
|
||||
gl.setVisible(def.key, on[def.key]);
|
||||
const button = layerBar.root.querySelector<HTMLButtonElement>(`[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<MapLayerKey, boolean>);
|
||||
legendBox.show(meta ? legendRows(shown, data) : [], glOn && on.satellite ? [lightRow] : []);
|
||||
@@ -202,7 +199,7 @@ export function createRouteEditLayers(opts: {
|
||||
function ensure(key: MapLayerKey): Promise<void> {
|
||||
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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<RouteEditChrome, "overlay" | "stationSnapButton">,
|
||||
chrome: Pick<RouteEditChrome, "stationSnapButton" | "snapStep">,
|
||||
projectId: string,
|
||||
stationIntervalM: number,
|
||||
onChange: (toggle: boolean) => void,
|
||||
): SnapStep {
|
||||
const select = chrome.overlay.querySelector<HTMLSelectElement>('[data-act="snap-step"]')!;
|
||||
// 고르기 메뉴 하나(634-3 · 옛 날 <select> 대신) — 값 = "10" · "station"
|
||||
const menu = chrome.snapStep;
|
||||
const value = (): string => menu.value() ?? "station";
|
||||
const saved = readState<string>("routeedit-snap-step", projectId);
|
||||
select.value = saved && STEPS.includes(saved) ? saved : "station";
|
||||
select.addEventListener("change", () => {
|
||||
writeState("routeedit-snap-step", select.value, projectId);
|
||||
menu.set(saved && STEPS.includes(saved) ? saved : "station");
|
||||
menu.onPick(() => {
|
||||
writeState("routeedit-snap-step", value(), projectId);
|
||||
onChange(false);
|
||||
});
|
||||
chrome.stationSnapButton.addEventListener("click", () => onChange(true));
|
||||
const on = () => chrome.stationSnapButton.getAttribute("aria-pressed") === "true";
|
||||
const step = (): number | null => (select.value === "station" ? null : Number(select.value));
|
||||
const step = (): number | null => (value() === "station" ? null : Number(value()));
|
||||
const draft = (): SnapSetting => ({ stationSnap: on(), stationSnapStep: on() ? step() : null });
|
||||
let loaded: SnapSetting = { stationSnap: false, stationSnapStep: null };
|
||||
const stations = (line: ReadonlyArray<Vertex>) =>
|
||||
on() ? stationsAlong(line, stationIntervalM, false) : null;
|
||||
const stepM = () => step() ?? stationIntervalM;
|
||||
return Object.assign(stepM, {
|
||||
text: () => snapFitText(select.value),
|
||||
text: () => snapFitText(value()),
|
||||
draft,
|
||||
load: (plan: SavedSnap) => {
|
||||
const snapOn = plan.station_snap === true;
|
||||
const kept = String(plan.station_snap_step ?? "station");
|
||||
if (snapOn) select.value = STEPS.includes(kept) ? kept : "station";
|
||||
if (snapOn) menu.set(STEPS.includes(kept) ? kept : "station");
|
||||
loaded = { stationSnap: snapOn, stationSnapStep: snapOn ? step() : null };
|
||||
return snapOn;
|
||||
},
|
||||
|
||||
@@ -14,16 +14,11 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 도구줄 — 지도 왼쪽 위(공용 `ui-map-tools` · 넘치면 가로 밀기 · 빈 자리는 지도 조작 통과) · [?] 안내 판은 그 아래 ·
|
||||
공용 겹 줄(624-3)은 그 아래 줄. */
|
||||
/* 지도 아이콘 한 행(634-3 · 공용 `ui-map-row`) — 지도 왼쪽 위 · 왼쪽 겹 줄 · 오른쪽 도구줄 · 영역마다 가로 밀기 ·
|
||||
[?] 안내 판은 행 아래 · 빈 자리는 지도 조작 통과. */
|
||||
.b05-routeedit__tools {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
pointer-events: none;
|
||||
top: var(--spacing-16);
|
||||
right: var(--spacing-12);
|
||||
left: var(--spacing-16);
|
||||
@@ -320,26 +315,6 @@ html.is-bm-route .b05-routeedit__tools {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임 · 글에 유리 바탕(352-1). */
|
||||
.b05-routeedit__terrain {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4, 4px);
|
||||
height: 32px;
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent);
|
||||
border-radius: var(--radius-cards);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 72%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
font-size: 12px;
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.b05-routeedit__terrain[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 설계 지형 칩이 숨으면 앞 구분선도(390-1 · 626-1) · 스냅 끔이면 간격 고르기 흐림(390-2). */
|
||||
.ui-toolbar__divider:has(+ [data-act="terrain"][hidden]) {
|
||||
display: none;
|
||||
|
||||
@@ -84,19 +84,22 @@ export function createCanvasLayers(opts: {
|
||||
{
|
||||
base: true,
|
||||
draw: (context, map) => {
|
||||
if (!on.bands || !data.bands) return;
|
||||
if (!data.bands) return;
|
||||
const road = themeColor("--draw-map-band-road", "#facc15");
|
||||
const stream = themeColor("--draw-map-band-stream", "#22d3ee");
|
||||
fillLayer(context, data.bands.road, map.view, road, 0.16, {
|
||||
color: road,
|
||||
width: 1.2,
|
||||
dash: [6, 4],
|
||||
});
|
||||
fillLayer(context, data.bands.stream, map.view, stream, 0.18, {
|
||||
color: stream,
|
||||
width: 1.2,
|
||||
dash: [6, 4],
|
||||
});
|
||||
// 도로 300 m 띠 = 「도로300m」 겹 · 계류 100 m 띠 = 「계류100m」 겹(634-2)
|
||||
if (on.roadBands)
|
||||
fillLayer(context, data.bands.road, map.view, road, 0.16, {
|
||||
color: road,
|
||||
width: 1.2,
|
||||
dash: [6, 4],
|
||||
});
|
||||
if (on.bands)
|
||||
fillLayer(context, data.bands.stream, map.view, stream, 0.18, {
|
||||
color: stream,
|
||||
width: 1.2,
|
||||
dash: [6, 4],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,10 +26,12 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { createMapToolRow } from "@ui/ui_template_map_tools";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
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 { bandCollection } from "../A00_Common/map/map_layer_data";
|
||||
import { LAYERS, LINE_LAYERS, ZONE_COLORS, type RoadPick } from "../A00_Common/map/map_layer_list";
|
||||
import type { TerrainSource } from "../A00_Common/map/map_terrain_chip";
|
||||
import { createMapLegend } from "../A00_Common/map/map_legend";
|
||||
@@ -91,9 +93,9 @@ const CANVAS_PREF_KEY = "aislo:pref:d03.canvas";
|
||||
const NO_SATELLITE_NOTE = "옛 그림은 위성 바탕 없이 그림";
|
||||
type DataKey = keyof Prepared;
|
||||
|
||||
/** 겹 → 받는 자료(임도 짐작 = 도로 자료 · 조각만 쓰는 겹 = 없음). */
|
||||
/** 겹 → 받는 자료(임도 짐작 = 도로 자료 · 도로 띠 = 띠 자료 · 조각만 쓰는 겹 = 없음). */
|
||||
const dataKey = (key: Exclude<PlanLayerKey, TileOnlyKey>): DataKey =>
|
||||
key === "forestRoad" ? "road" : key;
|
||||
key === "forestRoad" ? "road" : key === "roadBands" ? "bands" : key;
|
||||
|
||||
export interface PlanLayers {
|
||||
bar: HTMLElement;
|
||||
@@ -189,12 +191,10 @@ export function createPlanLayers(opts: {
|
||||
sync();
|
||||
map.redrawBase();
|
||||
}
|
||||
const bar = layerBar.root;
|
||||
bar.classList.add("d03-plan__layers");
|
||||
// 571-1 — 오른쪽 평면 도구줄이 이 줄 오른쪽부터(CSS `--d03-layers-w`)
|
||||
new ResizeObserver(() =>
|
||||
map.root.style.setProperty("--d03-layers-w", `${layerBar.bar.offsetWidth}px`),
|
||||
).observe(layerBar.bar);
|
||||
// 634-3 — 한 행 두 영역(왼쪽 겹 줄 · 오른쪽 평면 도구줄 · 영역마다 가로 밀기)
|
||||
const bar = createMapToolRow(layerBar.root, map.toolbar);
|
||||
bar.classList.add("d03-plan__row");
|
||||
layerBar.root.classList.add("d03-plan__layers"); // 가이드 촬영이 잡음
|
||||
const legendBox = createMapLegend("d03-plan__legend");
|
||||
const legend = legendBox.root;
|
||||
|
||||
@@ -543,23 +543,14 @@ export function createPlanLayers(opts: {
|
||||
} finally {
|
||||
window.clearTimeout(slow);
|
||||
}
|
||||
} else if (key === "bands") {
|
||||
} else if (key === "bands" || key === "roadBands") {
|
||||
const info = await fetchPlanBands(projectId);
|
||||
got.bands = {
|
||||
stream: prepare(info.stream.collection),
|
||||
road: prepare(info.road.collection),
|
||||
info,
|
||||
};
|
||||
const band = (name: "stream" | "road", fallback: string): GeoJsonCollection =>
|
||||
glTagged(info[name].collection, () => themeColor(`--draw-map-band-${name}`, fallback), {
|
||||
_band: name,
|
||||
});
|
||||
put("bands", {
|
||||
features: [
|
||||
...(band("road", "#facc15").features ?? []),
|
||||
...(band("stream", "#22d3ee").features ?? []),
|
||||
],
|
||||
});
|
||||
put("bands", bandCollection(info));
|
||||
} else {
|
||||
const collection = tiles.collection("지명");
|
||||
got.place = (collection.features ?? []).flatMap((feature) => {
|
||||
|
||||
@@ -69,8 +69,7 @@ export function createPlanTerrain(opts: {
|
||||
onClick: () =>
|
||||
pickFiles({ accept: ".las,.laz,.prj", multiple: true }, (files) => void send(files)),
|
||||
});
|
||||
upload.title =
|
||||
"라이다 LAS · LAZ(+ 좌표계 .prj 하나) — 올리면 서버가 지형을 지음 · 끝나면 「라이다」";
|
||||
// 마우스 글 = 지형 칸 표 호버제목(634-2 · 「수치지형도/라이다」 · 단추 글 「LAS 올리기」 그대로)
|
||||
const root = document.createElement("div");
|
||||
root.className = "d03-plan__terrain";
|
||||
root.append(chip.root, upload);
|
||||
|
||||
@@ -138,6 +138,8 @@ export interface PlanTools {
|
||||
|
||||
export interface PlanMap {
|
||||
root: HTMLElement;
|
||||
/** 평면 도구줄(634-3 · 겹 줄과 한 행 = `UI_Layers`). */
|
||||
toolbar: HTMLElement;
|
||||
update: (patch: Partial<PlanMapScene>) => void;
|
||||
setLayers: (layers: PlanMapLayer[]) => void;
|
||||
setTools: (tools: Partial<PlanTools>) => void;
|
||||
@@ -189,9 +191,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
baseCanvas.className = "d03-plan__base";
|
||||
const canvas = document.createElement("canvas");
|
||||
let marks = { stations: true, slopes: true };
|
||||
const markButton = (act: "stations" | "slopes", title: string): HTMLButtonElement => {
|
||||
const markButton = (act: "stations" | "slopes"): HTMLButtonElement => {
|
||||
const button = mapToolButton(act, {
|
||||
title,
|
||||
pressed: true,
|
||||
onClick: () => {
|
||||
marks = { ...marks, [act]: !marks[act] };
|
||||
@@ -235,8 +236,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
undo: undoButton,
|
||||
redo: redoButton,
|
||||
view3d: viewButton,
|
||||
stations: markButton("stations", "측점"),
|
||||
slopes: markButton("slopes", "구간 길이 · 기울기"),
|
||||
stations: markButton("stations"),
|
||||
slopes: markButton("slopes"),
|
||||
measure: measureButton,
|
||||
terrain: events.terrain,
|
||||
}),
|
||||
@@ -530,6 +531,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
|
||||
return {
|
||||
root,
|
||||
toolbar,
|
||||
update(patch) {
|
||||
const metaChanged = "meta" in patch && patch.meta !== scene.meta;
|
||||
scene = { ...scene, ...patch };
|
||||
|
||||
@@ -43,21 +43,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 571-1 — 왼쪽은 겹 단추줄(폭 = JS `--d03-layers-w`) 오른쪽까지 · 모자라면 도구줄이 가로 밀림 · 오른쪽 붙임 그대로 */
|
||||
.d03-plan__tools {
|
||||
/* 634-3 지도 아이콘 한 행(공용 `ui-map-row`) — 왼쪽 겹 줄 · 오른쪽 평면 도구줄 · 영역마다 가로 밀기 · [?] 판은 행 아래 */
|
||||
.d03-plan__row {
|
||||
position: absolute;
|
||||
top: var(--spacing-8);
|
||||
right: var(--spacing-8);
|
||||
left: calc(2 * var(--spacing-8) + var(--d03-layers-w, 222px));
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* 625-4 — 좁은 폭에서 오른쪽 끝(지형 칩 · LAS 올리기)이 가로 밀림에 잘리지 않게 줄바꿈(공용 한 줄 밀기는 B05 그대로) */
|
||||
.ui-map-tools.d03-plan__tools > .ui-toolbar {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
overflow: visible;
|
||||
mask-image: none;
|
||||
left: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 624-4 도구줄 끝 지형 칸 — 지형 칩 · LAS 올리기 · 진행 서클 한 줄 */
|
||||
@@ -67,14 +58,7 @@
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 569-2 · 581-1 바탕 겹 단추 — 왼쪽 위 · 지도 반 넘으면 가로 밀림 · 범례 — 왼쪽 아래(켠 겹마다 한 줄) */
|
||||
.d03-plan__layers {
|
||||
position: absolute;
|
||||
top: var(--spacing-8);
|
||||
left: var(--spacing-8);
|
||||
max-width: calc(55% - var(--spacing-8));
|
||||
}
|
||||
|
||||
/* 581-1 범례 — 왼쪽 아래(켠 겹마다 한 줄) */
|
||||
.d03-plan__legend {
|
||||
position: absolute;
|
||||
bottom: var(--spacing-8);
|
||||
|
||||
@@ -104,7 +104,8 @@ def test_편집기가_발자국을_그리고_단추로_켬끔() -> None:
|
||||
# 등고선 뒤 · 노선 앞(노선이 발자국 위에 보임)
|
||||
assert render.index("drawFootprint(context") < render.index("scene.expected,")
|
||||
chrome = (B05 / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text(encoding="utf-8")
|
||||
assert 'mapToolButton("footprint"' in chrome and "bindFootprintToggle(" in chrome
|
||||
# 634-2 — 단추 = 공용 표 기본 단추(`mapToolGroups`) · 켬끔 배선은 페이지
|
||||
assert 'bindFootprintToggle(mapTools.button("footprint")!)' in chrome
|
||||
footprint = (B05 / "B05_Profile_UI_RouteEdit_Footprint.ts").read_text(encoding="utf-8")
|
||||
assert 'subscribe("footprint"' in footprint and "let shown = true;" in footprint
|
||||
css = (B05 / "B05_Profile_UI_Style_RouteEdit_Footprint.css").read_text(encoding="utf-8")
|
||||
|
||||
@@ -195,5 +195,6 @@ def test_깎기_쌓기_바뀜_자리는_도로_끝(pieces) -> None:
|
||||
|
||||
def test_단추_이름은_절성토선() -> None:
|
||||
chrome = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Chrome.ts").read_text("utf-8")
|
||||
at = chrome.index('mapToolButton("footprint"')
|
||||
assert 'title: "절/성토선 — ' in chrome[at : at + 200] and "발자국" not in chrome
|
||||
# 634-2 — 호버 = 공용 표 「절성토선」(요구 546 · 페이지 덮어쓰기 없음)
|
||||
table = (ROOT / "A00_Common" / "map" / "map_toolbar.ts").read_text("utf-8")
|
||||
assert 'key: "footprint", title: "절성토선"' in table and "발자국" not in chrome
|
||||
|
||||
@@ -59,15 +59,16 @@ def test_편집기는_고른_간격_글을_씀() -> None:
|
||||
assert edit.count("applyEdit(snap.text())") == 1 # 켬 · 간격 바꿈
|
||||
assert 'applyEdit(stationSnap ? snap.text() : "옮겼습니다.")' in edit # 놓을 때(430)
|
||||
snap = SNAP.read_text(encoding="utf-8")
|
||||
assert "text: () => snapFitText(select.value)" in snap
|
||||
assert "text: () => snapFitText(value())" in snap # 634-3 고르기 메뉴 값
|
||||
|
||||
|
||||
def test_고르기는_10_m_측점_No_둘() -> None:
|
||||
"""280-4 — 고르기 option 둘 · STEPS 둘(옛 pref 0.1 · 1 = 측점 No.)."""
|
||||
"""280-4 — 고르기 줄 둘 · STEPS 둘(옛 pref 0.1 · 1 = 측점 No.) · 634-3 날 <select> → 고르기 메뉴 하나."""
|
||||
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("</select>")]
|
||||
assert re.findall(r'<option value="([^"]+)"', block) == ["10", "station"]
|
||||
block = chrome[chrome.index('icon: "spacing"') :]
|
||||
block = block[: block.index("onPick")]
|
||||
assert re.findall(r'value: "([^"]+)"', block) == ["10", "station"]
|
||||
assert "<select" not in chrome
|
||||
assert 'const STEPS = ["10", "station"];' in SNAP.read_text(encoding="utf-8")
|
||||
|
||||
@@ -70,32 +70,15 @@ def test_d03_parts():
|
||||
)
|
||||
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)
|
||||
assert keys == [
|
||||
("satellite", "true"),
|
||||
("hillshade", "false"),
|
||||
("contour", "true"),
|
||||
("slope", "false"),
|
||||
("stream", "true"),
|
||||
("water", "false"),
|
||||
("road", "false"),
|
||||
("nationalRoad", "false"),
|
||||
("forestRoadFile", "false"),
|
||||
("cadastral", "false"),
|
||||
("zones", "false"),
|
||||
("soils", "false"),
|
||||
("ecoForest", "false"),
|
||||
("ecology", "false"),
|
||||
("landslide", "false"),
|
||||
("landslideZones", "false"),
|
||||
("faults", "false"),
|
||||
("lineaments", "false"),
|
||||
("zeroOrder", "false"),
|
||||
("bands", "false"),
|
||||
("building", "false"),
|
||||
("place", "false"),
|
||||
], keys
|
||||
assert ".d03-plan__layers" in css and ".d03-plan__legend" in css
|
||||
# 634-2 — 표 차례(요구 546 그룹1 22 + 도로300m) · row(열쇠, 아이콘, 호버제목, 묶음[, true])
|
||||
rows = re.findall(r'row\("(\w+)", "\w+", "[^"]+", (?:"\w+"|null)(, true)?\)', defs)
|
||||
keys = [(key, "true" if first else "false") for key, first in rows]
|
||||
on = {"satellite", "contour", "stream"}
|
||||
order = "satellite hillshade contour slope stream water zeroOrder bands road nationalRoad"
|
||||
order += " forestRoadFile roadBands cadastral building place zones soils ecoForest ecology"
|
||||
order += " landslide landslideZones faults lineaments"
|
||||
assert keys == [(key, str(key in on).lower()) for key in order.split()], keys
|
||||
assert ".d03-plan__row" in css and ".d03-plan__legend" in css # 634-3 한 행
|
||||
# 599-1 — 방식 칸 = 마디 고름 2 × 2 · 길이 · 기울기 두 열 · 노드 판(598-4)과 같은 함수
|
||||
# 626-3 — 방식 칸은 공용 `A00_Common/map/map_draw_kind`(B05 평면도 씀) · D03 칸은 다시 내보냄
|
||||
kind = _src("A00_Common/map/map_draw_kind.ts")
|
||||
|
||||
@@ -86,8 +86,9 @@ def test_soils_fail_raises(tmp_path, monkeypatch) -> None:
|
||||
|
||||
def test_screen_def_and_colors() -> None:
|
||||
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)
|
||||
assert entry and entry.group(2) == "false" # 처음 값 끔
|
||||
# 634-2 표 한 줄 = row(열쇠, 아이콘, 호버제목, 묶음[, true]) · true 없음 = 처음 끔
|
||||
entry = re.search(r'row\("soils", "(\w+)", "[^"]+", (?:"\w+"|null)(, true)?\)', defs)
|
||||
assert entry and not entry.group(2) # 처음 값 끔
|
||||
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
|
||||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M)
|
||||
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
|
||||
|
||||
@@ -199,8 +199,9 @@ def test_screen_defs_icons_colors() -> None:
|
||||
("lineaments", "lineament"),
|
||||
("zeroOrder", "zero-order"),
|
||||
):
|
||||
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
|
||||
assert entry and entry.group(2) == "false", key # 처음 값 끔
|
||||
# 634-2 표 한 줄 = row(열쇠, 아이콘, 호버제목, 묶음[, true]) · true 없음 = 처음 끔
|
||||
entry = re.search(rf'row\("{key}", "(\w+)", "[^"]+", (?:"\w+"|null)(, true)?\)', defs)
|
||||
assert entry and not entry.group(2), key # 처음 값 끔
|
||||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), key
|
||||
assert f"--draw-map-{color}:" in theme and f'"--draw-map-{color}"' in defs
|
||||
for kind in layers.LINE_KINDS:
|
||||
|
||||
@@ -339,8 +339,9 @@ def test_screen_defs() -> None:
|
||||
("forestRoadFile", "forest-road-file"),
|
||||
("landslideZones", "landslide-zone"),
|
||||
):
|
||||
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
|
||||
assert entry and entry.group(2) == "false", key # 처음 값 끔
|
||||
# 634-2 표 한 줄 = row(열쇠, 아이콘, 호버제목, 묶음[, true]) · true 없음 = 처음 끔
|
||||
entry = re.search(rf'row\("{key}", "(\w+)", "[^"]+", (?:"\w+"|null)(, true)?\)', defs)
|
||||
assert entry and not entry.group(2), key # 처음 값 끔
|
||||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), key
|
||||
assert f"--draw-map-{color}:" in theme and f'"--draw-map-{color}"' in page
|
||||
site = (folder / "D03_PlanAnalysis_UI_Site.ts").read_text("utf-8") # 596-2 올리기 = 위치 칸
|
||||
|
||||
@@ -36,13 +36,14 @@ def test_fixed_sources_follow_plan_622_routes() -> None:
|
||||
def test_layer_list_five_off_by_default() -> None:
|
||||
defs = _read(MAP / "map_layer_list.ts")
|
||||
icons = _read(ROOT / "ui_template" / "ui_template_icons.ts")
|
||||
# 634-2 — 표 한 줄 = row(열쇠, 아이콘, 호버제목, 묶음[, 처음 켬]) · 처음 켬 빠짐 = 끔
|
||||
for key in ("nationalRoad", "soils", "ecoForest", "ecology", "landslide"):
|
||||
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
|
||||
assert entry and entry.group(2) == "false", key
|
||||
entry = re.search(rf'row\("{key}", "(\w+)", "[^"]+", "\w+"(, true)?\)', defs)
|
||||
assert entry and not entry.group(2), key
|
||||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), entry.group(1)
|
||||
# 630 — 임도망도 = 짐작 자리 · route 아이콘 · 짐작 겹 걷음
|
||||
assert re.search(r'key: "nationalRoad",\s*icon: "route",\s*title:\s*"임도망도\(', defs)
|
||||
assert 'key: "forestRoad",' not in defs
|
||||
# 630 — 임도망도 = 짐작 자리 · route 아이콘 · 짐작 겹 걷음 · 호버 = 표 「임도망도」
|
||||
assert 'row("nationalRoad", "route", "임도망도", "road")' in defs
|
||||
assert 'row("forestRoad",' not in defs
|
||||
assert "item(`임도망도 ${label}`" in _read(MAP / "map_forest_layers.ts")
|
||||
forest = _read(MAP / "map_forest_layers.ts")
|
||||
assert '"nationalRoad-label"' in forest and "minzoom: 14" in forest # 534 줌인 글
|
||||
|
||||
@@ -30,8 +30,9 @@ def _toolbar_rows() -> list[list[tuple[str, set[str]]]]:
|
||||
def test_toolbar_table_d03_same_as_before_plus_terrain() -> None:
|
||||
d03 = [[k for k, p in g if "d03" in p] for g in _toolbar_rows()]
|
||||
d03 = [g for g in d03 if g]
|
||||
# 옛 D03 도구줄 세 묶음 그대로 + 끝에 지형 칩
|
||||
# 옛 D03 도구줄 세 묶음 그대로 + 끝에 지형 칩 · 맨 앞 [?] 도움말 줄(634-2 표 17 줄 · 단추는 `hint`)
|
||||
assert d03 == [
|
||||
["hint-toggle"],
|
||||
["undo", "redo"],
|
||||
["view3d", "rotate-ccw", "rotate-cw"],
|
||||
["stations", "slopes", "measure"],
|
||||
@@ -47,17 +48,23 @@ def test_toolbar_table_d03_same_as_before_plus_terrain() -> None:
|
||||
"terrain",
|
||||
} <= b05
|
||||
tools = _read(ROOT / "ui_template" / "ui_template_map_tools.ts")
|
||||
for key in {k for g in _toolbar_rows() for k, _ in g} - {"snap-step", "recommend", "terrain"}:
|
||||
for key in {k for g in _toolbar_rows() for k, _ in g} - {
|
||||
"hint-toggle",
|
||||
"snap-step",
|
||||
"recommend",
|
||||
"terrain",
|
||||
}:
|
||||
assert f'"{key}": {{' in tools or f"{key}: {{" in tools, key # 아이콘 · 이름 = MAP_TOOLS
|
||||
|
||||
|
||||
def test_layer_pages() -> None:
|
||||
src = _read(MAP / "map_layer_list.ts")
|
||||
assert 'd03: { hide: [], pref: "aislo:pref:d03.layers" }' in src
|
||||
assert 'd03: { pref: "aislo:pref:d03.layers" }' in src
|
||||
b05 = re.search(r"b05: \{(.*?)\},", src, re.S).group(1)
|
||||
assert 'hide: ["bands", "forestRoadFile"]' in b05 # A⑨
|
||||
# 634-2 — 숨김 표 걷음(요구 546 표 = 22 겹 두 페이지 사용) · 보일 페이지 = 겹 줄 `pages`
|
||||
assert "hide" not in b05
|
||||
assert 'on: ["contour", "stream"]' in b05 # A② 지금 그대로
|
||||
assert "for (const key of hide) on[key] = false;" in src
|
||||
assert "if (!def.pages.includes(page)) on[def.key] = false;" in src
|
||||
|
||||
|
||||
def test_common_parts_use_kit_and_no_page_imports() -> None:
|
||||
|
||||
@@ -16,7 +16,8 @@ read = lambda path: path.read_text(encoding="utf-8") # noqa: E731
|
||||
def test_도구줄_표_겹_줄_범례() -> None:
|
||||
chrome = read(B05 / "B05_Profile_UI_RouteEdit_Chrome.ts")
|
||||
assert 'mapToolGroups("b05", {' in chrome
|
||||
assert "toolRows.append(mapTools.root, layers.bar)" in chrome
|
||||
# 634-3 — 한 행 두 영역(왼쪽 겹 줄 · 오른쪽 도구줄)
|
||||
assert "createMapToolRow(layers.bar, mapTools.root)" in chrome
|
||||
assert "info.prepend(layers.legend)" in chrome
|
||||
layers = read(B05 / "B05_Profile_UI_RouteEdit_Layers.ts")
|
||||
assert 'page: "b05"' in layers and "legendRows(" in layers and "mapLayerSpecs(" in layers
|
||||
@@ -36,7 +37,8 @@ def test_띠_자르기는_등고선_하천만() -> None:
|
||||
def test_처음_겹_숨긴_겹() -> None:
|
||||
src = read(ROOT / "A00_Common" / "map" / "map_layer_list.ts")
|
||||
b05 = re.search(r"b05: \{(.*?)\},", src, re.S).group(1)
|
||||
assert 'hide: ["bands", "forestRoadFile"]' in b05 and 'on: ["contour", "stream"]' in b05
|
||||
# 634-2 — 숨김 걷음(띠 · 임도(업로드) 보임 · 임도(업로드) = 받은 자료 없으면 막힘 + 까닭)
|
||||
assert "hide" not in b05 and 'on: ["contour", "stream"]' in b05
|
||||
|
||||
|
||||
def test_D03_안_부름_파일_한도() -> None:
|
||||
|
||||
@@ -186,4 +186,5 @@ def test_놓인_노드_미리보기_부채꼴_원_위에_그림():
|
||||
body = body[: body.index("\n}\n")]
|
||||
assert body.index("extra();") < body.index("paintNodes(")
|
||||
css = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Style.css").read_text(encoding="utf-8")
|
||||
assert re.search(r"\.d03-plan__tools > \.ui-toolbar \{[^}]*flex-wrap: wrap", css)
|
||||
# 634-3 — 줄바꿈 걷음 · 한 행(공용 `ui-map-row`) · 영역마다 가로 밀기
|
||||
assert ".d03-plan__row {" in css and "flex-wrap: wrap" not in css
|
||||
|
||||
@@ -98,8 +98,13 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
||||
props = _src(D03 / "D03_PlanAnalysis_UI_Props.ts")
|
||||
assert "!!params.oneLine ||" in props and "...(params.oneLine ? [] :" in props
|
||||
tools = _src(ROOT / "A00_Common" / "map" / "map_toolbar.ts")
|
||||
assert re.search(r'key: "expected-draw",\s+does: "[^"]+",\s+pages: B05', tools)
|
||||
assert re.search(r'key: "expected-original", does: "[^"]+", pages: B05', tools)
|
||||
# 634-2 — 표 줄에 호버제목(title) · 걷음은 635-3
|
||||
assert re.search(
|
||||
r'key: "expected-draw",\s+title: "[^"]+",\s+does: "[^"]+",\s+pages: B05', tools
|
||||
)
|
||||
assert re.search(
|
||||
r'key: "expected-original",\s+title: "[^"]+",\s+does: "[^"]+",\s+pages: B05', tools
|
||||
)
|
||||
render = _src(B05 / "B05_Profile_UI_RouteEdit_Render.ts")
|
||||
# 맨 위 = 구조물 마커 뒤 · 회전 끝 앞
|
||||
assert render.index("scene.overAll?.(context);") < render.index("context.restore(); // 회전 끝")
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""634-2 표 두 장(요구 546) — 그룹1 22 겹 호버제목 · 묶음 · 그룹2 17 줄 호버제목 · 페이지 덮어쓰기 없음 · 계류 · 도로 띠 둘."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MAP = ROOT / "A00_Common" / "map"
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# 요구 546 그룹1 표(호버제목 · 아이콘 그룹명) + 도로300m(표 밖 · 「도로」 묶음)
|
||||
GROUP1 = [
|
||||
("satellite", "위성사진", None),
|
||||
("hillshade", "음영기복도", None),
|
||||
("contour", "등고선", None),
|
||||
("slope", "급경사", None),
|
||||
("stream", "세류", "water"),
|
||||
("water", "하천", "water"),
|
||||
("zeroOrder", "0차곡", "water"),
|
||||
("bands", "계류100m", "water"),
|
||||
("road", "도로", "road"),
|
||||
("nationalRoad", "임도망도", "road"),
|
||||
("forestRoadFile", "임도(업로드)", "road"),
|
||||
("roadBands", "도로300m", "road"),
|
||||
("cadastral", "지적도", "land"),
|
||||
("building", "건물", "land"),
|
||||
("place", "지명", "land"),
|
||||
("zones", "보호규제구역", "limit"),
|
||||
("soils", "산림입지도", "limit"),
|
||||
("ecoForest", "국유림·사유림", "limit"),
|
||||
("ecology", "생태자연도", "limit"),
|
||||
("landslide", "산사태위험지도", "limit"),
|
||||
("landslideZones", "산사태취약지역", "limit"),
|
||||
("faults", "단층", "limit"),
|
||||
("lineaments", "지질구조선", "limit"),
|
||||
]
|
||||
GROUP2 = [
|
||||
("hint-toggle", "도움말"),
|
||||
("undo", "되돌리기"),
|
||||
("redo", "다시하기"),
|
||||
("view3d", "3D"),
|
||||
("rotate-ccw", "좌 회전"),
|
||||
("rotate-cw", "우 회전"),
|
||||
("contour-minor", "등고선 1m"),
|
||||
("stations", "측점"),
|
||||
("slopes", "길이·기울기"),
|
||||
("footprint", "절성토선"),
|
||||
("measure", "길이측정"),
|
||||
("station-snap", "스냅"),
|
||||
("snap-step", "스냅 간격"),
|
||||
("expected-draw", "예정노선 변경"),
|
||||
("expected-original", "예정노선 원본"),
|
||||
("recommend", "추천노선"),
|
||||
("terrain", "수치지형도/라이다"),
|
||||
]
|
||||
|
||||
|
||||
def test_그룹1_표_차례_호버_묶음():
|
||||
src = _read(MAP / "map_layer_list.ts")
|
||||
rows = re.findall(r'row\("(\w+)", "\w+", "([^"]+)", (?:"(\w+)"|null)', src)
|
||||
assert [(k, t, g or None) for k, t, g in rows] == GROUP1
|
||||
for name in ("수계망도", "도로", "토지·건물", "제한·재해"):
|
||||
assert f'name: "{name}"' in src
|
||||
# 표 「사용」 = 두 페이지 모두 · 숨김 표 없음
|
||||
assert "pages: BOTH" in src and "hide" not in src[src.index("MAP_LAYER_PAGES") :]
|
||||
|
||||
|
||||
def test_그룹2_표_호버():
|
||||
src = _read(MAP / "map_toolbar.ts")
|
||||
rows = re.findall(r'key: "([\w-]+)",\s+title: "([^"]+)"', src)
|
||||
assert rows == GROUP2
|
||||
tools = _read(ROOT / "ui_template" / "ui_template_map_tools.ts")
|
||||
for key, title in GROUP2[1:-4]:
|
||||
if key != "snap-step":
|
||||
assert re.search(rf'"?{key}"?: \{{ icon: "\w+", title: "{title}"', tools), key
|
||||
|
||||
|
||||
def test_페이지_호버_덮어쓰기_없음():
|
||||
files = [
|
||||
ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Chrome.ts",
|
||||
ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Map.ts",
|
||||
ROOT / "A00_Common" / "design" / "design_view_profile_3d_modal.ts",
|
||||
]
|
||||
for path in files:
|
||||
for call in re.findall(r"mapToolButton\([^)]*\)", _read(path), re.S):
|
||||
assert "title" not in call, (path.name, call)
|
||||
layers = _read(ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Layers.ts")
|
||||
assert (
|
||||
"layerBar.root.querySelector" not in layers
|
||||
and "layerBar.sync({ ready: !!meta, glOn, why })" in layers
|
||||
)
|
||||
assert 'forestRoadFile: "받은 임도 자료 없음"' in layers # B05 임도(업로드) 막힘 + 까닭
|
||||
edit = _read(ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit.ts")
|
||||
assert 'mapToolTip(minorContourButton, contours?.minor ? null : "LAS 등고선 없음")' in edit
|
||||
bar = _read(MAP / "map_layer_bar.ts")
|
||||
assert 'const GL_TITLE = "그래픽 카드";' in bar and "multi: true" in bar
|
||||
|
||||
|
||||
def test_계류_도로_띠_둘():
|
||||
specs = _read(MAP / "map_layer_specs.ts")
|
||||
assert 'fillEdge("roadBands", "bands"' in specs and 'fillEdge("bands", "bands"' in specs
|
||||
assert specs.index('fillEdge("roadBands"') < specs.index('fillEdge("bands", "bands"') # 옛 차례
|
||||
legend = _read(MAP / "map_legend.ts")
|
||||
assert "...(on.bands" in legend and "...(on.roadBands" in legend
|
||||
paint = _read(ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerPaint.ts")
|
||||
assert "if (on.roadBands)" in paint and "if (on.bands)" in paint
|
||||
b05 = _read(ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Layers.ts")
|
||||
assert 'key === "bands" || key === "roadBands"' in b05 and "fetchPlanBands(projectId)" in b05
|
||||
@@ -211,12 +211,7 @@ def test_노선_추천_다듬기_23_3():
|
||||
'mapToolButton("redo"',
|
||||
"view3d: tools[0]", # 부른 쪽 단추([3D])
|
||||
'mapToolButton("contour-minor"',
|
||||
'mapToolButton("stations"',
|
||||
'mapToolButton("slopes"',
|
||||
'mapToolButton("footprint"',
|
||||
'mapToolButton("measure"',
|
||||
'mapToolButton("station-snap"',
|
||||
'"snap-step": snapStep,',
|
||||
'"snap-step": snapStep.root,', # 634-3 고르기 메뉴 · 측점 · 기울기 · 절성토선 · 재기 · 스냅 = 표 기본 단추
|
||||
"recommend: preview.root,",
|
||||
"terrain: terrain.root,",
|
||||
]
|
||||
|
||||
@@ -168,6 +168,8 @@ const PATHS = {
|
||||
streams: "M12 21v-7|M12 14l-5 -7|M12 14l6 -9|M8.8 9.5l-4.8 -1.5|M15.6 8.6l4.4 -0.6",
|
||||
signpost: "M12 21v-11|M12 5v-2|M5 5h11l3 2.5l-3 2.5h-11z|M9 21h6",
|
||||
parcelHouse: "M3 21h18|M5 21v-9l7 -6l7 6v9|M10 21v-5h4v5",
|
||||
roadBand:
|
||||
"M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z|M10 4l-2 16|M14 4l2 16",
|
||||
ban: "M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0|M5.7 5.7l12.6 12.6",
|
||||
} as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user