Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_LayerDefs.ts
T
eomsangdonandClaude Opus 5.5 edc20df849 feat(D 예정노선): 급경사 범위 옵션 모달 — 기울기 % 값 그림 · 1 % 칸 수 · 화면 칠하기 · 범례 ⚙ (PLAN 603 · 요구 466~468)
- 서버: 15 · 35 · 40° 단계 PNG 길을 걷고 기울기 % 값 그림 한 장(칸 = % 내림 0~250 · 빈 칸 255) + 1 % 칸 수 · 파생 저장본 판 2(열쇠 = 격자 지문)
- 화면: 고른 범위대로 값 그림을 칠함(서버 왕복 없음) · 범례 비율 = 칸 수로 바로 셈 · 범례 급경사 줄 ⚙ → 모달(아래 % · 위 % · 색 · ° 환산 · 범위 추가 · 지움 · 기본값 · 빈 값 · 겹침 막음)
- 기본 = 지식DB 15 · 35 · 40° 의 % 26.8 · 70.0 · 83.9 · 기억 = 화면 취향 aislo:pref:d03.slope-bands(계정)
- 시험: 569_1 급경사 시험을 값 그림 · 칸 수로 고침, 581_1 은 bands 대신 counts(옛 단계 응답이 없어짐) · 새 603 시험(칸 수 합 = 유효 칸 · 기본 비율 = 옛 15 · 35 · 40° 비율)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:12:27 +09:00

312 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* D03_PlanAnalysis_UI_LayerDefs.ts
* D03 바탕 겹 정의(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2) — 겹 단추 묶음 · 처음 값 · 보호구역 · 토양형 색 ·
* 산사태위험지도 범례 · 선 겹(단층 · 지질구조선 · 0차곡) 꼴 · 켬끔 취향 읽기 · 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 ·
* GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림).
* 부르는 곳 = `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 {
drawPreparedLayer,
normalizedToScreen,
type GeoJsonCollection,
type PreparedLayer,
type ViewState,
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
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"
| "cadastral"
| "zones"
| "soils"
| "landslide"
| "landslideZones"
| "faults"
| "lineaments"
| "zeroOrder"
| "bands"
| "building"
| "place";
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: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false },
{
key: "zones",
icon: "shield",
title: "보호 · 규제 구역(브이월드 여덟 종 · 참고용)",
on: false,
},
{
key: "soils",
icon: "leaf",
title: "산림입지도(토양형 · 산림청 산림입지토양도 · 참고용)",
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 등급 = 가장 위험). */
export const LANDSLIDE_LEGEND: LegendItem[] = [1, 2, 3, 4, 5].map((grade) => ({
label: `${grade}등급`,
color: `var(--draw-map-landslide-${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 SOIL_COLORS = [1, 2, 3, 4, 5, 6].map((n) => `--draw-map-soil-${n}`);
/** 도로 중 기존 임도로 짐작하는 구분. */
export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]);
/** 물 면 = 도엽 수계(E) 면 셋. */
export const WATER_KEYS = ["실폭하천", "수계면E003", "수계면E005"];
export const hectare = (m2: number): string => `${(m2 / 10000).toFixed(1)} ha`;
/** 면 겹 채움 — 지물마다 한 길(evenodd = 구멍) · 화면 밖 지물 거름(돌린 지도 귀퉁이까지 여유). */
export function fillLayer(
context: CanvasRenderingContext2D,
layer: PreparedLayer,
view: ViewState,
fill: string,
alpha: number,
edge?: { color: string; width: number; dash?: number[] },
): void {
const [ox, oy] = normalizedToScreen(view, 0, 0);
const [ex, ey] = normalizedToScreen(view, 1, 1);
const ax = ex - ox;
const ay = ey - oy;
const pad = Math.max(view.width, view.height) / 2;
context.fillStyle = fill;
if (edge) {
context.strokeStyle = edge.color;
context.lineWidth = edge.width;
context.setLineDash(edge.dash ?? []);
}
for (const feature of layer.features) {
if (
feature.maxX * ax + ox < -pad ||
feature.minX * ax + ox > view.width + pad ||
feature.maxY * ay + oy < -pad ||
feature.minY * ay + oy > view.height + pad
)
continue;
context.beginPath();
for (const { coords } of feature.parts) {
context.moveTo(coords[0] * ax + ox, coords[1] * ay + oy);
for (let i = 2; i < coords.length; i += 2)
context.lineTo(coords[i] * ax + ox, coords[i + 1] * ay + oy);
context.closePath();
}
context.globalAlpha = alpha;
context.fill("evenodd");
if (edge) {
context.globalAlpha = 0.9;
context.stroke();
}
}
context.setLineDash([]);
}
export const loadImage = (src: string): Promise<HTMLImageElement> =>
new Promise((done, fail) => {
const image = new Image();
image.onload = () => done(image);
image.onerror = () => fail(new Error("그림을 읽지 못했습니다"));
image.src = src;
});
/** 선 겹 긋기(점 솎음 `dot` · `everyM` = 등고 간격 거름). */
export function stroke(
context: CanvasRenderingContext2D,
map: ScreenMap,
layer: PreparedLayer,
color: string,
width: number,
everyM?: number,
): void {
context.strokeStyle = color;
context.lineWidth = width;
drawPreparedLayer(context, layer, map.view, "dot", everyM);
}
/** 그림 겹(작업 좌표계 m 상자 · 위 = 북). */
export function drawRaster(
context: CanvasRenderingContext2D,
map: ScreenMap,
raster: { raster: PlanRaster; image: HTMLImageElement },
): void {
// 세 귀퉁이로 놓음 — TM 격자는 경위도 바탕에 대해 수렴각만큼 돌아 있음(586-0)
const [x0, y0, x1, y1] = raster.raster.box_m;
const [left, top] = map.toScreen([x0, y1]);
const [right, rightY] = map.toScreen([x1, y1]);
const [bottomX, bottom] = map.toScreen([x0, y0]);
const { naturalWidth: w, naturalHeight: h } = raster.image;
context.transform(
(right - left) / w,
(rightY - top) / w,
(bottomX - left) / h,
(bottom - top) / h,
left,
top,
);
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,
color: (props: Record<string, unknown>) => string,
extra: Record<string, unknown> = {},
): GeoJsonCollection {
return {
...collection,
features: (collection.features ?? []).map((feature) => ({
...feature,
properties: { ...feature.properties, ...extra, _color: color(feature.properties ?? {}) },
})),
};
}
/** GL 그림 자리 — 서버 경위도 귀퉁이(왼쪽 아래부터 돎) → 왼쪽 위부터 · 없으면 null. */
export function glImage(raster: PlanRaster, url = raster.image): GlImage | null {
const box = raster.box;
return box?.length === 4 ? { url, corners: [box[3], box[2], box[1], box[0]] } : null;
}
/** 음영 그늘 그림 — 위성 위 「곱하기 0.75」와 같은 검은 반투명(밝기 g → 투명도 0.75 · (1 − g)). */
export function shadeImage(image: HTMLImageElement): string {
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0);
const pixels = context.getImageData(0, 0, canvas.width, canvas.height);
const px = pixels.data;
for (let i = 0; i < px.length; i += 4) {
px[i + 3] = Math.round(0.75 * (255 - px[i]) * (px[i + 3] / 255));
px[i] = px[i + 1] = px[i + 2] = 0;
}
context.putImageData(pixels, 0, 0);
return canvas.toDataURL("image/png");
}