- 서버: 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>
260 lines
9.5 KiB
TypeScript
260 lines
9.5 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_LayerLegend.ts
|
|
* D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-3) — 켠 겹의 받은 범위 안 수 · 넓이.
|
|
* 급경사 = 고른 범위(603)마다 % · ° · 비율(1 % 칸 수로 바로 셈).
|
|
* 부르는 곳 = `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";
|
|
import {
|
|
SLOPE_MAX_PCT,
|
|
bandRatios,
|
|
pctToDeg,
|
|
type SlopeBand,
|
|
} from "./logic/D03_PlanAnalysis_SlopeBands";
|
|
|
|
/** 선 겹 — 그릴 선 · 받은 범위 안 가닥 · 길이 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 };
|
|
/** 급경사(603) — 값 그림 칸 · 고른 범위 · 칠한 그림(blob 주소) · 칠하기 ms. */
|
|
slope: {
|
|
raster: PlanRaster;
|
|
image: HTMLImageElement;
|
|
values: ImageData;
|
|
bands: SlopeBand[];
|
|
url: string;
|
|
paintMs: number;
|
|
};
|
|
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 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<PlanLayerKey, boolean>,
|
|
data: Partial<Prepared>,
|
|
): LegendItem[][] {
|
|
const rows: LegendItem[][] = [];
|
|
if (on.slope && data.slope) rows.push(slopeRow(data.slope));
|
|
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.slope && {
|
|
slope_px: data.slope.image.naturalWidth,
|
|
slope_cells: (data.slope.raster.counts ?? []).reduce((sum, n) => sum + n, 0),
|
|
slope_paint_ms: data.slope.paintMs,
|
|
...Object.fromEntries(
|
|
bandRatios(data.slope.raster.counts ?? [], data.slope.bands).map((r, i) => [
|
|
`slope_band_${i}`,
|
|
r,
|
|
]),
|
|
),
|
|
}),
|
|
...(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],
|
|
]
|
|
: [];
|
|
}),
|
|
),
|
|
});
|