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

132 lines
5.6 KiB
Python

"""DEM store: import (GeoTIFF, zipped ASC), bilinear sampling across CRSs, preview, TIN from points, HTTP."""
import io
import zipfile
import numpy as np
import pytest
import rasterio
from fastapi.testclient import TestClient
from pyproj import CRS, Transformer
from rasterio.transform import from_origin
from backend import dem as dem_store
from backend.batch import BatchError
from backend.main import app
EPSG = "EPSG:5186"
X0, Y0, CELL, W, H = 200_000.0, 550_000.0, 10.0, 60, 40 # top-left corner
def plane(x, y):
return 100 + 0.01 * (x - X0) - 0.02 * (Y0 - y)
@pytest.fixture(autouse=True)
def store(tmp_path, monkeypatch):
monkeypatch.setattr(dem_store, "DEM_DIR", tmp_path / "dem")
def make_tif(path):
xs = X0 + (np.arange(W) + 0.5) * CELL
ys = Y0 - (np.arange(H) + 0.5) * CELL
z = plane(*np.meshgrid(xs, ys)).astype("float32")
z[0, 0] = -9999
with rasterio.open(path, "w", driver="GTiff", width=W, height=H, count=1, dtype="float32", crs=EPSG,
transform=from_origin(X0, Y0, CELL, CELL), nodata=-9999) as dst:
dst.write(z, 1)
def test_import_sample_is_bilinear_and_handles_outside(tmp_path):
src = tmp_path / "up.bin"
make_tif(tmp_path / "a.tif")
src.write_bytes((tmp_path / "a.tif").read_bytes())
info = dem_store.import_dem(str(src), "a.tif")
assert info["width"] == W and info["resolution"] == [CELL, CELL] and len(info["corners"]) == 4
pts = [[X0 + 123.4, Y0 - 77.7], [X0 + 300, Y0 - 200], [X0 - 50, Y0 - 50]]
vals = dem_store.sample(info["id"], pts, EPSG)
assert vals[0] == pytest.approx(plane(*pts[0]), abs=1e-3)
assert vals[1] == pytest.approx(plane(*pts[1]), abs=1e-3)
assert vals[2] is None
# Same point given in WGS84 lands on the same value.
lon, lat = Transformer.from_crs(EPSG, "EPSG:4326", always_xy=True).transform(*pts[0])
assert dem_store.sample(info["id"], [[lon, lat]], "EPSG:4326")[0] == pytest.approx(vals[0], abs=1e-3)
assert dem_store.preview_png(info["id"])[:8] == b"\x89PNG\r\n\x1a\n"
assert dem_store.dem_info(info["id"])["id"] == info["id"]
def test_zipped_asc_with_prj(tmp_path):
xs = X0 + (np.arange(W) + 0.5) * CELL
ys = Y0 - (np.arange(H) + 0.5) * CELL
z = plane(*np.meshgrid(xs, ys))
asc = f"ncols {W}\nnrows {H}\nxllcorner {X0}\nyllcorner {Y0 - H * CELL}\ncellsize {CELL}\nNODATA_value -9999\n"
asc += "\n".join(" ".join(f"{v:.4f}" for v in row) for row in z)
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("dem/ground.asc", asc)
zf.writestr("dem/ground.prj", CRS.from_user_input(EPSG).to_wkt("WKT1_ESRI"))
up = tmp_path / "up.zip"
up.write_bytes(buf.getvalue())
info = dem_store.import_dem(str(up), "ground.zip")
p = [X0 + 250, Y0 - 150]
assert dem_store.sample(info["id"], [p], EPSG)[0] == pytest.approx(plane(*p), abs=1e-3)
def test_errors(tmp_path):
with pytest.raises(BatchError):
dem_store.dem_info("0" * 32)
with pytest.raises(BatchError):
dem_store.dem_info("../../etc")
bad = tmp_path / "x.bin"
bad.write_bytes(b"not a raster")
with pytest.raises(BatchError):
dem_store.import_dem(str(bad), "x.tif")
with pytest.raises(BatchError):
dem_store.import_dem(str(bad), "x.docx")
def test_tin_from_points_reproduces_a_plane():
rng = np.random.default_rng(1)
xy = np.column_stack([X0 + rng.uniform(0, 500, 300), Y0 - rng.uniform(0, 300, 300)])
pts = [[x, y, plane(x, y)] for x, y in xy]
pts += [[X0, Y0, plane(X0, Y0)], [X0 + 500, Y0, plane(X0 + 500, Y0)], [X0, Y0 - 300, plane(X0, Y0 - 300)], [X0 + 500, Y0 - 300, plane(X0 + 500, Y0 - 300)]]
info = dem_store.from_points(pts, EPSG, 5.0)
q = [X0 + 211, Y0 - 133]
assert dem_store.sample(info["id"], [q], EPSG)[0] == pytest.approx(plane(*q), abs=0.05)
with pytest.raises(BatchError):
dem_store.from_points([[0, 0, 1], [1, 1, 2], [2, 2, 3]], EPSG, 1.0) # collinear
def test_http_roundtrip(tmp_path):
make_tif(tmp_path / "a.tif")
with TestClient(app) as c:
r = c.post("/api/dem", files={"file": ("a.tif", (tmp_path / "a.tif").read_bytes(), "image/tiff")})
assert r.status_code == 200, r.text
dem_id = r.json()["id"]
assert c.get(f"/api/dem/{dem_id}/preview.png").headers["content-type"] == "image/png"
s = c.post(f"/api/dem/{dem_id}/sample", json={"points": [[X0 + 100, Y0 - 100]], "crs": EPSG})
assert s.json()["values"][0] == pytest.approx(plane(X0 + 100, Y0 - 100), abs=1e-3)
assert c.get("/api/dem/" + "f" * 32).status_code == 400
assert c.post("/api/dem", files={"file": ("a.txt", b"x", "text/plain")}).status_code == 400
def test_terrain_tile_terrarium(tmp_path):
make_tif(tmp_path / "a.tif")
up = tmp_path / "up.bin"
up.write_bytes((tmp_path / "a.tif").read_bytes())
info = dem_store.import_dem(str(up), "a.tif")
import math
lon, lat = Transformer.from_crs(EPSG, "EPSG:4326", always_xy=True).transform(X0 + 300, Y0 - 200)
z = 15
n = 2 ** z
fx = (lon + 180) / 360 * n
fy = (1 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2 * n
xt, yt = int(fx), int(fy)
png = dem_store.terrain_tile(info["id"], z, xt, yt)
with rasterio.MemoryFile(png) as mem, mem.open() as ds:
rgb = ds.read().astype(float)
elev = rgb[0] * 256 + rgb[1] + rgb[2] / 256 - 32768
px, py = int((fx - xt) * 256), int((fy - yt) * 256)
assert elev[py, px] == pytest.approx(plane(X0 + 300, Y0 - 200), abs=0.2)
with pytest.raises(BatchError):
dem_store.terrain_tile(info["id"], 3, 99, 0)