Files
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

120 lines
4.6 KiB
Python

"""Vector GIS input via GDAL (pyogrio): any supported format -> GeoJSON in WGS84.
Shapefiles must arrive zipped (they are several files); everything else is a
single file. Files are read straight from a temp copy (zip through GDAL's
/vsizip/, never extracted), so archive paths cannot escape the temp dir.
"""
from __future__ import annotations
import os
import tempfile
import numpy as np
import pyogrio
import shapely
from pyogrio import raw as ogr_raw
from pyproj import CRS, Transformer
from shapely.geometry import mapping
from .batch import BatchError
VECTOR_EXTENSIONS = {
".zip": "ESRI Shapefile (zip)",
".geojson": "GeoJSON",
".json": "GeoJSON",
".gpkg": "GeoPackage",
".dxf": "DXF",
".dgn": "MicroStation DGN",
".mif": "MapInfo MIF",
".tab": "MapInfo TAB",
".gml": "GML",
".kml": "KML",
".e00": "ArcInfo E00",
".csv": "CSV (WKT/X,Y)",
}
MAX_FEATURES = 200_000
def _gdal_path(path: str, ext: str) -> str:
if ext == ".zip":
return "/vsizip/" + path.replace("\\", "/")
return path
def _clean(v):
if isinstance(v, (np.floating, float)):
f = float(v)
return None if f != f or f in (float("inf"), float("-inf")) else f
if isinstance(v, np.integer):
return int(v)
if isinstance(v, bytes):
try:
return v.decode("utf-8")
except UnicodeDecodeError:
return v.decode("cp949", errors="replace")
if hasattr(v, "isoformat"):
return v.isoformat()
return v
def read_vector(contents: bytes, filename: str, layer: str | None = None, src_epsg: int | None = None, default_crs: str | None = None) -> dict:
"""`default_crs` applies only when the file itself carries no CRS (DXF, bare CSV ...)."""
ext = os.path.splitext(filename.lower())[1]
if ext not in VECTOR_EXTENSIONS:
raise BatchError(f"지원하지 않는 형식입니다: {ext or filename}. 지원: {', '.join(sorted(VECTOR_EXTENSIONS))}")
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "input" + ext)
with open(path, "wb") as f:
f.write(contents)
src = _gdal_path(path, ext)
try:
layers = [str(row[0]) for row in pyogrio.list_layers(src)]
except Exception as e: # GDAL raises its own error types
raise BatchError(f"파일을 읽을 수 없습니다: {e}") from e
if not layers:
raise BatchError("파일에 레이어가 없습니다.")
name = layer if layer in layers else layers[0]
try:
info = pyogrio.read_info(src, layer=name, encoding=None)
meta, _fids, geometry, field_data = ogr_raw.read(src, layer=name, max_features=MAX_FEATURES + 1)
except Exception as e:
raise BatchError(f"레이어 {name}을(를) 읽을 수 없습니다: {e}") from e
count = len(geometry)
if count > MAX_FEATURES:
raise BatchError(f"도형이 {MAX_FEATURES:,}개를 넘습니다.")
crs_text = meta.get("crs") or info.get("crs")
assumed = False
src_crs = CRS.from_epsg(src_epsg) if src_epsg else (CRS.from_user_input(crs_text) if crs_text else None)
if src_crs is None and default_crs:
try:
src_crs = CRS.from_user_input(default_crs)
assumed = True
except Exception as e:
raise BatchError(f"기본 좌표계를 읽을 수 없습니다: {default_crs[:60]}") from e
src_code = src_crs.to_epsg() if src_crs else None
geoms = shapely.from_wkb(np.array(geometry, dtype=object))
if src_crs is not None and not src_crs.equals(CRS.from_epsg(4326)):
tr = Transformer.from_crs(src_crs, CRS.from_epsg(4326), always_xy=True)
geoms = shapely.transform(geoms, lambda xy: np.column_stack(tr.transform(xy[:, 0], xy[:, 1])))
fields = list(meta.get("fields", []))
features = []
for i in range(count):
g = geoms[i]
props = {fields[k]: _clean(field_data[k][i]) for k in range(len(fields))}
features.append({"type": "Feature", "geometry": mapping(g) if g is not None else None, "properties": props})
return {
"layers": layers,
"layer": name,
"crs": f"EPSG:{src_code}" if src_code else (src_crs.to_string() if src_crs else None),
"reprojected": src_crs is not None,
"crsAssumed": assumed,
"geometryType": meta.get("geometry_type"),
"count": count,
"fields": fields,
"featureCollection": {"type": "FeatureCollection", "features": features},
}