305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_Api.ts
|
|
* D03 분석 화면 서버 요청(PLAN 563-1) — 지형 받기 범위 · 받기 · 진행 ·
|
|
* 사업 전체 지형면 한 벌(`models_plan/` · 격자) · 그은 선 정본(562-2 GET · PUT network · 지점 · 기준점 함께 591-2 ·
|
|
* 지점 · 선 이름 · 노드 기록 · [저장] 기준점 594) · 주소 · 장소 찾기(594-4 공용 `/map/search`) ·
|
|
* 지도 겹(569 — 지적도 · 도엽 겹 GeoJSON 경위도 · 급경사 그림 · 581 — 음영기복 · 보호구역 · 띠 · 산림입지도).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { fetchCachedBytes } from "../A00_Common/b_asset_cache";
|
|
import { listSurfaceModels } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
|
import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_sampler";
|
|
import type { PlanLine, PlanPiece, Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
|
|
|
export interface BasePoint {
|
|
lon: number;
|
|
lat: number;
|
|
x: number;
|
|
y: number;
|
|
/** 지점 이름(594-1) — 없음 = 「지점 N」. */
|
|
name?: string;
|
|
}
|
|
|
|
/** 지점(긋기에 붙는 점 · 작업 좌표계 m + 경위도) · 등록 기준점(처음 받는 기준 · 591-2) ·
|
|
* epsg = 파일 머리 · 없으면 기준점 경도 TM 띠. */
|
|
export interface PlanBaseView {
|
|
epsg: number | null;
|
|
points: BasePoint[];
|
|
site: { lon: number; lat: number; address: string | null } | null;
|
|
}
|
|
|
|
export interface PlanJob {
|
|
running: boolean;
|
|
percent: number;
|
|
message: string | null;
|
|
error: string | null;
|
|
/** 서버 다시 켜짐 등으로 끊김(595-3). */
|
|
interrupted?: boolean;
|
|
}
|
|
|
|
/** 받기 시작 답(595-2) — 받음 · 멂(받은 범위에서 5 km 넘음 → 묻고 `reset`) · 다른 TM 띠(노선 지움 확인 뒤 `reset`). */
|
|
export type TerrainStart =
|
|
| (PlanJob & { status?: "accepted" })
|
|
| { status: "far"; distance_m: number; message: string }
|
|
| { status: "band"; epsg: number; center_epsg: number; message: string };
|
|
|
|
/** 위치 칸 기준점(캐시 · [저장] 전) — 서버가 둘레 ±1 km 를 더 덮음 · `reset` = 옛 받은 범위 버림. */
|
|
export interface TerrainCenter {
|
|
center?: { lon: number; lat: number } | null;
|
|
reset?: boolean;
|
|
}
|
|
|
|
/** 받기 전 범위(561-2) — 상자(작업 좌표계 m · 경위도 네 귀) · 받은 범위 · 도엽 · 칸 수 어림. */
|
|
export interface TerrainExtent {
|
|
epsg: number;
|
|
margin_m: number;
|
|
box_m: [number, number, number, number];
|
|
box: Array<[number, number]>;
|
|
have_m: [number, number, number, number] | null;
|
|
have: Array<[number, number]> | null;
|
|
covered: boolean;
|
|
sheets: Array<{ no: string; ring: Array<[number, number]>; stored: boolean }>;
|
|
cells: number;
|
|
}
|
|
|
|
/** 그은 선 서버 꼴(562-2) — 마크 = 조각 경계 · 조각 없는 구역 = 연도 없음. */
|
|
export interface ServerNetwork {
|
|
epsg: number;
|
|
/** 지점(PUT 몸만 · 빈 값 = 저장된 지점 그대로). */
|
|
points?: Vertex[];
|
|
/** 지점 이름(PUT 몸만 · points 와 같은 차례 · points 와 함께 · 빈 칸 = 「지점 N」 · 594-1). */
|
|
point_names?: Array<string | null>;
|
|
/** 기준점(PUT 몸만 · 바뀌면 서버가 등록 칸 `projects.site_*` 고침 · 주소 = 서버가 다시 찾음 · 594-3). */
|
|
site?: { lon: number; lat: number; address?: string | null };
|
|
lines: PlanLine[];
|
|
marks: Array<{ line: string; at_m: number }>;
|
|
pieces: PlanPiece[];
|
|
}
|
|
|
|
export interface PlanNetworkView {
|
|
base: PlanBaseView;
|
|
network: ServerNetwork | null;
|
|
derived: {
|
|
lines: Array<{ id: string; length_m: number; stations: number[] }>;
|
|
pieces: Array<PlanPiece & { length_m: number; planned: Vertex[] }>;
|
|
} | null;
|
|
/** 사업연도 시작(있으면). */
|
|
year_start?: number | null;
|
|
}
|
|
|
|
export interface PlanSurface {
|
|
modelId: number;
|
|
grid: DtmGrid;
|
|
}
|
|
|
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
...init,
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", ...(init.headers ?? {}) },
|
|
});
|
|
const body = (await response.json().catch(() => ({}))) as T & { detail?: unknown };
|
|
if (!response.ok) {
|
|
const detail = typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`;
|
|
throw new Error(detail);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
const base = (projectId: string): string => `/projects/${projectId}/plan`;
|
|
|
|
/** 받기 전 범위 — `extra` = 더 덮을 점(그은 꺾임점). */
|
|
export const fetchTerrainExtent = (
|
|
projectId: string,
|
|
marginM: number | null,
|
|
extra: Vertex[],
|
|
center: TerrainCenter["center"] = null,
|
|
): Promise<TerrainExtent> => {
|
|
const query = new URLSearchParams();
|
|
if (marginM !== null) query.set("margin_m", String(marginM));
|
|
if (extra.length) query.set("extra", JSON.stringify(extra));
|
|
if (center) {
|
|
query.set("lon", String(center.lon));
|
|
query.set("lat", String(center.lat));
|
|
}
|
|
return request(`${base(projectId)}/terrain/extent?${query}`);
|
|
};
|
|
|
|
export const startTerrain = (
|
|
projectId: string,
|
|
marginM: number | null,
|
|
extra: Vertex[],
|
|
where: TerrainCenter = {},
|
|
): Promise<TerrainStart> =>
|
|
request(`${base(projectId)}/terrain`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ margin_m: marginM, extra, ...where }),
|
|
});
|
|
|
|
export const fetchPlanProgress = (projectId: string): Promise<{ terrain: PlanJob }> =>
|
|
request(`${base(projectId)}/progress`);
|
|
|
|
export const fetchPlanNetwork = (projectId: string): Promise<PlanNetworkView> =>
|
|
request(`${base(projectId)}/network`);
|
|
|
|
export const savePlanNetwork = (
|
|
projectId: string,
|
|
network: ServerNetwork,
|
|
): Promise<PlanNetworkView> =>
|
|
request(`${base(projectId)}/network`, { method: "PUT", body: JSON.stringify(network) });
|
|
|
|
/** 주소 · 장소 찾기 후보(594-4) — 주소 = 도로명 · 지번 · 장소 = 이름(`title`) + 주소. */
|
|
export interface PlaceHit {
|
|
kind: "address" | "place";
|
|
title: string;
|
|
address: string;
|
|
lon: number;
|
|
lat: number;
|
|
}
|
|
|
|
/** 주소 · 장소 찾기(서버 VWorld · 등록 D01 · 예정노선 위치 칸이 같이 씀) — 없음 = 빈 줄. */
|
|
export const searchPlaces = async (query: string): Promise<PlaceHit[]> =>
|
|
(await request<{ items: PlaceHit[] }>(`/map/search?q=${encodeURIComponent(query)}`)).items;
|
|
|
|
/** 사업 전체 지형면 — 모델 행 경로 `…/models_plan/…` · 격자 = 평활본 · 없으면 null. */
|
|
export async function fetchPlanSurface(projectId: string): Promise<PlanSurface | null> {
|
|
const { models } = await listSurfaceModels(projectId);
|
|
const model = models.find((entry) => /models_plan\//.test(entry.model_file_path ?? ""));
|
|
if (!model) return null;
|
|
const stamp = String(model.generation_params?.file_stamp ?? "");
|
|
const url =
|
|
`${API_BASE_URL}/projects/${projectId}/surface/models/${model.id}/grid` +
|
|
`?smooth=true&sig=${encodeURIComponent(stamp)}`;
|
|
return { modelId: model.id, grid: parseDtmGrid(await fetchCachedBytes(projectId, url)) };
|
|
}
|
|
|
|
/** 지도 겹 그림(569-1) — 작업 좌표계 m 상자(칸 가장자리) · data URL(위 = 북) · 단계 색 · 넓이 비율. */
|
|
export interface PlanRaster {
|
|
box_m: [number, number, number, number];
|
|
/** 경위도 네 귀퉁이(왼쪽 아래 · 오른쪽 아래 · 오른쪽 위 · 왼쪽 위) — GL 바탕(586-1). */
|
|
box?: Array<[number, number]>;
|
|
image: string;
|
|
bands: Array<{ min_deg: number; max_deg: number; color: string; ratio: number }>;
|
|
}
|
|
|
|
export const fetchPlanCadastral = (projectId: string): Promise<GeoJsonCollection> =>
|
|
request(`${base(projectId)}/layers/cadastral`);
|
|
|
|
/** 도엽 겹(`도로중심선` 등) — 없으면 서버가 받은 도엽에서 풀어 냄. */
|
|
export const fetchPlanSheetLayer = (projectId: string, key: string): Promise<GeoJsonCollection> =>
|
|
request(`${base(projectId)}/layers/sheet?key=${encodeURIComponent(key)}`);
|
|
|
|
export const fetchPlanRaster = (
|
|
projectId: string,
|
|
kind: "slope" | "hillshade" | "landslide",
|
|
): Promise<PlanRaster> => request(`${base(projectId)}/layers/raster?kind=${kind}`);
|
|
|
|
/** 보호 · 규제 구역(581-1) — 지물 `kind` · `name` · `area_m2` + 종류별 개수 · 넓이(실패 = `failed`). */
|
|
export interface PlanZones extends GeoJsonCollection {
|
|
summary: Record<string, { label: string; count: number; area_m2: number; failed?: boolean }>;
|
|
}
|
|
|
|
export const fetchPlanZones = (projectId: string): Promise<PlanZones> =>
|
|
request(`${base(projectId)}/layers/zones`);
|
|
|
|
/** 산림입지도(581-2) — 지물 `code` · `name` · `area_m2` + 토양형별 개수 · 넓이(코드 차례). */
|
|
export interface PlanSoils extends GeoJsonCollection {
|
|
summary: Array<{ code: string; name: string; count: number; area_m2: number }>;
|
|
}
|
|
|
|
export const fetchPlanSoils = (projectId: string): Promise<PlanSoils> =>
|
|
request(`${base(projectId)}/layers/soils`);
|
|
|
|
/** 계류 100 m · 도로 300 m 띠(581-1) — 받은 범위 안 넓이 · 비율. */
|
|
export interface PlanBand {
|
|
dist_m: number;
|
|
lines: number;
|
|
area_m2: number;
|
|
ratio: number;
|
|
collection: GeoJsonCollection;
|
|
}
|
|
|
|
/** 선 겹(584-1) — 받은 범위 안 선 + 가닥 · 길이 m. */
|
|
export interface PlanLines extends GeoJsonCollection {
|
|
summary: { count: number; length_m: number };
|
|
}
|
|
|
|
/** 단층 · 지질구조선(브이월드 WFS · 서버 `LINE_KINDS` 열쇠). */
|
|
export const fetchPlanLines = (projectId: string, kind: string): Promise<PlanLines> =>
|
|
request(`${base(projectId)}/layers/lines?kind=${encodeURIComponent(kind)}`);
|
|
|
|
/** 0차곡(584-1 · 지형 추정) — 위 집수 넓이 [min_m2, max_m2) 선 + 가닥 · 길이 m. */
|
|
export interface PlanZeroOrder {
|
|
min_m2: number;
|
|
max_m2: number;
|
|
cell_m: number;
|
|
count: number;
|
|
length_m: number;
|
|
collection: GeoJsonCollection;
|
|
}
|
|
|
|
export const fetchPlanZeroOrder = (projectId: string): Promise<PlanZeroOrder> =>
|
|
request(`${base(projectId)}/layers/zero-order`);
|
|
|
|
export const fetchPlanBands = (projectId: string): Promise<{ stream: PlanBand; road: PlanBand }> =>
|
|
request(`${base(projectId)}/layers/bands`);
|
|
|
|
/** 올린 임도망도(584-3) — 받은 범위로 자른 선 + 가닥 · 길이 m · 원본 선 수 · 좌표계 · 올린 파일. */
|
|
export interface PlanForestRoads {
|
|
count: number;
|
|
length_m: number;
|
|
total: number;
|
|
crs: string[];
|
|
source: { files: string[]; bytes: number; uploaded_at: string } | null;
|
|
collection: GeoJsonCollection;
|
|
/** 잘린 임도 상자 [서, 남, 동, 북] 경위도 · 없으면 null(595-4). */
|
|
bounds?: [number, number, number, number] | null;
|
|
/** 기준점 ~ 가장 가까운 임도(m). */
|
|
distance_m?: number | null;
|
|
}
|
|
|
|
/** 올린 적 없으면 오류(404 글 그대로). */
|
|
export const fetchPlanForestRoads = (projectId: string): Promise<PlanForestRoads> =>
|
|
request(`${base(projectId)}/layers/forest-roads`);
|
|
|
|
/** 임도망도 SHP 올리기(zip 하나 · shp · shx · dbf · prj 묶음) — 못 읽으면 서버 글 그대로 오류. */
|
|
export async function uploadPlanForestRoads(
|
|
projectId: string,
|
|
files: File[],
|
|
): Promise<PlanForestRoads> {
|
|
const form = new FormData();
|
|
for (const file of files) form.append("files", file);
|
|
const response = await fetch(`${API_BASE_URL}${base(projectId)}/layers/forest-roads`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
body: form,
|
|
});
|
|
const body = (await response.json().catch(() => ({}))) as PlanForestRoads & {
|
|
detail?: unknown;
|
|
};
|
|
if (!response.ok)
|
|
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
|
|
return body;
|
|
}
|
|
|
|
/** 산사태취약지역(584-3) — 받은 범위 안 점 · 지번 맞는 필지 테두리 · 시도별 표 · 표 없는 시도. */
|
|
export interface PlanLandslideZones {
|
|
points: Array<{
|
|
lon: number;
|
|
lat: number;
|
|
kind: string | null;
|
|
area_m2: number | null;
|
|
place: string;
|
|
}>;
|
|
parcels: GeoJsonCollection;
|
|
sidos: Array<{ code: string; name: string; rows: number; located: number; basis?: string }>;
|
|
missing: string[];
|
|
/** 지적도를 받았나(못 받으면 점만). */
|
|
cadastral: boolean;
|
|
}
|
|
|
|
export const fetchPlanLandslideZones = (projectId: string): Promise<PlanLandslideZones> =>
|
|
request(`${base(projectId)}/layers/landslide-zones`);
|