Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2', 'origin/sub_laptop_3' and 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -138,6 +138,8 @@ export const KEY_ACTIONS: readonly KeyAction[] = [
|
||||
A("plan.redo", "plan", ["Ctrl+Shift+Z", "Ctrl+Y"], ["다시", "Redo"]),
|
||||
A("plan.drawFinish", "plan", ["Enter"], ["그리기 끝", "Finish drawing"]),
|
||||
A("plan.drawBack", "plan", ["Backspace"], ["그리기 마지막 점 빼기", "Drawing: remove last point"]),
|
||||
A("plan.drawKind", "plan", ["Tab"], ["긋는 중 방식 자유지정 ↔ 길이·기울기 고정", "Drawing: free ↔ length + slope"]),
|
||||
A("plan.drawBetween", "plan", ["Shift 누른 채"], ["길이·기울기 고정 긋는 중 부채꼴 사이 기울기 자리(길이 그대로)", "Drawing: pick between-slope on fan (length kept)"], F),
|
||||
A("plan.delete", "plan", ["Delete"], ["고른 노드 · 선 · 구역 · 전체 · 마커 지움", "Delete picked node · segment · region · all · marker"]),
|
||||
A("plan.escape", "plan", ["Escape"], ["그리기 취소 · 고름 풀기 · 재기 끝", "Cancel drawing · clear selection · end measure"], F),
|
||||
A("plan.zoom", "plan", ["휠"], ["커서 자리 확대 · 축소", "Zoom at cursor"], F),
|
||||
|
||||
+7
-7
@@ -1,24 +1,24 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_ForestLayers.ts
|
||||
* D03 받은 산림 · 환경 자료 겹(PLAN 622-2 · 요구 529) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴).
|
||||
* map_forest_layers.ts
|
||||
* 공용 지도 — 받은 산림 · 환경 자료 겹(PLAN 622-2 · 624-2 · 요구 529 · 530) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴).
|
||||
* 벡터 = 국유림 임도(forest_road · 간선 · 작업 · 산불 색 · 줌 따라 굵기) · 산림입지도(forest_soil · 토양 무리 색 ·
|
||||
* 반투명 · z10 아래 숨김) · 경제림(eco_forest · 국유 · 사유 테두리) /
|
||||
* 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로).
|
||||
* 조각만 쓰는 겹(`TILE_ONLY`) = 받기 없음 · 옛 캔버스(그래픽 카드 끔)엔 안 보임.
|
||||
* 토양 무리 색(`soilColor`) = 조각(`code` = SLTP_CD 두 자리) · 브이월드 요약(기호 「B₁」) 같은 색 — 범례 · 옛 캔버스도 이것.
|
||||
* 부르는 곳 = `D03_PlanAnalysis_UI_MapGl`(꼴 · 소스) · `UI_Layers`(받기 거름) · `UI_LayerLegend`(범례).
|
||||
* 부르는 곳 = `map_layer_specs`(꼴) · D03 `UI_MapGl`(소스) · `UI_Layers`(받기 거름) · `map_legend`(범례).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ExpressionSpecification, SourceSpecification } from "maplibre-gl";
|
||||
import type { LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { tileSource, type GlSpec } from "@ui/ui_template_gl_map";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { PlanLayerKey } from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import type { MapLayerKey } from "./map_layer_list";
|
||||
|
||||
/** 조각만 쓰는 겹 — 받을 자료 없음(위성 포함). */
|
||||
const TILE_ONLY = ["satellite", "nationalRoad", "ecoForest", "ecology"] as const;
|
||||
export type TileOnlyKey = (typeof TILE_ONLY)[number];
|
||||
export const tileOnly = (key: PlanLayerKey): key is TileOnlyKey =>
|
||||
export const tileOnly = (key: MapLayerKey): key is TileOnlyKey =>
|
||||
(TILE_ONLY as readonly string[]).includes(key);
|
||||
|
||||
/** 받는 동안 서클에 세는 조각 소스(등고선 포함). */
|
||||
@@ -89,7 +89,7 @@ const ROUND = { "line-join": "round", "line-cap": "round" } as const;
|
||||
const zoomWidth = (z8: number, z12: number, z15: number): ExpressionSpecification =>
|
||||
["interpolate", ["linear"], ["zoom"], 8, z8, 12, z12, 15, z15] as ExpressionSpecification;
|
||||
|
||||
type Spec = GlSpec<PlanLayerKey>;
|
||||
type Spec = GlSpec<MapLayerKey>;
|
||||
const raster = (key: "landslide" | "ecology", opacity: number): Spec =>
|
||||
({
|
||||
key,
|
||||
@@ -172,7 +172,7 @@ export function forestRoadSpecs(): Spec[] {
|
||||
}
|
||||
|
||||
/** 범례 — 켠 조각 겹(산사태위험 = 범례 자리 `LANDSLIDE_LEGEND` 따로). */
|
||||
export function forestLegendRows(on: Record<PlanLayerKey, boolean>): LegendItem[][] {
|
||||
export function forestLegendRows(on: Record<MapLayerKey, boolean>): LegendItem[][] {
|
||||
const item = (label: string, color: string, kind?: LegendItem["kind"]): LegendItem => ({
|
||||
label,
|
||||
color: `var(${color})`,
|
||||
@@ -0,0 +1,96 @@
|
||||
/* =============================================================================
|
||||
* map_layer_bar.ts
|
||||
* 공용 지도 겹 줄(PLAN 569-2 · 586-2 · 624-2 · 요구 530) — 겹 단추 한 줄(공용 지도 도구줄 `createMapTools` · 묶음 사이 나눔선) +
|
||||
* 끝에 그래픽 카드 단추(GL ↔ 옛 캔버스).
|
||||
* 보일 겹 · 처음 켬 · 취향 열쇠 = 페이지 표(`map_layer_list` `MAP_LAYER_PAGES`) · 누르면 켬끔 뒤집고 취향에 적고 `onToggle`.
|
||||
* 막힘 = 지형을 아직 안 받음(`sync({ ready: false })`) · 조각만 쓰는 겹은 옛 캔버스에서 「안 보임」 글.
|
||||
* 받기 · 그리기 · 범례는 페이지 몫 — 켬끔 값(`on`)은 같은 객체를 같이 씀.
|
||||
* 쓰는 법 = `createMapLayerBar({ page: "d03", glOn, onToggle, onGl })` → `{ root, bar, on, toggle, save, sync }`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createIconButton } from "@ui/ui_template_button";
|
||||
import { createMapTools } from "@ui/ui_template_map_tools";
|
||||
import { writeByKey } from "../b_page_state";
|
||||
import { tileOnly } from "./map_forest_layers";
|
||||
import {
|
||||
MAP_LAYER_PAGES,
|
||||
pageGroups,
|
||||
readLayerPref,
|
||||
type MapLayerKey,
|
||||
type MapPage,
|
||||
} from "./map_layer_list";
|
||||
|
||||
export interface MapLayerBar {
|
||||
/** 도구줄 감싼 것(자리는 페이지 CSS). */
|
||||
root: HTMLElement;
|
||||
/** 도구줄 자체(폭 재기). */
|
||||
bar: HTMLElement;
|
||||
/** 켬끔 — 페이지가 바꾸면 `save` · 받기 실패로 끄는 것은 안 적음. */
|
||||
on: Record<MapLayerKey, boolean>;
|
||||
/** 뒤집고 적고 `onToggle`. */
|
||||
toggle: (key: MapLayerKey) => void;
|
||||
save: () => void;
|
||||
/** 단추 눌림 · 막힘 · 글을 켬끔 값에 맞춤. */
|
||||
sync: (state: { ready: boolean; glOn: boolean }) => void;
|
||||
}
|
||||
|
||||
export function createMapLayerBar(opts: {
|
||||
page: MapPage;
|
||||
glOn: boolean;
|
||||
onToggle: (key: MapLayerKey) => void;
|
||||
/** 그래픽 카드 단추 누름. */
|
||||
onGl: () => void;
|
||||
}): MapLayerBar {
|
||||
const { page } = opts;
|
||||
const on = readLayerPref(page);
|
||||
const save = (): void => writeByKey(MAP_LAYER_PAGES[page].pref, JSON.stringify(on));
|
||||
const toggle = (key: MapLayerKey): void => {
|
||||
on[key] = !on[key];
|
||||
save();
|
||||
opts.onToggle(key);
|
||||
};
|
||||
const defs = pageGroups(page);
|
||||
const buttons = new Map<MapLayerKey, HTMLButtonElement>();
|
||||
const groups = defs.map((group) =>
|
||||
group.map((def) => {
|
||||
const button = createIconButton({
|
||||
icon: def.icon,
|
||||
title: def.title,
|
||||
size: 32,
|
||||
pressed: on[def.key],
|
||||
onClick: () => toggle(def.key),
|
||||
});
|
||||
button.dataset.layer = def.key;
|
||||
buttons.set(def.key, button);
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
const glButton = createIconButton({
|
||||
icon: "gauge",
|
||||
title: "그래픽 카드 그림(바탕 겹 · 글을 GPU 로 · 끔 = 옛 캔버스 그림)",
|
||||
size: 32,
|
||||
pressed: opts.glOn,
|
||||
onClick: opts.onGl,
|
||||
});
|
||||
glButton.dataset.layer = "gl";
|
||||
groups.push([glButton]);
|
||||
const tools = createMapTools({ label: "지도 겹", groups });
|
||||
|
||||
function sync({ ready, glOn }: { ready: boolean; glOn: boolean }): void {
|
||||
for (const def of defs.flat()) {
|
||||
const button = buttons.get(def.key)!;
|
||||
button.setAttribute("aria-pressed", String(on[def.key]));
|
||||
button.disabled = !ready;
|
||||
const title = !ready
|
||||
? `${def.title} — 지형 받기 뒤 보임`
|
||||
: tileOnly(def.key) && !glOn
|
||||
? `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`
|
||||
: def.title;
|
||||
button.title = title;
|
||||
button.setAttribute("aria-label", title);
|
||||
}
|
||||
glButton.setAttribute("aria-pressed", String(glOn));
|
||||
}
|
||||
|
||||
return { root: tools.root, bar: tools.bar, on, toggle, save, sync };
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/* =============================================================================
|
||||
* map_layer_list.ts
|
||||
* 공용 지도 겹 목록(PLAN 624-2 · 요구 530 · 결정 A② · A⑧ · A⑨) — 예정노선(D03) · B05 평면이 같이 씀.
|
||||
* 겹 = 열쇠 · 아이콘 · 이름 · 처음 값(`on` = 예정노선) · 단추 묶음(바탕 · 물 · 길 · 구역 · 사이 나눔선).
|
||||
* 페이지 표(`MAP_LAYER_PAGES`) = 보일 겹(숨길 겹) · 처음 켬(없으면 겹의 `on`) · 켬끔 취향 열쇠.
|
||||
* 선 겹 꼴 · 보호구역 색 · 산사태위험 범례 = GL 꼴(`map_layer_specs`) · 범례(`map_legend`) · 옛 캔버스가 같이 읽음.
|
||||
* 쓰는 법 = 겹 줄 `createMapLayerBar({ page })`(`map_layer_bar`) · 새 겹 = `GROUPS` 한 줄 + 꼴 한 벌.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { IconName } from "@ui/ui_template_icons";
|
||||
import type { LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { readByKey } from "../b_page_state";
|
||||
|
||||
export type MapLayerKey =
|
||||
| "satellite"
|
||||
| "hillshade"
|
||||
| "contour"
|
||||
| "slope"
|
||||
| "stream"
|
||||
| "water"
|
||||
| "road"
|
||||
| "forestRoad"
|
||||
| "forestRoadFile"
|
||||
| "nationalRoad"
|
||||
| "ecoForest"
|
||||
| "ecology"
|
||||
| "cadastral"
|
||||
| "zones"
|
||||
| "soils"
|
||||
| "landslide"
|
||||
| "landslideZones"
|
||||
| "faults"
|
||||
| "lineaments"
|
||||
| "zeroOrder"
|
||||
| "bands"
|
||||
| "building"
|
||||
| "place";
|
||||
|
||||
/** 올린 임도망 고름(605-2) — 묶음 하나 또는 그 안의 선 하나(지도 강조). */
|
||||
export interface RoadPick {
|
||||
bundle: string;
|
||||
line?: number;
|
||||
}
|
||||
|
||||
export interface LayerDef {
|
||||
key: MapLayerKey;
|
||||
icon: IconName;
|
||||
title: string;
|
||||
/** 예정노선 처음 값 — 다른 페이지는 `MAP_LAYER_PAGES[page].on`. */
|
||||
on: boolean;
|
||||
}
|
||||
|
||||
/** 단추 묶음(사이 나눔선) — 바탕 · 물 · 길 · 구역. 「지금」 겹 처음 값 = 끔(`_분석_440-C` 1-4). */
|
||||
export const GROUPS: readonly (readonly LayerDef[])[] = [
|
||||
[
|
||||
{ key: "satellite", icon: "satellite", title: "위성사진", on: true },
|
||||
{ key: "hillshade", icon: "sun", title: "음영기복(2 m 지형 · 산줄기 · 골)", on: false },
|
||||
{ key: "contour", icon: "mountain", title: "도엽 등고선(굵은 선 = 계곡선 25 m)", on: true },
|
||||
{ key: "slope", icon: "angle", title: "급경사 색(기울기 % 범위 · 범례 ⚙ 로 바꿈)", on: false },
|
||||
],
|
||||
[
|
||||
{ key: "stream", icon: "droplet", title: "세류선(가는 파랑) · 하천(굵은 파랑)", on: true },
|
||||
{ key: "water", icon: "ripple", title: "물 면(도엽 실폭하천 · 저수지 등)", on: false },
|
||||
{ key: "road", icon: "road", title: "기존 도로(도엽 도로중심선)", on: false },
|
||||
{
|
||||
key: "forestRoad",
|
||||
icon: "route",
|
||||
title: "기존 임도 짐작(도엽 도로 소로 · 미분류 = 주황 점선 · 정식 임도망도 아님)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "forestRoadFile",
|
||||
icon: "map",
|
||||
title:
|
||||
"올린 임도망도(산림청 시군구 SHP = 밝은 분홍 실선 · 왼쪽 「위치」 칸에서 올리고 묶음 고름 · 지움)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "nationalRoad",
|
||||
icon: "flag",
|
||||
title:
|
||||
"국유림 임도(산림청 전국 국가임도 · 간선 · 작업 · 산불 색 · 지방 · 민간임도 없음 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{ key: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false },
|
||||
{
|
||||
key: "zones",
|
||||
icon: "shield",
|
||||
title: "보호 · 규제 구역(브이월드 여덟 종 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "soils",
|
||||
icon: "leaf",
|
||||
title: "산림입지도(토양형 · 산림청 산림입지토양도 1:25,000 · 줌 10 이상 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "ecoForest",
|
||||
icon: "trees",
|
||||
title: "경제림육성단지(산림청 2014 · 국유 · 사유 테두리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "ecology",
|
||||
icon: "plant",
|
||||
title: "생태자연도(환경부 · 1~3 등급 · 별도관리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "landslide",
|
||||
icon: "alert",
|
||||
title: "산사태위험지도(1~5 등급 · 행안부 생활안전지도 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "landslideZones",
|
||||
icon: "alertOctagon",
|
||||
title: "산사태취약지역(시도 지정 현황 · 점 + 지번 필지 테두리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{ key: "faults", icon: "bolt", title: "단층(브이월드 지질 · 참고용)", on: false },
|
||||
{
|
||||
key: "lineaments",
|
||||
icon: "lineDashed",
|
||||
title: "지질구조선(브이월드 지질 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "zeroOrder",
|
||||
icon: "arrowsJoin",
|
||||
title: "0차곡(지형 추정 골짜기 머리 · 위 집수 0.1~1 ha · 곡두 통과 피함)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "bands",
|
||||
icon: "borderOuter",
|
||||
title: "계류 100 m · 도로 300 m 띠(타당성평가)",
|
||||
on: false,
|
||||
},
|
||||
{ key: "building", icon: "building", title: "건물(도엽 · 하류 민가 · 인가)", on: false },
|
||||
{ key: "place", icon: "mapPin", title: "지명(도엽 계곡 · 령 · 부락)", on: false },
|
||||
],
|
||||
];
|
||||
export const LAYERS: readonly LayerDef[] = GROUPS.flat();
|
||||
|
||||
/** 지도가 있는 페이지. */
|
||||
export type MapPage = "d03" | "b05";
|
||||
|
||||
/** 페이지 표 — 숨길 겹 · 처음 켬(없음 = 겹의 `on`) · 켬끔 취향 열쇠. */
|
||||
export const MAP_LAYER_PAGES: Record<
|
||||
MapPage,
|
||||
{ hide: readonly MapLayerKey[]; on?: readonly MapLayerKey[]; pref: string }
|
||||
> = {
|
||||
d03: { hide: [], pref: "aislo:pref:d03.layers" },
|
||||
// A② 처음 = 지금 그대로(등고선 · 하천) · A⑨ 띠(타당성) · 올린 임도망도 숨김
|
||||
b05: {
|
||||
hide: ["bands", "forestRoadFile"],
|
||||
on: ["contour", "stream"],
|
||||
pref: "aislo:pref:b05.layers",
|
||||
},
|
||||
};
|
||||
|
||||
/** 그 페이지에 보일 겹 묶음(빈 묶음 뺌). */
|
||||
export const pageGroups = (page: MapPage): LayerDef[][] =>
|
||||
GROUPS.map((group) =>
|
||||
group.filter((def) => !MAP_LAYER_PAGES[page].hide.includes(def.key)),
|
||||
).filter((group) => group.length);
|
||||
|
||||
/** 켬끔 취향 — 없거나 깨지면 페이지 처음 값 · 숨긴 겹 = 끔. */
|
||||
export function readLayerPref(page: MapPage): Record<MapLayerKey, boolean> {
|
||||
const { hide, on: first, pref } = MAP_LAYER_PAGES[page];
|
||||
const on = Object.fromEntries(
|
||||
LAYERS.map((def) => [def.key, first ? first.includes(def.key) : def.on]),
|
||||
) as Record<MapLayerKey, boolean>;
|
||||
try {
|
||||
const saved = JSON.parse(readByKey(pref) ?? "{}") as Partial<Record<MapLayerKey, unknown>>;
|
||||
for (const def of LAYERS)
|
||||
if (typeof saved[def.key] === "boolean") on[def.key] = saved[def.key] as boolean;
|
||||
} catch {
|
||||
// 깨진 값 = 처음 값
|
||||
}
|
||||
for (const key of hide) on[key] = false;
|
||||
return on;
|
||||
}
|
||||
|
||||
/** 보호구역 종류 → 그림 색 값(서버 `ZONE_KINDS` 열쇠). */
|
||||
export const ZONE_COLORS: Record<string, string> = {
|
||||
forest: "--draw-map-zone-forest",
|
||||
baekdu: "--draw-map-zone-baekdu",
|
||||
water: "--draw-map-zone-water",
|
||||
park: "--draw-map-zone-park",
|
||||
heritage: "--draw-map-zone-heritage",
|
||||
wetland: "--draw-map-zone-wetland",
|
||||
wildlife: "--draw-map-zone-wildlife",
|
||||
greenbelt: "--draw-map-zone-greenbelt",
|
||||
};
|
||||
/** 산사태위험지도 범례(1 등급 = 가장 위험 · 색 = WMS 그림 색). */
|
||||
export const LANDSLIDE_LEGEND: LegendItem[] = [1, 2, 3, 4, 5].map((grade) => ({
|
||||
label: `산사태 ${grade}등급`,
|
||||
color: `var(--draw-map-landslide-grade-${grade})`,
|
||||
}));
|
||||
/** 선 겹 — 이름 · 그림 꼴(서버 `LINE_KINDS` 열쇠 = `kind` · 없으면 0차곡). 새 선 겹 = 여기 한 줄. */
|
||||
export type LineLayerKey = "faults" | "lineaments" | "zeroOrder";
|
||||
export const LINE_LAYERS: Record<
|
||||
LineLayerKey,
|
||||
{ label: string; kind?: string; color: string; width: number; dash: number[] }
|
||||
> = {
|
||||
faults: { label: "단층", kind: "faults", color: "--draw-map-fault", width: 2.4, dash: [] },
|
||||
lineaments: {
|
||||
label: "지질구조선",
|
||||
kind: "lineaments",
|
||||
color: "--draw-map-lineament",
|
||||
width: 1.8,
|
||||
dash: [8, 4],
|
||||
},
|
||||
zeroOrder: { label: "0차곡", color: "--draw-map-zero-order", width: 1.8, dash: [4, 3] },
|
||||
};
|
||||
export const LINE_KEYS = Object.keys(LINE_LAYERS) as LineLayerKey[];
|
||||
@@ -0,0 +1,317 @@
|
||||
/* =============================================================================
|
||||
* map_layer_specs.ts
|
||||
* 공용 지도 GL 겹 꼴(PLAN 586-2 · 624-2 · 요구 530) — 바탕 겹 스물의 MapLibre 꼴 · 차례(아래 → 위):
|
||||
* 위성 · 그림 겹(음영 · 급경사 · 산사태위험 · 생태자연도) · 면 겹(산림입지 · 경제림 · 보호구역 · 띠 · 물 면 · 건물) ·
|
||||
* 선 겹(지적 · 등고선 · 도로 · 임도 짐작 · 올린 임도망도 · 국유림 임도 · 세류 · 단층 · 지질구조선 · 0차곡 · 산사태취약 필지) ·
|
||||
* 점 · 글(산사태취약 · 지명 · 등고 수치).
|
||||
* 자료 = 받는 쪽이 지물에 붙인 `_color` · 고정 소스(위성 · 등고 · 산림 조각) 이름 = `map_forest_layers` · `ui_template_gl_map`.
|
||||
* 카메라 · 지도 세우기는 페이지 몫(D03 `UI_MapGl.createGlBase` · B05 `createTmGl`) — 여기는 꼴만.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ExpressionSpecification, FilterSpecification } from "maplibre-gl";
|
||||
import { GL_FONT, type GlSpec as GlMapSpec } from "@ui/ui_template_gl_map";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { forestAreaSpecs, forestRasterSpecs, forestRoadSpecs } from "./map_forest_layers";
|
||||
import {
|
||||
LINE_KEYS,
|
||||
LINE_LAYERS,
|
||||
type LineLayerKey,
|
||||
type MapLayerKey,
|
||||
type RoadPick,
|
||||
} from "./map_layer_list";
|
||||
|
||||
/** 자료 소스(임도 짐작 = 도로 소스를 거름 · 위성 = 조각). */
|
||||
export type GlSource = Exclude<MapLayerKey, "satellite" | "forestRoad">;
|
||||
|
||||
const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT];
|
||||
|
||||
type GlSpec = GlMapSpec<MapLayerKey>;
|
||||
|
||||
const HALO = "rgba(0, 0, 0, 0.55)";
|
||||
const ROUND = { "line-join": "round", "line-cap": "round" } as const;
|
||||
const COLOR: ExpressionSpecification = ["get", "_color"];
|
||||
/** 등고 조각 굵은 선(서버가 줌마다 정함) — 가는 선은 모두라 굵은 선은 두 번 겹쳐 짙음(캔버스처럼). */
|
||||
const BOLD = ["==", ["get", "k"], 1] as unknown as FilterSpecification;
|
||||
/** 캔버스 점선(px) → MapLibre 점선(선 굵기 배수). */
|
||||
const dash = (px: number[], width: number): number[] => px.map((v) => v / width);
|
||||
|
||||
/** 선 겹 — 검은 테두리 + 선(점선이면 둘 다 같은 px 무늬). */
|
||||
function haloLine(
|
||||
key: MapLayerKey,
|
||||
source: GlSource,
|
||||
color: string,
|
||||
width: number,
|
||||
pattern: number[] = [],
|
||||
halo = { color: HALO, width: width + 1.6 },
|
||||
filter?: FilterSpecification,
|
||||
): GlSpec[] {
|
||||
const line = (id: string, paint: Record<string, unknown>, w: number): GlSpec =>
|
||||
({
|
||||
key,
|
||||
id,
|
||||
type: "line",
|
||||
source,
|
||||
...(filter ? { filter } : {}),
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
...paint,
|
||||
"line-width": w,
|
||||
...(pattern.length ? { "line-dasharray": dash(pattern, w) } : {}),
|
||||
},
|
||||
}) as GlSpec;
|
||||
return [
|
||||
line(`${key}-halo`, { "line-color": halo.color }, halo.width),
|
||||
line(key, { "line-color": color }, width),
|
||||
];
|
||||
}
|
||||
|
||||
/** 면 겹 — 채움 + 테두리(캔버스 `fillLayer` 처럼 테두리 투명도 0.9). */
|
||||
function fillEdge(
|
||||
key: MapLayerKey,
|
||||
source: GlSource,
|
||||
fill: string | ExpressionSpecification,
|
||||
opacity: number | ExpressionSpecification,
|
||||
edge: { color: string | ExpressionSpecification; width: number; dash?: number[] },
|
||||
): GlSpec[] {
|
||||
return [
|
||||
{
|
||||
key,
|
||||
id: key,
|
||||
type: "fill",
|
||||
source,
|
||||
paint: { "fill-color": fill, "fill-opacity": opacity },
|
||||
} as GlSpec,
|
||||
{
|
||||
key,
|
||||
id: `${key}-edge`,
|
||||
type: "line",
|
||||
source,
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": edge.color,
|
||||
"line-width": edge.width,
|
||||
"line-opacity": 0.9,
|
||||
...(edge.dash ? { "line-dasharray": dash(edge.dash, edge.width) } : {}),
|
||||
},
|
||||
} as GlSpec,
|
||||
];
|
||||
}
|
||||
|
||||
/** 겹 차례(아래 → 위)와 꼴 — 옛 캔버스(D03 `UI_LayerPaint`)의 색 · 굵기 · 투명도 · 차례 그대로 · `light` = 위성 밝기 상한. */
|
||||
export function mapLayerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
const contour = themeColor("--draw-map-contour", "#ffd68c");
|
||||
const place = themeColor("--draw-map-place", "#f8fafc");
|
||||
const landslideZone = themeColor("--draw-map-landslide-zone", "#e11d48");
|
||||
const raster = (
|
||||
key: "satellite" | "hillshade" | "slope",
|
||||
opacity: number,
|
||||
extra: Record<string, number> = {},
|
||||
): GlSpec =>
|
||||
({
|
||||
key,
|
||||
id: key,
|
||||
type: "raster",
|
||||
source: key,
|
||||
paint: { "raster-opacity": opacity, "raster-fade-duration": 0, ...extra },
|
||||
}) as GlSpec;
|
||||
const lineKind = (key: LineLayerKey): GlSpec[] => {
|
||||
const { color, width, dash } = LINE_LAYERS[key];
|
||||
return haloLine(key, key, themeColor(color, "#f43f5e"), width, dash);
|
||||
};
|
||||
return [
|
||||
// 위성 = 조금 어둡고 연하게(요구 475 · 노선 선 대비) — 밝기 상한은 사용자 조절
|
||||
raster("satellite", 1, { "raster-brightness-max": light, "raster-saturation": SAT_SATURATION }),
|
||||
raster("hillshade", 1), // 위성 위 = 「곱하기」와 같은 검은 그늘 그림(받는 쪽이 바꿈)
|
||||
raster("slope", 0.55),
|
||||
...forestRasterSpecs(), // 산사태위험 · 생태자연도 = 조각 그림(622-2)
|
||||
...forestAreaSpecs(), // 산림입지도 · 경제림 = 벡터 조각
|
||||
...fillEdge("zones", "zones", COLOR, 0.22, { color: COLOR, width: 1.6 }),
|
||||
...fillEdge("bands", "bands", COLOR, ["case", ["==", ["get", "_band"], "road"], 0.16, 0.18], {
|
||||
color: COLOR,
|
||||
width: 1.2,
|
||||
dash: [6, 4],
|
||||
}),
|
||||
{
|
||||
key: "cadastral",
|
||||
id: "cadastral",
|
||||
type: "line",
|
||||
source: "cadastral",
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": themeColor("--draw-map-parcel", "#f0abfc"),
|
||||
"line-width": 0.8,
|
||||
"line-opacity": 0.8,
|
||||
},
|
||||
} as GlSpec,
|
||||
...((): GlSpec[] => {
|
||||
const water = themeColor("--draw-map-water", "#0ea5e9");
|
||||
return fillEdge("water", "water", water, 0.45, { color: water, width: 1 });
|
||||
})(),
|
||||
...fillEdge("building", "building", themeColor("--draw-map-building", "#fb7185"), 0.7, {
|
||||
color: HALO,
|
||||
width: 0.6,
|
||||
}),
|
||||
{
|
||||
key: "contour",
|
||||
id: "contour-minor",
|
||||
type: "line",
|
||||
source: "contour",
|
||||
"source-layer": "contour",
|
||||
layout: ROUND,
|
||||
paint: { "line-color": contour, "line-width": 0.8, "line-opacity": 0.7 },
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "contour",
|
||||
id: "contour-index",
|
||||
type: "line",
|
||||
source: "contour",
|
||||
"source-layer": "contour",
|
||||
filter: BOLD,
|
||||
layout: ROUND,
|
||||
paint: { "line-color": contour, "line-width": 1.6, "line-opacity": 0.7 },
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "contour",
|
||||
id: "contour-label",
|
||||
type: "symbol",
|
||||
source: "contour",
|
||||
"source-layer": "contour",
|
||||
filter: BOLD,
|
||||
layout: {
|
||||
"symbol-placement": "line",
|
||||
"symbol-spacing": 320,
|
||||
"text-field": ["to-string", ["get", "h"]],
|
||||
"text-font": [GL_FONT],
|
||||
"text-size": 10,
|
||||
"text-max-angle": 30,
|
||||
"text-padding": 2,
|
||||
},
|
||||
paint: {
|
||||
"text-color": contour,
|
||||
"text-halo-color": "rgba(0, 0, 0, 0.6)",
|
||||
"text-halo-width": 1.5,
|
||||
},
|
||||
} as GlSpec,
|
||||
...haloLine("road", "road", themeColor("--draw-map-road", "#f8fafc"), 1.6, [], {
|
||||
color: HALO,
|
||||
width: 3.2,
|
||||
}),
|
||||
...haloLine(
|
||||
"forestRoad",
|
||||
"road",
|
||||
themeColor("--draw-map-forest-road", "#f59e0b"),
|
||||
2,
|
||||
[7, 4],
|
||||
{ color: HALO, width: 3.6 },
|
||||
[
|
||||
"in",
|
||||
["get", "도로구분"],
|
||||
["literal", ["소로", "미분류"]],
|
||||
] as unknown as FilterSpecification,
|
||||
),
|
||||
...haloLine(
|
||||
"forestRoadFile",
|
||||
"forestRoadFile",
|
||||
themeColor("--draw-map-forest-road-file", "#ff2fd0"),
|
||||
2.4,
|
||||
[],
|
||||
{ color: HALO, width: 4 },
|
||||
),
|
||||
...forestRoadSpecs(), // 국유림 임도 = 벡터 조각
|
||||
// 고른 묶음 · 선 강조(605-2) — 같은 자료에서 걸러 한 겹 더 굵게
|
||||
...["-pick-halo", "-pick"].map(
|
||||
(tail, i) =>
|
||||
({
|
||||
key: "forestRoadFile",
|
||||
id: `forestRoadFile${tail}`,
|
||||
type: "line",
|
||||
source: "forestRoadFile",
|
||||
filter: pickFilter(pick),
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": i ? themeColor("--draw-map-sketch", "#ffe94d") : HALO,
|
||||
"line-width": i ? 4.2 : 7.4,
|
||||
},
|
||||
}) as GlSpec,
|
||||
),
|
||||
{
|
||||
key: "stream",
|
||||
id: "stream",
|
||||
type: "line",
|
||||
source: "stream",
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": themeColor("--draw-map-stream", "#38bdf8"),
|
||||
"line-width": ["case", ["==", ["get", "구분"], "세류"], 1, 2.4],
|
||||
},
|
||||
} as GlSpec,
|
||||
...LINE_KEYS.flatMap(lineKind),
|
||||
// 산사태취약 — 필지 = 「이 필지 안 어딘가」 테두리만 · 점 = 지정 자리
|
||||
...haloLine(
|
||||
"landslideZones",
|
||||
"landslideZones",
|
||||
landslideZone,
|
||||
1.8,
|
||||
[6, 3],
|
||||
{ color: "rgba(255, 255, 255, 0.7)", width: 3.2 },
|
||||
["!=", ["geometry-type"], "Point"] as unknown as FilterSpecification,
|
||||
),
|
||||
{
|
||||
key: "landslideZones",
|
||||
id: "landslideZones-point",
|
||||
type: "circle",
|
||||
source: "landslideZones",
|
||||
filter: ["==", ["geometry-type"], "Point"],
|
||||
paint: {
|
||||
"circle-radius": 5,
|
||||
"circle-color": landslideZone,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-width": 1.5,
|
||||
},
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "place",
|
||||
id: "place-dot",
|
||||
type: "circle",
|
||||
source: "place",
|
||||
paint: { "circle-radius": 2.5, "circle-color": place },
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "place",
|
||||
id: "place",
|
||||
type: "symbol",
|
||||
source: "place",
|
||||
layout: {
|
||||
"text-field": ["get", "name"],
|
||||
"text-font": GL_FONT_BOLD,
|
||||
"text-size": 11,
|
||||
"text-anchor": "bottom",
|
||||
"text-offset": [0, -0.36],
|
||||
"text-padding": 2,
|
||||
},
|
||||
paint: {
|
||||
"text-color": place,
|
||||
"text-halo-color": "rgba(0, 0, 0, 0.7)",
|
||||
"text-halo-width": 1.5,
|
||||
},
|
||||
} as GlSpec,
|
||||
];
|
||||
}
|
||||
|
||||
/** 고른 묶음 · 선만 거름(고름 없음 = 아무것도 안 맞는 거름). */
|
||||
export function pickFilter(pick: RoadPick | null): FilterSpecification {
|
||||
const all: unknown[] = ["all", ["==", ["get", "__bundle"], pick?.bundle ?? ""]];
|
||||
if (pick?.line !== undefined) all.push(["==", ["get", "__line"], pick.line]);
|
||||
return all as unknown as FilterSpecification;
|
||||
}
|
||||
|
||||
/** 겹 색 지문 — 밝음 · 어두움이 바뀌면 색을 다시. */
|
||||
export const MAP_THEME_NAMES = [
|
||||
"--draw-map-contour",
|
||||
"--draw-map-road",
|
||||
"--draw-map-stream",
|
||||
"--draw-map-parcel",
|
||||
] as const;
|
||||
|
||||
/** 위성 채도(−1~1) — 조금 연하게. */
|
||||
const SAT_SATURATION = -0.3;
|
||||
@@ -0,0 +1,36 @@
|
||||
/* =============================================================================
|
||||
* map_legend.ts
|
||||
* 공용 지도 범례(PLAN 569-2 · 622-2 · 624-2 · 요구 530) — 켠 겹 범례 칸 하나(줄마다 `createLegend`) + 페이지가 붙이는 덧 줄.
|
||||
* 조각 겹 범례(산사태위험 · 국유림 임도 · 경제림 · 생태자연도) = 받을 자료 없음 → 켬끔만 보고 여기서 만듦(`tileLegendRows`).
|
||||
* 받은 자료 범례(수 · 넓이 · 급경사 범위)는 받는 페이지 몫 — 줄을 만들어 `show` 에 넘김.
|
||||
* 쓰는 법 = `const legend = createMapLegend("d03-plan__legend")` → `legend.show(rows, [덧 줄])` · 줄 · 덧 없으면 숨김.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createLegend, type LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { forestLegendRows } from "./map_forest_layers";
|
||||
import { LANDSLIDE_LEGEND, type MapLayerKey } from "./map_layer_list";
|
||||
|
||||
export interface MapLegend {
|
||||
root: HTMLElement;
|
||||
show: (rows: LegendItem[][], tail?: HTMLElement[]) => void;
|
||||
}
|
||||
|
||||
/** 켠 조각 겹 범례 — 산사태위험 다음 산림 셋. */
|
||||
export const tileLegendRows = (on: Record<MapLayerKey, boolean>): LegendItem[][] => [
|
||||
...(on.landslide ? [LANDSLIDE_LEGEND] : []),
|
||||
...forestLegendRows(on),
|
||||
];
|
||||
|
||||
/** `className` = 자리 · 꼴(페이지 CSS). */
|
||||
export function createMapLegend(className: string): MapLegend {
|
||||
const root = document.createElement("div");
|
||||
root.className = className;
|
||||
root.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이";
|
||||
return {
|
||||
root,
|
||||
show(rows, tail = []) {
|
||||
root.replaceChildren(...rows.map((row) => createLegend(row)), ...tail);
|
||||
root.hidden = !rows.length && !tail.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -257,21 +257,26 @@ def _apply_new_defaults(
|
||||
if definition is None:
|
||||
continue
|
||||
options = dict(point.options or {})
|
||||
given = set(options)
|
||||
for field in definition.options:
|
||||
if field.key not in options and field.default not in (None, ""):
|
||||
options[field.key] = field.default
|
||||
# 집수정 모양도 실제 값으로(54-39 · 54-36 기본 L형 — 화면 짝 `DEFAULT_BASIN_SHAPE`)
|
||||
if options.get("inlet_type") == "집수정":
|
||||
if "pipe_diameter_mm" not in given and isinstance(options.get("pipe_diameter_mm"), str):
|
||||
options["pipe_diameter_mm"] = int(_float(options["pipe_diameter_mm"])) # 폼 = 수(623)
|
||||
# 집수정 = 모양 L(54-39 · `DEFAULT_BASIN_SHAPE`) · 동물이동형 유 · 유입 흙막이 없음(623)
|
||||
basin = options.get("inlet_type") == "집수정"
|
||||
if basin:
|
||||
options.setdefault("inlet_structure", "L")
|
||||
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · 화면 짝 `PIPE_HEIGHT_KEYS`)
|
||||
options.setdefault("inlet_basin_animal", "true")
|
||||
for key in [k for k in options if k.startswith("inlet_revet_") and k not in given]:
|
||||
del options[key]
|
||||
# 보호공 · 집수정 높이 = 관경 + 0.5(139-1 · `PIPE_HEIGHT_KEYS`) · 유입 흙막이 + 0.7(211-1)
|
||||
keys = {field.key for field in definition.options}
|
||||
diameter_m = _float(options.get("pipe_diameter_mm")) / 1000
|
||||
if diameter_m > 0:
|
||||
for key in PIPE_HEIGHT_KEYS:
|
||||
if key in keys:
|
||||
side = (
|
||||
"inlet" if key == "inlet_revet_height_m" else ""
|
||||
) # 유입 흙막이 + 0.7(211-1)
|
||||
if key in keys and not (basin and key.startswith("inlet_revet_")):
|
||||
side = "inlet" if key == "inlet_revet_height_m" else ""
|
||||
options.setdefault(key, pipe_wall_min_height_m(diameter_m, side))
|
||||
if point.facility == "ford_bridge" and "ford_height_m" not in options:
|
||||
flow = (flows or {}).get(round(point.chainage_m, 2))
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 긋기는 해를 안 물음(585-3) — 새 선 = 사업연도 시작 해 · 갈래 = 붙은 자리 구간 해 · 이어 긋기 = 끝 구간이 늘어남 ·
|
||||
* 여러 해 = 다 그은 뒤 그리기 칸 「연도 구역 마커」를 선 위로 끌어 구역을 나누고 속성 판에서 해를 고름.
|
||||
* 고르기 · 고치기 · 마커(쉴 때) = `D03_PlanAnalysis_UI_Edit`.
|
||||
* 키 = Enter 끝 · Esc 이번 긋기 취소 · Backspace 마지막 점 빼기(설정 창 단축키 표 D03 줄).
|
||||
* 키 = Enter 끝 · Esc 이번 긋기 취소 · Backspace 마지막 점 빼기 · Tab 방식 바꿈(설정 창 단축키 표 D03 줄).
|
||||
* 셈 = 562-1 짝(`logic/D03_PlanAnalysis_Draw`) · 꼴 다루기 = `logic/D03_PlanAnalysis_Plan` ·
|
||||
* 값을 바꾸면 `commit(다음 꼴)` 한 번 = 되돌리기 한 걸음(스택 · 캐시 = 페이지).
|
||||
* ========================================================================== */
|
||||
@@ -120,7 +120,8 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
let strokeStart: PlanNetwork | null = null; // Esc 로 돌아갈 꼴
|
||||
let preview: DrawPreview | null = null;
|
||||
let candidates: Vertex[] = [];
|
||||
let kind: DrawConstraint = "free";
|
||||
let kind: DrawConstraint = "both"; // 625-1 · 기억 안 함(B9)
|
||||
let hoverAt: { px: number; py: number; map: ScreenMap } | null = null; // Tab 뒤 미리보기 다시
|
||||
let lengthM = DRAW_DEFAULT_LENGTH_M;
|
||||
let slopePct = DRAW_DEFAULT_SLOPE_PCT;
|
||||
// 다시 긋기 — 선 id · 앞 · 뒤 꺾임점 · 새로 찍은 속 점 · 그 기록
|
||||
@@ -472,9 +473,11 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
return false;
|
||||
},
|
||||
hover(px, py, map) {
|
||||
hoverAt = { px, py, map };
|
||||
preview = mode === "draw" ? previewAt(px, py, map) : null;
|
||||
},
|
||||
leave() {
|
||||
hoverAt = null;
|
||||
preview = null;
|
||||
},
|
||||
};
|
||||
@@ -501,6 +504,13 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
panelState();
|
||||
},
|
||||
key(event) {
|
||||
// Tab = 자유지정 ↔ 길이·기울기 고정(단독 고정이면 길이·기울기로 · 625-1) · 긋기 아닐 때 = 초점 이동
|
||||
if (mode === "draw" && keyIs(event, "plan.drawKind")) {
|
||||
kind = kind === "both" ? "free" : "both";
|
||||
kindFields.set({ kind });
|
||||
if (hoverAt) preview = previewAt(hoverAt.px, hoverAt.py, hoverAt.map);
|
||||
return true;
|
||||
}
|
||||
if (mode === "draw" && keyIs(event, "plan.drawFinish")) {
|
||||
if (span) showToast("다시 긋기는 뒤 노드에 붙어야 끝 — 취소했습니다.", "info");
|
||||
setMode("idle");
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* D03_PlanAnalysis_UI_DrawPanel.ts
|
||||
* D03 좌측 「그리기」 칸 몸(PLAN 599-1 · 599-4 · 요구 463-10 · 22) — [긋기] · 연도 구역 마커 · 긋기 방식 칸.
|
||||
*
|
||||
* 방식 칸 `createDrawKindFields` = 마디 고름 2 × 2(자유 · 길이 고정 · 기울기 고정 · 길이·기울기 고정) +
|
||||
* 길이 · 기울기 두 열(기울기 아래 ≈ °) — 그리기 칸 · 노드 판(598-4)이 같은 부품을 씀.
|
||||
* 방식 칸 `createDrawKindFields` = 마디 고름 2 × 2(자유지정 · 길이·기울기 고정 / 길이 고정 · 기울기 고정 ·
|
||||
* 625-1) + 길이 · 기울기 두 열(기울기 아래 ≈ °) — 방식에 안 쓰는 칸은 숨김(막힘 = 권한 없음만) ·
|
||||
* 그리기 칸 · 노드 판(598-4)이 같은 부품을 씀.
|
||||
* 연도 구역 마커 = 끌어 선 위에 놓기(`YEAR_MARK_DRAG` · 받음 = `D03_PlanAnalysis_UI_Edit` drop).
|
||||
* 설명 글 없음(요구 463-6) — 칸 오류 · ≈ ° 값만.
|
||||
* ========================================================================== */
|
||||
@@ -29,10 +30,10 @@ export interface DrawKindValue {
|
||||
}
|
||||
|
||||
export const DRAW_KIND_LABELS: Record<DrawConstraint, string> = {
|
||||
free: "자유",
|
||||
free: "자유지정",
|
||||
both: "길이·기울기 고정",
|
||||
length: "길이 고정",
|
||||
slope: "기울기 고정",
|
||||
both: "길이·기울기 고정",
|
||||
};
|
||||
|
||||
export interface DrawKindFields {
|
||||
@@ -118,8 +119,10 @@ export function createDrawKindFields(opts: {
|
||||
|
||||
function fieldsState(): void {
|
||||
for (const button of segment.root.querySelectorAll("button")) button.disabled = disabled;
|
||||
lengthField.input.disabled = disabled || value.kind === "free" || value.kind === "slope";
|
||||
slopeField.input.disabled = disabled || value.kind === "free" || value.kind === "length";
|
||||
lengthField.input.disabled = slopeField.input.disabled = disabled;
|
||||
fields.hidden = value.kind === "free";
|
||||
lengthField.root.hidden = value.kind === "slope";
|
||||
slopeColumn.hidden = value.kind === "length";
|
||||
}
|
||||
degText();
|
||||
fieldsState();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_LayerDefs.ts
|
||||
* D03 바탕 겹 정의(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2) — 겹 단추 묶음 · 처음 값 · 보호구역 · 토양형 색 ·
|
||||
* 산사태위험지도 범례 · 선 겹(단층 · 지질구조선 · 0차곡) 꼴 · 켬끔 취향 읽기 · 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 ·
|
||||
* GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림).
|
||||
* D03 바탕 겹 받기 · 옛 캔버스 도움(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2 · 624-2) — 임도 짐작 구분 · 물 면 ·
|
||||
* 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 · GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림).
|
||||
* 겹 목록 · 처음 값 · 켬끔 취향 · 선 겹 꼴 · 보호구역 색 = 공용 `A00_Common/map/map_layer_list`(624-2).
|
||||
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기 차례 · 범례).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { IconName } from "@ui/ui_template_icons";
|
||||
import type { LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { readByKey } from "../A00_Common/b_page_state";
|
||||
import type { MapLayerKey } from "../A00_Common/map/map_layer_list";
|
||||
import {
|
||||
drawPreparedLayer,
|
||||
normalizedToScreen,
|
||||
@@ -20,172 +18,15 @@ import type { PlanRaster } from "./D03_PlanAnalysis_Api";
|
||||
import type { ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
import type { GlImage } from "./D03_PlanAnalysis_UI_MapGl";
|
||||
|
||||
export type PlanLayerKey =
|
||||
| "satellite"
|
||||
| "hillshade"
|
||||
| "contour"
|
||||
| "slope"
|
||||
| "stream"
|
||||
| "water"
|
||||
| "road"
|
||||
| "forestRoad"
|
||||
| "forestRoadFile"
|
||||
| "nationalRoad"
|
||||
| "ecoForest"
|
||||
| "ecology"
|
||||
| "cadastral"
|
||||
| "zones"
|
||||
| "soils"
|
||||
| "landslide"
|
||||
| "landslideZones"
|
||||
| "faults"
|
||||
| "lineaments"
|
||||
| "zeroOrder"
|
||||
| "bands"
|
||||
| "building"
|
||||
| "place";
|
||||
/** 겹 목록 · 페이지 표 = 공용 `map_layer_list`(624-2) — 옛 이름으로 다시 내놓음(캔버스 · 조각 · 임도망 목록이 읽음). */
|
||||
export type PlanLayerKey = MapLayerKey;
|
||||
export {
|
||||
LINE_KEYS,
|
||||
LINE_LAYERS,
|
||||
ZONE_COLORS,
|
||||
type RoadPick,
|
||||
} from "../A00_Common/map/map_layer_list";
|
||||
|
||||
/** 올린 임도망 고름(605-2) — 묶음 하나 또는 그 안의 선 하나(지도 강조). */
|
||||
export interface RoadPick {
|
||||
bundle: string;
|
||||
line?: number;
|
||||
}
|
||||
|
||||
export interface LayerDef {
|
||||
key: PlanLayerKey;
|
||||
icon: IconName;
|
||||
title: string;
|
||||
on: boolean;
|
||||
}
|
||||
|
||||
/** 단추 묶음(사이 나눔선) — 바탕 · 물 · 길 · 구역. 「지금」 겹 처음 값 = 끔(`_분석_440-C` 1-4). */
|
||||
export const GROUPS: readonly (readonly LayerDef[])[] = [
|
||||
[
|
||||
{ key: "satellite", icon: "satellite", title: "위성사진", on: true },
|
||||
{ key: "hillshade", icon: "sun", title: "음영기복(2 m 지형 · 산줄기 · 골)", on: false },
|
||||
{ key: "contour", icon: "mountain", title: "도엽 등고선(굵은 선 = 계곡선 25 m)", on: true },
|
||||
{ key: "slope", icon: "angle", title: "급경사 색(기울기 % 범위 · 범례 ⚙ 로 바꿈)", on: false },
|
||||
],
|
||||
[
|
||||
{ key: "stream", icon: "droplet", title: "세류선(가는 파랑) · 하천(굵은 파랑)", on: true },
|
||||
{ key: "water", icon: "ripple", title: "물 면(도엽 실폭하천 · 저수지 등)", on: false },
|
||||
{ key: "road", icon: "road", title: "기존 도로(도엽 도로중심선)", on: false },
|
||||
{
|
||||
key: "forestRoad",
|
||||
icon: "route",
|
||||
title: "기존 임도 짐작(도엽 도로 소로 · 미분류 = 주황 점선 · 정식 임도망도 아님)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "forestRoadFile",
|
||||
icon: "map",
|
||||
title:
|
||||
"올린 임도망도(산림청 시군구 SHP = 밝은 분홍 실선 · 왼쪽 「위치」 칸에서 올리고 묶음 고름 · 지움)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "nationalRoad",
|
||||
icon: "flag",
|
||||
title:
|
||||
"국유림 임도(산림청 전국 국가임도 · 간선 · 작업 · 산불 색 · 지방 · 민간임도 없음 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{ key: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false },
|
||||
{
|
||||
key: "zones",
|
||||
icon: "shield",
|
||||
title: "보호 · 규제 구역(브이월드 여덟 종 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "soils",
|
||||
icon: "leaf",
|
||||
title: "산림입지도(토양형 · 산림청 산림입지토양도 1:25,000 · 줌 10 이상 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "ecoForest",
|
||||
icon: "trees",
|
||||
title: "경제림육성단지(산림청 2014 · 국유 · 사유 테두리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "ecology",
|
||||
icon: "plant",
|
||||
title: "생태자연도(환경부 · 1~3 등급 · 별도관리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "landslide",
|
||||
icon: "alert",
|
||||
title: "산사태위험지도(1~5 등급 · 행안부 생활안전지도 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "landslideZones",
|
||||
icon: "alertOctagon",
|
||||
title: "산사태취약지역(시도 지정 현황 · 점 + 지번 필지 테두리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{ key: "faults", icon: "bolt", title: "단층(브이월드 지질 · 참고용)", on: false },
|
||||
{
|
||||
key: "lineaments",
|
||||
icon: "lineDashed",
|
||||
title: "지질구조선(브이월드 지질 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "zeroOrder",
|
||||
icon: "arrowsJoin",
|
||||
title: "0차곡(지형 추정 골짜기 머리 · 위 집수 0.1~1 ha · 곡두 통과 피함)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "bands",
|
||||
icon: "borderOuter",
|
||||
title: "계류 100 m · 도로 300 m 띠(타당성평가)",
|
||||
on: false,
|
||||
},
|
||||
{ key: "building", icon: "building", title: "건물(도엽 · 하류 민가 · 인가)", on: false },
|
||||
{ key: "place", icon: "mapPin", title: "지명(도엽 계곡 · 령 · 부락)", on: false },
|
||||
],
|
||||
];
|
||||
export const LAYERS: readonly LayerDef[] = GROUPS.flat();
|
||||
/** 보호구역 종류 → 그림 색 값(서버 `ZONE_KINDS` 열쇠). */
|
||||
export const ZONE_COLORS: Record<string, string> = {
|
||||
forest: "--draw-map-zone-forest",
|
||||
baekdu: "--draw-map-zone-baekdu",
|
||||
water: "--draw-map-zone-water",
|
||||
park: "--draw-map-zone-park",
|
||||
heritage: "--draw-map-zone-heritage",
|
||||
wetland: "--draw-map-zone-wetland",
|
||||
wildlife: "--draw-map-zone-wildlife",
|
||||
greenbelt: "--draw-map-zone-greenbelt",
|
||||
};
|
||||
/** 산사태위험지도 범례(1 등급 = 가장 위험 · 색 = WMS 그림 색). */
|
||||
export const LANDSLIDE_LEGEND: LegendItem[] = [1, 2, 3, 4, 5].map((grade) => ({
|
||||
label: `산사태 ${grade}등급`,
|
||||
color: `var(--draw-map-landslide-grade-${grade})`,
|
||||
}));
|
||||
/** 선 겹 — 이름 · 그림 꼴(서버 `LINE_KINDS` 열쇠 = `kind` · 없으면 0차곡). 새 선 겹 = 여기 한 줄. */
|
||||
export type LineLayerKey = "faults" | "lineaments" | "zeroOrder";
|
||||
export const LINE_LAYERS: Record<
|
||||
LineLayerKey,
|
||||
{ label: string; kind?: string; color: string; width: number; dash: number[] }
|
||||
> = {
|
||||
faults: { label: "단층", kind: "faults", color: "--draw-map-fault", width: 2.4, dash: [] },
|
||||
lineaments: {
|
||||
label: "지질구조선",
|
||||
kind: "lineaments",
|
||||
color: "--draw-map-lineament",
|
||||
width: 1.8,
|
||||
dash: [8, 4],
|
||||
},
|
||||
zeroOrder: { label: "0차곡", color: "--draw-map-zero-order", width: 1.8, dash: [4, 3] },
|
||||
};
|
||||
export const LINE_KEYS = Object.keys(LINE_LAYERS) as LineLayerKey[];
|
||||
/** 도로 중 기존 임도로 짐작하는 구분. */
|
||||
export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]);
|
||||
/** 물 면 = 도엽 수계(E) 면 셋. */
|
||||
@@ -283,22 +124,6 @@ export function drawRaster(
|
||||
context.drawImage(raster.image, 0, 0);
|
||||
}
|
||||
|
||||
/** 켬끔 취향(`key`) — 없거나 깨지면 처음 값. */
|
||||
export function readPref(key: string): Record<PlanLayerKey, boolean> {
|
||||
const on = Object.fromEntries(LAYERS.map((def) => [def.key, def.on])) as Record<
|
||||
PlanLayerKey,
|
||||
boolean
|
||||
>;
|
||||
try {
|
||||
const saved = JSON.parse(readByKey(key) ?? "{}") as Partial<Record<PlanLayerKey, unknown>>;
|
||||
for (const def of LAYERS)
|
||||
if (typeof saved[def.key] === "boolean") on[def.key] = saved[def.key] as boolean;
|
||||
} catch {
|
||||
// 깨진 값 = 처음 값
|
||||
}
|
||||
return on;
|
||||
}
|
||||
|
||||
/** GL 자료 — 지물마다 그릴 색(`_color`) · 덧 속성을 붙인 사본. */
|
||||
export function glTagged(
|
||||
collection: GeoJsonCollection,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* D03_PlanAnalysis_UI_LayerLegend.ts
|
||||
* D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-3) — 켠 겹의 받은 범위 안 수 · 넓이.
|
||||
* 급경사 = 고른 범위(603)마다 % · ° · 비율(1 % 칸 수로 바로 셈).
|
||||
* 범례 칸 · 조각 겹 범례 = 공용 `A00_Common/map/map_legend`(624-2) · 여기는 받은 자료 줄만.
|
||||
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -16,15 +17,14 @@ import type {
|
||||
PlanZones,
|
||||
} from "./D03_PlanAnalysis_Api";
|
||||
import {
|
||||
LANDSLIDE_LEGEND,
|
||||
LINE_KEYS,
|
||||
LINE_LAYERS,
|
||||
ZONE_COLORS,
|
||||
hectare,
|
||||
type LineLayerKey,
|
||||
type PlanLayerKey,
|
||||
} from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import { forestLegendRows } from "./D03_PlanAnalysis_UI_ForestLayers";
|
||||
type MapLayerKey,
|
||||
} from "../A00_Common/map/map_layer_list";
|
||||
import { tileLegendRows } from "../A00_Common/map/map_legend";
|
||||
import { hectare } from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import {
|
||||
SLOPE_MAX_PCT,
|
||||
bandRatios,
|
||||
@@ -111,13 +111,12 @@ function landslideZoneRow({ info }: Prepared["landslideZones"]): LegendItem[] {
|
||||
}
|
||||
|
||||
export function legendRows(
|
||||
on: Record<PlanLayerKey, boolean>,
|
||||
on: Record<MapLayerKey, boolean>,
|
||||
data: Partial<Prepared>,
|
||||
): LegendItem[][] {
|
||||
const rows: LegendItem[][] = [];
|
||||
if (on.slope && data.slope) rows.push(slopeRow(data.slope));
|
||||
if (on.landslide) rows.push(LANDSLIDE_LEGEND); // GL = 조각(받기 없음 · 622-2)
|
||||
rows.push(...forestLegendRows(on));
|
||||
rows.push(...tileLegendRows(on)); // 조각 겹 = 받기 없음(622-2 · 공용 범례)
|
||||
if (on.landslideZones && data.landslideZones) rows.push(landslideZoneRow(data.landslideZones));
|
||||
if (on.zones && data.zones) {
|
||||
const items = Object.entries(data.zones.summary)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_Layers.ts
|
||||
* D03 평면 바탕 겹 켜고 끔(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 요구 395-2 · 440-8 · 442 · 443) — 지도 왼쪽 위 아이콘 한 줄
|
||||
* (공용 지도 도구줄 `createMapTools` · 넘치면 가로 밀기 571-1 · 오른쪽 평면 도구줄과 따로).
|
||||
* D03 평면 바탕 겹 켜고 끔(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 624-2 · 요구 395-2 · 440-8 · 442 · 443) — 지도 왼쪽 위 아이콘 한 줄
|
||||
* (공용 겹 줄 `A00_Common/map/map_layer_bar` · 넘치면 가로 밀기 571-1 · 오른쪽 평면 도구줄과 따로).
|
||||
*
|
||||
* 겹 = 바탕(위성 · 음영기복 · 도엽 등고선(계곡선 25 m 굵게 · 수치) · 급경사 색) ·
|
||||
* 물 · 길(세류선 · 하천 · 물 면(실폭하천 · 저수지) · 기존 도로 · 기존 임도 짐작(소로 · 미분류) ·
|
||||
@@ -9,28 +9,29 @@
|
||||
* 구역(지적도 · 보호 · 규제 구역 여덟 종 · 산림입지도(토양형) · 산사태위험지도 · 산사태취약지역(시도 표 점 · 지번 필지 테두리) ·
|
||||
* 선 겹(단층 · 지질구조선 · 0차곡) ·
|
||||
* 계류 100 m · 도로 300 m 띠 · 건물 · 지명).
|
||||
* 처음 켬 = 위성 · 등고선 · 세류 · 고른 값 = 화면 취향(`aislo:pref:d03.layers`) 기억.
|
||||
* 겹 목록 · 처음 켬(위성 · 등고선 · 세류) · 취향(`aislo:pref:d03.layers`) = 공용 `map_layer_list` 페이지 표 「d03」.
|
||||
* 받기 = 켤 때 · 받은 지형 범위 지문(`load(have)` · 페이지가 지형면을 다 읽은 뒤 넘김)이 바뀌면 차례 +1 · 켠 겹 모두 다시 ·
|
||||
* 옛 차례 답은 버림(475) · 도엽 선 겹(등고 · 세류 · 물 면 · 도로 · 건물 · 지명) = 보이는 도엽 조각(606 · `UI_SheetTiles`) · 지형을 안 받았으면 단추를 막음 ·
|
||||
* 받기 실패 = 그 겹만 꺼짐 + 알림 · 받는 일은 페이지 서클에 셈(`track`).
|
||||
* 그리기 차례(아래 → 위) = 위성(GL 조각만 · 옛 캔버스 = 위성 없음 · 590-2) → 음영기복 → 급경사 → 산사태위험지도 → 산림입지도 → 보호구역 → 띠 → 지적 → 물 면 →
|
||||
* 건물 → 등고선 → 도로 → 임도 짐작 → 올린 임도망도 → 세류 → 선 겹 → 산사태취약지역 → 지명 → 그은 선.
|
||||
* 겹 모두 = 바탕 캔버스(`base` · 582) · 등고선 = 줌 단계 색인 · 글 폭 기억.
|
||||
* 범례 = 켠 겹의 받은 범위 안 수 · 넓이(왼쪽 아래 · `D03_PlanAnalysis_UI_LayerLegend`).
|
||||
* 범례 = 켠 겹의 받은 범위 안 수 · 넓이(왼쪽 아래 · 칸 = 공용 `map_legend` · 줄 = `D03_PlanAnalysis_UI_LayerLegend`).
|
||||
* 급경사 = 서버 값 그림을 고른 범위대로 칠함 · 범례 ⚙ → 범위 모달(603 · `D03_PlanAnalysis_UI_SlopeBands`).
|
||||
* GL 바탕(586-2 · 처음 = 켬 · 끝 단추 끔 = 옛 캔버스 · `aislo:pref:d03.canvas`) — 겹 스물 · 등고 글 · 지명을
|
||||
* MapLibre 가 그림(`D03_PlanAnalysis_UI_MapGl`) · 캔버스 바탕 겹은 쉼(등고 간격 셈만) · 받기 · 범례는 같음 ·
|
||||
* WebGL 못 씀 · 잃음 = 저절로 옛 캔버스 + 알림(취향은 안 바꿈).
|
||||
* 받은 산림 · 환경 자료(622-2) = 국유림 임도 · 경제림 · 생태자연도(조각만 · 받기 없음) · 산림입지도 · 산사태위험(GL = 조각 ·
|
||||
* 옛 캔버스 = 지금 받기 그대로) — 꼴 · 범례 = `D03_PlanAnalysis_UI_ForestLayers`.
|
||||
* 옛 캔버스 = 지금 받기 그대로) — 꼴 · 범례 = 공용 `A00_Common/map/map_forest_layers`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { createLegend } from "@ui/ui_template_flow_legend";
|
||||
import { createIconButton } from "@ui/ui_template_button";
|
||||
import { createMapTools } from "@ui/ui_template_map_tools";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
import { soilColor, tileOnly, type TileOnlyKey } from "../A00_Common/map/map_forest_layers";
|
||||
import { createMapLayerBar } from "../A00_Common/map/map_layer_bar";
|
||||
import { LAYERS, LINE_LAYERS, ZONE_COLORS, type RoadPick } from "../A00_Common/map/map_layer_list";
|
||||
import { createMapLegend } from "../A00_Common/map/map_legend";
|
||||
import { fetchVWorldMeta, type VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import {
|
||||
createNormalizer,
|
||||
@@ -57,23 +58,16 @@ import {
|
||||
} from "./D03_PlanAnalysis_Api";
|
||||
import {
|
||||
FOREST_ROAD_KINDS,
|
||||
GROUPS,
|
||||
LAYERS,
|
||||
LINE_LAYERS,
|
||||
WATER_KEYS,
|
||||
ZONE_COLORS,
|
||||
glImage,
|
||||
glTagged,
|
||||
loadImage,
|
||||
readPref,
|
||||
shadeImage,
|
||||
type PlanLayerKey,
|
||||
type RoadPick,
|
||||
} from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend";
|
||||
import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint";
|
||||
import { createGlBase, glFont, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
|
||||
import { soilColor, tileOnly, type TileOnlyKey } from "./D03_PlanAnalysis_UI_ForestLayers";
|
||||
import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight";
|
||||
import {
|
||||
createSheetTiles,
|
||||
@@ -90,7 +84,6 @@ import {
|
||||
} from "./D03_PlanAnalysis_UI_SlopeBands";
|
||||
import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
|
||||
|
||||
const PREF_KEY = "aislo:pref:d03.layers";
|
||||
/** 옛 캔버스로 그림("1") — 처음 = GL. */
|
||||
const CANVAS_PREF_KEY = "aislo:pref:d03.canvas";
|
||||
/** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */
|
||||
@@ -137,7 +130,6 @@ export function createPlanLayers(opts: {
|
||||
track: <T>(job: Promise<T>) => Promise<T>;
|
||||
}): PlanLayers {
|
||||
const { projectId, map, track } = opts;
|
||||
const on = readPref(PREF_KEY);
|
||||
const data: Partial<Prepared> = {};
|
||||
const pending = new Map<DataKey | "satellite", Promise<void>>();
|
||||
let meta: VWorldMeta | null = null;
|
||||
@@ -152,6 +144,22 @@ export function createPlanLayers(opts: {
|
||||
useGl(false);
|
||||
});
|
||||
let glOn = readByKey(CANVAS_PREF_KEY) !== "1";
|
||||
/** 겹 줄 · 켬끔 · 취향 = 공용 겹 목록(624-2 · 페이지 표 「d03」). */
|
||||
const layerBar = createMapLayerBar({
|
||||
page: "d03",
|
||||
glOn,
|
||||
onToggle: (key) => {
|
||||
void ensure(key);
|
||||
sync();
|
||||
map.redrawBase();
|
||||
},
|
||||
onGl: () => {
|
||||
writeByKey(CANVAS_PREF_KEY, glOn ? "1" : "0");
|
||||
useGl(!glOn);
|
||||
if (!glOn) showToast(NO_SATELLITE_NOTE, "info");
|
||||
},
|
||||
});
|
||||
const { on, toggle } = layerBar;
|
||||
/** 도엽 조각(606) — 조각이 오면 그 겹만 합친 자료로 다시 얹음. */
|
||||
const tiles = createSheetTiles({
|
||||
projectId,
|
||||
@@ -166,78 +174,34 @@ export function createPlanLayers(opts: {
|
||||
},
|
||||
});
|
||||
|
||||
const buttons = new Map<PlanLayerKey, HTMLButtonElement>();
|
||||
const groups = GROUPS.map((group) =>
|
||||
group.map((def) => {
|
||||
const button = createIconButton({
|
||||
icon: def.icon,
|
||||
title: def.title,
|
||||
size: 32,
|
||||
pressed: on[def.key],
|
||||
onClick: () => toggle(def.key),
|
||||
});
|
||||
button.dataset.layer = def.key;
|
||||
buttons.set(def.key, button);
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
const glButton = createIconButton({
|
||||
icon: "gauge",
|
||||
title: "그래픽 카드 그림(바탕 겹 · 글을 GPU 로 · 끔 = 옛 캔버스 그림)",
|
||||
size: 32,
|
||||
pressed: glOn,
|
||||
onClick: () => {
|
||||
writeByKey(CANVAS_PREF_KEY, glOn ? "1" : "0");
|
||||
useGl(!glOn);
|
||||
if (!glOn) showToast(NO_SATELLITE_NOTE, "info");
|
||||
},
|
||||
});
|
||||
glButton.dataset.layer = "gl";
|
||||
/** GL ↔ 옛 캔버스 — 못 켜면 캔버스(알림은 `onFail`). */
|
||||
function useGl(want: boolean): void {
|
||||
glOn = want && map.setGl(gl);
|
||||
if (!glOn) map.setGl(null);
|
||||
glButton.setAttribute("aria-pressed", String(glOn));
|
||||
if (!glOn) void Promise.all(LAYERS.map((def) => ensure(def.key))); // 산사태위험 한 장(622-2)
|
||||
sync();
|
||||
map.redrawBase();
|
||||
}
|
||||
groups.push([glButton]);
|
||||
const tools = createMapTools({ label: "지도 겹", groups });
|
||||
const bar = tools.root;
|
||||
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", `${tools.bar.offsetWidth}px`),
|
||||
).observe(tools.bar);
|
||||
const legend = document.createElement("div");
|
||||
legend.className = "d03-plan__legend";
|
||||
legend.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이";
|
||||
map.root.style.setProperty("--d03-layers-w", `${layerBar.bar.offsetWidth}px`),
|
||||
).observe(layerBar.bar);
|
||||
const legendBox = createMapLegend("d03-plan__legend");
|
||||
const legend = legendBox.root;
|
||||
|
||||
/** 받음 — 위성 · 산림 조각 = GL 조각(받을 것 없음 · 옛 캔버스 = 안 그림) · 산사태위험 = GL 이면 조각(622-2). */
|
||||
const loadedOf = (key: PlanLayerKey): boolean =>
|
||||
tileOnly(key) || (glOn && key === "landslide") || !!data[dataKey(key)];
|
||||
|
||||
function sync(): void {
|
||||
for (const def of LAYERS) {
|
||||
const button = buttons.get(def.key)!;
|
||||
button.setAttribute("aria-pressed", String(on[def.key]));
|
||||
button.disabled = !meta;
|
||||
const title = !meta
|
||||
? `${def.title} — 지형 받기 뒤 보임`
|
||||
: tileOnly(def.key) && !glOn
|
||||
? `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`
|
||||
: def.title;
|
||||
button.title = title;
|
||||
button.setAttribute("aria-label", title);
|
||||
}
|
||||
layerBar.sync({ ready: !!meta, glOn });
|
||||
for (const def of LAYERS) gl.setVisible(def.key, on[def.key]);
|
||||
gl.setData("hillshade", hillshade && (on.satellite ? hillshade.shade : hillshade.plain));
|
||||
const rows = meta ? legendRows(on, data) : [];
|
||||
const light = meta && glOn && on.satellite; // 옛 캔버스 = 위성 없음
|
||||
legend.replaceChildren(...rows.map((row) => createLegend(row)), ...(light ? [lightRow] : []));
|
||||
legendBox.show(meta ? legendRows(on, data) : [], light ? [lightRow] : []);
|
||||
if (meta && on.slope && data.slope) legend.firstElementChild?.append(gear);
|
||||
legend.hidden = !rows.length && !light;
|
||||
}
|
||||
|
||||
/** 위성 밝기(요구 475) — 위성 켠 동안 범례 맨 아래 한 줄 · 바로 반영. */
|
||||
@@ -279,14 +243,6 @@ export function createPlanLayers(opts: {
|
||||
sync();
|
||||
useGl(glOn);
|
||||
|
||||
function toggle(key: PlanLayerKey): void {
|
||||
on[key] = !on[key];
|
||||
writeByKey(PREF_KEY, JSON.stringify(on));
|
||||
void ensure(key);
|
||||
sync();
|
||||
map.redrawBase();
|
||||
}
|
||||
|
||||
/** 켜진 겹의 자료를 받음(한 번) — 실패 = 그 겹만 끔(기억은 안 바꿈 · 다음에 다시 받음) + 알림. */
|
||||
function ensure(key: PlanLayerKey): Promise<void> {
|
||||
if (!meta || !on[key] || loadedOf(key)) return Promise.resolve();
|
||||
@@ -366,7 +322,7 @@ export function createPlanLayers(opts: {
|
||||
gl.setRoadPick(null);
|
||||
}
|
||||
on.forestRoadFile = !!info;
|
||||
writeByKey(PREF_KEY, JSON.stringify(on));
|
||||
layerBar.save();
|
||||
}
|
||||
|
||||
async function deleteRoad(bundle: string): Promise<boolean> {
|
||||
|
||||
@@ -101,7 +101,7 @@ export interface PlanMapLayer {
|
||||
base?: boolean;
|
||||
draw: (context: CanvasRenderingContext2D, map: ScreenMap) => void;
|
||||
press?: (px: number, py: number, map: ScreenMap, button: 0 | 2, shift: boolean) => boolean;
|
||||
hover?: (px: number, py: number, map: ScreenMap) => void;
|
||||
hover?: (px: number, py: number, map: ScreenMap, shift: boolean) => void;
|
||||
release?: (map: ScreenMap) => void;
|
||||
/** 「연도 마커」를 끌고 지나감(참 = 받을 겹) · 놓음(참 = 받음) — 585-3. */
|
||||
dragOver?: (px: number, py: number, map: ScreenMap) => boolean;
|
||||
@@ -210,6 +210,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
"Enter = 긋기 끝",
|
||||
"Esc = 긋기 취소",
|
||||
"Backspace = 마지막 점 빼기",
|
||||
"Tab(긋는 중) = 자유지정 ↔ 길이·기울기 고정",
|
||||
"Shift 누른 채(길이·기울기 고정) = 부채꼴 사이 기울기 자리 · 길이 그대로",
|
||||
"[다시 긋기] = 앞 노드부터 긋고 뒤 노드에 붙으면 끝",
|
||||
"누름 = 노드 · 선 고름 · 다시 누름 = 선 → 구역 → 전체로 넓힘",
|
||||
"Shift+누름 = 선 더함 · 뺌",
|
||||
@@ -458,7 +460,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
perfTime("plan:pointermove", () => {
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
for (const layer of layers) layer.hover?.(px, py, map);
|
||||
for (const layer of layers) layer.hover?.(px, py, map, event.shiftKey);
|
||||
drawTop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,47 +1,37 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_MapGl.ts
|
||||
* D03 GL 바탕(PLAN 586-1 · 586-2 · 요구 445 B · 448) — 바탕 겹 스물을 MapLibre(WebGL · BSD-3)로:
|
||||
* 위성 조각(서버 대리 · 서버 기억 z6~19) · 그림 겹(음영 · 급경사 · 산사태위험 = image) ·
|
||||
* 면 겹(산림입지 · 보호구역 · 띠 · 물 면 · 건물 = fill) · 선 겹(지적 · 등고선 · 도로 · 임도 짐작 · 올린 임도망도 ·
|
||||
* 세류 · 단층 · 지질구조선 · 0차곡 · 산사태취약 필지 = line · 점선 포함) · 점(산사태취약 · 지명 = circle) ·
|
||||
* 글(등고 수치 · 지명 = symbol · 겹치면 뺌 · 글꼴 = 화면 Noto Sans KR(OFL · 자체 보관)을 그 자리에서 그림 —
|
||||
* 꼴에 글꼴 주소가 없으면 MapLibre 가 모든 글자를 화면 글꼴로 그림 · 글꼴 조각 파일 없음).
|
||||
* D03 GL 바탕(PLAN 586-1 · 586-2 · 624-2 · 요구 445 B · 448) — 바탕 겹 스물을 MapLibre(WebGL · BSD-3)로.
|
||||
* 겹 꼴 · 차례 = 공용 `A00_Common/map/map_layer_specs`(624-2 · B05 평면과 같은 꼴) · 여기는 D03 지도 세우기만:
|
||||
* 고정 소스(위성 조각 z6~19 · 등고선 615 줌별 벡터 조각 · 산림 조각 다섯 622-2) · 받는 동안 서클 · 임도망 고름 · 위성 밝기.
|
||||
* 글 = 화면 Noto Sans KR(OFL · 자체 보관)을 그 자리에서 그림 — 꼴에 글꼴 주소가 없으면 MapLibre 가 모든 글자를 화면 글꼴로.
|
||||
* 그은 선 · 재기 · 고르기 · 누름 = 위 캔버스 그대로(위에 겹침).
|
||||
* 카메라 주인 = D03 화면(`ViewState` · 돌림) 하나 — 여기는 따라가기만 · 조작은 안 받음.
|
||||
* 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 색 = 받는 쪽이 지물에 붙인 `_color`.
|
||||
* 등고선 = 줌별 벡터 조각(615 · 위성처럼 고정 소스 · 가는 선 = 모두 · 굵은 선 · 글 = `k` 1) · 받는 동안 서클.
|
||||
* 받은 산림 · 환경 자료(622-2) = 같은 꼴 고정 소스 다섯(`D03_PlanAnalysis_UI_ForestLayers`) · 받는 동안 서클.
|
||||
* 그래픽 카드 · WebGL 이 없거나 잃으면 `onFail` — 부르는 쪽이 옛 캔버스로.
|
||||
* 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2) · 여기는 겹 꼴만.
|
||||
* 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ExpressionSpecification, FilterSpecification, Map as MapLibreMap } from "maplibre-gl";
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import {
|
||||
contourTileSource,
|
||||
createGlMap,
|
||||
GL_FONT,
|
||||
satelliteTileSource,
|
||||
type GlImage,
|
||||
type GlSpec as GlMapSpec,
|
||||
} from "@ui/ui_template_gl_map";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import { forestSources, TRACKED_SOURCES } from "../A00_Common/map/map_forest_layers";
|
||||
import {
|
||||
forestAreaSpecs,
|
||||
forestRasterSpecs,
|
||||
forestRoadSpecs,
|
||||
forestSources,
|
||||
TRACKED_SOURCES,
|
||||
} from "./D03_PlanAnalysis_UI_ForestLayers";
|
||||
MAP_THEME_NAMES,
|
||||
mapLayerSpecs,
|
||||
pickFilter,
|
||||
type GlSource,
|
||||
} from "../A00_Common/map/map_layer_specs";
|
||||
import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import { SAT_LIGHT_DEFAULT } from "./D03_PlanAnalysis_UI_SatLight";
|
||||
|
||||
export { GL_FONT, type GlImage };
|
||||
export type { GlImage };
|
||||
|
||||
/** 자료 소스(임도 짐작 = 도로 소스를 거름 · 위성 = 조각). */
|
||||
export type GlSource = Exclude<PlanLayerKey, "satellite" | "forestRoad">;
|
||||
|
||||
const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT];
|
||||
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
|
||||
export const glFont = (weight: string, text: string): Promise<unknown> =>
|
||||
document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null);
|
||||
@@ -62,304 +52,6 @@ export interface GlBase {
|
||||
resize: () => void;
|
||||
}
|
||||
|
||||
type GlSpec = GlMapSpec<PlanLayerKey>;
|
||||
|
||||
const HALO = "rgba(0, 0, 0, 0.55)";
|
||||
const ROUND = { "line-join": "round", "line-cap": "round" } as const;
|
||||
const COLOR: ExpressionSpecification = ["get", "_color"];
|
||||
/** 등고 조각 굵은 선(서버가 줌마다 정함) — 가는 선은 모두라 굵은 선은 두 번 겹쳐 짙음(캔버스처럼). */
|
||||
const BOLD = ["==", ["get", "k"], 1] as unknown as FilterSpecification;
|
||||
/** 캔버스 점선(px) → MapLibre 점선(선 굵기 배수). */
|
||||
const dash = (px: number[], width: number): number[] => px.map((v) => v / width);
|
||||
|
||||
/** 선 겹 — 검은 테두리 + 선(점선이면 둘 다 같은 px 무늬). */
|
||||
function haloLine(
|
||||
key: PlanLayerKey,
|
||||
source: GlSource,
|
||||
color: string,
|
||||
width: number,
|
||||
pattern: number[] = [],
|
||||
halo = { color: HALO, width: width + 1.6 },
|
||||
filter?: FilterSpecification,
|
||||
): GlSpec[] {
|
||||
const line = (id: string, paint: Record<string, unknown>, w: number): GlSpec =>
|
||||
({
|
||||
key,
|
||||
id,
|
||||
type: "line",
|
||||
source,
|
||||
...(filter ? { filter } : {}),
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
...paint,
|
||||
"line-width": w,
|
||||
...(pattern.length ? { "line-dasharray": dash(pattern, w) } : {}),
|
||||
},
|
||||
}) as GlSpec;
|
||||
return [
|
||||
line(`${key}-halo`, { "line-color": halo.color }, halo.width),
|
||||
line(key, { "line-color": color }, width),
|
||||
];
|
||||
}
|
||||
|
||||
/** 면 겹 — 채움 + 테두리(캔버스 `fillLayer` 처럼 테두리 투명도 0.9). */
|
||||
function fillEdge(
|
||||
key: PlanLayerKey,
|
||||
source: GlSource,
|
||||
fill: string | ExpressionSpecification,
|
||||
opacity: number | ExpressionSpecification,
|
||||
edge: { color: string | ExpressionSpecification; width: number; dash?: number[] },
|
||||
): GlSpec[] {
|
||||
return [
|
||||
{
|
||||
key,
|
||||
id: key,
|
||||
type: "fill",
|
||||
source,
|
||||
paint: { "fill-color": fill, "fill-opacity": opacity },
|
||||
} as GlSpec,
|
||||
{
|
||||
key,
|
||||
id: `${key}-edge`,
|
||||
type: "line",
|
||||
source,
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": edge.color,
|
||||
"line-width": edge.width,
|
||||
"line-opacity": 0.9,
|
||||
...(edge.dash ? { "line-dasharray": dash(edge.dash, edge.width) } : {}),
|
||||
},
|
||||
} as GlSpec,
|
||||
];
|
||||
}
|
||||
|
||||
/** 겹 차례(아래 → 위)와 꼴 — 캔버스 `D03_PlanAnalysis_UI_Layers` 의 색 · 굵기 · 투명도 · 차례 그대로. */
|
||||
function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
const contour = themeColor("--draw-map-contour", "#ffd68c");
|
||||
const place = themeColor("--draw-map-place", "#f8fafc");
|
||||
const landslideZone = themeColor("--draw-map-landslide-zone", "#e11d48");
|
||||
const raster = (
|
||||
key: "satellite" | "hillshade" | "slope",
|
||||
opacity: number,
|
||||
extra: Record<string, number> = {},
|
||||
): GlSpec =>
|
||||
({
|
||||
key,
|
||||
id: key,
|
||||
type: "raster",
|
||||
source: key,
|
||||
paint: { "raster-opacity": opacity, "raster-fade-duration": 0, ...extra },
|
||||
}) as GlSpec;
|
||||
const lineKind = (key: "faults" | "lineaments" | "zeroOrder", width: number, pattern: number[]) =>
|
||||
haloLine(key, key, themeColor(LINE_COLOR[key], "#f43f5e"), width, pattern);
|
||||
return [
|
||||
// 위성 = 조금 어둡고 연하게(요구 475 · 노선 선 대비) — 밝기 상한은 사용자 조절
|
||||
raster("satellite", 1, { "raster-brightness-max": light, "raster-saturation": SAT_SATURATION }),
|
||||
raster("hillshade", 1), // 위성 위 = 「곱하기」와 같은 검은 그늘 그림(받는 쪽이 바꿈)
|
||||
raster("slope", 0.55),
|
||||
...forestRasterSpecs(), // 산사태위험 · 생태자연도 = 조각 그림(622-2)
|
||||
...forestAreaSpecs(), // 산림입지도 · 경제림 = 벡터 조각
|
||||
...fillEdge("zones", "zones", COLOR, 0.22, { color: COLOR, width: 1.6 }),
|
||||
...fillEdge("bands", "bands", COLOR, ["case", ["==", ["get", "_band"], "road"], 0.16, 0.18], {
|
||||
color: COLOR,
|
||||
width: 1.2,
|
||||
dash: [6, 4],
|
||||
}),
|
||||
{
|
||||
key: "cadastral",
|
||||
id: "cadastral",
|
||||
type: "line",
|
||||
source: "cadastral",
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": themeColor("--draw-map-parcel", "#f0abfc"),
|
||||
"line-width": 0.8,
|
||||
"line-opacity": 0.8,
|
||||
},
|
||||
} as GlSpec,
|
||||
...((): GlSpec[] => {
|
||||
const water = themeColor("--draw-map-water", "#0ea5e9");
|
||||
return fillEdge("water", "water", water, 0.45, { color: water, width: 1 });
|
||||
})(),
|
||||
...fillEdge("building", "building", themeColor("--draw-map-building", "#fb7185"), 0.7, {
|
||||
color: HALO,
|
||||
width: 0.6,
|
||||
}),
|
||||
{
|
||||
key: "contour",
|
||||
id: "contour-minor",
|
||||
type: "line",
|
||||
source: "contour",
|
||||
"source-layer": "contour",
|
||||
layout: ROUND,
|
||||
paint: { "line-color": contour, "line-width": 0.8, "line-opacity": 0.7 },
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "contour",
|
||||
id: "contour-index",
|
||||
type: "line",
|
||||
source: "contour",
|
||||
"source-layer": "contour",
|
||||
filter: BOLD,
|
||||
layout: ROUND,
|
||||
paint: { "line-color": contour, "line-width": 1.6, "line-opacity": 0.7 },
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "contour",
|
||||
id: "contour-label",
|
||||
type: "symbol",
|
||||
source: "contour",
|
||||
"source-layer": "contour",
|
||||
filter: BOLD,
|
||||
layout: {
|
||||
"symbol-placement": "line",
|
||||
"symbol-spacing": 320,
|
||||
"text-field": ["to-string", ["get", "h"]],
|
||||
"text-font": [GL_FONT],
|
||||
"text-size": 10,
|
||||
"text-max-angle": 30,
|
||||
"text-padding": 2,
|
||||
},
|
||||
paint: {
|
||||
"text-color": contour,
|
||||
"text-halo-color": "rgba(0, 0, 0, 0.6)",
|
||||
"text-halo-width": 1.5,
|
||||
},
|
||||
} as GlSpec,
|
||||
...haloLine("road", "road", themeColor("--draw-map-road", "#f8fafc"), 1.6, [], {
|
||||
color: HALO,
|
||||
width: 3.2,
|
||||
}),
|
||||
...haloLine(
|
||||
"forestRoad",
|
||||
"road",
|
||||
themeColor("--draw-map-forest-road", "#f59e0b"),
|
||||
2,
|
||||
[7, 4],
|
||||
{ color: HALO, width: 3.6 },
|
||||
[
|
||||
"in",
|
||||
["get", "도로구분"],
|
||||
["literal", ["소로", "미분류"]],
|
||||
] as unknown as FilterSpecification,
|
||||
),
|
||||
...haloLine(
|
||||
"forestRoadFile",
|
||||
"forestRoadFile",
|
||||
themeColor("--draw-map-forest-road-file", "#ff2fd0"),
|
||||
2.4,
|
||||
[],
|
||||
{ color: HALO, width: 4 },
|
||||
),
|
||||
...forestRoadSpecs(), // 국유림 임도 = 벡터 조각
|
||||
// 고른 묶음 · 선 강조(605-2) — 같은 자료에서 걸러 한 겹 더 굵게
|
||||
...["-pick-halo", "-pick"].map(
|
||||
(tail, i) =>
|
||||
({
|
||||
key: "forestRoadFile",
|
||||
id: `forestRoadFile${tail}`,
|
||||
type: "line",
|
||||
source: "forestRoadFile",
|
||||
filter: pickFilter(pick),
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": i ? themeColor("--draw-map-sketch", "#ffe94d") : HALO,
|
||||
"line-width": i ? 4.2 : 7.4,
|
||||
},
|
||||
}) as GlSpec,
|
||||
),
|
||||
{
|
||||
key: "stream",
|
||||
id: "stream",
|
||||
type: "line",
|
||||
source: "stream",
|
||||
layout: ROUND,
|
||||
paint: {
|
||||
"line-color": themeColor("--draw-map-stream", "#38bdf8"),
|
||||
"line-width": ["case", ["==", ["get", "구분"], "세류"], 1, 2.4],
|
||||
},
|
||||
} as GlSpec,
|
||||
...lineKind("faults", 2.4, []),
|
||||
...lineKind("lineaments", 1.8, [8, 4]),
|
||||
...lineKind("zeroOrder", 1.8, [4, 3]),
|
||||
// 산사태취약 — 필지 = 「이 필지 안 어딘가」 테두리만 · 점 = 지정 자리
|
||||
...haloLine(
|
||||
"landslideZones",
|
||||
"landslideZones",
|
||||
landslideZone,
|
||||
1.8,
|
||||
[6, 3],
|
||||
{ color: "rgba(255, 255, 255, 0.7)", width: 3.2 },
|
||||
["!=", ["geometry-type"], "Point"] as unknown as FilterSpecification,
|
||||
),
|
||||
{
|
||||
key: "landslideZones",
|
||||
id: "landslideZones-point",
|
||||
type: "circle",
|
||||
source: "landslideZones",
|
||||
filter: ["==", ["geometry-type"], "Point"],
|
||||
paint: {
|
||||
"circle-radius": 5,
|
||||
"circle-color": landslideZone,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-width": 1.5,
|
||||
},
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "place",
|
||||
id: "place-dot",
|
||||
type: "circle",
|
||||
source: "place",
|
||||
paint: { "circle-radius": 2.5, "circle-color": place },
|
||||
} as GlSpec,
|
||||
{
|
||||
key: "place",
|
||||
id: "place",
|
||||
type: "symbol",
|
||||
source: "place",
|
||||
layout: {
|
||||
"text-field": ["get", "name"],
|
||||
"text-font": GL_FONT_BOLD,
|
||||
"text-size": 11,
|
||||
"text-anchor": "bottom",
|
||||
"text-offset": [0, -0.36],
|
||||
"text-padding": 2,
|
||||
},
|
||||
paint: {
|
||||
"text-color": place,
|
||||
"text-halo-color": "rgba(0, 0, 0, 0.7)",
|
||||
"text-halo-width": 1.5,
|
||||
},
|
||||
} as GlSpec,
|
||||
];
|
||||
}
|
||||
|
||||
/** 고른 묶음 · 선만 거름(고름 없음 = 아무것도 안 맞는 거름). */
|
||||
function pickFilter(pick: RoadPick | null): FilterSpecification {
|
||||
const all: unknown[] = ["all", ["==", ["get", "__bundle"], pick?.bundle ?? ""]];
|
||||
if (pick?.line !== undefined) all.push(["==", ["get", "__line"], pick.line]);
|
||||
return all as unknown as FilterSpecification;
|
||||
}
|
||||
|
||||
/** 선 겹 색 값(캔버스 `LINE_LAYERS` 와 같음). */
|
||||
const LINE_COLOR = {
|
||||
faults: "--draw-map-fault",
|
||||
lineaments: "--draw-map-lineament",
|
||||
zeroOrder: "--draw-map-zero-order",
|
||||
} as const;
|
||||
|
||||
/** 겹 색 지문 — 밝음 · 어두움이 바뀌면 색을 다시. */
|
||||
const THEME_NAMES = [
|
||||
"--draw-map-contour",
|
||||
"--draw-map-road",
|
||||
"--draw-map-stream",
|
||||
"--draw-map-parcel",
|
||||
] as const;
|
||||
|
||||
/** 위성 채도(−1~1) — 조금 연하게. */
|
||||
const SAT_SATURATION = -0.3;
|
||||
|
||||
/** `track` = 받는 일을 진행 서클에 셈(등고 · 산림 조각 받는 동안). */
|
||||
export function createGlBase(
|
||||
projectId: string,
|
||||
@@ -369,13 +61,13 @@ export function createGlBase(
|
||||
let light = SAT_LIGHT_DEFAULT;
|
||||
let pick: RoadPick | null = null;
|
||||
const base = createGlMap<PlanLayerKey>({
|
||||
specs: () => layerSpecs(light, pick),
|
||||
specs: () => mapLayerSpecs(light, pick),
|
||||
fixed: {
|
||||
satellite: satelliteTileSource(projectId),
|
||||
contour: contourTileSource(projectId),
|
||||
...forestSources(projectId),
|
||||
},
|
||||
themeNames: THEME_NAMES,
|
||||
themeNames: MAP_THEME_NAMES,
|
||||
className: "d03-plan__gl",
|
||||
onFail,
|
||||
});
|
||||
|
||||
@@ -192,6 +192,10 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.d03-plan__draw-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d03-plan__draw-fields .ui-field {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/* 623 헬퍼 — 서버가 새로 만든 관 옵션(`_apply_new_defaults`)을 공용 명세 폼 `write → read` 에 돌리고
|
||||
* 같은 종류 병합(`mergeFacilityOptions`)까지 — 폼을 처음 거칠 때 파일이 바뀌는 칸(지워짐 · 더함 · 바뀜).
|
||||
* 입력 = 표준입력 JSON(옵션 목록) · 출력 = 표준출력 JSON 목록 · 짝 = test_623_inlet_defaults.py.
|
||||
* 가짜 DOM 틀 = helper_173_7_inlet_animal.cjs. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
class FakeNode {
|
||||
constructor(tag) {
|
||||
this.tagName = tag.toUpperCase();
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.style = {};
|
||||
this._classes = new Set();
|
||||
this._on = {};
|
||||
this._text = "";
|
||||
this.value = "";
|
||||
const self = this;
|
||||
this.classList = {
|
||||
add: (...n) => n.forEach((c) => self._classes.add(c)),
|
||||
remove: (...n) => n.forEach((c) => self._classes.delete(c)),
|
||||
toggle: (c, f) =>
|
||||
(f ?? !self._classes.has(c)) ? self._classes.add(c) : self._classes.delete(c),
|
||||
contains: (c) => self._classes.has(c),
|
||||
};
|
||||
}
|
||||
set className(v) {
|
||||
this._classes = new Set(String(v).split(/\s+/).filter(Boolean));
|
||||
}
|
||||
set textContent(v) {
|
||||
this._text = String(v);
|
||||
this.children = [];
|
||||
}
|
||||
get textContent() {
|
||||
return this._text + this.children.map((c) => c.textContent ?? "").join("");
|
||||
}
|
||||
append(...kids) {
|
||||
kids.forEach((k) => this.children.push(typeof k === "string" ? { textContent: k } : k));
|
||||
}
|
||||
replaceChildren(...kids) {
|
||||
this.children = [];
|
||||
this.append(...kids);
|
||||
}
|
||||
add(kid) {
|
||||
this.children.push(kid);
|
||||
}
|
||||
setAttribute(n, v) {
|
||||
this[n] = v;
|
||||
}
|
||||
addEventListener(type, fn) {
|
||||
(this._on[type] ??= []).push(fn);
|
||||
}
|
||||
fire(type) {
|
||||
(this._on[type] ?? []).forEach((fn) => fn({ target: this }));
|
||||
}
|
||||
closest() {
|
||||
return null;
|
||||
}
|
||||
matches(sel) {
|
||||
return sel.split(",").some((s) => {
|
||||
s = s.trim();
|
||||
return s.startsWith(".") ? this._classes.has(s.slice(1)) : this.tagName === s.toUpperCase();
|
||||
});
|
||||
}
|
||||
querySelectorAll(sel) {
|
||||
const found = [];
|
||||
const walk = (n) =>
|
||||
(n.children ?? []).forEach((k) => {
|
||||
if (k.matches?.(sel)) found.push(k);
|
||||
walk(k);
|
||||
});
|
||||
walk(this);
|
||||
return found;
|
||||
}
|
||||
querySelector(sel) {
|
||||
return this.querySelectorAll(sel)[0] ?? null;
|
||||
}
|
||||
}
|
||||
globalThis.document = { createElement: (t) => new FakeNode(t) };
|
||||
globalThis.queueMicrotask = (fn) => fn();
|
||||
globalThis.Option = function (text, value) {
|
||||
const o = new FakeNode("option");
|
||||
o.textContent = text;
|
||||
o.value = value;
|
||||
return o;
|
||||
};
|
||||
|
||||
const STUBS = {
|
||||
"@ui/ui_template_elements": { showToast: () => {} },
|
||||
"@config/config_frontend": { FORD_MANNING_N: 0.03, FORD_SLOPE: 0.02 },
|
||||
"./ui_template_checkbox.css": {},
|
||||
"../../B05_Profile/B05_Profile_UI_Structures_Summary_Row": { buildSummaryRow: () => null },
|
||||
};
|
||||
|
||||
const { loadTs } = makeLoader({
|
||||
root: ROOT,
|
||||
requireFor: (full) => (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith("@util/"))
|
||||
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
|
||||
return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
},
|
||||
});
|
||||
|
||||
const { createSpecForm } = loadTs(
|
||||
path.join(ROOT, "A00_Common", "design", "design_structure_form.ts"),
|
||||
);
|
||||
const { PIPE_SPEC } = loadTs(
|
||||
path.join(ROOT, "A00_Common", "design", "logic", "design_structure_pipe.ts"),
|
||||
);
|
||||
const { mergeFacilityOptions } = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "logic", "B05_Profile_UI_Drainage_Facility_Merge.ts"),
|
||||
);
|
||||
|
||||
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||
const inputs = JSON.parse(fs.readFileSync(0, "utf-8"));
|
||||
const out = inputs.map((options) => {
|
||||
const form = createSpecForm(null, {});
|
||||
form.write(PIPE_SPEC, options);
|
||||
const merged =
|
||||
mergeFacilityOptions({ facility: "pipe", options }, { facility: "pipe", options: form.read() })
|
||||
.options ?? {};
|
||||
return {
|
||||
removed: Object.keys(options).filter((k) => !(k in merged)),
|
||||
added: Object.keys(merged).filter((k) => !(k in options)),
|
||||
changed: Object.keys(options).filter((k) => k in merged && !same(options[k], merged[k])),
|
||||
};
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -47,7 +47,12 @@ def test_파이썬_기본_높이_유입만_0_7():
|
||||
|
||||
|
||||
def test_새_관_기본값_유입_흙막이만_0_7():
|
||||
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 1200})
|
||||
# 유입 기슭막이 관만 유입 흙막이 칸을 받음 — 기본 집수정이면 안 채움(623)
|
||||
fresh = PipePoint(
|
||||
chainage_m=10.0,
|
||||
source="stream",
|
||||
options={"pipe_diameter_mm": 1200, "inlet_type": "기슭막이"},
|
||||
)
|
||||
basins._apply_new_defaults([fresh])
|
||||
assert fresh.options["inlet_revet_height_m"] == pytest.approx(1.9)
|
||||
assert fresh.options["outlet_revet_height_m"] == pytest.approx(1.7)
|
||||
|
||||
@@ -58,10 +58,11 @@ def test_d03_parts():
|
||||
paint = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts") # 580 — 그림 나눔
|
||||
assert "map.stations" in paint and "map.slopes" in paint
|
||||
# 569-2 · 581-1 · 581-2 — 바탕 겹 단추(공용 지도 도구줄 한 줄 · 묶음 셋) · 처음 켬 셋 · 취향 기억
|
||||
defs = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_LayerDefs.ts")
|
||||
assert "createMapTools({" in layers and "createIconButton({" in layers
|
||||
assert "createToolbar(" not in layers
|
||||
assert '"aislo:pref:d03.layers"' in layers and "writeByKey(PREF_KEY" in layers
|
||||
defs = _src("A00_Common/map/map_layer_list.ts") # 624-2 공용 겹 목록
|
||||
bar = _src("A00_Common/map/map_layer_bar.ts")
|
||||
assert "createMapTools({" in bar and "createIconButton({" in bar and "createMapLayerBar({" in layers
|
||||
assert "createToolbar(" not in layers and "createToolbar(" not in bar
|
||||
assert '"aislo:pref:d03.layers"' in defs and "writeByKey(MAP_LAYER_PAGES[page].pref" in bar
|
||||
keys = re.findall(r'key: "(\w+)",\s+icon: "\w+",\s+title:\s+"[^"]+",\s+on: (true|false)', defs)
|
||||
assert keys == [
|
||||
("satellite", "true"),
|
||||
@@ -73,9 +74,12 @@ def test_d03_parts():
|
||||
("road", "false"),
|
||||
("forestRoad", "false"),
|
||||
("forestRoadFile", "false"),
|
||||
("nationalRoad", "false"),
|
||||
("cadastral", "false"),
|
||||
("zones", "false"),
|
||||
("soils", "false"),
|
||||
("ecoForest", "false"),
|
||||
("ecology", "false"),
|
||||
("landslide", "false"),
|
||||
("landslideZones", "false"),
|
||||
("faults", "false"),
|
||||
|
||||
@@ -85,7 +85,7 @@ def test_soils_fail_raises(tmp_path, monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_screen_def_and_colors() -> None:
|
||||
defs = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8")
|
||||
defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
|
||||
entry = re.search(r'key: "soils",\s*icon: "(\w+)",[^}]*on: (\w+)', defs)
|
||||
assert entry and entry.group(2) == "false" # 처음 값 끔
|
||||
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_zero_order_no_dem(tmp_path) -> None:
|
||||
|
||||
|
||||
def test_screen_defs_icons_colors() -> None:
|
||||
defs = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8")
|
||||
defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
|
||||
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
|
||||
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
|
||||
for key, color in (
|
||||
|
||||
@@ -324,7 +324,7 @@ def test_zone_layer_saved_and_no_cadastral(tmp_path, monkeypatch) -> None:
|
||||
|
||||
def test_screen_defs() -> None:
|
||||
folder = ROOT / "D03_PlanAnalysis"
|
||||
defs = (folder / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8")
|
||||
defs = (ROOT / "A00_Common" / "map" / "map_layer_list.ts").read_text("utf-8") # 624-2
|
||||
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
|
||||
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
|
||||
paint = (folder / "D03_PlanAnalysis_UI_LayerPaint.ts").read_text(
|
||||
|
||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
read = lambda rel: (ROOT / rel).read_text(encoding="utf-8") # noqa: E731
|
||||
|
||||
MAPGL = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts")
|
||||
MAPGL = read("A00_Common/map/map_layer_specs.ts") # 624-2 GL 겹 꼴 = 공용
|
||||
LIGHT = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts")
|
||||
LAYERS = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")
|
||||
|
||||
|
||||
@@ -94,4 +94,5 @@ def test_screen_uses_fixed_vector_source() -> None:
|
||||
assert "/plan/contour/{z}/{x}/{y}.pbf" in gl
|
||||
base = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_MapGl.ts").read_text(encoding="utf-8")
|
||||
assert "contour: contourTileSource(projectId)" in base
|
||||
assert base.count('"source-layer": "contour"') == 3
|
||||
specs = (ROOT / "A00_Common" / "map" / "map_layer_specs.ts").read_text(encoding="utf-8")
|
||||
assert specs.count('"source-layer": "contour"') == 3 # 624-2 GL 겹 꼴 = 공용
|
||||
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
D03 = ROOT / "D03_PlanAnalysis"
|
||||
MAP = ROOT / "A00_Common" / "map" # 624-2 공용 지도 부품
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
@@ -18,7 +19,7 @@ def test_fixed_sources_follow_plan_622_routes() -> None:
|
||||
gl = _read(ROOT / "ui_template" / "ui_template_gl_map.ts")
|
||||
assert "export function tileSource" in gl
|
||||
assert "/plan/${path}/{z}/{x}/{y}.${ext}" in gl
|
||||
forest = _read(D03 / "D03_PlanAnalysis_UI_ForestLayers.ts")
|
||||
forest = _read(MAP / "map_forest_layers.ts")
|
||||
for name in ("forest_road", "forest_soil", "eco_forest"):
|
||||
assert f'vt("{name}")' in forest
|
||||
assert f'...vector("{name}")' in forest or f'"source-layer": "{name}"' in forest
|
||||
@@ -33,7 +34,7 @@ def test_fixed_sources_follow_plan_622_routes() -> None:
|
||||
|
||||
|
||||
def test_layer_list_five_off_by_default() -> None:
|
||||
defs = _read(D03 / "D03_PlanAnalysis_UI_LayerDefs.ts")
|
||||
defs = _read(MAP / "map_layer_list.ts")
|
||||
icons = _read(ROOT / "ui_template" / "ui_template_icons.ts")
|
||||
for key in ("nationalRoad", "soils", "ecoForest", "ecology", "landslide"):
|
||||
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
|
||||
@@ -43,7 +44,7 @@ def test_layer_list_five_off_by_default() -> None:
|
||||
|
||||
def test_colors_are_draw_map_tokens() -> None:
|
||||
theme = _read(ROOT / "ui_template" / "ui_template_theme.css")
|
||||
forest = _read(D03 / "D03_PlanAnalysis_UI_ForestLayers.ts")
|
||||
forest = _read(MAP / "map_forest_layers.ts")
|
||||
for token in set(re.findall(r'"(--draw-map-[\w-]+)"', forest)):
|
||||
assert f"{token}:" in theme, token
|
||||
assert all(f"--draw-map-soil-{n}:" in theme for n in range(1, 7))
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""623 새 관 기본값 — 유입 집수정이면 유입 흙막이 칸 안 채움 · 동물이동형 유(요구 530 E1 · 분석 620).
|
||||
|
||||
못박는 것
|
||||
- 서버가 새로 만든 관(`_apply_new_defaults`) = 집수정 갈래 · `inlet_revet_*` 없음 · `inlet_basin_animal` "true".
|
||||
- 그 옵션을 공용 명세 폼 `write → read` + 병합(`mergeFacilityOptions`)에 돌리면 차이 0
|
||||
(지워짐 0 · 더함 0 · 바뀜 0) — 폼을 처음 거쳐도 파일이 안 바뀜(`helper_623_inlet_defaults.cjs`).
|
||||
- 유입 기슭막이로 받은 관 · 이미 적힌 유입 흙막이 칸은 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B04_PreProcess import B04_PreProcess_Router_Basins as basins
|
||||
from common_util.common_util_drainage_pipes import PipePoint
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
def _fresh(**options) -> dict:
|
||||
pipe = PipePoint(chainage_m=10.0, source="stream", options=options or None)
|
||||
basins._apply_new_defaults([pipe])
|
||||
return pipe.options
|
||||
|
||||
|
||||
def test_집수정이면_유입_흙막이_없음_동물이동형_유():
|
||||
for options in (_fresh(), _fresh(pipe_diameter_mm=800)):
|
||||
assert options["inlet_type"] == "집수정"
|
||||
assert not [k for k in options if k.startswith("inlet_revet_")]
|
||||
assert options["inlet_basin_animal"] == "true" and options["inlet_structure"] == "L"
|
||||
assert _fresh()["pipe_diameter_mm"] == 1000 # 등록부 글 "1000" → 수(폼과 같음)
|
||||
|
||||
|
||||
def test_기슭막이_관과_적힌_칸은_그대로():
|
||||
soil = _fresh(inlet_type="기슭막이")
|
||||
assert soil["inlet_revet_form"] == "돌쌓기(찰)" and soil["inlet_revet_height_m"] == 1.7
|
||||
assert "inlet_basin_animal" not in soil
|
||||
kept = _fresh(inlet_revet_form="돌쌓기(메)", inlet_basin_animal="false")
|
||||
assert kept["inlet_revet_form"] == "돌쌓기(메)" and kept["inlet_basin_animal"] == "false"
|
||||
|
||||
|
||||
def test_서버가_만든_관_폼_왕복_차이_0():
|
||||
if not (ROOT / "config" / "node_modules").is_dir():
|
||||
pytest.skip("의존성 미설치")
|
||||
inputs = [_fresh(), _fresh(pipe_diameter_mm=800), _fresh(pipe_diameter_mm=1200)]
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_623_inlet_defaults.cjs")],
|
||||
input=json.dumps(inputs, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
assert json.loads(done.stdout) == [{"removed": [], "added": [], "changed": []}] * 3
|
||||
@@ -99,7 +99,8 @@ def test_new_pipe_gets_master_default_not_recommendation(monkeypatch, facility,
|
||||
"""새로 놓인 관 = 배관 · 마스터 기본 관경 — 추천(관경 · BOX암거 · 세월교)은 안 얹는다."""
|
||||
carried = _run_build(monkeypatch, _basin(100.0, facility, diameter), None)
|
||||
assert carried[0].facility == PIPE_FACILITY_PIPE
|
||||
assert carried[0].options["pipe_diameter_mm"] == _master_pipe_diameter()
|
||||
# 등록부 글 "1000" → 수로 적힘(623 · 폼 왕복과 같음)
|
||||
assert carried[0].options["pipe_diameter_mm"] == int(_master_pipe_diameter())
|
||||
|
||||
|
||||
def test_existing_auto_pipe_is_untouched(monkeypatch):
|
||||
|
||||
@@ -30,13 +30,19 @@ def _defaults(type_id: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _basin_defaults() -> dict:
|
||||
"""새 관(유입 집수정) — 유입 흙막이 칸 없음 · 모양 L · 동물이동형 유 · 관경 수(623)."""
|
||||
kept = {k: v for k, v in _defaults("pipe").items() if not k.startswith("inlet_revet_")}
|
||||
return {**kept, "pipe_diameter_mm": 1000, "inlet_structure": "L", "inlet_basin_animal": "true"}
|
||||
|
||||
|
||||
def test_새_관만_기본값_빈_키에만():
|
||||
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 800})
|
||||
basins._apply_new_defaults([fresh])
|
||||
# 추천(먼저 들어간 값)이 이김 · 집수정 모양 L 도 실제 값(54-39)
|
||||
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3) · 유입 흙막이만 + 0.7(211-1 → 1.5)
|
||||
heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3), "inlet_revet_height_m": 1.5}
|
||||
expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L", **heights}
|
||||
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3) · 집수정이면 유입 흙막이 없음(623)
|
||||
heights = {"outlet_revet_height_m": 1.3, "inlet_basin_height_m": 1.3}
|
||||
expected = {**_basin_defaults(), "pipe_diameter_mm": 800, **heights}
|
||||
assert fresh.options == expected
|
||||
assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "집수정"
|
||||
box = PipePoint(chainage_m=20.0, source="stream", facility="box_culvert")
|
||||
@@ -65,10 +71,10 @@ def test_원본에_있던_관은_빈칸_그대로(monkeypatch):
|
||||
_, points = basins._build("p", Ctx(), [old])
|
||||
by = {p.chainage_m: p for p in points}
|
||||
assert by[100.0].options is None # 옛 빈칸 그대로
|
||||
# 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39)
|
||||
# 기본 관경 1000 → 1.5(139-1) · 유입 흙막이 1.7(211-1)
|
||||
heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5), "inlet_revet_height_m": 1.7}
|
||||
assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L", **heights}
|
||||
# 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39) · 동물이동형 유 · 유입 흙막이 없음(623)
|
||||
# 기본 관경 1000 → 1.5(139-1)
|
||||
heights = {"outlet_revet_height_m": 1.5, "inlet_basin_height_m": 1.5}
|
||||
assert by[300.0].options == {**_basin_defaults(), **heights}
|
||||
|
||||
|
||||
def test_월류_폭_기본값은_등록부_엔진과_같음():
|
||||
|
||||
Reference in New Issue
Block a user