feat(B07): 용지도 필지마다 지번·지목·면적·소유자 표시
- 연속지적도 속성(jibun·jimok·parea·owner_nm) 네 줄 · 빈 값은 그 줄만 비움 - 도곽에 잘려 조각이 여럿인 필지도 정보는 한 번만(가장 큰 조각 가운데) · 작은 필지 솎기 없앰 - 검증_sub4 용지도: 구역 안 필지 1 = 라벨 묶음 1(산77-15임 / 임야 / 10,640,598㎡ / 국유지) - 같은 지적도 전체(176필지 · 조각 198)를 한 구역으로: 필지 176 = 라벨 묶음 176 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
넣을 것. 연속지적도에 지번 정보가 있는지 확인하고, 없으면 일단 그림만」.
|
||||
|
||||
지번은 있다(2026-09-04 실측: 저장된 연속지적도 GeoJSON 필지마다 `jibun`·`jimok`·
|
||||
`parea`·`owner_nm` + 시도·시군구·읍면동·리 이름). 이번 판은 **지번만** 적는다 —
|
||||
지목·면적·소유 구분은 용지 조서(표)에서 쓸 값이라 도면에는 넣지 않는다.
|
||||
`parea`·`owner_nm` + 시도·시군구·읍면동·리 이름). 필지마다 **지번·지목·면적·소유자**
|
||||
네 줄을 적는다(사용자: 「연속지적도에 해당 정보를 띄우고 판단」) — 빈 값은 그 줄만 비운다.
|
||||
|
||||
축척·도곽·장 나눔은 계획평면도와 **같다**(1/1,200 고정). 배경도 같은 창구를 쓴다.
|
||||
|
||||
@@ -65,10 +65,8 @@ _ROUTE_WIDTH = 3
|
||||
_SGG_WIDTH = 3
|
||||
_EMD_WIDTH = 2
|
||||
_JIBUN_FONT_SIZE = 1.8
|
||||
_INFO_LINE_GAP = _JIBUN_FONT_SIZE * 1.3 # 필지 정보 줄 간격(mm)
|
||||
_LEGEND_FONT_SIZE = 2.4
|
||||
# 지번을 적을 최소 필지 크기(종이 mm) — 이보다 작으면 글자가 겹쳐 읽히지 않는다.
|
||||
_JIBUN_MIN_W_MM = 6.0
|
||||
_JIBUN_MIN_H_MM = 3.0
|
||||
|
||||
_LEGEND_ROW_H = 6.0
|
||||
_LEGEND_SAMPLE_W = 12.0
|
||||
@@ -92,6 +90,25 @@ def _ring_center(ring: list[tuple[float, float]]) -> tuple[float, float]:
|
||||
return ((min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0)
|
||||
|
||||
|
||||
def _parcel_info_lines(props: dict[str, Any]) -> list[str]:
|
||||
"""지번·지목·면적·소유자 네 줄 — 값이 없으면 그 줄은 빈 문자열."""
|
||||
|
||||
def text(value: Any) -> str:
|
||||
return value.strip() if isinstance(value, str) else ""
|
||||
|
||||
area = props.get("parea")
|
||||
try:
|
||||
area_text = f"{float(area):,.0f}㎡" if area not in (None, "") else ""
|
||||
except (TypeError, ValueError):
|
||||
area_text = text(area)
|
||||
return [
|
||||
text(props.get("jibun")),
|
||||
text(props.get("jimok")),
|
||||
area_text,
|
||||
text(props.get("owner_nm")),
|
||||
]
|
||||
|
||||
|
||||
def _legend_entities(drawing_id: str, origin: tuple[float, float]) -> list[dict[str, Any]]:
|
||||
"""범례 — 유역도에서 유역 정보표가 있던 자리(오른쪽 칸)에 놓는다."""
|
||||
entities: list[dict[str, Any]] = []
|
||||
@@ -197,48 +214,48 @@ def build_landuse_drawing(
|
||||
drawing_id, contours, CONTOUR_LAYER_ID, CONTOUR_COLOR, 1, None, paper, "contour"
|
||||
)
|
||||
)
|
||||
# 필지 경계 + 지번.
|
||||
jibun: list[dict[str, Any]] = []
|
||||
# 필지 경계 + 필지 정보. 도곽에 잘려 조각이 여럿인 필지도 정보는 한 번만 —
|
||||
# 가장 큰 조각 가운데에 적는다(구역 안 필지 수 = 정보 묶음 수).
|
||||
spots: dict[str, tuple[float, tuple[float, float], dict[str, Any]]] = {}
|
||||
for index, parcel in enumerate(parcels):
|
||||
ring = parcel.get("ring") or []
|
||||
props = parcel.get("props") or {}
|
||||
label_at = parcel.get("label_at")
|
||||
if len(ring) >= 2:
|
||||
paper_ring = [paper(point) for point in ring]
|
||||
outline = polyline_entity(
|
||||
drawing_id,
|
||||
[paper(point) for point in ring],
|
||||
PARCEL_LAYER_ID,
|
||||
PARCEL_COLOR,
|
||||
suffix=f":parcel:{index}",
|
||||
drawing_id, paper_ring, PARCEL_LAYER_ID, PARCEL_COLOR, suffix=f":parcel:{index}"
|
||||
)
|
||||
if outline:
|
||||
entities.append(outline)
|
||||
elif label_at is None:
|
||||
continue
|
||||
if label_at is not None:
|
||||
# 도곽을 통째로 감싼 필지 — 경계선이 없으니 지정된 자리에 지번만 적는다.
|
||||
center = paper(tuple(label_at))
|
||||
else:
|
||||
paper_ring = [paper(point) for point in ring]
|
||||
width = max(x for x, _y in paper_ring) - min(x for x, _y in paper_ring)
|
||||
height = max(y for _x, y in paper_ring) - min(y for _x, y in paper_ring)
|
||||
# 작은 필지는 지번을 솎는다 — 글자가 겹치면 큰 필지 것까지 못 읽는다.
|
||||
if width < _JIBUN_MIN_W_MM or height < _JIBUN_MIN_H_MM:
|
||||
continue
|
||||
center = _ring_center(paper_ring)
|
||||
text = (parcel.get("props") or {}).get("jibun")
|
||||
if not isinstance(text, str) or not text:
|
||||
size, center = width * height, _ring_center(paper_ring)
|
||||
elif label_at is not None:
|
||||
# 도곽을 통째로 감싼 필지 — 경계선이 없으니 지정된 자리에 정보만 적는다.
|
||||
size, center = 0.0, paper(tuple(label_at))
|
||||
else:
|
||||
continue
|
||||
jibun.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:jibun:{index}",
|
||||
text,
|
||||
center[0],
|
||||
center[1],
|
||||
JIBUN_LAYER_ID,
|
||||
_JIBUN_FONT_SIZE,
|
||||
JIBUN_COLOR,
|
||||
)
|
||||
)
|
||||
key = str(props.get("pnu") or f"#{index}")
|
||||
if key not in spots or size > spots[key][0]:
|
||||
spots[key] = (size, center, props)
|
||||
jibun: list[dict[str, Any]] = []
|
||||
for key, (_size, center, props) in spots.items():
|
||||
lines = _parcel_info_lines(props)
|
||||
top = center[1] + (len(lines) - 1) / 2.0 * _INFO_LINE_GAP
|
||||
for row, text in enumerate(lines):
|
||||
if text:
|
||||
jibun.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:parcel-info:{key}:{row}",
|
||||
text,
|
||||
center[0],
|
||||
top - row * _INFO_LINE_GAP,
|
||||
JIBUN_LAYER_ID,
|
||||
_JIBUN_FONT_SIZE,
|
||||
JIBUN_COLOR,
|
||||
)
|
||||
)
|
||||
# 행정 경계는 필지 위에, 노선은 그 위에 — 아래에 깔리면 필지 선에 묻힌다.
|
||||
entities.extend(
|
||||
_boundary_entities(
|
||||
@@ -267,7 +284,7 @@ def build_landuse_drawing(
|
||||
)
|
||||
if route:
|
||||
entities.append(route)
|
||||
entities.extend(jibun) # 지번은 가장 위 — 선에 가리면 못 읽는다.
|
||||
entities.extend(jibun) # 필지 정보는 가장 위 — 선에 가리면 못 읽는다.
|
||||
|
||||
# 오른쪽 칸: 방위표가 맨 위, 그 아래로 범례(유역도에서 유역 정보표가 있던 자리).
|
||||
if map_bbox:
|
||||
@@ -323,7 +340,7 @@ def build_landuse_drawing(
|
||||
"layers": [
|
||||
_layer(CONTOUR_LAYER_ID, "등고선", locked=True),
|
||||
_layer(PARCEL_LAYER_ID, "필지 경계"),
|
||||
_layer(JIBUN_LAYER_ID, "지번"),
|
||||
_layer(JIBUN_LAYER_ID, "필지 정보(지번·지목·면적·소유자)"),
|
||||
_layer(EMD_LAYER_ID, "읍면동·리 경계"),
|
||||
_layer(SGG_LAYER_ID, "시군구 경계"),
|
||||
_layer(ROUTE_LAYER_ID, "계획노선"),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""B07 용지도 — 필지마다 지번·지목·면적·소유자 한 묶음 · 빈 값은 빈 줄 (59-6)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Landuse import (
|
||||
JIBUN_LAYER_ID,
|
||||
MM,
|
||||
build_landuse_drawing,
|
||||
)
|
||||
|
||||
|
||||
def _square(x0: float, size: float) -> list[tuple[float, float]]:
|
||||
return [(x0, 0.0), (x0 + size, 0.0), (x0 + size, size), (x0, size), (x0, 0.0)]
|
||||
|
||||
|
||||
def test_one_label_group_per_parcel() -> None:
|
||||
full = {"pnu": "A", "jibun": "95전", "jimok": "전", "parea": 4916, "owner_nm": "개인"}
|
||||
split = {"pnu": "B", "jibun": "산1임", "jimok": "임야", "parea": 12.4, "owner_nm": ""}
|
||||
parcels = [
|
||||
{"ring": _square(0.0, 40.0), "props": full},
|
||||
# 도곽에 잘려 두 조각이 된 필지 — 정보는 큰 조각에 한 번만
|
||||
{"ring": _square(100.0, 10.0), "props": split},
|
||||
{"ring": _square(200.0, 30.0), "props": split},
|
||||
# 도곽을 통째로 감싼 필지 — 경계 없이 정보만
|
||||
{"ring": [], "props": {"pnu": "C", "jibun": "산77"}, "label_at": (500.0, 500.0)},
|
||||
]
|
||||
drawing = build_landuse_drawing("landuse", "용지도", [], [], parcels, [], [])
|
||||
texts = [e for e in drawing["entities"] if e.get("layerId") == JIBUN_LAYER_ID]
|
||||
labels = [e["shapeData"]["label"] for e in texts]
|
||||
assert labels == ["95전", "전", "4,916㎡", "개인", "산1임", "임야", "12㎡", "산77"]
|
||||
groups = {round(e["shapeData"]["basePoint"]["x"], 6) for e in texts}
|
||||
assert len(groups) == 3
|
||||
# 쪼개진 필지는 큰 조각(200~230 m) 가운데에
|
||||
split_x = {
|
||||
e["shapeData"]["basePoint"]["x"] for e in texts if e["shapeData"]["label"] == "산1임"
|
||||
}
|
||||
assert split_x.pop() == pytest.approx(215.0 * MM)
|
||||
Reference in New Issue
Block a user