feat(D 예정노선): 도엽 조각 — 공용 창고 storage/_shared/sheets · 보이는 도엽만 받기 (PLAN 606-1 · 606-2 · 요구 477)

- 새 D03_PlanAnalysis_SheetTiles.py — 도엽 도곽으로 자른 선 겹 조각(등고 · 계곡 = 전국 등고선 · 세류 · 물 면 · 도로 · 건물 · 지명 = 도엽 zip) · zip 은 프로젝트 · 공용 지형 창고에서 복사(원본 안 지움) 뒤 받기 · 빈 도엽 = 빈 조각 · 받기 실패 저장 안 함 · B04 함수 부르기만
- 라우터 길 둘 — 보이는 도엽 목록(bbox) · 조각 하나
- 새 UI_SheetTiles.ts — 보기 멈춘 뒤 보이는 도엽 · 없는 조각만 하나씩 · 12 장 이하 다 · 13~30 계곡선만 · 넘음 숨김 · 서클
- UI_Layers — 도엽 선 겹 원천을 조각으로 · 시험 606 새 · 475 옛 겹 길 단언 바꿈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 22:14:11 +09:00
co-authored by Claude Opus 5.5
parent a3f4576802
commit bc1d5165ff
7 changed files with 646 additions and 17 deletions
+28 -14
View File
@@ -11,7 +11,7 @@
* 계류 100 m · 도로 300 m 띠 · 건물 · 지명).
* 처음 켬 = 위성 · 등고선 · 세류 · 고른 값 = 화면 취향(`aislo:pref:d03.layers`) 기억.
* 받기 = 켤 때 · 받은 지형 범위 지문(`load(have)` · 페이지가 지형면을 다 읽은 뒤 넘김)이 바뀌면 차례 +1 · 켠 겹 모두 다시 ·
* 옛 차례 답은 버림(475) · 범위는 서버가 받은 범위로 정함(등고 · 세류 · 도엽 겹 = 서버가 자름) · 지형을 안 받았으면 단추를 막음 ·
* 옛 차례 답은 버림(475) · 도엽 선 겹(등고 · 세류 · 물 면 · 도로 · 건물 · 지명) = 보이는 도엽 조각(606 · `UI_SheetTiles`) · 지형을 안 받았으면 단추를 막음 ·
* 받기 실패 = 그 겹만 꺼짐 + 알림 · 받는 일은 페이지 서클에 셈(`track`).
* 그리기 차례(아래 → 위) = 위성(GL 조각만 · 옛 캔버스 = 위성 없음 · 590-2) → 음영기복 → 급경사 → 산사태위험지도 → 산림입지도 → 보호구역 → 띠 → 지적 → 물 면 →
* 건물 → 등고선 → 도로 → 임도 짐작 → 올린 임도망도 → 세류 → 선 겹 → 산사태취약지역 → 지명 → 그은 선.
@@ -47,7 +47,6 @@ import {
fetchPlanLandslideZones,
fetchPlanLines,
fetchPlanRaster,
fetchPlanSheetLayer,
fetchPlanSoils,
fetchPlanZeroOrder,
fetchPlanZones,
@@ -75,6 +74,11 @@ import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_La
import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint";
import { createGlBase, GL_FONT, type GlImage } from "./D03_PlanAnalysis_UI_MapGl";
import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight";
import {
createSheetTiles,
SHEET_TILE_NAMES,
type SheetTiles,
} from "./D03_PlanAnalysis_UI_SheetTiles";
import {
openSlopeBands,
paintSlope,
@@ -122,6 +126,7 @@ export interface PlanLayers {
/** 겹이 받은 범위(마지막 `load` 값) · 차례 번호(475 검증). */
area: [number, number, number, number] | null;
turn: number;
tiles: ReturnType<SheetTiles["state"]>;
};
}
@@ -147,6 +152,18 @@ export function createPlanLayers(opts: {
useGl(false);
});
let glOn = readByKey(CANVAS_PREF_KEY) !== "1";
/** 도엽 조각(606) — 조각이 오면 그 겹만 합친 자료로 다시 얹음. */
const tiles = createSheetTiles({
projectId,
track,
wanted: () =>
meta ? LAYERS.flatMap((def) => (on[def.key] && SHEET_TILE_NAMES[def.key]) || []) : [],
onChange: (names) => {
for (const def of LAYERS)
if (meta && on[def.key] && SHEET_TILE_NAMES[def.key]?.some((name) => names.has(name)))
void fetchLayer(def.key, meta, areaTurn).then(() => (sync(), map.redrawBase()));
},
});
const buttons = new Map<PlanLayerKey, HTMLButtonElement>();
const groups = GROUPS.map((group) =>
@@ -437,15 +454,12 @@ export function createPlanLayers(opts: {
features: (collection.features ?? []).filter((feature) => test(feature.properties ?? {})),
});
if (key === "contour") {
// 받은 범위로 서버가 자름 — 범위가 늘면 `load` 가 다시 받게 함
const [collection] = await Promise.all([
fetchPlanSheetLayer(projectId, "등고선"),
glFont("400", "0123456789-"),
]);
await glFont("400", "0123456789-");
const collection = tiles.collection("등고선");
got.contour = indexLayerLod(prepare(collection, ["등고수치"]));
put("contour", collection);
} else if (key === "stream") {
const collection = await fetchPlanSheetLayer(projectId, "하천중심선");
const collection = tiles.collection("하천중심선");
got.stream = [
prepare(pick(collection, (props) => props["구분"] === "세류")),
prepare(pick(collection, (props) => props["구분"] !== "세류")),
@@ -456,7 +470,7 @@ export function createPlanLayers(opts: {
got.cadastral = prepare(collection);
put("cadastral", collection);
} else if (key === "road" || key === "forestRoad") {
const collection = await fetchPlanSheetLayer(projectId, "도로중심선");
const collection = tiles.collection("도로중심선");
got.road = {
all: prepare(collection),
small: prepare(
@@ -500,14 +514,12 @@ export function createPlanLayers(opts: {
if (key !== "hillshade") put(key, plain);
else if (plain) shade = { plain, shade: glImage(raster, shadeImage(image))! };
} else if (key === "water") {
const parts = await Promise.all(
WATER_KEYS.map((name) => fetchPlanSheetLayer(projectId, name)),
);
const parts = WATER_KEYS.map((name) => tiles.collection(name));
const water = { features: parts.flatMap((part) => part.features ?? []) };
got.water = prepare(water);
put("water", water);
} else if (key === "building") {
const building = await fetchPlanSheetLayer(projectId, "건물");
const building = tiles.collection("건물");
got.building = prepare(building);
put("building", building);
} else if (key === "zones") {
@@ -587,7 +599,7 @@ export function createPlanLayers(opts: {
],
});
} else {
const collection = await fetchPlanSheetLayer(projectId, "지명");
const collection = tiles.collection("지명");
got.place = (collection.features ?? []).flatMap((feature) => {
const geometry = feature.geometry;
const name = feature.properties?.["명칭"];
@@ -623,6 +635,7 @@ export function createPlanLayers(opts: {
{
base: true,
draw: (_, m) => {
if (meta) tiles.view(m, meta);
if (glOn && on.contour && data.contour)
gl.setContourStep(pickLevelStep(data.contour, m.view, 5));
},
@@ -673,6 +686,7 @@ export function createPlanLayers(opts: {
),
area,
turn: areaTurn,
tiles: tiles.state(),
}),
};
}