Files
Aislo/A00_Common/map/map_geology.ts
T
eomsangdonandClaude Opus 5.5 f6cfaf446c feat(760-1): 암상 지질도 겹 · 노선 따라 바위 종류 구간 · 밖 겹 출처 줄
- 서버: 지오빅데이터(KIGAM) 수치지질도 1:50,000 — WFS 암상 면(깨진 한글 칸은 기호별 점 조회로 채움) · 받은 범위 ∪ 노선으로 자름 · 저장본 · 노선 따라 1 m 표본 구간 · 키는 서버만(.env GEOBIGDATA_API_KEY · 쿼리 key)
- 그림 조각 대리에 geology(키 쿼리 이름 칸 key_param · 옛 겹 serviceKey 그대로)
- 화면(D03 · B05): 「제한·재해」에 「암상 지질도」 — 그림 조각 + 갈래 색 면 · 「기호 · 암상」 글 · 범례 · 노선 구간 표(참고 — 토사 · 암 판정 아님)
- 켠 밖 겹마다 범례 맨 아래 출처 줄(브이월드 · 국토지리정보원 · 산림청 · 환경부 · 생활안전지도 공공누리 · 한국지질자원연구원 CC BY-NC)
- 범례 칸 높이 한도 · 밀기(낮은 화면)
- 시험: test_760_1 새로 · test_634_2 · test_553_4 겹 차례 표에 geology 한 줄 더함(새 겹 · 낡은 표)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:32:53 +09:00

162 lines
6.0 KiB
TypeScript

