- 열쇠 그대로(nationalRoad · forestRoadFile) — 옛 켬끔 취향 이어짐 · forestRoad 열쇠 · 도로 소로 거름은 B05 자료 셈 이음으로 남김(겹 목록 · 꼴 · 범례만 걷음) - 기존 도로(road) 겹 · 소스 · 범례 그대로 - 시험 553_4 겹 차례 · 584_3 범례 기대값을 새 이름으로 고침 · 622_2 에 630 · 534 지킴 덧 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
201 lines
7.7 KiB
TypeScript
201 lines
7.7 KiB
TypeScript
/* =============================================================================
|
|
* map_legend.ts
|
|
* 공용 지도 범례(PLAN 569-2 · 622-2 · 624-2 · 요구 530) — 켠 겹 범례 칸 하나(줄마다 `createLegend`) + 페이지가 붙이는 덧 줄.
|
|
* 조각 겹 범례(산사태위험 · 임도망도 · 경제림 · 생태자연도) = 받을 자료 없음 → 켬끔만 보고 여기서 만듦(`tileLegendRows`).
|
|
* 받은 자료 범례 줄(수 · 넓이 · 급경사 범위 · 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 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;
|
|
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;
|
|
},
|
|
};
|
|
}
|
|
|
|
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.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;
|
|
}
|