SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,131 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user