- 임도망도: 지도 겹 줄에 올리기 단추(zip 하나 · shp · shx · dbf · prj 묶음 · 100 MB · 넘으면 413) → 서버가 읽어 받은 범위로 자름(prj 없으면 EPSG:5179 · cpg 없으면 cp949) · 원본 = 프로젝트 D03_PlanAnalysis/임도망도.zip · 자른 선 = 파생 저장본 · 못 읽거나 범위 안 선 0 = 400 · 옛 것 그대로 - 범례: 「임도 짐작」(주황 점선) · 「올린 임도망도」(진한 주황 실선) 구분 · 짐작 겹 그대로 - 산사태취약지역: data.go.kr 시도 표 넷(경북 · 충남 · 광주 · 경남)을 resources/data_landslide_zones 에 한 벌(머리 = 이름 · 받은 날 · 기준일 · 출처) · 시도마다 다른 열 이름은 별칭으로 · 위경도 = 받은 범위 안 점 · 지번(시군구 · 읍면동 · 리 · 산 · 본번 · 부번) = 지적 필지 테두리만 · 사업지 시도에 표 없으면 범례 「이 시도 자료 없음」 · 지적 못 받으면 점만 - 두 겹 처음 끔 · 바탕 캔버스 · 색 --draw-map-forest-road-file · landslide-zone · 아이콘 alertOctagon · 고르기 창 pickFiles(부품 상자 ui_template_drop) - 범례 · 겹별 수 · 받은 자료 꼴을 D03_PlanAnalysis_UI_LayerLegend.ts 로 옮김(700줄 한도) - test_553_4: 겹 목록에 forestRoadFile · landslideZones 더함(새 겹 · 기대 목록 갱신) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
8.3 KiB
TypeScript
227 lines
8.3 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_LayerLegend.ts
|
|
* D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-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,
|
|
} from "./D03_PlanAnalysis_Api";
|
|
import {
|
|
LANDSLIDE_LEGEND,
|
|
LINE_KEYS,
|
|
LINE_LAYERS,
|
|
ZONE_COLORS,
|
|
hectare,
|
|
type LineLayerKey,
|
|
type PlanLayerKey,
|
|
} from "./D03_PlanAnalysis_UI_LayerDefs";
|
|
|
|
/** 선 겹 — 그릴 선 · 받은 범위 안 가닥 · 길이 m. */
|
|
export interface PreparedLines {
|
|
layer: PreparedLayer;
|
|
count: number;
|
|
length_m: number;
|
|
}
|
|
export type Prepared = Record<LineLayerKey, PreparedLines> & {
|
|
contour: PreparedLayer;
|
|
/** 세류 · 하천(소하천 · 지방하천 · 연결선). */
|
|
stream: [PreparedLayer, PreparedLayer];
|
|
cadastral: PreparedLayer;
|
|
/** 전체 · 그중 임도 짐작(소로 · 미분류) — `road` · `forestRoad` 가 함께 씀. */
|
|
road: { all: PreparedLayer; small: PreparedLayer };
|
|
/** 올린 임도망도(584-3). */
|
|
forestRoadFile: { layer: PreparedLayer; info: PlanForestRoads };
|
|
slope: { raster: PlanRaster; image: HTMLImageElement };
|
|
hillshade: { raster: PlanRaster; image: HTMLImageElement };
|
|
landslide: { raster: PlanRaster; image: HTMLImageElement };
|
|
/** 산사태취약지역(584-3) — 점(경위도) · 지번 맞는 필지 테두리. */
|
|
landslideZones: { parcels: PreparedLayer; info: PlanLandslideZones };
|
|
water: PreparedLayer;
|
|
zones: { kinds: Map<string, PreparedLayer>; summary: PlanZones["summary"] };
|
|
/** 토양형(코드 차례) → 지물 · 색 값. */
|
|
soils: { kinds: Array<{ layer: PreparedLayer; color: string }>; summary: PlanSoils["summary"] };
|
|
bands: { stream: PreparedLayer; road: PreparedLayer; info: { stream: PlanBand; road: PlanBand } };
|
|
building: PreparedLayer;
|
|
place: Array<{ lon: number; lat: number; name: string }>;
|
|
};
|
|
|
|
const km = (m: number): string => `${(m / 1000).toFixed(2)} km`;
|
|
|
|
/** 산사태취약지역 범례 — 점 · 필지 · 위치 없는 시도 · 표 없는 시도 · 지적 못 받음. */
|
|
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<PlanLayerKey, boolean>,
|
|
data: Partial<Prepared>,
|
|
): LegendItem[][] {
|
|
const rows: LegendItem[][] = [];
|
|
if (on.slope && data.slope)
|
|
rows.push(
|
|
data.slope.raster.bands.map((band) => ({
|
|
label: `${band.min_deg}${band.max_deg < 90 ? `~${band.max_deg}°` : "°↑"} ${(band.ratio * 100).toFixed(1)} %`,
|
|
color: band.color,
|
|
})),
|
|
);
|
|
if (on.landslide && data.landslide) rows.push(LANDSLIDE_LEGEND);
|
|
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 }),
|
|
...(data.water && { water: data.water.features.length }),
|
|
...(data.road && {
|
|
road: data.road.all.features.length,
|
|
forestRoad: data.road.small.features.length,
|
|
}),
|
|
...(data.forestRoadFile && {
|
|
forestRoadFile: data.forestRoadFile.info.count,
|
|
forestRoadFile_m: data.forestRoadFile.info.length_m,
|
|
forestRoadFile_total: data.forestRoadFile.info.total,
|
|
}),
|
|
...(data.landslideZones && {
|
|
landslideZone_points: data.landslideZones.info.points.length,
|
|
landslideZone_parcels: data.landslideZones.parcels.features.length,
|
|
landslideZone_missing: data.landslideZones.info.missing.length,
|
|
}),
|
|
...(data.place && { place: data.place.length }),
|
|
...(data.zones &&
|
|
Object.fromEntries(
|
|
Object.entries(data.zones.summary).flatMap(([kind, value]) => [
|
|
[`zone_${kind}`, value.count],
|
|
[`zone_${kind}_m2`, value.area_m2],
|
|
]),
|
|
)),
|
|
...(data.soils &&
|
|
Object.fromEntries(
|
|
data.soils.summary.flatMap((soil) => [
|
|
[`soil_${soil.code}`, soil.count],
|
|
[`soil_${soil.code}_m2`, soil.area_m2],
|
|
]),
|
|
)),
|
|
...(data.bands && {
|
|
band_stream_m2: data.bands.info.stream.area_m2,
|
|
band_road_m2: data.bands.info.road.area_m2,
|
|
}),
|
|
...(data.hillshade && { hillshade_px: data.hillshade.image.naturalWidth }),
|
|
...(data.landslide && { landslide_px: data.landslide.image.naturalWidth }),
|
|
...Object.fromEntries(
|
|
LINE_KEYS.flatMap((key) => {
|
|
const got = data[key];
|
|
return got
|
|
? [
|
|
[key, got.count],
|
|
[`${key}_m`, got.length_m],
|
|
]
|
|
: [];
|
|
}),
|
|
),
|
|
});
|