feat(D 예정노선): 영역 루틴 — 받기 끝 지형면 · 받은 범위 먼저 → 겹 다시(차례 번호) · 도엽 겹 서버 자름 · 위치 칸 제목 서클 하나 · 10 km 밖 알림 (PLAN 604 · 요구 475 · 476-2)
- 받기 끝 = 지형면 · 받은 범위 다 읽은 뒤 겹 판정(동시 읽기 = 옛 범위 판정 뿌리) · 되돌리기 · 다시하기도 영역 확인 - 겹 load(have) — 받은 범위 지문 · 위성 틀 바뀜 = 차례 +1 · 자료 · 받는 중 비움 · 켠 겹 모두 다시 · 옛 차례 답은 자료 · GL 에 안 씀 - 등고 · 세류 = 예정노선 겹 길(layers/sheet) · 도엽 겹 모두 서버가 받은 범위로 자른 사본(화면 상자 · 보관함 지문 걷음) - 서클 = 위치 칸 제목 오른쪽 하나 · 들어간 일 수(지형 받기 · 지형면 · 겹 · 임도망 올리기) · 부품 상자 원에 제목 줄 꼴(inline) 덧 · 지도 귀퉁이 서클 걷음 - 기준점 10 km 밖 점 = 위치 칸 알림 한 줄 - 시험 553_4 서클 줄 = 새 자리로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,9 @@
|
||||
* 선 겹(단층 · 지질구조선 · 0차곡) ·
|
||||
* 계류 100 m · 도로 300 m 띠 · 건물 · 지명).
|
||||
* 처음 켬 = 위성 · 등고선 · 세류 · 고른 값 = 화면 취향(`aislo:pref:d03.layers`) 기억.
|
||||
* 받기 = 켤 때 · 받은 지형 범위(`have`)가 바뀌면 다시(596-4 · 등고 · 세류 주소에 범위 지문) · 지형을 안 받았으면 단추를 막음 ·
|
||||
* 받기 실패 = 그 겹만 꺼짐 + 알림.
|
||||
* 받기 = 켤 때 · 받은 지형 범위 지문(`load(have)` · 페이지가 지형면을 다 읽은 뒤 넘김)이 바뀌면 차례 +1 · 켠 겹 모두 다시 ·
|
||||
* 옛 차례 답은 버림(475) · 범위는 서버가 받은 범위로 정함(등고 · 세류 · 도엽 겹 = 서버가 자름) · 지형을 안 받았으면 단추를 막음 ·
|
||||
* 받기 실패 = 그 겹만 꺼짐 + 알림 · 받는 일은 페이지 서클에 셈(`track`).
|
||||
* 그리기 차례(아래 → 위) = 위성(GL 조각만 · 옛 캔버스 = 위성 없음 · 590-2) → 음영기복 → 급경사 → 산사태위험지도 → 산림입지도 → 보호구역 → 띠 → 지적 → 물 면 →
|
||||
* 건물 → 등고선 → 도로 → 임도 짐작 → 올린 임도망도 → 세류 → 선 겹 → 산사태취약지역 → 지명 → 그은 선.
|
||||
* 겹 모두 = 바탕 캔버스(`base` · 582) · 등고선 = 줌 단계 색인 · 글 폭 기억.
|
||||
@@ -27,8 +28,6 @@ import { createLegend } from "@ui/ui_template_flow_legend";
|
||||
import { createIconButton } from "@ui/ui_template_button";
|
||||
import { createMapTools } from "@ui/ui_template_map_tools";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { fetchCachedJson } from "../A00_Common/b_asset_cache";
|
||||
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
||||
import { fetchVWorldMeta, type VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import {
|
||||
@@ -83,7 +82,6 @@ import {
|
||||
writeSlopeBands,
|
||||
} from "./D03_PlanAnalysis_UI_SlopeBands";
|
||||
import type { PlanMap, PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
|
||||
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
|
||||
const PREF_KEY = "aislo:pref:d03.layers";
|
||||
/** GL 글꼴 미리 받음 — 받기 전에 그린 글자는 대체 글꼴로 굳음(못 받아도 그림). */
|
||||
@@ -93,8 +91,6 @@ const glFont = (weight: string, text: string): Promise<unknown> =>
|
||||
const CANVAS_PREF_KEY = "aislo:pref:d03.canvas";
|
||||
/** 옛 캔버스 알림 — 위성 = GL 조각만(590-2). */
|
||||
const NO_SATELLITE_NOTE = "옛 그림은 위성 바탕 없이 그림";
|
||||
/** 등고선을 받아 올 그은 선 둘레(m) — 받은 범위를 모를 때만. */
|
||||
const CONTOUR_PAD_M = 600;
|
||||
type DataKey = keyof Prepared;
|
||||
|
||||
/** 겹 → 받는 자료(임도 짐작 = 도로 자료). */
|
||||
@@ -106,8 +102,8 @@ export interface PlanLayers {
|
||||
legend: HTMLElement;
|
||||
/** 바탕 겹(아래 → 위 · 위성 뺌) — 그은 선 겹 아래에 둠. */
|
||||
layers: PlanMapLayer[];
|
||||
/** 지형(위성 틀)을 다시 읽음 — 처음 · 지형 받은 뒤 · 받은 범위가 바뀌면 겹을 새로 받음. */
|
||||
load: () => Promise<void>;
|
||||
/** 지형(위성 틀)을 다시 읽음 — 처음 · 지형 받은 뒤 · 받은 범위(`have` · 지형면을 다 읽은 뒤 값)가 바뀌면 켠 겹 모두 새로. */
|
||||
load: (have: [number, number, number, number] | null) => Promise<void>;
|
||||
/** 임도망도 올리기(왼쪽 「위치」 칸) — 받으면 그 겹을 켬 · 못 받으면 알림 + null. */
|
||||
uploadRoads: (files: File[]) => Promise<PlanForestRoads | null>;
|
||||
/** 검증 창구 — 켬끔 · 받은 겹 · 겹별 수(지물 · 넓이 m²). */
|
||||
@@ -117,23 +113,26 @@ export interface PlanLayers {
|
||||
counts: Record<string, number>;
|
||||
/** GL 겹마다 화면에 그린 지물 수(보임만 · GL 끔 = 빈 것). */
|
||||
gl: Record<string, number>;
|
||||
/** 겹이 받은 범위(마지막 `load` 값) · 차례 번호(475 검증). */
|
||||
area: [number, number, number, number] | null;
|
||||
turn: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function createPlanLayers(opts: {
|
||||
projectId: string;
|
||||
map: PlanMap;
|
||||
/** 등고선 받을 범위를 정할 점(시점 · 종점 · 그은 선) — 받은 범위를 모를 때만. */
|
||||
points: () => Vertex[];
|
||||
/** 받은 지형 범위(작업 좌표계 m) — 바뀜 판정 · 등고 · 세류 주소 지문. */
|
||||
have: () => [number, number, number, number] | null;
|
||||
/** 받는 일을 진행 서클에 셈(끝나면 뺌). */
|
||||
track: <T>(job: Promise<T>) => Promise<T>;
|
||||
}): PlanLayers {
|
||||
const { projectId, map } = opts;
|
||||
const { projectId, map, track } = opts;
|
||||
const on = readPref(PREF_KEY);
|
||||
const data: Partial<Prepared> = {};
|
||||
const pending = new Map<DataKey | "satellite", Promise<void>>();
|
||||
let meta: VWorldMeta | null = null;
|
||||
let loadedHave = "";
|
||||
let area: [number, number, number, number] | null = null;
|
||||
/** 차례 — 받은 범위 · 위성 틀이 바뀔 때 +1 · 옛 차례 답은 자료 · GL 에 안 씀. */
|
||||
let areaTurn = 0;
|
||||
/** 음영 GL 그림 — 그대로 · 위성 위 그늘(「곱하기」 꼴). */
|
||||
let hillshade: { plain: GlImage; shade: GlImage } | null = null;
|
||||
const gl = createGlBase(projectId, (why) => {
|
||||
@@ -268,14 +267,15 @@ export function createPlanLayers(opts: {
|
||||
const slot = key === "satellite" ? key : dataKey(key);
|
||||
const running = pending.get(slot);
|
||||
if (running) return running;
|
||||
const at = meta;
|
||||
const job = fetchLayer(key, at)
|
||||
const mine = areaTurn;
|
||||
const job: Promise<void> = track(fetchLayer(key, meta, mine))
|
||||
.then(() => {
|
||||
if (meta !== at) return;
|
||||
if (mine !== areaTurn) return;
|
||||
sync();
|
||||
map.redrawBase();
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
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))
|
||||
@@ -284,7 +284,7 @@ export function createPlanLayers(opts: {
|
||||
map.redraw();
|
||||
showToast(`${LAYERS.find((def) => def.key === key)!.title} 받지 못함 — ${text}`, "error");
|
||||
})
|
||||
.finally(() => pending.delete(slot));
|
||||
.finally(() => pending.get(slot) === job && pending.delete(slot));
|
||||
pending.set(slot, job);
|
||||
return job;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ export function createPlanLayers(opts: {
|
||||
/** 올리기 — 받으면 그 겹을 켜고 바로 그림(지형 받기 전이면 받은 뒤 그림) · 못 받으면 옛 것 그대로 + 서버 까닭 알림. */
|
||||
async function uploadRoads(files: File[]): Promise<PlanForestRoads | null> {
|
||||
const at = meta;
|
||||
const job = uploadPlanForestRoads(projectId, files).then((info) => {
|
||||
const job = track(uploadPlanForestRoads(projectId, files)).then((info) => {
|
||||
if (meta !== at) return info;
|
||||
if (at)
|
||||
data.forestRoadFile = { layer: prepareLayer(info.collection, createNormalizer(at)), info };
|
||||
@@ -328,209 +328,206 @@ export function createPlanLayers(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
/** 받은 범위 지문 — 등고 · 세류 주소에 붙여 브라우저 보관함이 옛 범위를 안 줌. */
|
||||
const haveTag = (): string => (opts.have() ?? []).map((v) => Math.round(v)).join("_");
|
||||
const sheetLayer = (layer: string, extra = ""): Promise<GeoJsonCollection> =>
|
||||
fetchCachedJson<GeoJsonCollection>(
|
||||
projectId,
|
||||
`${API_BASE_URL}/projects/${projectId}/geojson?layer=${encodeURIComponent(layer)}${extra}&v=${haveTag()}`,
|
||||
);
|
||||
/** 겹 하나 받기 — 자료 · GL 은 모아 두었다가 지금 차례일 때만 씀(옛 받은 범위 답 버림 · 475). */
|
||||
async function fetchLayer(key: PlanLayerKey, at: VWorldMeta, mine: number): Promise<void> {
|
||||
const got: Partial<Prepared> = {};
|
||||
const sets: Array<Parameters<typeof gl.setData>> = [];
|
||||
const put = (...args: Parameters<typeof gl.setData>): void => void sets.push(args);
|
||||
let shade = null as typeof hillshade;
|
||||
await fill();
|
||||
if (mine !== areaTurn) {
|
||||
if (got.slope?.url) URL.revokeObjectURL(got.slope.url);
|
||||
return;
|
||||
}
|
||||
if (got.slope && data.slope?.url) URL.revokeObjectURL(data.slope.url);
|
||||
Object.assign(data, got);
|
||||
if (shade) hillshade = shade;
|
||||
for (const args of sets) gl.setData(...args);
|
||||
// 칠하는 동안 범위가 바뀜 = 새 범위로 다시
|
||||
if (got.slope && got.slope.bands !== slopeBands) {
|
||||
got.slope.bands = slopeBands;
|
||||
void repaintSlope();
|
||||
}
|
||||
|
||||
async function fetchLayer(key: PlanLayerKey, at: VWorldMeta): Promise<void> {
|
||||
const normalizer = createNormalizer(at);
|
||||
const prepare = (collection: GeoJsonCollection, labels?: string[]): PreparedLayer =>
|
||||
prepareLayer(collection, normalizer, labels);
|
||||
const pick = (
|
||||
collection: GeoJsonCollection,
|
||||
test: (props: Record<string, unknown>) => boolean,
|
||||
): GeoJsonCollection => ({
|
||||
...collection,
|
||||
features: (collection.features ?? []).filter((feature) => test(feature.properties ?? {})),
|
||||
});
|
||||
if (key === "contour") {
|
||||
// 받은 범위 통째(모르면 그은 선 둘레) — 범위가 늘면 `load` 가 다시 받게 함
|
||||
const points = opts.points();
|
||||
const xs = points.map(([x]) => x);
|
||||
const ys = points.map(([, y]) => y);
|
||||
const bbox = (
|
||||
opts.have() ?? [
|
||||
Math.min(...xs) - CONTOUR_PAD_M,
|
||||
Math.min(...ys) - CONTOUR_PAD_M,
|
||||
Math.max(...xs) + CONTOUR_PAD_M,
|
||||
Math.max(...ys) + CONTOUR_PAD_M,
|
||||
]
|
||||
).map((v) => v.toFixed(0));
|
||||
const [collection] = await Promise.all([
|
||||
sheetLayer("도엽_등고선", `&scope=route&bbox=${bbox.join(",")}`),
|
||||
glFont("400", "0123456789-"),
|
||||
]);
|
||||
data.contour = indexLayerLod(prepare(collection, ["등고수치"]));
|
||||
gl.setData("contour", collection);
|
||||
} else if (key === "stream") {
|
||||
const collection = await sheetLayer("도엽_하천중심선");
|
||||
data.stream = [
|
||||
prepare(pick(collection, (props) => props["구분"] === "세류")),
|
||||
prepare(pick(collection, (props) => props["구분"] !== "세류")),
|
||||
];
|
||||
gl.setData("stream", collection);
|
||||
} else if (key === "cadastral") {
|
||||
const collection = await fetchPlanCadastral(projectId);
|
||||
data.cadastral = prepare(collection);
|
||||
gl.setData("cadastral", collection);
|
||||
} else if (key === "road" || key === "forestRoad") {
|
||||
const collection = await fetchPlanSheetLayer(projectId, "도로중심선");
|
||||
data.road = {
|
||||
all: prepare(collection),
|
||||
small: prepare(
|
||||
pick(collection, (props) => FOREST_ROAD_KINDS.has(String(props["도로구분"]))),
|
||||
),
|
||||
};
|
||||
gl.setData("road", collection);
|
||||
} else if (key === "forestRoadFile") {
|
||||
const info = await fetchPlanForestRoads(projectId);
|
||||
data.forestRoadFile = { layer: prepare(info.collection), info };
|
||||
gl.setData("forestRoadFile", info.collection);
|
||||
} else if (key === "landslideZones") {
|
||||
const info = await fetchPlanLandslideZones(projectId);
|
||||
data.landslideZones = { parcels: prepare(info.parcels), info };
|
||||
gl.setData("landslideZones", {
|
||||
features: [
|
||||
...(info.parcels.features ?? []),
|
||||
...info.points.map((point) => ({
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: { type: "Point", coordinates: [point.lon, point.lat] },
|
||||
})),
|
||||
],
|
||||
} as GeoJsonCollection);
|
||||
if (info.missing.length)
|
||||
showToast(`산사태취약지역 — ${info.missing.join(" · ")} 자료 없음`, "info");
|
||||
} else if (key === "slope") {
|
||||
const raster = await fetchPlanRaster(projectId, key);
|
||||
const values = slopeValues(await loadImage(raster.image));
|
||||
const bands = slopeBands;
|
||||
const started = performance.now();
|
||||
const painted = await paintSlope(values, bands);
|
||||
if (data.slope?.url) URL.revokeObjectURL(data.slope.url);
|
||||
data.slope = { raster, values, bands, ...painted, paintMs: 0 };
|
||||
data.slope.paintMs = Math.round(performance.now() - started);
|
||||
gl.setData("slope", glImage(raster, painted.url));
|
||||
// 칠하는 동안 범위가 바뀜 = 새 범위로 다시
|
||||
if (bands !== slopeBands) {
|
||||
data.slope.bands = slopeBands;
|
||||
void repaintSlope();
|
||||
}
|
||||
} else if (key === "hillshade" || key === "landslide") {
|
||||
const raster = await fetchPlanRaster(projectId, key);
|
||||
const image = await loadImage(raster.image);
|
||||
data[key] = { raster, image };
|
||||
const plain = glImage(raster);
|
||||
if (key !== "hillshade") gl.setData(key, plain);
|
||||
else if (plain) hillshade = { plain, shade: glImage(raster, shadeImage(image))! };
|
||||
} else if (key === "water") {
|
||||
const parts = await Promise.all(
|
||||
WATER_KEYS.map((name) => fetchPlanSheetLayer(projectId, name)),
|
||||
);
|
||||
const water = { features: parts.flatMap((part) => part.features ?? []) };
|
||||
data.water = prepare(water);
|
||||
gl.setData("water", water);
|
||||
} else if (key === "building") {
|
||||
const building = await fetchPlanSheetLayer(projectId, "건물");
|
||||
data.building = prepare(building);
|
||||
gl.setData("building", building);
|
||||
} else if (key === "zones") {
|
||||
const zones = await fetchPlanZones(projectId);
|
||||
const kinds = new Map<string, PreparedLayer>();
|
||||
for (const kind of Object.keys(zones.summary))
|
||||
kinds.set(kind, prepare(pick(zones, (props) => props["kind"] === kind)));
|
||||
data.zones = { kinds, summary: zones.summary };
|
||||
gl.setData(
|
||||
"zones",
|
||||
glTagged(zones, (props) =>
|
||||
themeColor(ZONE_COLORS[String(props["kind"])] ?? "--draw-warning", "#eab308"),
|
||||
),
|
||||
);
|
||||
const failed = Object.values(zones.summary).filter((kind) => kind.failed);
|
||||
if (failed.length)
|
||||
showToast(
|
||||
`보호 · 규제 구역 일부 받지 못함 — ${failed.map((kind) => kind.label).join(" · ")}`,
|
||||
"error",
|
||||
);
|
||||
} else if (key === "soils") {
|
||||
const soils = await fetchPlanSoils(projectId);
|
||||
data.soils = {
|
||||
kinds: soils.summary.map((soil, i) => ({
|
||||
layer: prepare(pick(soils, (props) => props["code"] === soil.code)),
|
||||
color: SOIL_COLORS[i % SOIL_COLORS.length],
|
||||
})),
|
||||
summary: soils.summary,
|
||||
};
|
||||
const colorOf = new Map(
|
||||
soils.summary.map((soil, i) => [soil.code, data.soils!.kinds[i].color]),
|
||||
);
|
||||
gl.setData(
|
||||
"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차곡 첫 셈 = 몇 초 — 늦으면 알림
|
||||
const slow = window.setTimeout(
|
||||
() => showToast(`${LINE_LAYERS[key].label} 셈 중 — 처음 한 번만`, "info"),
|
||||
600,
|
||||
);
|
||||
try {
|
||||
const got = kind
|
||||
? await fetchPlanLines(projectId, kind).then((lines) => ({
|
||||
collection: lines,
|
||||
...lines.summary,
|
||||
}))
|
||||
: await fetchPlanZeroOrder(projectId);
|
||||
data[key] = {
|
||||
layer: prepare(got.collection),
|
||||
count: got.count,
|
||||
length_m: got.length_m,
|
||||
async function fill(): Promise<void> {
|
||||
const normalizer = createNormalizer(at);
|
||||
const prepare = (collection: GeoJsonCollection, labels?: string[]): PreparedLayer =>
|
||||
prepareLayer(collection, normalizer, labels);
|
||||
const pick = (
|
||||
collection: GeoJsonCollection,
|
||||
test: (props: Record<string, unknown>) => boolean,
|
||||
): GeoJsonCollection => ({
|
||||
...collection,
|
||||
features: (collection.features ?? []).filter((feature) => test(feature.properties ?? {})),
|
||||
});
|
||||
if (key === "contour") {
|
||||
// 받은 범위로 서버가 자름 — 범위가 늘면 `load` 가 다시 받게 함
|
||||
const [collection] = await Promise.all([
|
||||
fetchPlanSheetLayer(projectId, "등고선"),
|
||||
glFont("400", "0123456789-"),
|
||||
]);
|
||||
got.contour = indexLayerLod(prepare(collection, ["등고수치"]));
|
||||
put("contour", collection);
|
||||
} else if (key === "stream") {
|
||||
const collection = await fetchPlanSheetLayer(projectId, "하천중심선");
|
||||
got.stream = [
|
||||
prepare(pick(collection, (props) => props["구분"] === "세류")),
|
||||
prepare(pick(collection, (props) => props["구분"] !== "세류")),
|
||||
];
|
||||
put("stream", collection);
|
||||
} else if (key === "cadastral") {
|
||||
const collection = await fetchPlanCadastral(projectId);
|
||||
got.cadastral = prepare(collection);
|
||||
put("cadastral", collection);
|
||||
} else if (key === "road" || key === "forestRoad") {
|
||||
const collection = await fetchPlanSheetLayer(projectId, "도로중심선");
|
||||
got.road = {
|
||||
all: prepare(collection),
|
||||
small: prepare(
|
||||
pick(collection, (props) => FOREST_ROAD_KINDS.has(String(props["도로구분"]))),
|
||||
),
|
||||
};
|
||||
gl.setData(key, got.collection);
|
||||
} finally {
|
||||
window.clearTimeout(slow);
|
||||
}
|
||||
} else if (key === "bands") {
|
||||
const info = await fetchPlanBands(projectId);
|
||||
data.bands = {
|
||||
stream: prepare(info.stream.collection),
|
||||
road: prepare(info.road.collection),
|
||||
info,
|
||||
};
|
||||
const band = (name: "stream" | "road", fallback: string): GeoJsonCollection =>
|
||||
glTagged(info[name].collection, () => themeColor(`--draw-map-band-${name}`, fallback), {
|
||||
_band: name,
|
||||
put("road", collection);
|
||||
} else if (key === "forestRoadFile") {
|
||||
const info = await fetchPlanForestRoads(projectId);
|
||||
got.forestRoadFile = { layer: prepare(info.collection), info };
|
||||
put("forestRoadFile", info.collection);
|
||||
} else if (key === "landslideZones") {
|
||||
const info = await fetchPlanLandslideZones(projectId);
|
||||
got.landslideZones = { parcels: prepare(info.parcels), info };
|
||||
put("landslideZones", {
|
||||
features: [
|
||||
...(info.parcels.features ?? []),
|
||||
...info.points.map((point) => ({
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: { type: "Point", coordinates: [point.lon, point.lat] },
|
||||
})),
|
||||
],
|
||||
} as GeoJsonCollection);
|
||||
if (info.missing.length)
|
||||
showToast(`산사태취약지역 — ${info.missing.join(" · ")} 자료 없음`, "info");
|
||||
} else if (key === "slope") {
|
||||
const raster = await fetchPlanRaster(projectId, key);
|
||||
const values = slopeValues(await loadImage(raster.image));
|
||||
const bands = slopeBands;
|
||||
const started = performance.now();
|
||||
const painted = await paintSlope(values, bands);
|
||||
const paintMs = Math.round(performance.now() - started);
|
||||
got.slope = { raster, values, bands, ...painted, paintMs };
|
||||
put("slope", glImage(raster, painted.url));
|
||||
} else if (key === "hillshade" || key === "landslide") {
|
||||
const raster = await fetchPlanRaster(projectId, key);
|
||||
const image = await loadImage(raster.image);
|
||||
got[key] = { raster, image };
|
||||
const plain = glImage(raster);
|
||||
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 water = { features: parts.flatMap((part) => part.features ?? []) };
|
||||
got.water = prepare(water);
|
||||
put("water", water);
|
||||
} else if (key === "building") {
|
||||
const building = await fetchPlanSheetLayer(projectId, "건물");
|
||||
got.building = prepare(building);
|
||||
put("building", building);
|
||||
} else if (key === "zones") {
|
||||
const zones = await fetchPlanZones(projectId);
|
||||
const kinds = new Map<string, PreparedLayer>();
|
||||
for (const kind of Object.keys(zones.summary))
|
||||
kinds.set(kind, prepare(pick(zones, (props) => props["kind"] === kind)));
|
||||
got.zones = { kinds, summary: zones.summary };
|
||||
put(
|
||||
"zones",
|
||||
glTagged(zones, (props) =>
|
||||
themeColor(ZONE_COLORS[String(props["kind"])] ?? "--draw-warning", "#eab308"),
|
||||
),
|
||||
);
|
||||
const failed = Object.values(zones.summary).filter((kind) => kind.failed);
|
||||
if (failed.length)
|
||||
showToast(
|
||||
`보호 · 규제 구역 일부 받지 못함 — ${failed.map((kind) => kind.label).join(" · ")}`,
|
||||
"error",
|
||||
);
|
||||
} else if (key === "soils") {
|
||||
const soils = await fetchPlanSoils(projectId);
|
||||
got.soils = {
|
||||
kinds: soils.summary.map((soil, i) => ({
|
||||
layer: prepare(pick(soils, (props) => props["code"] === soil.code)),
|
||||
color: SOIL_COLORS[i % SOIL_COLORS.length],
|
||||
})),
|
||||
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차곡 첫 셈 = 몇 초 — 늦으면 알림
|
||||
const slow = window.setTimeout(
|
||||
() => showToast(`${LINE_LAYERS[key].label} 셈 중 — 처음 한 번만`, "info"),
|
||||
600,
|
||||
);
|
||||
try {
|
||||
const found = kind
|
||||
? await fetchPlanLines(projectId, kind).then((lines) => ({
|
||||
collection: lines,
|
||||
...lines.summary,
|
||||
}))
|
||||
: await fetchPlanZeroOrder(projectId);
|
||||
got[key] = {
|
||||
layer: prepare(found.collection),
|
||||
count: found.count,
|
||||
length_m: found.length_m,
|
||||
};
|
||||
put(key, found.collection);
|
||||
} finally {
|
||||
window.clearTimeout(slow);
|
||||
}
|
||||
} else if (key === "bands") {
|
||||
const info = await fetchPlanBands(projectId);
|
||||
got.bands = {
|
||||
stream: prepare(info.stream.collection),
|
||||
road: prepare(info.road.collection),
|
||||
info,
|
||||
};
|
||||
const band = (name: "stream" | "road", fallback: string): GeoJsonCollection =>
|
||||
glTagged(info[name].collection, () => themeColor(`--draw-map-band-${name}`, fallback), {
|
||||
_band: name,
|
||||
});
|
||||
put("bands", {
|
||||
features: [
|
||||
...(band("road", "#facc15").features ?? []),
|
||||
...(band("stream", "#22d3ee").features ?? []),
|
||||
],
|
||||
});
|
||||
gl.setData("bands", {
|
||||
features: [
|
||||
...(band("road", "#facc15").features ?? []),
|
||||
...(band("stream", "#22d3ee").features ?? []),
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const collection = await fetchPlanSheetLayer(projectId, "지명");
|
||||
data.place = (collection.features ?? []).flatMap((feature) => {
|
||||
const geometry = feature.geometry;
|
||||
const name = feature.properties?.["명칭"];
|
||||
if (geometry?.type !== "Point" || typeof name !== "string" || !name) return [];
|
||||
const [lon, lat] = geometry.coordinates as number[];
|
||||
return [{ lon, lat, name }];
|
||||
});
|
||||
await glFont("600", data.place.map((place) => place.name).join(""));
|
||||
gl.setData("place", {
|
||||
features: data.place.map(({ lon, lat, name }) => ({
|
||||
type: "Feature",
|
||||
properties: { name },
|
||||
geometry: { type: "Point", coordinates: [lon, lat] },
|
||||
})),
|
||||
} as GeoJsonCollection);
|
||||
} else {
|
||||
const collection = await fetchPlanSheetLayer(projectId, "지명");
|
||||
got.place = (collection.features ?? []).flatMap((feature) => {
|
||||
const geometry = feature.geometry;
|
||||
const name = feature.properties?.["명칭"];
|
||||
if (geometry?.type !== "Point" || typeof name !== "string" || !name) return [];
|
||||
const [lon, lat] = geometry.coordinates as number[];
|
||||
return [{ lon, lat, name }];
|
||||
});
|
||||
await glFont("600", got.place.map((place) => place.name).join(""));
|
||||
put("place", {
|
||||
features: got.place.map(({ lon, lat, name }) => ({
|
||||
type: "Feature",
|
||||
properties: { name },
|
||||
geometry: { type: "Point", coordinates: [lon, lat] },
|
||||
})),
|
||||
} as GeoJsonCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,16 +552,16 @@ export function createPlanLayers(opts: {
|
||||
},
|
||||
];
|
||||
|
||||
async function load(): Promise<void> {
|
||||
async function load(have: [number, number, number, number] | null): Promise<void> {
|
||||
const next = await fetchVWorldMeta(projectId, "satellite").catch(() => null);
|
||||
// 바뀜 = 위성 틀 또는 받은 범위(같은 도엽 안에서 늘어도 겹을 새로 · 596-4)
|
||||
const have = JSON.stringify(opts.have());
|
||||
const changed =
|
||||
have !== loadedHave ||
|
||||
JSON.stringify(next && [next.x_min, next.x_max, next.y_min, next.y_max]) !==
|
||||
JSON.stringify(meta && [meta.x_min, meta.x_max, meta.y_min, meta.y_max]);
|
||||
[meta, loadedHave] = [next, have];
|
||||
// 바뀜 = 받은 범위 지문 또는 위성 틀 → 차례 +1 · 자료 · 받는 중 비움 · 켠 겹 모두 다시(475 · 596-4)
|
||||
const frame = (m: VWorldMeta | null) =>
|
||||
JSON.stringify(m && [m.x_min, m.x_max, m.y_min, m.y_max]);
|
||||
const changed = JSON.stringify(have) !== JSON.stringify(area) || frame(next) !== frame(meta);
|
||||
[meta, area] = [next, have];
|
||||
if (changed) {
|
||||
areaTurn += 1;
|
||||
pending.clear();
|
||||
for (const key of Object.keys(data)) delete data[key as DataKey];
|
||||
hillshade = null;
|
||||
for (const def of LAYERS)
|
||||
@@ -595,6 +592,8 @@ export function createPlanLayers(opts: {
|
||||
: gl.map()!.queryRenderedFeatures({ layers: [layer.id] }).length,
|
||||
]),
|
||||
),
|
||||
area,
|
||||
turn: areaTurn,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user