refactor(공용 지도): 겹 받기 · 범례 줄 · 급경사 범위 · 위성 밝기 · 겹 자료 도움을 A00_Common/map 으로 — D03 은 다시 내놓기 · 동작 그대로 (PLAN 624-3 · 요구 530)

- D03_Api 겹 받기 → map_layer_api.ts(D03_Api 가 다시 내놓음)
- UI_SlopeBands · logic SlopeBands · UI_SatLight → map_slope_bands · logic/map_slope_bands · map_sat_light
- 범례 줄 legendRows → map_legend.ts(받은 자료 꼴 LegendData · D03 LayerLegend 가 다시 내놓음)
- glImage · glTagged · shadeImage · loadImage · WATER_KEYS · FOREST_ROAD_KINDS · hectare → map_layer_data.ts
- 시험 경로 고침 — test_603(TS 자리) · test_606_1(밝기 파일) · test_584_3(범례 글 = 공용 파일도 읽음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 20:47:50 +09:00
co-authored by Claude Opus 5.5
parent 82c19c96f5
commit c6a1efad0c
14 changed files with 496 additions and 413 deletions
@@ -1,10 +1,10 @@
/* =============================================================================
* D03_PlanAnalysis_SlopeBands.ts
* D03 급경사 범위(PLAN 603 · 요구 466 · 467 · 468) — 기울기 % 범위 · 색 · 비율 셈(화면 · 서버 없이).
* map_slope_bands.ts(logic)
* 공용 지도 급경사 범위(624-3 공용으로 옮김)(PLAN 603 · 요구 466 · 467 · 468) — 기울기 % 범위 · 색 · 비율 셈(화면 · 서버 없이).
* 서버 값 그림 칸 = 기울기 % 내림(0~250 · 250 = 250 % 이상 · `D03_PlanAnalysis_Layers.slope_values_png`) ·
* 칸 v 는 가운데(v + 0.5)가 [아래, 위) 안이면 그 범위(위 = 250 이면 250 % 이상도) · 앞 범위 먼저.
* 기본 = 지식DB 15 · 35 · 40° 의 % — `임도망_편성.md:20` · `타당성평가.md:59` · `노선_배치.md:29`.
* 부르는 곳 = `D03_PlanAnalysis_UI_SlopeBands.ts` · `D03_PlanAnalysis_UI_LayerLegend.ts`.
* 부르는 곳 = `map_slope_bands.ts` · `map_legend.ts` · D03 `UI_LayerLegend`.
* ========================================================================== */
export interface SlopeBand {
+221
View File
@@ -0,0 +1,221 @@
/* =============================================================================
* map_layer_api.ts
* 공용 지도 겹 받기(PLAN 569 · 581 · 584 · 603 · 605 · 606 · 624-3 · 요구 530) — D03 예정노선 · B05 평면이 같이 씀(D03_Api 에서 옮김).
* 서버 길 = `/api/projects/{id}/plan/…`(624-1 · 임도 프로젝트도 · 띠 · 올린 임도망도 = 예정노선만).
* 지적도 · 도엽 겹 GeoJSON 경위도 · 도엽 조각 · 그림 겹(급경사 · 음영기복 · 산사태위험) · 보호구역 · 산림입지도 ·
* 선 겹(단층 · 지질구조선) · 0차곡 · 띠 · 올린 임도망도(올리기 · 지움) · 산사태취약지역.
* D03 = `D03_PlanAnalysis_Api` 가 다시 내놓음(부르는 곳 그대로).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
/** 경위도 GeoJSON 묶음 — B04 `GeoJsonCollection` 과 같은 꼴(공용 폴더는 페이지 파일을 안 부름). */
export type GeoJsonCollection = {
features?: Array<{
geometry?: { type: string; coordinates: unknown } | null;
properties?: Record<string, unknown> | null;
}>;
};
async function request<T>(path: string): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
credentials: "include",
headers: { "Content-Type": "application/json" },
});
const body = (await response.json().catch(() => ({}))) as T & { detail?: unknown };
if (!response.ok) {
const detail = typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`;
throw new Error(detail);
}
return body;
}
const base = (projectId: string): string => `/projects/${projectId}/plan`;
/** 지도 겹 그림(569-1) — 작업 좌표계 m 상자(칸 가장자리) · data URL(위 = 북). */
export interface PlanRaster {
box_m: [number, number, number, number];
/** 경위도 네 귀퉁이(왼쪽 아래 · 오른쪽 아래 · 오른쪽 위 · 왼쪽 위) — GL 바탕(586-1). */
box?: Array<[number, number]>;
image: string;
/** 급경사만(603) — `counts[v]` = 기울기 % 내림 v(0~250) 칸 수 · 그림 = 회색 칸 값(255 = 빈 칸). */
counts?: number[];
}
export const fetchPlanCadastral = (projectId: string): Promise<GeoJsonCollection> =>
request(`${base(projectId)}/layers/cadastral`);
/** 도엽 겹(`도로중심선` 등) — 없으면 서버가 받은 도엽에서 풀어 냄. */
export const fetchPlanSheetLayer = (projectId: string, key: string): Promise<GeoJsonCollection> =>
request(`${base(projectId)}/layers/sheet?key=${encodeURIComponent(key)}`);
/** 보이는 도엽(606-2) — 지도 보기 경위도 상자 → 수 · 번호(가운데부터 · 30 장 넘음 = 빈 목록). */
export const fetchPlanSheetTiles = (
projectId: string,
bbox: [number, number, number, number],
): Promise<{ count: number; sheets: string[] }> =>
request(`${base(projectId)}/sheet-tiles?bbox=${bbox.map((v) => v.toFixed(5)).join(",")}`);
/** 도엽 조각 하나(606-1) — 공용 창고 · 없으면 서버가 만듦. */
export const fetchPlanSheetTile = (
projectId: string,
sheet: string,
name: string,
): Promise<GeoJsonCollection> =>
request(`${base(projectId)}/sheet-tiles/${sheet}/${encodeURIComponent(name)}`);
/** 그림 겹 — 급경사 = PNG 바이트 + 머리 `X-Plan-Raster`(616-4 · `image` = blob 주소 · 다 읽으면
* `slopeValues` 가 놓음) · 그 밖 = JSON(base64 그림). */
export const fetchPlanRaster = async (
projectId: string,
kind: "slope" | "hillshade" | "landslide",
): Promise<PlanRaster> => {
const path = `${base(projectId)}/layers/raster?kind=${kind}`;
if (kind !== "slope") return request(path);
const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { detail?: unknown };
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
}
const meta = JSON.parse(response.headers.get("X-Plan-Raster") ?? "{}") as PlanRaster;
return { ...meta, image: URL.createObjectURL(await response.blob()) };
};
/** 보호 · 규제 구역(581-1) — 지물 `kind` · `name` · `area_m2` + 종류별 개수 · 넓이(실패 = `failed`). */
export interface PlanZones extends GeoJsonCollection {
summary: Record<string, { label: string; count: number; area_m2: number; failed?: boolean }>;
}
export const fetchPlanZones = (projectId: string): Promise<PlanZones> =>
request(`${base(projectId)}/layers/zones`);
/** 산림입지도(581-2) — 지물 `code` · `name` · `area_m2` + 토양형별 개수 · 넓이(코드 차례). */
export interface PlanSoils extends GeoJsonCollection {
summary: Array<{ code: string; name: string; count: number; area_m2: number }>;
}
export const fetchPlanSoils = (projectId: string): Promise<PlanSoils> =>
request(`${base(projectId)}/layers/soils`);
/** 계류 100 m · 도로 300 m 띠(581-1) — 받은 범위 안 넓이 · 비율. */
export interface PlanBand {
dist_m: number;
lines: number;
area_m2: number;
ratio: number;
collection: GeoJsonCollection;
}
/** 선 겹(584-1) — 받은 범위 안 선 + 가닥 · 길이 m. */
export interface PlanLines extends GeoJsonCollection {
summary: { count: number; length_m: number };
}
/** 단층 · 지질구조선(브이월드 WFS · 서버 `LINE_KINDS` 열쇠). */
export const fetchPlanLines = (projectId: string, kind: string): Promise<PlanLines> =>
request(`${base(projectId)}/layers/lines?kind=${encodeURIComponent(kind)}`);
/** 0차곡(584-1 · 지형 추정) — 위 집수 넓이 [min_m2, max_m2) 선 + 가닥 · 길이 m. */
export interface PlanZeroOrder {
min_m2: number;
max_m2: number;
cell_m: number;
count: number;
length_m: number;
collection: GeoJsonCollection;
}
export const fetchPlanZeroOrder = (projectId: string): Promise<PlanZeroOrder> =>
request(`${base(projectId)}/layers/zero-order`);
export const fetchPlanBands = (projectId: string): Promise<{ stream: PlanBand; road: PlanBand }> =>
request(`${base(projectId)}/layers/bands`);
/** 올린 묶음 한 줄(605-1) — 올릴 때마다 하나 · 자른 가닥 · 길이 m · 원본 선 수 · 잘린 상자 · 기준점 거리. */
export interface PlanRoadBundle {
id: string;
name: string;
files: string[];
bytes: number | null;
uploaded_at: string | null;
count: number;
length_m: number;
total: number;
bounds: [number, number, number, number] | null;
distance_m: number | null;
}
/** 올린 임도망도(584-3 · 595-4 · 605-1) — 묶음 모두를 합친 값: 기준점 ±5 km ∪ 받은 지형으로 자른 선
* (선마다 `__bundle` 묶음 번호 · `__line` 묶음 안 차례) + 가닥 · 길이 m · 원본 선 수 · 좌표계 ·
* 잘린 상자([서, 남, 동, 북] 경위도) · 기준점 ~ 가장 가까운 임도 m · 마지막 올린 파일 · 묶음 줄. */
export interface PlanForestRoads {
bundles: PlanRoadBundle[];
count: number;
length_m: number;
total: number;
crs: string[];
bounds: [number, number, number, number] | null;
distance_m: number | null;
source: { files: string[]; bytes: number; uploaded_at: string } | null;
collection: GeoJsonCollection;
}
/** 올린 적 없으면 오류(404 글 그대로). */
export const fetchPlanForestRoads = (projectId: string): Promise<PlanForestRoads> =>
request(`${base(projectId)}/layers/forest-roads`);
/** 묶음 하나 지움(605-1) — 남은 묶음 합친 값 · 하나도 안 남으면 null. */
export async function deletePlanForestRoad(
projectId: string,
bundle: string,
): Promise<PlanForestRoads | null> {
const response = await fetch(
`${API_BASE_URL}${base(projectId)}/layers/forest-roads/${encodeURIComponent(bundle)}`,
{ method: "DELETE", credentials: "include" },
);
const body = (await response.json().catch(() => ({}))) as PlanForestRoads & {
detail?: unknown;
};
if (!response.ok)
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
return body.bundles.length ? body : null;
}
/** 임도망도 SHP 올리기(zip 하나 · shp · shx · dbf · prj 묶음) — 못 읽으면 서버 글 그대로 오류. */
export async function uploadPlanForestRoads(
projectId: string,
files: File[],
): Promise<PlanForestRoads> {
const form = new FormData();
for (const file of files) form.append("files", file);
const response = await fetch(`${API_BASE_URL}${base(projectId)}/layers/forest-roads`, {
method: "POST",
credentials: "include",
body: form,
});
const body = (await response.json().catch(() => ({}))) as PlanForestRoads & {
detail?: unknown;
};
if (!response.ok)
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
return body;
}
/** 산사태취약지역(584-3) — 받은 범위 안 점 · 지번 맞는 필지 테두리 · 시도별 표 · 표 없는 시도. */
export interface PlanLandslideZones {
points: Array<{
lon: number;
lat: number;
kind: string | null;
area_m2: number | null;
place: string;
}>;
parcels: GeoJsonCollection;
sidos: Array<{ code: string; name: string; rows: number; located: number; basis?: string }>;
missing: string[];
/** 지적도를 받았나(못 받으면 점만). */
cadastral: boolean;
}
export const fetchPlanLandslideZones = (projectId: string): Promise<PlanLandslideZones> =>
request(`${base(projectId)}/layers/landslide-zones`);
+61
View File
@@ -0,0 +1,61 @@
/* =============================================================================
* map_layer_data.ts
* 공용 지도 겹 자료 도움(PLAN 569-2 · 581 · 586-2 · 624-3) — D03 예정노선 · B05 평면이 같이 씀(D03 `UI_LayerDefs` 에서 옮김).
* 임도 짐작 구분 · 물 면 도엽 이름 · 넓이 글 · 그림 읽기 · GL 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림).
* ========================================================================== */
import type { GlImage } from "@ui/ui_template_gl_map";
import type { GeoJsonCollection, PlanRaster } from "./map_layer_api";
/** 도로 중 기존 임도로 짐작하는 구분. */
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`;
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;
});
/** 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");
}
+172 -2
View File
@@ -2,13 +2,52 @@
* map_legend.ts
* 공용 지도 범례(PLAN 569-2 · 622-2 · 624-2 · 요구 530) — 켠 겹 범례 칸 하나(줄마다 `createLegend`) + 페이지가 붙이는 덧 줄.
* 조각 겹 범례(산사태위험 · 국유림 임도 · 경제림 · 생태자연도) = 받을 자료 없음 → 켬끔만 보고 여기서 만듦(`tileLegendRows`).
* 받은 자료 범례(수 · 넓이 · 급경사 범위)는 받는 페이지 몫 — 줄을 만들어 `show` 에 넘김.
* 받은 자료 범례 줄(수 · 넓이 · 급경사 범위 · 624-3 D03 에서 옮김) = `legendRows(on, data)` — 받은 자료 꼴은 `LegendData`
* (D03 옛 캔버스 자료 · B05 GL 자료 둘 다 맞음 · 받는 일은 페이지 몫).
* 쓰는 법 = `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";
import type {
PlanBand,
PlanForestRoads,
PlanLandslideZones,
PlanRaster,
PlanSoils,
PlanZones,
} from "./map_layer_api";
import { hectare } from "./map_layer_data";
import {
LANDSLIDE_LEGEND,
LINE_KEYS,
LINE_LAYERS,
ZONE_COLORS,
type MapLayerKey,
} from "./map_layer_list";
import { SLOPE_MAX_PCT, bandRatios, pctToDeg, type SlopeBand } from "./logic/map_slope_bands";
/** 지물 수만 보는 겹(옛 캔버스 자료 · GeoJSON 둘 다). */
interface Counted {
features?: ArrayLike<unknown>;
}
/** 범례가 읽는 받은 자료 — 받은 겹만. */
export interface LegendData {
slope: { raster: PlanRaster; bands: SlopeBand[] };
landslideZones: { info: PlanLandslideZones };
zones: { summary: PlanZones["summary"] };
/** 토양형(코드 차례) 색 값 · 요약. */
soils: { kinds: Array<{ color: string }>; summary: PlanSoils["summary"] };
bands: { info: { stream: PlanBand; road: PlanBand } };
water: Counted;
building: Counted;
road: { small: Counted };
forestRoadFile: { info: PlanForestRoads };
faults: { count: number; length_m: number };
lineaments: { count: number; length_m: number };
zeroOrder: { count: number; length_m: number };
}
export interface MapLegend {
root: HTMLElement;
@@ -34,3 +73,134 @@ export function createMapLegend(className: string): MapLegend {
},
};
}
const km = (m: number): string => `${(m / 1000).toFixed(2)} km`;
/** 급경사 범례 — 범위마다 「아래~위 %(°) · 비율」(위 = 끝 = 「↑」). */
function slopeRow({ raster, bands }: LegendData["slope"]): LegendItem[] {
const ratios = bandRatios(raster.counts ?? [], bands);
const deg = (pct: number): string => pctToDeg(pct).toFixed(0);
return bands.map((band, i) => {
const open = band.high >= SLOPE_MAX_PCT;
const range = open
? `${band.low} %↑(${deg(band.low)}°↑)`
: `${band.low}~${band.high} %(${deg(band.low)}~${deg(band.high)}°)`;
return { label: `${range} · ${(ratios[i] * 100).toFixed(1)} %`, color: `var(${band.color})` };
});
}
/** 산사태취약지역 범례 — 점 · 필지 · 위치 없는 시도 · 표 없는 시도 · 지적 못 받음. */
function landslideZoneRow({ info }: LegendData["landslideZones"]): LegendItem[] {
const color = "var(--draw-map-landslide-zone)";
const items: LegendItem[] = [
{
label: `산사태취약지역 점 ${info.points.length}곳`,
color,
kind: "dot",
off: !info.points.length,
},
{
label: `지번 맞는 필지 ${info.parcels.features?.length ?? 0}곳(테두리)`,
color,
kind: "dash",
off: !info.parcels.features?.length,
},
];
for (const sido of info.sidos)
if (!sido.located) items.push({ label: `${sido.name} 위치 없음 — 필지만`, color, off: true });
for (const name of info.missing)
items.push({ label: `${name} — 이 시도 자료 없음`, color: "transparent", off: true });
if (!info.cadastral)
items.push({ label: "지적도 못 받음 — 필지 테두리 없음", color: "transparent", off: true });
return items;
}
/** 켠 겹의 받은 자료 범례 줄(급경사 · 조각 겹 · 산사태취약 · 보호구역 · 토양 · 띠 · 선 겹 · 물 면 · 건물 · 임도). */
export function legendRows(
on: Record<MapLayerKey, boolean>,
data: Partial<LegendData>,
): LegendItem[][] {
const rows: LegendItem[][] = [];
if (on.slope && data.slope) rows.push(slopeRow(data.slope));
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)
.filter(([, kind]) => kind.count || kind.failed)
.map(([key, kind]) => ({
label: kind.failed
? `${kind.label} 받지 못함`
: `${kind.label} ${kind.count}곳 ${hectare(kind.area_m2)}`,
color: `var(${ZONE_COLORS[key]})`,
off: !!kind.failed,
}));
rows.push(
items.length
? items
: [{ label: "보호 · 규제 구역 없음(받은 범위 안)", color: "transparent", off: true }],
);
}
if (on.soils && data.soils) {
const { kinds, summary } = data.soils;
rows.push(
summary.length
? summary.map((soil, i) => ({
label: `${soil.name} ${hectare(soil.area_m2)}`,
color: `var(${kinds[i].color})`,
}))
: [{ label: "산림입지도 없음(받은 범위 안)", color: "transparent", off: true }],
);
}
if (on.bands && data.bands) {
const { stream, road } = data.bands.info;
rows.push([
{
label: `계류 ${stream.dist_m} m 띠 ${hectare(stream.area_m2)} (${(stream.ratio * 100).toFixed(1)} %)`,
color: "var(--draw-map-band-stream)",
},
{
label: `도로 ${road.dist_m} m 띠 ${hectare(road.area_m2)} (${(road.ratio * 100).toFixed(1)} %)`,
color: "var(--draw-map-band-road)",
},
]);
}
const lines = LINE_KEYS.flatMap((key) => {
const got = on[key] && data[key];
if (!got) return [];
const { label, color, dash } = LINE_LAYERS[key];
return [
{
label: `${label} ${got.count}가닥 ${km(got.length_m)}`,
color: `var(${color})`,
kind: dash.length ? ("dash" as const) : ("line" as const),
off: !got.count,
},
];
});
if (lines.length) rows.push(lines);
const tail: LegendItem[] = [];
if (on.water && data.water)
tail.push({
label: `물 면 ${data.water.features?.length ?? 0}곳`,
color: "var(--draw-map-water)",
});
if (on.building && data.building)
tail.push({
label: `건물 ${(data.building.features?.length ?? 0).toLocaleString()}동`,
color: "var(--draw-map-building)",
});
if (on.forestRoad && data.road)
tail.push({
label: `임도 짐작 ${data.road.small.features?.length ?? 0}가닥`,
color: "var(--draw-map-forest-road)",
kind: "dash",
});
if (on.forestRoadFile && data.forestRoadFile)
tail.push({
label: `올린 임도망도 ${data.forestRoadFile.info.count}가닥 ${km(data.forestRoadFile.info.length_m)}`,
color: "var(--draw-map-forest-road-file)",
kind: "line",
});
if (tail.length) rows.push(tail);
return rows;
}
@@ -1,11 +1,11 @@
/* =============================================================================
* D03_PlanAnalysis_UI_SatLight.ts
* D03 위성 바탕 밝기(요구 475) — 노선 선 대비를 위해 위성을 조금 어둡게 · 사용자가 범례에서 조절.
* 값 = 밝기 상한(0.4~1) · 계정 화면 취향으로 기억. 칠하기 = `D03_PlanAnalysis_UI_MapGl` `setSatelliteLight`.
* map_sat_light.ts
* 공용 지도 위성 바탕 밝기(D03 · B05 · 624-3 공용으로 옮김)(요구 475) — 노선 선 대비를 위해 위성을 조금 어둡게 · 사용자가 범례에서 조절.
* 값 = 밝기 상한(0.4~1) · 계정 화면 취향으로 기억. 칠하기 = D03 `UI_MapGl` `setSatelliteLight` · B05 `UI_RouteEdit_Layers`.
* ========================================================================== */
import { createSlider } from "@ui/ui_template_input";
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
import { readByKey, writeByKey } from "../b_page_state";
const PREF_KEY = "aislo:pref:d03.satellite-light";
export const SAT_LIGHT_MIN = 0.4;
@@ -1,10 +1,10 @@
/* =============================================================================
* D03_PlanAnalysis_UI_SlopeBands.ts
* D03 급경사 범위 옵션(PLAN 603 · 요구 466 · 467 · 468) — 범례 급경사 줄 ⚙ → 모달
* map_slope_bands.ts
* 공용 지도 급경사 범위 옵션(D03 · B05 · 624-3 공용으로 옮김)(PLAN 603 · 요구 466 · 467 · 468) — 범례 급경사 줄 ⚙ → 모달
* (범위 줄 = 아래 % · 위 % · 색 · 지움 · 칸 옆 ° 환산 · [범위 추가] · [기본값] · [적용] · 빈 값 · 겹침 막음).
* 기억 = 화면 취향(`aislo:pref:d03.slope-bands` · 계정에 붙음 · 프로젝트 무관).
* 칠하기 = 서버 값 그림(회색 · 칸 = 기울기 % 내림) → 범위 색 그림(서버 왕복 없음 · CLAUDE.md 5장).
* 범위 · 비율 셈 = `logic/D03_PlanAnalysis_SlopeBands.ts` · 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`.
* 범위 · 비율 셈 = `logic/map_slope_bands.ts` · 부르는 곳 = D03 `UI_Layers` · B05 `UI_RouteEdit_Layers`.
* ========================================================================== */
import { createButton, createIconButton } from "@ui/ui_template_button";
@@ -13,8 +13,8 @@ import { createLegend } from "@ui/ui_template_flow_legend";
import { openModal } from "@ui/ui_template_modal";
import { themeColor } from "@ui/ui_template_palette";
import { createText } from "@ui/ui_template_text";
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
import { loadImage } from "./D03_PlanAnalysis_UI_LayerDefs";
import { readByKey, writeByKey } from "../b_page_state";
import { loadImage } from "./map_layer_data";
import {
DEFAULT_SLOPE_BANDS,
SLOPE_COLORS,
@@ -25,7 +25,7 @@ import {
parseBands,
pctToDeg,
type SlopeBand,
} from "./logic/D03_PlanAnalysis_SlopeBands";
} from "./logic/map_slope_bands";
const PREF_KEY = "aislo:pref:d03.slope-bands";
/** 칠한 칸 투명도(겹 투명도 0.55 는 따로). */
+3 -189
View File
@@ -3,13 +3,12 @@
* D03 분석 화면 서버 요청(PLAN 563-1) — 지형 받기 범위 · 받기 · 진행 ·
* 사업 전체 지형면 한 벌(`models_plan/` · 격자) · 그은 선 정본(562-2 GET · PUT network · 지점 · 기준점 함께 591-2 ·
* 지점 · 선 이름 · 노드 기록 · [저장] 기준점 594) · 주소 · 장소 찾기(594-4 공용 `/map/search`) ·
* 지도 겹(569 — 지적도 · 도엽 겹 GeoJSON 경위도 · 급경사 그림 · 581 — 음영기복 · 보호구역 · 띠 · 산림입지도).
* 지도 겹 받기 = 공용 `A00_Common/map/map_layer_api`(624-3 · 다시 내놓음).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { fetchCachedBytes } from "../A00_Common/b_asset_cache";
import { listSurfaceModels } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_sampler";
import type { PlanLine, PlanPiece, Vertex } from "./logic/D03_PlanAnalysis_Plan";
@@ -180,190 +179,5 @@ export async function fetchPlanSurface(projectId: string): Promise<PlanSurface |
return { modelId: model.id, grid: parseDtmGrid(await fetchCachedBytes(projectId, url)) };
}
/** 지도 겹 그림(569-1) — 작업 좌표계 m 상자(칸 가장자리) · data URL(위 = 북). */
export interface PlanRaster {
box_m: [number, number, number, number];
/** 경위도 네 귀퉁이(왼쪽 아래 · 오른쪽 아래 · 오른쪽 위 · 왼쪽 위) — GL 바탕(586-1). */
box?: Array<[number, number]>;
image: string;
/** 급경사만(603) — `counts[v]` = 기울기 % 내림 v(0~250) 칸 수 · 그림 = 회색 칸 값(255 = 빈 칸). */
counts?: number[];
}
export const fetchPlanCadastral = (projectId: string): Promise<GeoJsonCollection> =>
request(`${base(projectId)}/layers/cadastral`);
/** 도엽 겹(`도로중심선` 등) — 없으면 서버가 받은 도엽에서 풀어 냄. */
export const fetchPlanSheetLayer = (projectId: string, key: string): Promise<GeoJsonCollection> =>
request(`${base(projectId)}/layers/sheet?key=${encodeURIComponent(key)}`);
/** 보이는 도엽(606-2) — 지도 보기 경위도 상자 → 수 · 번호(가운데부터 · 30 장 넘음 = 빈 목록). */
export const fetchPlanSheetTiles = (
projectId: string,
bbox: [number, number, number, number],
): Promise<{ count: number; sheets: string[] }> =>
request(`${base(projectId)}/sheet-tiles?bbox=${bbox.map((v) => v.toFixed(5)).join(",")}`);
/** 도엽 조각 하나(606-1) — 공용 창고 · 없으면 서버가 만듦. */
export const fetchPlanSheetTile = (
projectId: string,
sheet: string,
name: string,
): Promise<GeoJsonCollection> =>
request(`${base(projectId)}/sheet-tiles/${sheet}/${encodeURIComponent(name)}`);
/** 그림 겹 — 급경사 = PNG 바이트 + 머리 `X-Plan-Raster`(616-4 · `image` = blob 주소 · 다 읽으면
* `slopeValues` 가 놓음) · 그 밖 = JSON(base64 그림). */
export const fetchPlanRaster = async (
projectId: string,
kind: "slope" | "hillshade" | "landslide",
): Promise<PlanRaster> => {
const path = `${base(projectId)}/layers/raster?kind=${kind}`;
if (kind !== "slope") return request(path);
const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { detail?: unknown };
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
}
const meta = JSON.parse(response.headers.get("X-Plan-Raster") ?? "{}") as PlanRaster;
return { ...meta, image: URL.createObjectURL(await response.blob()) };
};
/** 보호 · 규제 구역(581-1) — 지물 `kind` · `name` · `area_m2` + 종류별 개수 · 넓이(실패 = `failed`). */
export interface PlanZones extends GeoJsonCollection {
summary: Record<string, { label: string; count: number; area_m2: number; failed?: boolean }>;
}
export const fetchPlanZones = (projectId: string): Promise<PlanZones> =>
request(`${base(projectId)}/layers/zones`);
/** 산림입지도(581-2) — 지물 `code` · `name` · `area_m2` + 토양형별 개수 · 넓이(코드 차례). */
export interface PlanSoils extends GeoJsonCollection {
summary: Array<{ code: string; name: string; count: number; area_m2: number }>;
}
export const fetchPlanSoils = (projectId: string): Promise<PlanSoils> =>
request(`${base(projectId)}/layers/soils`);
/** 계류 100 m · 도로 300 m 띠(581-1) — 받은 범위 안 넓이 · 비율. */
export interface PlanBand {
dist_m: number;
lines: number;
area_m2: number;
ratio: number;
collection: GeoJsonCollection;
}
/** 선 겹(584-1) — 받은 범위 안 선 + 가닥 · 길이 m. */
export interface PlanLines extends GeoJsonCollection {
summary: { count: number; length_m: number };
}
/** 단층 · 지질구조선(브이월드 WFS · 서버 `LINE_KINDS` 열쇠). */
export const fetchPlanLines = (projectId: string, kind: string): Promise<PlanLines> =>
request(`${base(projectId)}/layers/lines?kind=${encodeURIComponent(kind)}`);
/** 0차곡(584-1 · 지형 추정) — 위 집수 넓이 [min_m2, max_m2) 선 + 가닥 · 길이 m. */
export interface PlanZeroOrder {
min_m2: number;
max_m2: number;
cell_m: number;
count: number;
length_m: number;
collection: GeoJsonCollection;
}
export const fetchPlanZeroOrder = (projectId: string): Promise<PlanZeroOrder> =>
request(`${base(projectId)}/layers/zero-order`);
export const fetchPlanBands = (projectId: string): Promise<{ stream: PlanBand; road: PlanBand }> =>
request(`${base(projectId)}/layers/bands`);
/** 올린 묶음 한 줄(605-1) — 올릴 때마다 하나 · 자른 가닥 · 길이 m · 원본 선 수 · 잘린 상자 · 기준점 거리. */
export interface PlanRoadBundle {
id: string;
name: string;
files: string[];
bytes: number | null;
uploaded_at: string | null;
count: number;
length_m: number;
total: number;
bounds: [number, number, number, number] | null;
distance_m: number | null;
}
/** 올린 임도망도(584-3 · 595-4 · 605-1) — 묶음 모두를 합친 값: 기준점 ±5 km ∪ 받은 지형으로 자른 선
* (선마다 `__bundle` 묶음 번호 · `__line` 묶음 안 차례) + 가닥 · 길이 m · 원본 선 수 · 좌표계 ·
* 잘린 상자([서, 남, 동, 북] 경위도) · 기준점 ~ 가장 가까운 임도 m · 마지막 올린 파일 · 묶음 줄. */
export interface PlanForestRoads {
bundles: PlanRoadBundle[];
count: number;
length_m: number;
total: number;
crs: string[];
bounds: [number, number, number, number] | null;
distance_m: number | null;
source: { files: string[]; bytes: number; uploaded_at: string } | null;
collection: GeoJsonCollection;
}
/** 올린 적 없으면 오류(404 글 그대로). */
export const fetchPlanForestRoads = (projectId: string): Promise<PlanForestRoads> =>
request(`${base(projectId)}/layers/forest-roads`);
/** 묶음 하나 지움(605-1) — 남은 묶음 합친 값 · 하나도 안 남으면 null. */
export async function deletePlanForestRoad(
projectId: string,
bundle: string,
): Promise<PlanForestRoads | null> {
const response = await fetch(
`${API_BASE_URL}${base(projectId)}/layers/forest-roads/${encodeURIComponent(bundle)}`,
{ method: "DELETE", credentials: "include" },
);
const body = (await response.json().catch(() => ({}))) as PlanForestRoads & {
detail?: unknown;
};
if (!response.ok)
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
return body.bundles.length ? body : null;
}
/** 임도망도 SHP 올리기(zip 하나 · shp · shx · dbf · prj 묶음) — 못 읽으면 서버 글 그대로 오류. */
export async function uploadPlanForestRoads(
projectId: string,
files: File[],
): Promise<PlanForestRoads> {
const form = new FormData();
for (const file of files) form.append("files", file);
const response = await fetch(`${API_BASE_URL}${base(projectId)}/layers/forest-roads`, {
method: "POST",
credentials: "include",
body: form,
});
const body = (await response.json().catch(() => ({}))) as PlanForestRoads & {
detail?: unknown;
};
if (!response.ok)
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
return body;
}
/** 산사태취약지역(584-3) — 받은 범위 안 점 · 지번 맞는 필지 테두리 · 시도별 표 · 표 없는 시도. */
export interface PlanLandslideZones {
points: Array<{
lon: number;
lat: number;
kind: string | null;
area_m2: number | null;
place: string;
}>;
parcels: GeoJsonCollection;
sidos: Array<{ code: string; name: string; rows: number; located: number; basis?: string }>;
missing: string[];
/** 지적도를 받았나(못 받으면 점만). */
cadastral: boolean;
}
export const fetchPlanLandslideZones = (projectId: string): Promise<PlanLandslideZones> =>
request(`${base(projectId)}/layers/landslide-zones`);
/** 지도 겹 받기 = 공용(624-3 · B05 평면도 씀). */
export * from "../A00_Common/map/map_layer_api";
@@ -1,7 +1,7 @@
/* =============================================================================
* D03_PlanAnalysis_UI_LayerDefs.ts
* D03 바탕 겹 받기 · 옛 캔버스 도움(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2 · 624-2) — 임도 짐작 구분 · 물 면 ·
* 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 · GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림).
* D03 바탕 겹 옛 캔버스 도움(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2 · 624-2 · 624-3) — 면 겹 채우기 · 선 긋기 · 그림 겹 놓기.
* 임도 짐작 구분 · 물 면 · 그림 읽기 · GL 자료 = 공용 `A00_Common/map/map_layer_data`(624-3 · 다시 내놓음).
* 겹 목록 · 처음 값 · 켬끔 취향 · 선 겹 꼴 · 보호구역 색 = 공용 `A00_Common/map/map_layer_list`(624-2).
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기 차례 · 범례).
* ========================================================================== */
@@ -10,16 +10,23 @@ import type { MapLayerKey } from "../A00_Common/map/map_layer_list";
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";
/** 겹 목록 · 페이지 표 = 공용 `map_layer_list`(624-2) — 옛 이름으로 다시 내놓음(캔버스 · 조각 · 임도망 목록이 읽음). */
export type PlanLayerKey = MapLayerKey;
export {
FOREST_ROAD_KINDS,
WATER_KEYS,
glImage,
glTagged,
hectare,
loadImage,
shadeImage,
} from "../A00_Common/map/map_layer_data";
export {
LINE_KEYS,
LINE_LAYERS,
@@ -27,13 +34,6 @@ export {
type RoadPick,
} from "../A00_Common/map/map_layer_list";
/** 도로 중 기존 임도로 짐작하는 구분. */
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,
@@ -79,14 +79,6 @@ export function fillLayer(
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,
@@ -123,41 +115,3 @@ export function drawRaster(
);
context.drawImage(raster.image, 0, 0);
}
/** 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");
}
@@ -3,34 +3,23 @@
* D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-3) — 켠 겹의 받은 범위 안 수 · 넓이.
* 급경사 = 고른 범위(603)마다 % · ° · 비율(1 % 칸 수로 바로 셈).
* 범례 칸 · 조각 겹 범례 = 공용 `A00_Common/map/map_legend`(624-2) · 여기는 받은 자료 줄만.
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기).
* 범례 줄 셈 = 공용 `map_legend` `legendRows`(624-3) · 여기는 자료 꼴 · 겹별 수 · 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기).
* ========================================================================== */
import type { LegendItem } from "@ui/ui_template_flow_legend";
import type { PreparedLayer } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import type {
PlanBand,
PlanForestRoads,
PlanLandslideZones,
PlanRaster,
PlanSoils,
PlanZones,
PlanBand,
} from "./D03_PlanAnalysis_Api";
import {
LINE_KEYS,
LINE_LAYERS,
ZONE_COLORS,
type LineLayerKey,
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,
pctToDeg,
type SlopeBand,
} from "./logic/D03_PlanAnalysis_SlopeBands";
import { LINE_KEYS, type LineLayerKey } from "../A00_Common/map/map_layer_list";
import { bandRatios, type SlopeBand } from "../A00_Common/map/logic/map_slope_bands";
/** 범례 줄 = 공용(624-3 · B05 평면도 씀) — 옛 이름으로 다시 내놓음. */
export { legendRows } from "../A00_Common/map/map_legend";
/** 선 겹 — 그릴 선 · 받은 범위 안 가닥 · 길이 m. */
export interface PreparedLines {
@@ -69,133 +58,6 @@ export type Prepared = Record<LineLayerKey, PreparedLines> & {
place: Array<{ lon: number; lat: number; name: string }>;
};
const km = (m: number): string => `${(m / 1000).toFixed(2)} km`;
/** 급경사 범례 — 범위마다 「아래~위 %(°) · 비율」(위 = 끝 = 「↑」). */
function slopeRow({ raster, bands }: Prepared["slope"]): LegendItem[] {
const ratios = bandRatios(raster.counts ?? [], bands);
const deg = (pct: number): string => pctToDeg(pct).toFixed(0);
return bands.map((band, i) => {
const open = band.high >= SLOPE_MAX_PCT;
const range = open
? `${band.low} %↑(${deg(band.low)}°↑)`
: `${band.low}~${band.high} %(${deg(band.low)}~${deg(band.high)}°)`;
return { label: `${range} · ${(ratios[i] * 100).toFixed(1)} %`, color: `var(${band.color})` };
});
}
/** 산사태취약지역 범례 — 점 · 필지 · 위치 없는 시도 · 표 없는 시도 · 지적 못 받음. */
function landslideZoneRow({ info }: Prepared["landslideZones"]): LegendItem[] {
const color = "var(--draw-map-landslide-zone)";
const items: LegendItem[] = [
{
label: `산사태취약지역 점 ${info.points.length}곳`,
color,
kind: "dot",
off: !info.points.length,
},
{
label: `지번 맞는 필지 ${info.parcels.features?.length ?? 0}곳(테두리)`,
color,
kind: "dash",
off: !info.parcels.features?.length,
},
];
for (const sido of info.sidos)
if (!sido.located) items.push({ label: `${sido.name} 위치 없음 — 필지만`, color, off: true });
for (const name of info.missing)
items.push({ label: `${name} — 이 시도 자료 없음`, color: "transparent", off: true });
if (!info.cadastral)
items.push({ label: "지적도 못 받음 — 필지 테두리 없음", color: "transparent", off: true });
return items;
}
export function legendRows(
on: Record<MapLayerKey, boolean>,
data: Partial<Prepared>,
): LegendItem[][] {
const rows: LegendItem[][] = [];
if (on.slope && data.slope) rows.push(slopeRow(data.slope));
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)
.filter(([, kind]) => kind.count || kind.failed)
.map(([key, kind]) => ({
label: kind.failed
? `${kind.label} 받지 못함`
: `${kind.label} ${kind.count}곳 ${hectare(kind.area_m2)}`,
color: `var(${ZONE_COLORS[key]})`,
off: !!kind.failed,
}));
rows.push(
items.length
? items
: [{ label: "보호 · 규제 구역 없음(받은 범위 안)", color: "transparent", off: true }],
);
}
if (on.soils && data.soils) {
const { kinds, summary } = data.soils;
rows.push(
summary.length
? summary.map((soil, i) => ({
label: `${soil.name} ${hectare(soil.area_m2)}`,
color: `var(${kinds[i].color})`,
}))
: [{ label: "산림입지도 없음(받은 범위 안)", color: "transparent", off: true }],
);
}
if (on.bands && data.bands) {
const { stream, road } = data.bands.info;
rows.push([
{
label: `계류 ${stream.dist_m} m 띠 ${hectare(stream.area_m2)} (${(stream.ratio * 100).toFixed(1)} %)`,
color: "var(--draw-map-band-stream)",
},
{
label: `도로 ${road.dist_m} m 띠 ${hectare(road.area_m2)} (${(road.ratio * 100).toFixed(1)} %)`,
color: "var(--draw-map-band-road)",
},
]);
}
const lines = LINE_KEYS.flatMap((key) => {
const got = on[key] && data[key];
if (!got) return [];
const { label, color, dash } = LINE_LAYERS[key];
return [
{
label: `${label} ${got.count}가닥 ${km(got.length_m)}`,
color: `var(${color})`,
kind: dash.length ? ("dash" as const) : ("line" as const),
off: !got.count,
},
];
});
if (lines.length) rows.push(lines);
const tail: LegendItem[] = [];
if (on.water && data.water)
tail.push({ label: `물 면 ${data.water.features.length}곳`, color: "var(--draw-map-water)" });
if (on.building && data.building)
tail.push({
label: `건물 ${data.building.features.length.toLocaleString()}동`,
color: "var(--draw-map-building)",
});
if (on.forestRoad && data.road)
tail.push({
label: `임도 짐작 ${data.road.small.features.length}가닥`,
color: "var(--draw-map-forest-road)",
kind: "dash",
});
if (on.forestRoadFile && data.forestRoadFile)
tail.push({
label: `올린 임도망도 ${data.forestRoadFile.info.count}가닥 ${km(data.forestRoadFile.info.length_m)}`,
color: "var(--draw-map-forest-road-file)",
kind: "line",
});
if (tail.length) rows.push(tail);
return rows;
}
/** 검증 창구 — 겹별 수(지물 · 넓이 m² · 길이 m). */
export const layerCounts = (data: Partial<Prepared>): Record<string, number> => ({
...(data.building && { building: data.building.features.length }),
@@ -68,7 +68,7 @@ import {
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 { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight";
import { createSatLightRow, readSatLight } from "../A00_Common/map/map_sat_light";
import {
createSheetTiles,
SHEET_TILE_NAMES,
@@ -81,7 +81,7 @@ import {
slopeGear,
slopeValues,
writeSlopeBands,
} from "./D03_PlanAnalysis_UI_SlopeBands";
} from "../A00_Common/map/map_slope_bands";
import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
/** 옛 캔버스로 그림("1") — 처음 = GL. */
@@ -28,7 +28,7 @@ import {
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";
import { SAT_LIGHT_DEFAULT } from "../A00_Common/map/map_sat_light";
export type { GlImage };
@@ -332,6 +332,7 @@ def test_screen_defs() -> None:
) # 586-2 캔버스 그리기 따로
page = (folder / "D03_PlanAnalysis_UI_Layers.ts").read_text("utf-8") + paint
legend = (folder / "D03_PlanAnalysis_UI_LayerLegend.ts").read_text("utf-8")
legend += (ROOT / "A00_Common" / "map" / "map_legend.ts").read_text("utf-8") # 624-3 범례 줄 공용
for key, color in (
("forestRoadFile", "forest-road-file"),
("landslideZones", "landslide-zone"),
@@ -1,5 +1,5 @@
"""D03 급경사 범위(PLAN 603 · 요구 466 · 467 · 468) — 서버 1 % 칸 수 +
`logic/D03_PlanAnalysis_SlopeBands.ts`.
`A00_Common/map/logic/map_slope_bands.ts`(624-3 공용으로 옮김).
1 % 칸 수 합 = 유효 칸 · 기본 범위(26.8 · 70.0 · 83.9 %) 비율 = 옛 15 · 35 · 40° 비율 ·
범위 막음(빈 값 · 0~250 밖 · 아래 ≥ 위 · 겹침 · 없음) · 깨진 취향 = 기본 · 250 % 이상 칸 = 끝 범위.
@@ -22,7 +22,7 @@ from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
from E07_StabArea.E07_StabArea_Engine import slope_rad
ROOT = Path(__file__).resolve().parents[2]
TS_FILE = ROOT / "D03_PlanAnalysis" / "logic" / "D03_PlanAnalysis_SlopeBands.ts"
TS_FILE = ROOT / "A00_Common" / "map" / "logic" / "map_slope_bands.ts"
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
CELL = 2.0
OLD_DEG = ((15.0, 35.0), (35.0, 40.0), (40.0, 90.0)) # 옛 SLOPE_BANDS
@@ -36,7 +36,7 @@ def _hills(n: int = 300) -> np.ndarray:
_RUNNER = """
import { readFileSync, writeFileSync } from "node:fs";
import * as bands from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_SlopeBands.js";
import * as bands from "./A00_Common/map/logic/map_slope_bands.js";
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
const out = { DEFAULT: bands.DEFAULT_SLOPE_BANDS };
// 인자 null = 기본 범위
+1 -1
View File
@@ -10,7 +10,7 @@ ROOT = Path(__file__).resolve().parents[2]
read = lambda rel: (ROOT / rel).read_text(encoding="utf-8") # noqa: E731
MAPGL = read("A00_Common/map/map_layer_specs.ts") # 624-2 GL 겹 꼴 = 공용
LIGHT = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts")
LIGHT = read("A00_Common/map/map_sat_light.ts") # 624-3 공용
LAYERS = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")