/* =============================================================================
* map_geology.ts
* 공용 지도 암상 지질도 겹(PLAN 760-1 · 요구 597-3 · 598-3) — D03 예정노선 · B05 평면이 같이 씀.
* 그림 = KIGAM 5만 WMS 조각(`map_forest_layers` 「geology_map」) · 도형 = 서버 `/plan/layers/geology`
* (기호별 요약 · 경위도 면 · 노선마다 암상 구간 · 키는 서버만).
* 색 = 바위 갈래(서버 `ROCK_GROUPS` 차례) → `--draw-map-geology-1..6` · 면 위 글 = 「기호 · 첫 암상 낱말」.
* 범례 줄(`geologyRow`) · 노선 구간 표(`geologyTail` · 참고 — 토사 · 암 판정 아님) = 범례 칸.
* ========================================================================== */
import type { LegendItem } from "@ui/ui_template_flow_legend";
import { GL_FONT, type GlSpec } from "@ui/ui_template_gl_map";
import { themeColor } from "@ui/ui_template_palette";
import { createTable } from "@ui/ui_template_table";
import { el } from "@ui/ui_template_elements_base";
import { stationLabel } from "@util/common_util_station";
import { request, type GeoJsonCollection } from "./map_layer_api";
import { hectare } from "./map_layer_data";
import type { MapLayerKey } from "./map_layer_list";
/** 기호 하나(받은 범위 안). */
export interface GeologyUnit {
symnum: string;
symbol: string;
litho: string;
strata: string;
age: string;
sheet: string;
group: number;
count: number;
area_m2: number;
}
/** 노선 구간 — `i` = 요약 차례 · -1 = 자료 없음. */
export interface GeologySegment {
from_m: number;
to_m: number;
i: number;
}
export interface PlanGeology extends GeoJsonCollection {
source: string;
summary: GeologyUnit[];
groups: string[];
routes: Array<{ name: string; segments: GeologySegment[] }>;
}
const GROUP_FALLBACK = ["#f472b6", "#fb923c", "#fde047", "#818cf8", "#e5e7eb", "#5eead4"];
const groupToken = (group: number): string => `--draw-map-geology-${Math.min(group, 5) + 1}`;
const firstWord = (text: string): string => text.split(/\s+/)[0] ?? "";
/** 긴 암상 글 줄임(범례 · 표 폭). */
const short = (text: string, max = 22): string =>
text.length > max ? `${text.slice(0, max)}…` : text;
/** 받기 — 면 위 글 꼴을 먼저 읽어 둠(한글 글자). */
export async function fetchPlanGeology(projectId: string): Promise<PlanGeology> {
const got = await request<PlanGeology>(`/projects/${projectId}/plan/layers/geology`);
const text = got.summary.map((unit) => unit.symbol + unit.litho).join("") || "가";
await document.fonts.load(`600 24px "${GL_FONT}"`, text).catch(() => null);
return got;
}
/** GL 자료 — 지물마다 색(`_color`) · 글(`_label`). */
export function geologyCollection(info: PlanGeology): GeoJsonCollection {
return {
features: (info.features ?? []).map((feature) => {
const unit = info.summary[Number(feature.properties?.["i"])];
const group = unit?.group ?? 5;
return {
...feature,
properties: {
...feature.properties,
_color: themeColor(groupToken(group), GROUP_FALLBACK[group] ?? GROUP_FALLBACK[5]),
_label: unit ? `${unit.symbol} · ${firstWord(unit.litho)}` : "",
},
};
}),
};
}
/** GL 꼴 — 연한 채움 · 갈래 색 테두리 · 면 위 글(겹치면 뺌). */
export function geologySpecs(): GlSpec<MapLayerKey>[] {
const color = ["get", "_color"];
return [
{
key: "geology",
id: "geology",
type: "fill",
source: "geology",
paint: { "fill-color": color, "fill-opacity": 0.2 },
},
{
key: "geology",
id: "geology-edge",
type: "line",
source: "geology",
layout: { "line-join": "round" },
paint: { "line-color": color, "line-width": 1.6, "line-opacity": 0.95 },
},
{
key: "geology",
id: "geology-label",
type: "symbol",
source: "geology",
layout: {
"text-field": ["get", "_label"],
"text-font": [GL_FONT],
"text-size": 12,
"text-padding": 4,
},
paint: {
"text-color": color,
"text-halo-color": "rgba(0, 0, 0, 0.75)",
"text-halo-width": 1.5,
},
},
] as unknown as GlSpec<MapLayerKey>[];
}
const unitColor = (unit: GeologyUnit | undefined): string =>
unit ? `var(${groupToken(unit.group)})` : "transparent";
/** 범례 줄 — 기호마다 「기호 대표암상 넓이」(갈래 색) · 없으면 한 줄. */
export function geologyRow(info: PlanGeology): LegendItem[] {
if (!info.summary.length)
return [{ label: "암상 지질도 없음(받은 범위 안)", color: "transparent", off: true }];
return info.summary.map((unit) => ({
label: `${unit.symbol} ${short(unit.litho || unit.strata)} ${hectare(unit.area_m2)}`,
color: unitColor(unit),
}));
}
/** 노선 따라 바위 종류 구간 표(노선마다) + 참고 글 — 켬 · 받음 · 노선 있을 때만. */
export function geologyTail(on: Record<MapLayerKey, boolean>, info?: PlanGeology): HTMLElement[] {
if (!on.geology || !info?.routes.length) return [];
const columns = [
{ label: "측점" },
{ label: "길이", align: "end" as const },
{ label: "기호" },
{ label: "대표암상" },
];
const tables = info.routes.map((route) => {
const rows = route.segments.map((seg) => {
const unit = info.summary[seg.i];
return [
`${stationLabel(seg.from_m, 20)} ~ ${stationLabel(seg.to_m, 20)}`,
`${(seg.to_m - seg.from_m).toFixed(0)} m`,
unit?.symbol ?? "—",
unit ? short(unit.litho || unit.strata, 30) : "자료 없음",
];
});
return el("div", {
className: "ui-legend ui-legend--vertical",
children: [`암상 구간 — ${route.name}`, createTable(columns, rows, { density: "data" })],
});
});
const note = el("div", {
className: "ui-legend",
children: ["참고 — 토사 · 암 판정 아님(1:50,000 바위 종류 · 경계 수십 m 어긋날 수 있음)"],
});
return [...tables, note];
}