- 새 D03_PlanAnalysis_UI_LidarUpload — 공용 칩(안 고침) + 라이다 없으면 [LAS 올리기](부품 단추 · LAS · LAZ · .prj) · 진행 = 위치 칸 서클 「라이다」 · 고른 원천 = 세션 d03-terrain(등록부 한 줄) - 공용 겹 받기 fetchPlanRaster · fetchPlanZeroOrder 에 terrain 인자(안 넘김 = 옛 그대로 · B05 영향 없음) - 겹 reterrain = 지형 겹 셋만 비우고 다시 · 높이 = 고른 원천 격자(라이다 범위 밖 = 빈칸) - 지도는 지형 칸을 넘겨받기만 · 낡은 시험 둘(475 · 624_2)은 글 대조 줄만 새 꼴로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
235 lines
10 KiB
TypeScript
235 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* map_layer_api.ts
|
|
* 공용 지도 겹 받기(PLAN 569 · 581 · 584 · 603 · 605 · 606 · 624-3 · 624-4 · 요구 530) — D03 예정노선 · B05 평면이 같이 씀(D03_Api 에서 옮김).
|
|
* 서버 길 = `/api/projects/{id}/plan/…`(624-1 · 임도 프로젝트도 · 띠 · 올린 임도망도 = 예정노선만).
|
|
* 급경사 · 음영 · 0차곡 = 지형 원천 인자 `terrain`(지형 칩 값 · 624-4).
|
|
* 지적도 · 도엽 겹 GeoJSON 경위도 · 도엽 조각 · 그림 겹(급경사 · 음영기복 · 산사태위험) · 보호구역 · 산림입지도 ·
|
|
* 선 겹(단층 · 지질구조선) · 0차곡 · 띠 · 올린 임도망도(올리기 · 지움) · 산사태취약지역.
|
|
* D03 = `D03_PlanAnalysis_Api` 가 다시 내놓음(부르는 곳 그대로).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import type { TerrainSource } from "./map_terrain_chip";
|
|
|
|
/** 경위도 GeoJSON 묶음 — B04 `GeoJsonCollection` 과 같은 꼴(공용 폴더는 페이지 파일을 안 부름). */
|
|
export type GeoJsonCollection = {
|
|
features?: Array<{
|
|
geometry?: { type: string; coordinates: unknown } | null;
|
|
properties?: Record<string, unknown> | null;
|
|
}>;
|
|
};
|
|
|
|
async function request<T>(path: string): Promise<T> {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
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`;
|
|
|
|
/** 지도 겹 그림(569-1) — 작업 좌표계 m 상자(칸 가장자리) · data URL(위 = 북). */
|
|
export interface PlanRaster {
|
|
box_m: [number, number, number, number];
|
|
/** 경위도 네 귀퉁이(왼쪽 아래 · 오른쪽 아래 · 오른쪽 위 · 왼쪽 위) — GL 바탕(586-1). */
|
|
box?: Array<[number, number]>;
|
|
image: string;
|
|
/** 급경사만(603) — `counts[v]` = 기울기 % 내림 v(0~250) 칸 수 · 그림 = 회색 칸 값(255 = 빈 칸). */
|
|
counts?: number[];
|
|
/** 쓴 지형 원천(624-1 · 급경사 · 음영). */
|
|
source?: string;
|
|
}
|
|
|
|
/** 지형 원천 인자(624-1) — 안 넘김 = 서버 기본(D = 수치지형도 · B = 설계 지형). */
|
|
const terrainQuery = (terrain?: TerrainSource): string => (terrain ? `&terrain=${terrain}` : "");
|
|
|
|
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)}`);
|
|
|
|
/** 보이는 도엽(606-2) — 지도 보기 경위도 상자 → 수 · 번호(가운데부터 · 30 장 넘음 = 빈 목록). */
|
|
export const fetchPlanSheetTiles = (
|
|
projectId: string,
|
|
bbox: [number, number, number, number],
|
|
): Promise<{ count: number; sheets: string[] }> =>
|
|
request(`${base(projectId)}/sheet-tiles?bbox=${bbox.map((v) => v.toFixed(5)).join(",")}`);
|
|
|
|
/** 도엽 조각 하나(606-1) — 공용 창고 · 없으면 서버가 만듦. */
|
|
export const fetchPlanSheetTile = (
|
|
projectId: string,
|
|
sheet: string,
|
|
name: string,
|
|
): Promise<GeoJsonCollection> =>
|
|
request(`${base(projectId)}/sheet-tiles/${sheet}/${encodeURIComponent(name)}`);
|
|
|
|
/** 그림 겹 — 급경사 = PNG 바이트 + 머리 `X-Plan-Raster`(616-4 · `image` = blob 주소 · 다 읽으면
|
|
* `slopeValues` 가 놓음) · 그 밖 = JSON(base64 그림). */
|
|
export const fetchPlanRaster = async (
|
|
projectId: string,
|
|
kind: "slope" | "hillshade" | "landslide",
|
|
terrain?: TerrainSource,
|
|
): Promise<PlanRaster> => {
|
|
const path = `${base(projectId)}/layers/raster?kind=${kind}${terrainQuery(terrain)}`;
|
|
if (kind !== "slope") return request(path);
|
|
const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => ({}))) as { detail?: unknown };
|
|
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
|
|
}
|
|
const meta = JSON.parse(response.headers.get("X-Plan-Raster") ?? "{}") as PlanRaster;
|
|
return { ...meta, image: URL.createObjectURL(await response.blob()) };
|
|
};
|
|
|
|
/** 보호 · 규제 구역(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;
|
|
/** 쓴 지형 원천(624-1). */
|
|
source?: string;
|
|
}
|
|
|
|
export const fetchPlanZeroOrder = (
|
|
projectId: string,
|
|
terrain?: TerrainSource,
|
|
): Promise<PlanZeroOrder> =>
|
|
request(`${base(projectId)}/layers/zero-order?${terrainQuery(terrain).slice(1)}`);
|
|
|
|
export const fetchPlanBands = (projectId: string): Promise<{ stream: PlanBand; road: PlanBand }> =>
|
|
request(`${base(projectId)}/layers/bands`);
|
|
|
|
/** 올린 묶음 한 줄(605-1) — 올릴 때마다 하나 · 자른 가닥 · 길이 m · 원본 선 수 · 잘린 상자 · 기준점 거리. */
|
|
export interface PlanRoadBundle {
|
|
id: string;
|
|
name: string;
|
|
files: string[];
|
|
bytes: number | null;
|
|
uploaded_at: string | null;
|
|
count: number;
|
|
length_m: number;
|
|
total: number;
|
|
bounds: [number, number, number, number] | null;
|
|
distance_m: number | null;
|
|
}
|
|
|
|
/** 올린 임도망도(584-3 · 595-4 · 605-1) — 묶음 모두를 합친 값: 기준점 ±5 km ∪ 받은 지형으로 자른 선
|
|
* (선마다 `__bundle` 묶음 번호 · `__line` 묶음 안 차례) + 가닥 · 길이 m · 원본 선 수 · 좌표계 ·
|
|
* 잘린 상자([서, 남, 동, 북] 경위도) · 기준점 ~ 가장 가까운 임도 m · 마지막 올린 파일 · 묶음 줄. */
|
|
export interface PlanForestRoads {
|
|
bundles: PlanRoadBundle[];
|
|
count: number;
|
|
length_m: number;
|
|
total: number;
|
|
crs: string[];
|
|
bounds: [number, number, number, number] | null;
|
|
distance_m: number | null;
|
|
source: { files: string[]; bytes: number; uploaded_at: string } | null;
|
|
collection: GeoJsonCollection;
|
|
}
|
|
|
|
/** 올린 적 없으면 오류(404 글 그대로). */
|
|
export const fetchPlanForestRoads = (projectId: string): Promise<PlanForestRoads> =>
|
|
request(`${base(projectId)}/layers/forest-roads`);
|
|
|
|
/** 묶음 하나 지움(605-1) — 남은 묶음 합친 값 · 하나도 안 남으면 null. */
|
|
export async function deletePlanForestRoad(
|
|
projectId: string,
|
|
bundle: string,
|
|
): Promise<PlanForestRoads | null> {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}${base(projectId)}/layers/forest-roads/${encodeURIComponent(bundle)}`,
|
|
{ method: "DELETE", credentials: "include" },
|
|
);
|
|
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.bundles.length ? body : null;
|
|
}
|
|
|
|
/** 임도망도 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`);
|