Files
Aislo/A00_Common/map/map_legend.ts
T
eomsangdonandClaude Opus 5.5 c6a1efad0c 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>
2026-10-31 20:47:50 +09:00

207 lines
7.9 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.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;
}