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>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* 공용 지도 — 받은 산림 · 환경 자료 겹(PLAN 622-2 · 624-2 · 요구 529 · 530) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴).
|
||||
* 벡터 = 임도망도(forest_road · 전국 국가임도 · 간선 · 작업 · 산불 색 · 줌 따라 굵기 · 줌 14 부터 연도 · 종류 글) · 산림입지도(forest_soil · 토양 무리 색 ·
|
||||
* 반투명 · z10 아래 숨김) · 경제림(eco_forest · 국유 · 사유 테두리) /
|
||||
* 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로).
|
||||
* 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로) · 암상 지질도(geology_map · 760-1 · 도형 = `map_geology`).
|
||||
* 조각만 쓰는 겹(`TILE_ONLY`) = 받기 없음 · 옛 캔버스(그래픽 카드 끔)엔 안 보임.
|
||||
* 토양 무리 색(`soilColor`) = 조각(`code` = SLTP_CD 두 자리) · 브이월드 요약(기호 「B₁」) 같은 색 — 범례 · 옛 캔버스도 이것.
|
||||
* 부르는 곳 = `map_layer_specs`(꼴) · D03 `UI_MapGl`(소스) · `UI_Layers`(받기 거름) · `map_legend`(범례).
|
||||
@@ -29,6 +29,7 @@ export const TRACKED_SOURCES = [
|
||||
"eco_forest",
|
||||
"ecology",
|
||||
"landslide",
|
||||
"geology_map",
|
||||
];
|
||||
|
||||
/** 고정 소스 — 벡터 z8~15(서버 줌별 단순화) · 그림 z6~19. */
|
||||
@@ -41,6 +42,7 @@ export function forestSources(projectId: string): Record<string, SourceSpecifica
|
||||
eco_forest: vt("eco_forest"),
|
||||
ecology: wmt("ecology"),
|
||||
landslide: wmt("landslide"),
|
||||
geology_map: wmt("geology"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,17 +92,25 @@ const zoomWidth = (z8: number, z12: number, z15: number): ExpressionSpecificatio
|
||||
["interpolate", ["linear"], ["zoom"], 8, z8, 12, z12, 15, z15] as ExpressionSpecification;
|
||||
|
||||
type Spec = GlSpec<MapLayerKey>;
|
||||
const raster = (key: "landslide" | "ecology", opacity: number): Spec =>
|
||||
const raster = (
|
||||
key: "landslide" | "ecology" | "geology",
|
||||
opacity: number,
|
||||
source: string = key,
|
||||
): Spec =>
|
||||
({
|
||||
key,
|
||||
id: key,
|
||||
id: source === key ? key : source,
|
||||
type: "raster",
|
||||
source: key,
|
||||
source,
|
||||
paint: { "raster-opacity": opacity, "raster-fade-duration": 0 },
|
||||
}) as Spec;
|
||||
|
||||
/** 그림 겹(산사태위험 · 생태자연도) — 아래 차례(급경사 위). */
|
||||
export const forestRasterSpecs = (): Spec[] => [raster("landslide", 0.6), raster("ecology", 0.55)];
|
||||
/** 그림 겹(암상 지질도 · 산사태위험 · 생태자연도) — 아래 차례(급경사 위). */
|
||||
export const forestRasterSpecs = (): Spec[] => [
|
||||
raster("geology", 0.5, "geology_map"),
|
||||
raster("landslide", 0.6),
|
||||
raster("ecology", 0.55),
|
||||
];
|
||||
|
||||
/** 면 겹(산림입지도 · 경제림) — 보호구역 아래. */
|
||||
export function forestAreaSpecs(): Spec[] {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/* =============================================================================
|
||||
* 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];
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export type GeoJsonCollection = {
|
||||
}>;
|
||||
};
|
||||
|
||||
async function request<T>(path: string): Promise<T> {
|
||||
export async function request<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -29,6 +29,7 @@ export type MapLayerKey =
|
||||
| "soils"
|
||||
| "landslide"
|
||||
| "landslideZones"
|
||||
| "geology" // 암상 지질도(760-1 · KIGAM 5만 · 그림 조각 + 도형)
|
||||
| "faults"
|
||||
| "lineaments"
|
||||
| "zeroOrder"
|
||||
@@ -100,6 +101,7 @@ export const LAYERS: readonly LayerDef[] = [
|
||||
row("ecology", "plant", "생태자연도", "limit"),
|
||||
row("landslide", "alert", "산사태위험지도", "limit"),
|
||||
row("landslideZones", "alertOctagon", "산사태취약지역", "limit"),
|
||||
row("geology", "strata", "암상 지질도", "limit"),
|
||||
row("faults", "bolt", "단층", "limit"),
|
||||
row("lineaments", "lineament", "지질구조선", "limit"),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/* =============================================================================
|
||||
* map_layer_source.ts
|
||||
* 공용 지도 밖 겹 출처(PLAN 760-1 · 요구 598-4 「받아온 곳의 정보를 표기」) — 켠 겹의 받아온 곳을 범례 맨 아래 한 줄씩.
|
||||
* 받아온 곳 = 서버가 부르는 곳 그대로(브이월드 WFS · 위성 · 산림청 원자료 · 생활안전지도 WMS · 지오빅데이터 …) ·
|
||||
* 자체 셈 겹(음영 · 급경사 · 0차곡 · 띠) · 사용자 올림(임도(업로드)) · 자기 지형(등고선 · 세류)은 안 적음.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { LAYERS, type MapLayerKey } from "./map_layer_list";
|
||||
|
||||
/** 받아온 곳 → 겹(차례 = 범례 차례). */
|
||||
export const LAYER_SOURCES: ReadonlyArray<[string, readonly MapLayerKey[]]> = [
|
||||
["브이월드", ["satellite", "cadastral", "zones", "faults", "lineaments"]],
|
||||
["국토지리정보원 수치지형도", ["water", "road", "building", "place"]],
|
||||
["산림청", ["nationalRoad", "ecoForest", "soils"]],
|
||||
["시도 산사태취약지역 현황(공공데이터포털)", ["landslideZones"]],
|
||||
["환경부 생태자연도(공공데이터포털)", ["ecology"]],
|
||||
["행정안전부 생활안전지도(공공누리 제4유형)", ["landslide"]],
|
||||
["한국지질자원연구원 수치지질도 1:50,000(CC BY-NC)", ["geology"]],
|
||||
];
|
||||
|
||||
/** 켠 밖 겹의 출처 줄 — 「출처: 곳 — 겹 · 겹」 · 없으면 빈 것. */
|
||||
export function sourceRow(on: Record<MapLayerKey, boolean>): LegendItem[] {
|
||||
const title = (key: MapLayerKey): string => LAYERS.find((def) => def.key === key)?.title ?? key;
|
||||
return LAYER_SOURCES.flatMap(([where, keys]) => {
|
||||
const shown = keys.filter((key) => on[key]);
|
||||
return shown.length
|
||||
? [{ label: `출처: ${where} — ${shown.map(title).join(" · ")}`, color: "transparent" }]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
forestRoadLabelSpec,
|
||||
forestRoadSpecs,
|
||||
} from "./map_forest_layers";
|
||||
import { geologySpecs } from "./map_geology";
|
||||
import {
|
||||
LINE_KEYS,
|
||||
LINE_LAYERS,
|
||||
@@ -137,6 +138,7 @@ export function mapLayerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
raster("slope", 0.55),
|
||||
...forestRasterSpecs(), // 산사태위험 · 생태자연도 = 조각 그림(622-2)
|
||||
...forestAreaSpecs(), // 산림입지도 · 경제림 = 벡터 조각
|
||||
...geologySpecs(), // 암상 지질도 면 · 글(760-1)
|
||||
...fillEdge("zones", "zones", COLOR, 0.22, { color: COLOR, width: 1.6 }),
|
||||
// 띠 = 한 자료(`_band`) · 겹 둘(634-2 · 도로 300 m 아래 · 계류 100 m 위 — 옛 한 겹 차례 그대로)
|
||||
...fillEdge("roadBands", "bands", COLOR, 0.16, BAND_EDGE, ["==", ["get", "_band"], "road"]),
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
* 조각 겹 범례(산사태위험 · 임도망도 · 경제림 · 생태자연도) = 받을 자료 없음 → 켬끔만 보고 여기서 만듦(`tileLegendRows`).
|
||||
* 받은 자료 범례 줄(수 · 넓이 · 급경사 범위 · 624-3 D03 에서 옮김) = `legendRows(on, data)` — 받은 자료 꼴은 `LegendData`
|
||||
* (D03 옛 캔버스 자료 · B05 GL 자료 둘 다 맞음 · 받는 일은 페이지 몫).
|
||||
* 암상 지질도 줄 · 노선 구간 표 = `map_geology`(760-1) · 켠 밖 겹 출처 줄 = 맨 아래(`map_layer_source` · 598-4).
|
||||
* 쓰는 법 = `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 { geologyRow, type PlanGeology } from "./map_geology";
|
||||
import type {
|
||||
PlanBand,
|
||||
PlanForestRoads,
|
||||
@@ -18,6 +20,7 @@ import type {
|
||||
PlanZones,
|
||||
} from "./map_layer_api";
|
||||
import { hectare } from "./map_layer_data";
|
||||
import { sourceRow } from "./map_layer_source";
|
||||
import {
|
||||
LANDSLIDE_LEGEND,
|
||||
LINE_KEYS,
|
||||
@@ -47,6 +50,7 @@ export interface LegendData {
|
||||
faults: { count: number; length_m: number };
|
||||
lineaments: { count: number; length_m: number };
|
||||
zeroOrder: { count: number; length_m: number };
|
||||
geology: PlanGeology;
|
||||
}
|
||||
|
||||
export interface MapLegend {
|
||||
@@ -187,6 +191,7 @@ export function legendRows(
|
||||
];
|
||||
});
|
||||
if (lines.length) rows.push(lines);
|
||||
if (on.geology && data.geology) rows.push(geologyRow(data.geology));
|
||||
const tail: LegendItem[] = [];
|
||||
if (on.water && data.water)
|
||||
tail.push({
|
||||
@@ -205,5 +210,6 @@ export function legendRows(
|
||||
kind: "line",
|
||||
});
|
||||
if (tail.length) rows.push(tail);
|
||||
rows.push(...sourceRow(on).map((item) => [item]));
|
||||
return rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user