"""GIS layer export / reprojection / geometry operations (/api/gis/export, /op, /reproject).""" import io import os import tempfile import zipfile import numpy as np import pyogrio import pytest import shapely from fastapi.testclient import TestClient from pyproj import Transformer from backend import gisops from backend.batch import BatchError from backend.main import app EPSG = "EPSG:5186" TO_WGS = Transformer.from_crs(EPSG, "EPSG:4326", always_xy=True) def wgs(x, y): return list(TO_WGS.transform(x, y)) def fc(features): return {"type": "FeatureCollection", "features": features} SQUARE = fc([{"type": "Feature", "properties": {"name": "블록A", "dia": 150}, "geometry": {"type": "Polygon", "coordinates": [[wgs(200000, 500000), wgs(200100, 500000), wgs(200100, 500100), wgs(200000, 500100), wgs(200000, 500000)]]}}]) LINES = fc([ {"type": "Feature", "properties": {"id": "P1", "dia": 150}, "geometry": {"type": "LineString", "coordinates": [wgs(200000, 500000), wgs(200200, 500000)]}}, {"type": "Feature", "properties": {"id": "P2", "dia": None}, "geometry": {"type": "LineString", "coordinates": [wgs(200200, 500000), wgs(200200, 500200)]}}, ]) @pytest.fixture(scope="module") def client(): with TestClient(app, raise_server_exceptions=False) as c: yield c def _area_m2(feature_collection): from shapely.geometry import shape g = gisops._project(np.array([shape(f["geometry"]) for f in feature_collection["features"]], dtype=object), gisops.WGS84, gisops.CRS.from_user_input(EPSG)) return float(sum(x.area for x in g)) def test_export_shapefile_in_target_crs_with_korean_fields(): data, name, media = gisops.write_vector(LINES, EPSG, "shp", "관로") assert name == "관로.zip" and media == "application/zip" with tempfile.TemporaryDirectory() as tmp: zipfile.ZipFile(io.BytesIO(data)).extractall(tmp) shp = [f for f in os.listdir(tmp) if f.endswith(".shp")][0] df_info = pyogrio.read_info(os.path.join(tmp, shp)) assert "5186" in df_info["crs"] and df_info["features"] == 2 meta, _, geom, fields = pyogrio.raw.read(os.path.join(tmp, shp)) x0 = shapely.from_wkb(geom[0]).coords[0][0] assert x0 == pytest.approx(200000, abs=0.01) assert list(meta["fields"]) == ["id", "dia"] def test_export_gpkg_dxf_and_mixed_shapefile_refused(): gpkg, name, _ = gisops.write_vector(SQUARE, EPSG, "gpkg", "blocks") assert name == "blocks.gpkg" and gpkg[:15] == b"SQLite format 3" dxf, _, _ = gisops.write_vector(LINES, EPSG, "dxf", "lines") assert b"SECTION" in dxf mixed = fc(SQUARE["features"] + LINES["features"]) with pytest.raises(BatchError): gisops.write_vector(mixed, EPSG, "shp", "x") def test_reproject_fixes_a_layer_read_in_the_wrong_crs(): # Numbers really in EPSG:5186 but read as EPSG:5187: reassign -> lands on the 5186 position. wrong = Transformer.from_crs("EPSG:5187", "EPSG:4326", always_xy=True) bad = fc([{"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": list(wrong.transform(200000, 500000))}}]) fixed = gisops.reproject_fc(bad, "EPSG:5187", EPSG) assert fixed["features"][0]["geometry"]["coordinates"] == pytest.approx(wgs(200000, 500000), abs=1e-8) def test_buffer_clip_difference_dissolve(): buf = gisops.vector_op("buffer", {"crs": EPSG, "layer": LINES, "distance": 10}) assert len(buf["features"]) == 2 and buf["features"][0]["properties"]["id"] == "P1" # P1 buffered: 200 x 20 rectangle + two half-discs of r=10 from shapely.geometry import shape a = gisops._project(np.array([shape(buf["features"][0]["geometry"])], dtype=object), gisops.WGS84, gisops.CRS.from_user_input(EPSG))[0].area assert a == pytest.approx(200 * 20 + np.pi * 100, rel=0.01) clipped = gisops.vector_op("clip", {"crs": EPSG, "layer": LINES, "other": SQUARE}) assert len(clipped["features"]) == 1 # only P1 crosses the square diff = gisops.vector_op("difference", {"crs": EPSG, "layer": SQUARE, "other": buf}) assert _area_m2(diff) < 100 * 100 dis = gisops.vector_op("dissolve", {"crs": EPSG, "layer": buf, "field": None}) assert len(dis["features"]) == 1 and dis["features"][0]["properties"]["count"] == 2 def test_voronoi_and_grid(): pts = [[200000, 500000], [200100, 500000], [200000, 500100], [200100, 500100], [200050, 500050]] v = gisops.vector_op("voronoi", {"crs": EPSG, "points": pts, "ids": ["A", "B", "C", "D", "E"]}) assert sorted(f["properties"]["node"] for f in v["features"]) == ["A", "B", "C", "D", "E"] g = gisops.vector_op("grid", {"crs": EPSG, "bounds": [200000, 500000, 200100, 500050], "cell": 25}) assert len(g["features"]) == 8 with pytest.raises(BatchError): gisops.vector_op("grid", {"crs": "EPSG:4326", "bounds": [0, 0, 1, 1], "cell": 1}) def test_http(client): r = client.post("/api/gis/export", json={"featureCollection": LINES, "crs": EPSG, "format": "geojson", "name": "관로"}) assert r.status_code == 200 and "attachment" in r.headers["content-disposition"] r = client.post("/api/gis/op", json={"op": "buffer", "crs": EPSG, "layer": LINES, "distance": 5}) assert r.status_code == 200 and len(r.json()["features"]) == 2 assert client.post("/api/gis/op", json={"op": "nope", "crs": EPSG, "layer": LINES}).status_code == 400