perf(D03): 0차곡 무거운 셈 일꾼으로 · 급경사 값 그림 바이너리 · 칠하기 toDataURL (PLAN 616-3 · 616-4)
- 0차곡 길 = 창고 선으로 바로(스레드) · 셈 필요하면 서버 본체 밖 일꾼 · 응답 = json.dumps 바로(큰 선 묶음 jsonable_encoder 건넘) - 급경사 = PNG 바이트 + 머리 X-Plan-Raster(칸 수 · 범위) · Content-Encoding identity(PNG 다시 gzip 안 함) · 음영기복 · 산사태 = JSON 그대로 - Api fetchPlanRaster 급경사만 바이너리(blob 주소 · slopeValues 가 다 읽고 놓음) - 칠하기 = canvas.toDataURL(toBlob 한가 작업 기다림 최대 1 s 없앰) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,10 +212,22 @@ export const fetchPlanSheetTile = (
|
||||
): Promise<GeoJsonCollection> =>
|
||||
request(`${base(projectId)}/sheet-tiles/${sheet}/${encodeURIComponent(name)}`);
|
||||
|
||||
export const fetchPlanRaster = (
|
||||
/** 그림 겹 — 급경사 = PNG 바이트 + 머리 `X-Plan-Raster`(616-4 · `image` = blob 주소 · 다 읽으면
|
||||
* `slopeValues` 가 놓음) · 그 밖 = JSON(base64 그림). */
|
||||
export const fetchPlanRaster = async (
|
||||
projectId: string,
|
||||
kind: "slope" | "hillshade" | "landslide",
|
||||
): Promise<PlanRaster> => request(`${base(projectId)}/layers/raster?kind=${kind}`);
|
||||
): Promise<PlanRaster> => {
|
||||
const path = `${base(projectId)}/layers/raster?kind=${kind}`;
|
||||
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 {
|
||||
|
||||
@@ -281,6 +281,12 @@ def slope_layer(
|
||||
return value
|
||||
|
||||
|
||||
def raster_bytes(value: dict[str, Any]) -> tuple[bytes, dict[str, Any]]:
|
||||
"""저장본 값 그림(base64 PNG 글) → (PNG 바이트, 나머지 칸) — 바이너리 전송(616-4)."""
|
||||
meta = {k: v for k, v in value.items() if k != "image"}
|
||||
return base64.b64decode(value["image"].split(",", 1)[1]), meta
|
||||
|
||||
|
||||
def lonlat_corners(box: Any, epsg: int) -> list[list[float]]:
|
||||
"""TM 상자 → 경위도 네 귀퉁이(왼쪽 아래부터 돎) — GL 바탕 그림 자리(586)."""
|
||||
from common_util.common_util_crs import tm_to_lonlat
|
||||
|
||||
@@ -399,8 +399,9 @@ async def plan_layer_cadastral(project_id: UUID, request: Request) -> Response:
|
||||
|
||||
|
||||
@router.get("/{project_id}/plan/layers/raster", response_model=None)
|
||||
async def plan_layer_raster(project_id: UUID, kind: str = "slope") -> dict[str, Any]:
|
||||
"""급경사 기울기 % 값 그림 + 1 % 칸 수(603) · 음영기복(`hillshade`) — 격자 지문 같으면 저장본 ·
|
||||
async def plan_layer_raster(project_id: UUID, kind: str = "slope") -> dict[str, Any] | Response:
|
||||
"""급경사 기울기 % 값 그림 + 1 % 칸 수(603 · PNG 바이트 + 머리 `X-Plan-Raster` 616-4) ·
|
||||
음영기복(`hillshade`) — 격자 지문 같으면 저장본 ·
|
||||
산사태위험지도(`landslide` · 행안부 WMS) — 받은 범위를 덮으면 저장본."""
|
||||
if kind == "landslide":
|
||||
root, epsg, box = await _layer_root(project_id)
|
||||
@@ -422,6 +423,10 @@ async def plan_layer_raster(project_id: UUID, kind: str = "slope") -> dict[str,
|
||||
)
|
||||
except FileNotFoundError as error:
|
||||
raise HTTPException(status_code=404, detail=str(error)) from error
|
||||
if kind == "slope":
|
||||
png, meta = layers.raster_bytes(value)
|
||||
head = {"X-Plan-Raster": json.dumps({"kind": kind, **meta}), "Content-Encoding": "identity"}
|
||||
return Response(png, media_type="image/png", headers=head) # PNG = 다시 압축 안 함
|
||||
return {"status": "success", "kind": kind, **value}
|
||||
|
||||
|
||||
@@ -479,18 +484,23 @@ async def plan_layer_zero_order(
|
||||
project_id: UUID,
|
||||
min_m2: float = zero_order.ZERO_ORDER_MIN_M2,
|
||||
max_m2: float = zero_order.ZERO_ORDER_MAX_M2,
|
||||
) -> dict[str, Any]:
|
||||
"""0차곡 선(지형 추정 · 위 집수 `min_m2` ~ `max_m2`) — 격자 지문 · 임계 같으면 저장본."""
|
||||
) -> Response:
|
||||
"""0차곡 선(지형 추정 · 위 집수 `min_m2` ~ `max_m2`) — 창고 도엽 선 · 다른 임계는 저장본 ·
|
||||
창고에 없어 셈이 필요하면 서버 본체 밖 일꾼(616-3 · 다른 겹 요청을 안 막음)."""
|
||||
if not 0 < min_m2 < max_m2:
|
||||
raise HTTPException(status_code=400, detail="0 < min_m2 < max_m2 여야 합니다.")
|
||||
root, epsg, _ = await _layer_root(project_id)
|
||||
args = (root, terrain.models_dir_of(root), epsg, min_m2, max_m2)
|
||||
pool = zero_order.heavy_pool()
|
||||
try:
|
||||
value = await asyncio.to_thread(
|
||||
zero_order.zero_order_layer, root, terrain.models_dir_of(root), epsg, min_m2, max_m2
|
||||
)
|
||||
value = await asyncio.to_thread(zero_order.zero_order_layer, *args, False)
|
||||
if value is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
value = await loop.run_in_executor(pool, zero_order.zero_order_layer, *args)
|
||||
except FileNotFoundError as error:
|
||||
raise HTTPException(status_code=404, detail=str(error)) from error
|
||||
return {"status": "success", **value}
|
||||
body = json.dumps({"status": "success", **value}, ensure_ascii=False) # 큰 선 묶음 = 바로 글로
|
||||
return Response(body, media_type="application/json")
|
||||
|
||||
|
||||
async def _road_frame(project_id: UUID) -> tuple[Path, int, terrain.Box, tuple[float, float]]:
|
||||
|
||||
@@ -35,17 +35,19 @@ export const readSlopeBands = (): SlopeBand[] => parseBands(readByKey(PREF_KEY))
|
||||
export const writeSlopeBands = (bands: SlopeBand[]): void =>
|
||||
writeByKey(PREF_KEY, JSON.stringify(bands));
|
||||
|
||||
/** 값 그림 → 칸 값(빨강 자리 = 회색 값). */
|
||||
/** 값 그림 → 칸 값(빨강 자리 = 회색 값) · 받은 blob 주소는 다 읽었으니 놓음(616-4). */
|
||||
export function slopeValues(image: HTMLImageElement): ImageData {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true })!;
|
||||
context.drawImage(image, 0, 0);
|
||||
if (image.src.startsWith("blob:")) URL.revokeObjectURL(image.src);
|
||||
return context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
/** 범위대로 칠한 그림(주소 = blob · 다 쓰면 부르는 쪽이 `URL.revokeObjectURL`). */
|
||||
/** 범위대로 칠한 그림(주소 = data URL · toBlob 의 한가 작업 기다림 최대 1 s 없앰 616-4 ·
|
||||
* 부르는 쪽 `URL.revokeObjectURL` 은 그대로 둬도 됨). */
|
||||
export async function paintSlope(
|
||||
values: ImageData,
|
||||
bands: SlopeBand[],
|
||||
@@ -65,13 +67,7 @@ export async function paintSlope(
|
||||
canvas.width = values.width;
|
||||
canvas.height = values.height;
|
||||
canvas.getContext("2d")!.putImageData(out, 0, 0);
|
||||
const blob = await new Promise<Blob>((done, fail) =>
|
||||
canvas.toBlob(
|
||||
(got) => (got ? done(got) : fail(new Error("그림을 만들지 못했습니다"))),
|
||||
"image/png",
|
||||
),
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const url = canvas.toDataURL("image/png");
|
||||
return { url, image: await loadImage(url) };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user