feat(D03 지도 겹): 받은 산림 · 환경 자료 겹 다섯 화면 — 국유림 임도 · 산림입지도 · 경제림 · 생태자연도 · 산사태 = 서버 줌별 조각 고정 소스 · 범례 · 받는 동안 서클 (PLAN 622-2 · 요구 529)
- 공용 부품 tileSource(벡터 pbf · 그림 png) 덧 · 새 파일 UI_ForestLayers(꼴 · 소스 · 범례 · 토양 무리 색) - 산림입지도 그림 = forest_soil 조각(z10 이상) · 브이월드 받기 · 요약 그대로 · 범례 · 옛 캔버스 색 = 같은 토양 무리 색 - 산사태위험 GL = 조각 그림(받기 없음) · 옛 캔버스 = 받은 범위 한 장 그대로(끌 때 받음) - 새 겹 셋(국유림 임도 · 경제림 · 생태자연도) 처음 끔 · 옛 캔버스엔 안 보임 · 색 토큰 --draw-map-* 덧 · 아이콘 trees · plant 덧 - UI_Layers 688 → 680 줄(GL 글꼴 미리 받기 = UI_MapGl 로 · 브이월드 토양 GL 꼬리표 뺌) - 지킴 시험 test_622_2_forest_layers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_ForestLayers.ts
|
||||
* D03 받은 산림 · 환경 자료 겹(PLAN 622-2 · 요구 529) — 서버 줌별 조각(622-1)을 GL 고정 소스로(위성 · 등고선 615 꼴).
|
||||
* 벡터 = 국유림 임도(forest_road · 간선 · 작업 · 산불 색 · 줌 따라 굵기) · 산림입지도(forest_soil · 토양 무리 색 ·
|
||||
* 반투명 · z10 아래 숨김) · 경제림(eco_forest · 국유 · 사유 테두리) /
|
||||
* 그림 = 생태자연도(ecology) · 산사태위험지도(landslide · 옛 캔버스는 받은 범위 한 장 그대로).
|
||||
* 조각만 쓰는 겹(`TILE_ONLY`) = 받기 없음 · 옛 캔버스(그래픽 카드 끔)엔 안 보임.
|
||||
* 토양 무리 색(`soilColor`) = 조각(`code` = SLTP_CD 두 자리) · 브이월드 요약(기호 「B₁」) 같은 색 — 범례 · 옛 캔버스도 이것.
|
||||
* 부르는 곳 = `D03_PlanAnalysis_UI_MapGl`(꼴 · 소스) · `UI_Layers`(받기 거름) · `UI_LayerLegend`(범례).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { ExpressionSpecification, SourceSpecification } from "maplibre-gl";
|
||||
import type { LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { tileSource, type GlSpec } from "@ui/ui_template_gl_map";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { PlanLayerKey } from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
|
||||
/** 조각만 쓰는 겹 — 받을 자료 없음(위성 포함). */
|
||||
const TILE_ONLY = ["satellite", "nationalRoad", "ecoForest", "ecology"] as const;
|
||||
export type TileOnlyKey = (typeof TILE_ONLY)[number];
|
||||
export const tileOnly = (key: PlanLayerKey): key is TileOnlyKey =>
|
||||
(TILE_ONLY as readonly string[]).includes(key);
|
||||
|
||||
/** 받는 동안 서클에 세는 조각 소스(등고선 포함). */
|
||||
export const TRACKED_SOURCES = [
|
||||
"contour",
|
||||
"forest_road",
|
||||
"forest_soil",
|
||||
"eco_forest",
|
||||
"ecology",
|
||||
"landslide",
|
||||
];
|
||||
|
||||
/** 고정 소스 — 벡터 z8~15(서버 줌별 단순화) · 그림 z6~19. */
|
||||
export function forestSources(projectId: string): Record<string, SourceSpecification> {
|
||||
const vt = (name: string) => tileSource(projectId, `vt/${name}`, "pbf", 8, 15);
|
||||
const wmt = (name: string) => tileSource(projectId, `wmt/${name}`, "png", 6, 19);
|
||||
return {
|
||||
forest_road: vt("forest_road"),
|
||||
forest_soil: vt("forest_soil"),
|
||||
eco_forest: vt("eco_forest"),
|
||||
ecology: wmt("ecology"),
|
||||
landslide: wmt("landslide"),
|
||||
};
|
||||
}
|
||||
|
||||
/** 토양형 코드(산림입지토양도 SLTP_CD) → 기호. 무리 = 갈색 · 적황색 · 암적색 · 회갈색 · 화산회 · 침식 · 미숙 · 그 밖(비산림). */
|
||||
const SOIL_SYMBOLS: Record<string, string> = {
|
||||
"01": "B1", "02": "B2", "03": "B3", "04": "B4",
|
||||
"05": "rB1", "06": "rB2", "07": "R1", "08": "R2", "09": "Y",
|
||||
"10": "DR1", "11": "DR2", "12": "DR3", "13": "DRb1", "14": "DRb2",
|
||||
"15": "GrB1", "16": "GrB2", "17": "Va1", "18": "Va2", "19": "Va3", "20": "Va4",
|
||||
"21": "Va-R1", "22": "Va-R2", "23": "Va-gr",
|
||||
"24": "Er1", "25": "Er2", "26": "Er-c", "27": "Im", "28": "Li", "29": "Bs",
|
||||
}; // prettier-ignore
|
||||
/** 무리 첫 코드(차례 = `--draw-map-soil-1..6`). */
|
||||
const SOIL_GROUP_START = [1, 5, 10, 15, 24, 30];
|
||||
const soilToken = (group: number): string => `--draw-map-soil-${group + 1}`;
|
||||
const SUB = /[₀-₉]/g;
|
||||
|
||||
/** 토양형(조각 두 자리 코드 · 브이월드 기호 「B₁」) → 무리 색 값 · 모름 = 「그 밖」. */
|
||||
export function soilColor(code: string): string {
|
||||
const plain = code.trim().replace(SUB, (c) => String(c.charCodeAt(0) - 0x2080));
|
||||
const found = /^\d+$/.test(plain)
|
||||
? Number(plain)
|
||||
: Number(Object.keys(SOIL_SYMBOLS).find((key) => SOIL_SYMBOLS[key] === plain) ?? 99);
|
||||
return soilToken(Math.max(SOIL_GROUP_START.filter((start) => found >= start).length - 1, 0));
|
||||
}
|
||||
|
||||
/** 임도 사업 구분(`kind` 1 간선 · 2 작업 · 3 산불) 색 값. */
|
||||
const ROAD_KINDS = [
|
||||
{ kind: 1, label: "간선", color: "--draw-map-national-road-main" },
|
||||
{ kind: 2, label: "작업", color: "--draw-map-national-road-work" },
|
||||
{ kind: 3, label: "산불", color: "--draw-map-national-road-fire" },
|
||||
];
|
||||
const ECO_KINDS = [
|
||||
{ own: "국유", color: "--draw-map-eco-national" },
|
||||
{ own: "사유", color: "--draw-map-eco-private" },
|
||||
];
|
||||
/** 생태자연도 범례 = 환경부 원도 색(조각 그림 그대로). */
|
||||
const ECOLOGY_LEGEND = [
|
||||
{ label: "1등급", color: "--draw-map-ecology-1" },
|
||||
{ label: "2등급", color: "--draw-map-ecology-2" },
|
||||
{ label: "3등급", color: "--draw-map-ecology-3" },
|
||||
{ label: "별도관리", color: "--draw-map-ecology-x" },
|
||||
];
|
||||
|
||||
const HALO = "rgba(0, 0, 0, 0.55)";
|
||||
const ROUND = { "line-join": "round", "line-cap": "round" } as const;
|
||||
const zoomWidth = (z8: number, z12: number, z15: number): ExpressionSpecification =>
|
||||
["interpolate", ["linear"], ["zoom"], 8, z8, 12, z12, 15, z15] as ExpressionSpecification;
|
||||
|
||||
type Spec = GlSpec<PlanLayerKey>;
|
||||
const raster = (key: "landslide" | "ecology", opacity: number): Spec =>
|
||||
({
|
||||
key,
|
||||
id: key,
|
||||
type: "raster",
|
||||
source: key,
|
||||
paint: { "raster-opacity": opacity, "raster-fade-duration": 0 },
|
||||
}) as Spec;
|
||||
|
||||
/** 그림 겹(산사태위험 · 생태자연도) — 아래 차례(급경사 위). */
|
||||
export const forestRasterSpecs = (): Spec[] => [raster("landslide", 0.6), raster("ecology", 0.55)];
|
||||
|
||||
/** 면 겹(산림입지도 · 경제림) — 보호구역 아래. */
|
||||
export function forestAreaSpecs(): Spec[] {
|
||||
const soilAt = (group: number): string => themeColor(soilToken(group), "#e879f9");
|
||||
const soil = [
|
||||
"step",
|
||||
["to-number", ["get", "code"], 99],
|
||||
soilAt(0),
|
||||
...SOIL_GROUP_START.slice(1).flatMap((start, i) => [start, soilAt(i + 1)]),
|
||||
] as ExpressionSpecification;
|
||||
const eco = [
|
||||
"match",
|
||||
["get", "own"],
|
||||
...ECO_KINDS.flatMap(({ own, color }) => [own, themeColor(color, "#34d399")]),
|
||||
themeColor(ECO_KINDS[1].color, "#a78bfa"),
|
||||
] as unknown as ExpressionSpecification;
|
||||
const vector = (source: string) => ({ source, "source-layer": source });
|
||||
return [
|
||||
{
|
||||
key: "soils",
|
||||
id: "soils",
|
||||
type: "fill",
|
||||
...vector("forest_soil"),
|
||||
minzoom: 10,
|
||||
paint: { "fill-color": soil, "fill-opacity": 0.35 },
|
||||
} as Spec,
|
||||
{
|
||||
key: "soils",
|
||||
id: "soils-edge",
|
||||
type: "line",
|
||||
...vector("forest_soil"),
|
||||
minzoom: 10,
|
||||
layout: ROUND,
|
||||
paint: { "line-color": soil, "line-width": 0.8, "line-opacity": 0.9 },
|
||||
} as Spec,
|
||||
{
|
||||
key: "ecoForest",
|
||||
id: "ecoForest",
|
||||
type: "line",
|
||||
...vector("eco_forest"),
|
||||
layout: ROUND,
|
||||
paint: { "line-color": eco, "line-width": zoomWidth(1, 1.8, 2.4) },
|
||||
} as Spec,
|
||||
];
|
||||
}
|
||||
|
||||
/** 국유림 임도 선 — 검은 테 + 사업 구분 색(올린 임도망도 위). */
|
||||
export function forestRoadSpecs(): Spec[] {
|
||||
const color = [
|
||||
"match",
|
||||
["get", "kind"],
|
||||
...ROAD_KINDS.flatMap(({ kind, color }) => [kind, themeColor(color, "#ff6b00")]),
|
||||
themeColor(ROAD_KINDS[0].color, "#ff6b00"),
|
||||
] as unknown as ExpressionSpecification;
|
||||
const line = (id: string, paint: Record<string, unknown>): Spec =>
|
||||
({
|
||||
key: "nationalRoad",
|
||||
id,
|
||||
type: "line",
|
||||
source: "forest_road",
|
||||
"source-layer": "forest_road",
|
||||
layout: ROUND,
|
||||
paint,
|
||||
}) as Spec;
|
||||
return [
|
||||
line("nationalRoad-halo", { "line-color": HALO, "line-width": zoomWidth(2.2, 3.6, 5.6) }),
|
||||
line("nationalRoad", { "line-color": color, "line-width": zoomWidth(1, 2, 3.6) }),
|
||||
];
|
||||
}
|
||||
|
||||
/** 범례 — 켠 조각 겹(산사태위험 = 범례 자리 `LANDSLIDE_LEGEND` 따로). */
|
||||
export function forestLegendRows(on: Record<PlanLayerKey, boolean>): LegendItem[][] {
|
||||
const item = (label: string, color: string, kind?: LegendItem["kind"]): LegendItem => ({
|
||||
label,
|
||||
color: `var(${color})`,
|
||||
...(kind ? { kind } : {}),
|
||||
});
|
||||
const rows: LegendItem[][] = [];
|
||||
if (on.nationalRoad)
|
||||
rows.push(ROAD_KINDS.map(({ label, color }) => item(`국유림 임도 ${label}`, color, "line")));
|
||||
if (on.ecoForest)
|
||||
rows.push(ECO_KINDS.map(({ own, color }) => item(`경제림 ${own}`, color, "line")));
|
||||
if (on.ecology)
|
||||
rows.push(ECOLOGY_LEGEND.map(({ label, color }) => item(`생태자연도 ${label}`, color)));
|
||||
return rows;
|
||||
}
|
||||
@@ -30,6 +30,9 @@ export type PlanLayerKey =
|
||||
| "road"
|
||||
| "forestRoad"
|
||||
| "forestRoadFile"
|
||||
| "nationalRoad"
|
||||
| "ecoForest"
|
||||
| "ecology"
|
||||
| "cadastral"
|
||||
| "zones"
|
||||
| "soils"
|
||||
@@ -80,6 +83,13 @@ export const GROUPS: readonly (readonly LayerDef[])[] = [
|
||||
"올린 임도망도(산림청 시군구 SHP = 밝은 분홍 실선 · 왼쪽 「위치」 칸에서 올리고 묶음 고름 · 지움)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "nationalRoad",
|
||||
icon: "flag",
|
||||
title:
|
||||
"국유림 임도(산림청 전국 국가임도 · 간선 · 작업 · 산불 색 · 지방 · 민간임도 없음 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{ key: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false },
|
||||
@@ -92,7 +102,19 @@ export const GROUPS: readonly (readonly LayerDef[])[] = [
|
||||
{
|
||||
key: "soils",
|
||||
icon: "leaf",
|
||||
title: "산림입지도(토양형 · 산림청 산림입지토양도 · 참고용)",
|
||||
title: "산림입지도(토양형 · 산림청 산림입지토양도 1:25,000 · 줌 10 이상 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "ecoForest",
|
||||
icon: "trees",
|
||||
title: "경제림육성단지(산림청 2014 · 국유 · 사유 테두리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
key: "ecology",
|
||||
icon: "plant",
|
||||
title: "생태자연도(환경부 · 1~3 등급 · 별도관리 · 참고용)",
|
||||
on: false,
|
||||
},
|
||||
{
|
||||
@@ -164,8 +186,6 @@ export const LINE_LAYERS: Record<
|
||||
zeroOrder: { label: "0차곡", color: "--draw-map-zero-order", width: 1.8, dash: [4, 3] },
|
||||
};
|
||||
export const LINE_KEYS = Object.keys(LINE_LAYERS) as LineLayerKey[];
|
||||
/** 토양형 색 — 받은 범위 안 코드 차례로 돌려 씀. */
|
||||
export const SOIL_COLORS = [1, 2, 3, 4, 5, 6].map((n) => `--draw-map-soil-${n}`);
|
||||
/** 도로 중 기존 임도로 짐작하는 구분. */
|
||||
export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]);
|
||||
/** 물 면 = 도엽 수계(E) 면 셋. */
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type LineLayerKey,
|
||||
type PlanLayerKey,
|
||||
} from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import { forestLegendRows } from "./D03_PlanAnalysis_UI_ForestLayers";
|
||||
import {
|
||||
SLOPE_MAX_PCT,
|
||||
bandRatios,
|
||||
@@ -115,7 +116,8 @@ export function legendRows(
|
||||
): 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.landslide) rows.push(LANDSLIDE_LEGEND); // GL = 조각(받기 없음 · 622-2)
|
||||
rows.push(...forestLegendRows(on));
|
||||
if (on.landslideZones && data.landslideZones) rows.push(landslideZoneRow(data.landslideZones));
|
||||
if (on.zones && data.zones) {
|
||||
const items = Object.entries(data.zones.summary)
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
* GL 바탕(586-2 · 처음 = 켬 · 끝 단추 끔 = 옛 캔버스 · `aislo:pref:d03.canvas`) — 겹 스물 · 등고 글 · 지명을
|
||||
* MapLibre 가 그림(`D03_PlanAnalysis_UI_MapGl`) · 캔버스 바탕 겹은 쉼(등고 간격 셈만) · 받기 · 범례는 같음 ·
|
||||
* WebGL 못 씀 · 잃음 = 저절로 옛 캔버스 + 알림(취향은 안 바꿈).
|
||||
* 받은 산림 · 환경 자료(622-2) = 국유림 임도 · 경제림 · 생태자연도(조각만 · 받기 없음) · 산림입지도 · 산사태위험(GL = 조각 ·
|
||||
* 옛 캔버스 = 지금 받기 그대로) — 꼴 · 범례 = `D03_PlanAnalysis_UI_ForestLayers`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
@@ -58,7 +60,6 @@ import {
|
||||
GROUPS,
|
||||
LAYERS,
|
||||
LINE_LAYERS,
|
||||
SOIL_COLORS,
|
||||
WATER_KEYS,
|
||||
ZONE_COLORS,
|
||||
glImage,
|
||||
@@ -71,7 +72,8 @@ import {
|
||||
} from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend";
|
||||
import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint";
|
||||
import { createGlBase, GL_FONT, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
|
||||
import { createGlBase, glFont, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
|
||||
import { soilColor, tileOnly, type TileOnlyKey } from "./D03_PlanAnalysis_UI_ForestLayers";
|
||||
import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight";
|
||||
import {
|
||||
createSheetTiles,
|
||||
@@ -89,17 +91,14 @@ import {
|
||||
import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
|
||||
|
||||
const PREF_KEY = "aislo:pref:d03.layers";
|
||||
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
|
||||
const glFont = (weight: string, text: string): Promise<unknown> =>
|
||||
document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null);
|
||||
/** 옛 캔버스로 그림("1") — 처음 = GL. */
|
||||
const CANVAS_PREF_KEY = "aislo:pref:d03.canvas";
|
||||
/** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */
|
||||
const NO_SATELLITE_NOTE = "옛 그림은 위성 바탕 없이 그림";
|
||||
type DataKey = keyof Prepared;
|
||||
|
||||
/** 겹 → 받는 자료(임도 짐작 = 도로 자료). */
|
||||
const dataKey = (key: Exclude<PlanLayerKey, "satellite">): DataKey =>
|
||||
/** 겹 → 받는 자료(임도 짐작 = 도로 자료 · 조각만 쓰는 겹 = 없음). */
|
||||
const dataKey = (key: Exclude<PlanLayerKey, TileOnlyKey>): DataKey =>
|
||||
key === "forestRoad" ? "road" : key;
|
||||
|
||||
export interface PlanLayers {
|
||||
@@ -197,6 +196,7 @@ export function createPlanLayers(opts: {
|
||||
glOn = want && map.setGl(gl);
|
||||
if (!glOn) map.setGl(null);
|
||||
glButton.setAttribute("aria-pressed", String(glOn));
|
||||
if (!glOn) void Promise.all(LAYERS.map((def) => ensure(def.key))); // 산사태위험 한 장(622-2)
|
||||
sync();
|
||||
map.redrawBase();
|
||||
}
|
||||
@@ -212,8 +212,9 @@ export function createPlanLayers(opts: {
|
||||
legend.className = "d03-plan__legend";
|
||||
legend.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이";
|
||||
|
||||
/** 받음 — 위성 = GL 조각(받을 것 없음 · 옛 캔버스 = 위성 없음). */
|
||||
const loadedOf = (key: PlanLayerKey): boolean => key === "satellite" || !!data[dataKey(key)];
|
||||
/** 받음 — 위성 · 산림 조각 = GL 조각(받을 것 없음 · 옛 캔버스 = 안 그림) · 산사태위험 = GL 이면 조각(622-2). */
|
||||
const loadedOf = (key: PlanLayerKey): boolean =>
|
||||
tileOnly(key) || (glOn && key === "landslide") || !!data[dataKey(key)];
|
||||
|
||||
function sync(): void {
|
||||
for (const def of LAYERS) {
|
||||
@@ -222,8 +223,8 @@ export function createPlanLayers(opts: {
|
||||
button.disabled = !meta;
|
||||
const title = !meta
|
||||
? `${def.title} — 지형 받기 뒤 보임`
|
||||
: def.key === "satellite" && !glOn
|
||||
? `${def.title} — ${NO_SATELLITE_NOTE}`
|
||||
: tileOnly(def.key) && !glOn
|
||||
? `${def.title} — 옛 그림(그래픽 카드 끔)엔 안 보임`
|
||||
: def.title;
|
||||
button.title = title;
|
||||
button.setAttribute("aria-label", title);
|
||||
@@ -287,7 +288,7 @@ export function createPlanLayers(opts: {
|
||||
/** 켜진 겹의 자료를 받음(한 번) — 실패 = 그 겹만 끔(기억은 안 바꿈 · 다음에 다시 받음) + 알림. */
|
||||
function ensure(key: PlanLayerKey): Promise<void> {
|
||||
if (!meta || !on[key] || loadedOf(key)) return Promise.resolve();
|
||||
const slot = key === "satellite" ? key : dataKey(key);
|
||||
const slot = tileOnly(key) ? "satellite" : dataKey(key);
|
||||
const running = pending.get(slot);
|
||||
if (running) return running;
|
||||
const mine = areaTurn;
|
||||
@@ -301,7 +302,7 @@ export function createPlanLayers(opts: {
|
||||
if (mine !== areaTurn) return;
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
for (const def of LAYERS)
|
||||
if (def.key === key || (def.key !== "satellite" && dataKey(def.key) === slot))
|
||||
if (def.key === key || (!tileOnly(def.key) && dataKey(def.key) === slot))
|
||||
on[def.key] = false;
|
||||
sync();
|
||||
map.redraw();
|
||||
@@ -543,21 +544,12 @@ export function createPlanLayers(opts: {
|
||||
} else if (key === "soils") {
|
||||
const soils = await fetchPlanSoils(projectId);
|
||||
got.soils = {
|
||||
kinds: soils.summary.map((soil, i) => ({
|
||||
kinds: soils.summary.map((soil) => ({
|
||||
layer: prepare(pick(soils, (props) => props["code"] === soil.code)),
|
||||
color: SOIL_COLORS[i % SOIL_COLORS.length],
|
||||
color: soilColor(soil.code), // GL 조각과 같은 무리 색(그림 = 조각 · 622-2)
|
||||
})),
|
||||
summary: soils.summary,
|
||||
};
|
||||
const colorOf = new Map(
|
||||
soils.summary.map((soil, i) => [soil.code, got.soils!.kinds[i].color]),
|
||||
);
|
||||
put(
|
||||
"soils",
|
||||
glTagged(soils, (props) =>
|
||||
themeColor(colorOf.get(props["code"] as never) ?? "", "#facc15"),
|
||||
),
|
||||
);
|
||||
} else if (key === "faults" || key === "lineaments" || key === "zeroOrder") {
|
||||
const kind = LINE_LAYERS[key].kind;
|
||||
// 0차곡 첫 셈 = 몇 초 — 늦으면 알림
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 카메라 주인 = D03 화면(`ViewState` · 돌림) 하나 — 여기는 따라가기만 · 조작은 안 받음.
|
||||
* 자료 = 지금 받는 GeoJSON 그대로(조각내기는 MapLibre 일꾼 스레드) · 색 = 받는 쪽이 지물에 붙인 `_color`.
|
||||
* 등고선 = 줌별 벡터 조각(615 · 위성처럼 고정 소스 · 가는 선 = 모두 · 굵은 선 · 글 = `k` 1) · 받는 동안 서클.
|
||||
* 받은 산림 · 환경 자료(622-2) = 같은 꼴 고정 소스 다섯(`D03_PlanAnalysis_UI_ForestLayers`) · 받는 동안 서클.
|
||||
* 그래픽 카드 · WebGL 이 없거나 잃으면 `onFail` — 부르는 쪽이 옛 캔버스로.
|
||||
* 지도 세우기 · 자료 올리기 · 카메라 = 공용 부품 `ui_template_gl_map`(589-2) · 여기는 겹 꼴만.
|
||||
* ========================================================================== */
|
||||
@@ -26,6 +27,13 @@ import {
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||
import type { PlanLayerKey, RoadPick } from "./D03_PlanAnalysis_UI_LayerDefs";
|
||||
import {
|
||||
forestAreaSpecs,
|
||||
forestRasterSpecs,
|
||||
forestRoadSpecs,
|
||||
forestSources,
|
||||
TRACKED_SOURCES,
|
||||
} from "./D03_PlanAnalysis_UI_ForestLayers";
|
||||
import { SAT_LIGHT_DEFAULT } from "./D03_PlanAnalysis_UI_SatLight";
|
||||
|
||||
export { GL_FONT, type GlImage };
|
||||
@@ -34,6 +42,9 @@ export { GL_FONT, type GlImage };
|
||||
export type GlSource = Exclude<PlanLayerKey, "satellite" | "forestRoad">;
|
||||
|
||||
const GL_FONT_BOLD = [`${GL_FONT} SemiBold`, GL_FONT];
|
||||
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
|
||||
export const glFont = (weight: string, text: string): Promise<unknown> =>
|
||||
document.fonts.load(`${weight} 24px "${GL_FONT}"`, text || "가").catch(() => null);
|
||||
|
||||
export interface GlBase {
|
||||
/** 자료 담기(지도 없어도 · 켤 때 올림) — null = 비움. */
|
||||
@@ -129,7 +140,7 @@ function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
const place = themeColor("--draw-map-place", "#f8fafc");
|
||||
const landslideZone = themeColor("--draw-map-landslide-zone", "#e11d48");
|
||||
const raster = (
|
||||
key: GlSource | "satellite",
|
||||
key: "satellite" | "hillshade" | "slope",
|
||||
opacity: number,
|
||||
extra: Record<string, number> = {},
|
||||
): GlSpec =>
|
||||
@@ -147,8 +158,8 @@ function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
raster("satellite", 1, { "raster-brightness-max": light, "raster-saturation": SAT_SATURATION }),
|
||||
raster("hillshade", 1), // 위성 위 = 「곱하기」와 같은 검은 그늘 그림(받는 쪽이 바꿈)
|
||||
raster("slope", 0.55),
|
||||
raster("landslide", 0.6),
|
||||
...fillEdge("soils", "soils", COLOR, 0.35, { color: COLOR, width: 1.4 }),
|
||||
...forestRasterSpecs(), // 산사태위험 · 생태자연도 = 조각 그림(622-2)
|
||||
...forestAreaSpecs(), // 산림입지도 · 경제림 = 벡터 조각
|
||||
...fillEdge("zones", "zones", COLOR, 0.22, { color: COLOR, width: 1.6 }),
|
||||
...fillEdge("bands", "bands", COLOR, ["case", ["==", ["get", "_band"], "road"], 0.16, 0.18], {
|
||||
color: COLOR,
|
||||
@@ -241,6 +252,7 @@ function layerSpecs(light: number, pick: RoadPick | null): GlSpec[] {
|
||||
[],
|
||||
{ color: HALO, width: 4 },
|
||||
),
|
||||
...forestRoadSpecs(), // 국유림 임도 = 벡터 조각
|
||||
// 고른 묶음 · 선 강조(605-2) — 같은 자료에서 걸러 한 겹 더 굵게
|
||||
...["-pick-halo", "-pick"].map(
|
||||
(tail, i) =>
|
||||
@@ -348,7 +360,7 @@ const THEME_NAMES = [
|
||||
/** 위성 채도(−1~1) — 조금 연하게. */
|
||||
const SAT_SATURATION = -0.3;
|
||||
|
||||
/** `track` = 받는 일을 진행 서클에 셈(등고 조각 받는 동안). */
|
||||
/** `track` = 받는 일을 진행 서클에 셈(등고 · 산림 조각 받는 동안). */
|
||||
export function createGlBase(
|
||||
projectId: string,
|
||||
track: <T>(job: Promise<T>) => Promise<T>,
|
||||
@@ -358,12 +370,16 @@ export function createGlBase(
|
||||
let pick: RoadPick | null = null;
|
||||
const base = createGlMap<PlanLayerKey>({
|
||||
specs: () => layerSpecs(light, pick),
|
||||
fixed: { satellite: satelliteTileSource(projectId), contour: contourTileSource(projectId) },
|
||||
fixed: {
|
||||
satellite: satelliteTileSource(projectId),
|
||||
contour: contourTileSource(projectId),
|
||||
...forestSources(projectId),
|
||||
},
|
||||
themeNames: THEME_NAMES,
|
||||
className: "d03-plan__gl",
|
||||
onFail,
|
||||
});
|
||||
/** 등고 조각 받는 동안 서클 하나 — 다 받거나 지도가 쉬면(`idle`) · 지도를 버리면 끝. */
|
||||
/** 등고 · 산림 조각 받는 동안 서클 하나 — 다 받거나 지도가 쉬면(`idle`) · 지도를 버리면 끝. */
|
||||
let settle: (() => void) | null = null;
|
||||
const done = (): void => {
|
||||
settle?.();
|
||||
@@ -373,11 +389,12 @@ export function createGlBase(
|
||||
function watch(map: MapLibreMap): void {
|
||||
watched = map;
|
||||
map.on("sourcedataloading", (event) => {
|
||||
if (event.sourceId !== "contour" || settle) return;
|
||||
if (!TRACKED_SOURCES.includes(event.sourceId) || settle) return;
|
||||
void track(new Promise<void>((resolve) => (settle = resolve)));
|
||||
});
|
||||
map.on("sourcedata", (event) => {
|
||||
if (event.sourceId === "contour" && map.isSourceLoaded("contour")) done();
|
||||
const loaded = (id: string) => !map.getSource(id) || map.isSourceLoaded(id);
|
||||
if (TRACKED_SOURCES.includes(event.sourceId) && TRACKED_SOURCES.every(loaded)) done();
|
||||
});
|
||||
map.on("idle", done);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""PLAN 622-2 — 예정노선 받은 산림 · 환경 자료 겹 다섯(국유림 임도 · 산림입지도 · 경제림 · 생태자연도 · 산사태) 화면 지킴.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_622_2_forest_layers.py -q
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
D03 = ROOT / "D03_PlanAnalysis"
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_fixed_sources_follow_plan_622_routes() -> None:
|
||||
gl = _read(ROOT / "ui_template" / "ui_template_gl_map.ts")
|
||||
assert "export function tileSource" in gl
|
||||
assert "/plan/${path}/{z}/{x}/{y}.${ext}" in gl
|
||||
forest = _read(D03 / "D03_PlanAnalysis_UI_ForestLayers.ts")
|
||||
for name in ("forest_road", "forest_soil", "eco_forest"):
|
||||
assert f'vt("{name}")' in forest
|
||||
assert f'...vector("{name}")' in forest or f'"source-layer": "{name}"' in forest
|
||||
for name in ("ecology", "landslide"):
|
||||
assert f'wmt("{name}")' in forest
|
||||
assert '`vt/${name}`, "pbf", 8, 15' in forest and '`wmt/${name}`, "png", 6, 19' in forest
|
||||
assert "minzoom: 10" in forest # 산림입지도 z10 아래 숨김
|
||||
base = _read(D03 / "D03_PlanAnalysis_UI_MapGl.ts")
|
||||
assert "...forestSources(projectId)" in base
|
||||
assert 'raster("landslide"' not in base # 산사태 = 조각(한 장 GL 그림 없음)
|
||||
assert "TRACKED_SOURCES.includes(event.sourceId)" in base # 받는 동안 서클
|
||||
|
||||
|
||||
def test_layer_list_five_off_by_default() -> None:
|
||||
defs = _read(D03 / "D03_PlanAnalysis_UI_LayerDefs.ts")
|
||||
icons = _read(ROOT / "ui_template" / "ui_template_icons.ts")
|
||||
for key in ("nationalRoad", "soils", "ecoForest", "ecology", "landslide"):
|
||||
entry = re.search(rf'key: "{key}",\s*icon: "(\w+)",[^}}]*on: (\w+)', defs)
|
||||
assert entry and entry.group(2) == "false", key
|
||||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), entry.group(1)
|
||||
|
||||
|
||||
def test_colors_are_draw_map_tokens() -> None:
|
||||
theme = _read(ROOT / "ui_template" / "ui_template_theme.css")
|
||||
forest = _read(D03 / "D03_PlanAnalysis_UI_ForestLayers.ts")
|
||||
for token in set(re.findall(r'"(--draw-map-[\w-]+)"', forest)):
|
||||
assert f"{token}:" in theme, token
|
||||
assert all(f"--draw-map-soil-{n}:" in theme for n in range(1, 7))
|
||||
assert not re.search(r"#[0-9a-fA-F]{6}\b(?!\")", forest) # 색 = 토큰(뒷값만 글)
|
||||
|
||||
|
||||
def test_canvas_keeps_old_landslide_and_layers_file_not_grown() -> None:
|
||||
layers = _read(D03 / "D03_PlanAnalysis_UI_Layers.ts")
|
||||
assert len(layers.splitlines()) <= 688
|
||||
assert '(glOn && key === "landslide")' in layers # GL = 조각 · 받기 없음
|
||||
assert "if (!glOn) void Promise.all(LAYERS.map((def) => ensure(def.key)))" in layers
|
||||
paint = _read(D03 / "D03_PlanAnalysis_UI_LayerPaint.ts")
|
||||
assert "drawRaster(context, map, data.landslide);" in paint # 옛 캔버스 = 한 장 그대로
|
||||
@@ -76,6 +76,23 @@ export function contourTileSource(projectId: string): SourceSpecification {
|
||||
};
|
||||
}
|
||||
|
||||
/** 서버 줌별 조각(PLAN 622 · `…/plan/<path>/{z}/{x}/{y}.<ext>`) — pbf = 벡터(겹 이름 = 자료 이름) · png = 그림 256 px. */
|
||||
export function tileSource(
|
||||
projectId: string,
|
||||
path: string,
|
||||
ext: "pbf" | "png",
|
||||
minzoom: number,
|
||||
maxzoom: number,
|
||||
): SourceSpecification {
|
||||
const tiles = [
|
||||
`${location.origin}${API_BASE_URL}/projects/${projectId}/plan/${path}/{z}/{x}/{y}.${ext}`,
|
||||
];
|
||||
const bounds: [number, number, number, number] = [124, 33, 132, 39];
|
||||
return ext === "pbf"
|
||||
? { type: "vector", tiles, minzoom, maxzoom, bounds }
|
||||
: { type: "raster", tiles, tileSize: 256, minzoom, maxzoom, bounds };
|
||||
}
|
||||
|
||||
/** 느린 조작 잼(600-4) — `globalThis.__aisloPerf = true` 일 때만 `performance.measure(이름)` 으로 걸린 ms 를 남김(꺼져 있으면 그냥 실행). */
|
||||
export function perfTime<T>(name: string, run: () => T): T {
|
||||
if (!(globalThis as { __aisloPerf?: boolean }).__aisloPerf) return run();
|
||||
|
||||
@@ -153,6 +153,11 @@ const PATHS = {
|
||||
"M12.802 2.165l5.575 2.389c.48 .206 .863 .589 1.07 1.07l2.388 5.574c.22 .512 .22 1.092 0 1.604l-2.389 5.575c-.206 .48 -.589 .863 -1.07 1.07l-5.574 2.388c-.512 .22 -1.092 .22 -1.604 0l-5.575 -2.389a2.036 2.036 0 0 1 -1.07 -1.07l-2.388 -5.574a2.036 2.036 0 0 1 0 -1.604l2.389 -5.575c.206 -.48 .589 -.863 1.07 -1.07l5.574 -2.388a2.036 2.036 0 0 1 1.604 0z|M12 8v4|M12 16h.01",
|
||||
// 585-3 — 연도 구역 마커(D03 도구줄 끌어 놓기)
|
||||
flag: "M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0v9a5 5 0 0 1 -7 0a5 5 0 0 0 -7 0v-9z|M5 21v-7",
|
||||
// 622-2 — 지도 겹(경제림 · 생태자연도)
|
||||
trees:
|
||||
"M16 5l3 3l-2 1l4 4l-3 1l4 4h-9|M15 21l0 -3|M8 13l-2 -2|M8 12l2 -2|M8 21v-13|M5.824 16a3 3 0 0 1 -2.743 -3.69a3 3 0 0 1 .304 -4.833a3 3 0 0 1 4.615 -3.707a3 3 0 0 1 4.614 3.707a3 3 0 0 1 .305 4.833a3 3 0 0 1 -2.919 3.695h-4z",
|
||||
plant:
|
||||
"M7 15h10v4a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2z|M12 9a6 6 0 0 0 -6 -6h-3v2a6 6 0 0 0 6 6h3|M12 11a6 6 0 0 1 6 -6h3v1a6 6 0 0 1 -6 6h-3|M12 15l0 -6",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof PATHS;
|
||||
|
||||
@@ -569,7 +569,7 @@ html.is-bm-route .m01lab-src__body * {
|
||||
--draw-map-zone-wetland: #2dd4bf; /* 습지보호지역 */
|
||||
--draw-map-zone-wildlife: #eab308; /* 야생생물보호구역 */
|
||||
--draw-map-zone-greenbelt: #f97316; /* 개발제한구역 */
|
||||
--draw-map-soil-1: #fef3c7; /* 산림입지도 토양형 — 받은 범위 안 코드 차례(흙빛 · 급경사 노랑과 다름) */
|
||||
--draw-map-soil-1: #fef3c7; /* 산림입지도 토양 무리 — 갈색 · 적황색 · 암적색 · 회갈색 화산회 · 침식 미숙 · 그 밖(흙빛 · 급경사 노랑과 다름) */
|
||||
--draw-map-soil-2: #f5c08a;
|
||||
--draw-map-soil-3: #d9895a;
|
||||
--draw-map-soil-4: #bef264;
|
||||
@@ -585,6 +585,15 @@ html.is-bm-route .m01lab-src__body * {
|
||||
--draw-map-zero-order: #7dd3fc; /* 0차곡(지형 추정 골짜기 머리 · 점선) */
|
||||
--draw-map-forest-road-file: #ff2fd0; /* 올린 임도망도(실선 · 검은 테 · 위성 위 밝은 분홍 · 짐작 = 주황 점선) */
|
||||
--draw-map-landslide-zone: #e11d48; /* 산사태취약지역 점 · 필지 테두리 */
|
||||
--draw-map-national-road-main: #ff6b00; /* 국유림 임도 간선(실선 · 검은 테 · 짐작 = 주황 점선과 가름) */
|
||||
--draw-map-national-road-work: #ffd23f; /* 국유림 임도 작업 */
|
||||
--draw-map-national-road-fire: #ff3b3b; /* 국유림 임도 산불 */
|
||||
--draw-map-eco-national: #34d399; /* 경제림육성단지 국유(테두리) */
|
||||
--draw-map-eco-private: #a78bfa; /* 경제림육성단지 사유(테두리) */
|
||||
--draw-map-ecology-1: #2e9e4f; /* 생태자연도 1등급(환경부 원도 색 — 범례만) */
|
||||
--draw-map-ecology-2: #b5d96b; /* 2등급 */
|
||||
--draw-map-ecology-3: #f2f2c2; /* 3등급 */
|
||||
--draw-map-ecology-x: #9b7bd4; /* 별도관리지역 */
|
||||
--draw-map-sketch: #ffe94d; /* 지도 위 그림(테마 안 따름 · 검은 테와 함께) — 긋기 미리보기 · 그리는 선 · 고름 강조 · 마커 */
|
||||
--draw-map-measure: #ffffff; /* 거리재기 */
|
||||
--draw-map-warn: #ff5a4d; /* 긋기 막힘(경고) */
|
||||
|
||||
Reference in New Issue
Block a user