Files
Aislo/resources/tester/test_b07_landuse_parcel_info.py
T
eomsangdonandClaude Opus 5.5 3048dd96dd feat(B07): 용지도 = 평면도 종류 · 필지 상자 라벨 노선 비킴 · 배수유역도 평면도 묶음 · 제목 평면도(93-1~93-4)
- 조각 넷(필지 경계 · 필지 정보 · 읍면동 경계 · 시군구 경계) · 기본 종류 「용지도」 · 옛 종류 폴더에 한 번 더함
- 필지 라벨 = (필지 ∩ 도곽) − 노선 버퍼 대표점 + 상자 · 지시선 · 노선 · 상자끼리 안 겹침(새 Router_Support_Landuse)
- 옛 따로 용지도 길(landuse_source · build_landuse_drawing) 지움
- 배수유역도 목록 항목에 plan_kind 실음 · 종류 제목 「평면도(…)」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
2026-10-08 13:54:15 +09:00

164 lines
6.3 KiB
Python

"""B07 용지도 — 필지마다 지번·지목·면적·소유자 상자 + 지시선 (59-6 · 68-2 · 93-3).
① 필지마다 상자 하나 · 빈 값은 빈 줄 · 쪼개진 필지는 큰 조각에 지시선
② 구역이 한 필지 안 — 경계 없이 어느 필지인지 한 번
③ 상자 · 지시선 끝이 노선 버퍼와 안 겹침 · 상자끼리 안 겹침 · 도면에서도 노선과 안 겹침
"""
import json
from pathlib import Path
from shapely.geometry import LineString, Point
from shapely.geometry import box as box_polygon
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_MassHaul import balloon_size
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import MM
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
build_plan_kind_drawing,
layer_id,
)
from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import (
PARCEL_FONT,
ROUTE_CLEAR_M,
clip_parcels,
place_labels,
)
BOX = (0.0, 0.0, 400.0, 300.0)
def _square(x0: float, y0: float, size: float) -> list[list[float]]:
return [[x0, y0], [x0 + size, y0], [x0 + size, y0 + size], [x0, y0 + size], [x0, y0]]
def _geojson(tmp_path: Path, features: list[tuple[dict, dict]]) -> Path:
path = tmp_path / "parcels.geojson"
payload = {
"type": "FeatureCollection",
"features": [
{"type": "Feature", "properties": props, "geometry": geometry}
for props, geometry in features
],
}
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
return path
def _polygon(ring: list[list[float]]) -> dict:
return {"type": "Polygon", "coordinates": [ring]}
def _rect(item: dict) -> tuple[float, float, float, float]:
width, height = balloon_size(item["lines"], PARCEL_FONT)
cx, cy = item["center"]
return (cx - width / MM / 2, cy - height / MM / 2, cx + width / MM / 2, cy + height / MM / 2)
def test_one_box_per_parcel(tmp_path: Path) -> None:
full = {"pnu": "A", "jibun": "95전", "jimok": "전", "parea": 4916, "owner_nm": "개인"}
split = {"pnu": "B", "jibun": "산1임", "jimok": "임야", "parea": 12.4, "owner_nm": ""}
two_parts = {
"type": "MultiPolygon",
"coordinates": [[_square(250.0, 200.0, 20.0)], [_square(300.0, 150.0, 80.0)]],
}
path = _geojson(tmp_path, [(full, _polygon(_square(20.0, 20.0, 120.0))), (split, two_parts)])
parcels = clip_parcels(path, "EPSG:4326", BOX)
items = place_labels(parcels, [], BOX)
assert sorted(item["lines"] for item in items) == [
["95전", "전", "4,916㎡", "개인"],
["산1임", "임야", "12㎡", ""],
]
# 쪼개진 필지는 큰 조각(300~380 · 150~230)에 지시선 끝
(b,) = [item for item in items if item["lines"][0] == "산1임"]
assert 300.0 <= b["anchor"][0] <= 380.0 and 150.0 <= b["anchor"][1] <= 230.0
# 빈 줄은 글 없이 자리만 — 도면 글은 일곱
drawing = build_plan_kind_drawing(
"plan_landuse", "평면도(용지도)", BOX, {"parcel_info": {"items": items}}
)
texts = [
e["shapeData"]["label"]
for e in drawing["entities"]
if e.get("layerId") == layer_id("parcel_info") and e["type"] == "Text"
]
assert sorted(texts) == sorted(["95전", "전", "4,916㎡", "개인", "산1임", "임야", "12㎡"])
def test_zone_inside_one_parcel_is_named_once(tmp_path: Path) -> None:
props = {
"pnu": "P1",
"sgg_nm": "봉화군",
"emd_nm": "재산면",
"ri_nm": "갈산리",
"jibun": "산77-1임",
"jimok": "임야",
"parea": 1234567,
"owner_nm": "국",
}
path = _geojson(tmp_path, [(props, _polygon(_square(-500.0, -500.0, 2000.0)))])
parcels = clip_parcels(path, "EPSG:4326", BOX)
assert len(parcels) == 1 and parcels[0]["lines"] == [] and parcels[0]["whole"]
(item,) = place_labels(parcels, [(150.0, 100.0), (250.0, 200.0)], BOX)
assert item["lines"] == [
"구역 전체가 이 필지 안 (봉화군 재산면 갈산리)",
"산77-1임",
"임야",
"1,234,567㎡",
"국",
]
def test_boxes_clear_route_and_each_other(tmp_path: Path) -> None:
# 노선이 가운데를 가로지르는 필지 열둘(4 × 3)
features = [
(
{
"pnu": f"P{i}{j}",
"jibun": f"{i}{j}-1",
"jimok": "임야",
"parea": 9999,
"owner_nm": "홍길동",
},
_polygon(_square(i * 100.0, j * 100.0, 100.0)),
)
for i in range(4)
for j in range(3)
]
path = _geojson(tmp_path, features)
route = [(0.0, 140.0), (130.0, 160.0), (260.0, 120.0), (400.0, 170.0)]
items = place_labels(clip_parcels(path, "EPSG:4326", BOX), route, BOX)
assert len(items) == 12
clear = LineString(route).buffer(ROUTE_CLEAR_M)
rects = [_rect(item) for item in items]
for item, rect in zip(items, rects):
assert not box_polygon(*rect).intersects(clear) # 상자 · 노선 겹침 0
assert not clear.contains(Point(item["anchor"])) # 지시선 끝도 노선 밖
assert BOX[0] <= rect[0] and rect[2] <= BOX[2] and BOX[1] <= rect[1] and rect[3] <= BOX[3]
for index, rect in enumerate(rects):
for other in rects[index + 1 :]:
assert not box_polygon(*rect).intersects(box_polygon(*other))
# 도면(종이 mm)에서도 상자 테두리가 중심선과 안 닿음
pieces = {
"centerline": {"lines": [[list(p) for p in route]]},
"parcel_info": {"items": items},
}
drawing = build_plan_kind_drawing("plan_landuse", "평면도(용지도)", BOX, pieces)
(center,) = [e for e in drawing["entities"] if e.get("layerId") == layer_id("centerline")]
center_line = _polyline(center)
# 상자 = 변 넷 · 지시선 = 계단 둘
outlines = [
e
for e in drawing["entities"]
if e.get("layerId") == layer_id("parcel_info") and len(e.get("children") or []) == 4
]
assert len(outlines) == 12
for outline in outlines:
assert _polyline(outline).distance(center_line) > 0
def _polyline(entity: dict) -> LineString:
children = entity["children"]
points = [children[0]["shapeData"]["startPoint"]] + [
c["shapeData"]["endPoint"] for c in children
]
return LineString([(p["x"], p["y"]) for p in points])