feat(b07): 평면도 종류마다 축척 고르기 — 1/600 · 1/1,000 · 1/1,200 기본 · 유역도 1/6,000 고정(119-1)
- 종류 파일 scale(없거나 목록 밖이면 1200) · 추가 · 업데이트 인자 · 목록 밖 거절 - 장 나눔 · 도곽 범위 · 배경 절취 · 축척 글 · 도곽 칸 · IP 표 · 필지 라벨 · 측점 눈금 실거리 환산이 축척 따라감(함수 인자) - 「종류」 폼에 공용 「축척」 고르기 · 유역도 양식은 잠금 표시 - 시험 test_119_b07_plan_scale 새로 · test_b07_plan_kinds 종류 dict 에 scale Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""B07 계획평면도 종류 · 정보 조각 파일 (PLAN 60장).
|
||||
|
||||
저장 자리(프로젝트 저장소 `B07_DesignDetail/`):
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout} — 파일 하나씩
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout, scale} — 파일 하나씩
|
||||
plan_pieces/{장}/{조각}.json 정보 조각 하나 = 그 장 범위의 기하(사업지 좌표 m)
|
||||
plan_marks.json 사용자가 정한 대피소 · 사토장 자리
|
||||
|
||||
@@ -65,7 +65,11 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import (
|
||||
)
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from config.config_system import DRAWING_SCALE_PLAN
|
||||
from config.config_system import (
|
||||
DRAWING_SCALE_BASIN,
|
||||
DRAWING_SCALE_PLAN,
|
||||
DRAWING_SCALE_PLAN_CHOICES,
|
||||
)
|
||||
|
||||
KIND_ID = re.compile(r"^[a-z][a-z0-9]{0,31}$")
|
||||
PLAN_KIND_ID = re.compile(r"^plan_([a-z][a-z0-9]{0,31})(?:_(\d+))?$")
|
||||
@@ -91,12 +95,21 @@ def _kinds_dir(project_root: Path) -> Path:
|
||||
_ADDED_ONCE = ("landuse", "watershed")
|
||||
|
||||
|
||||
def _scale(layout: str, value: Any) -> int:
|
||||
"""종류 축척 분모 — 유역도 양식은 1/6,000 고정 · 없거나 목록 밖이면 1/1,200(옛 파일)."""
|
||||
if layout == WATERSHED_LAYOUT:
|
||||
return DRAWING_SCALE_BASIN
|
||||
return value if value in DRAWING_SCALE_PLAN_CHOICES else DRAWING_SCALE_PLAN
|
||||
|
||||
|
||||
def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
|
||||
layout = kind.get("layout", "plan")
|
||||
return {
|
||||
"name": kind["name"],
|
||||
"pieces": list(kind["pieces"]),
|
||||
"order": order,
|
||||
"layout": kind.get("layout", "plan"),
|
||||
"layout": layout,
|
||||
"scale": _scale(layout, kind.get("scale")),
|
||||
}
|
||||
|
||||
|
||||
@@ -122,11 +135,17 @@ def _add_defaults_once(folder: Path) -> None:
|
||||
|
||||
|
||||
def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""종류 목록 [{id, name, pieces, layout}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
"""종류 목록 [{id, name, pieces, layout, scale}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
folder = _kinds_dir(project_root)
|
||||
if not folder.is_dir():
|
||||
return [
|
||||
{"layout": "plan", **kind, "pieces": list(kind["pieces"])} for kind in DEFAULT_KINDS
|
||||
{
|
||||
"layout": "plan",
|
||||
**kind,
|
||||
"pieces": list(kind["pieces"]),
|
||||
"scale": _scale(kind.get("layout", "plan"), None),
|
||||
}
|
||||
for kind in DEFAULT_KINDS
|
||||
]
|
||||
_add_defaults_once(folder)
|
||||
rows: list[tuple[int, str, dict[str, Any]]] = []
|
||||
@@ -151,6 +170,7 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
"name": str(document.get("name") or path.stem),
|
||||
"pieces": pieces,
|
||||
"layout": layout,
|
||||
"scale": _scale(layout, document.get("scale")),
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -183,20 +203,44 @@ def _checked(name: str, pieces: list[str], layout: str = "plan") -> tuple[str, l
|
||||
return text, chosen
|
||||
|
||||
|
||||
def _checked_scale(layout: str, scale: int | None, current: int = DRAWING_SCALE_PLAN) -> int:
|
||||
"""고른 축척 — 없으면 지금 값 · 목록 밖이면 거절 · 유역도 양식은 1/6,000 고정."""
|
||||
if layout == WATERSHED_LAYOUT:
|
||||
return DRAWING_SCALE_BASIN
|
||||
if scale is None:
|
||||
return current
|
||||
if scale not in DRAWING_SCALE_PLAN_CHOICES:
|
||||
raise ValueError("고를 수 없는 축척입니다.")
|
||||
return scale
|
||||
|
||||
|
||||
def add_kind(
|
||||
project_root: Path, name: str, pieces: list[str], layout: str = "plan"
|
||||
project_root: Path,
|
||||
name: str,
|
||||
pieces: list[str],
|
||||
layout: str = "plan",
|
||||
scale: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
text, chosen = _checked(name, pieces, layout)
|
||||
chosen_scale = _checked_scale(layout, scale)
|
||||
kinds = _materialize(project_root)
|
||||
if any(kind["name"] == text for kind in kinds):
|
||||
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
|
||||
kind = {"id": f"k{uuid.uuid4().hex[:8]}", "name": text, "pieces": chosen, "layout": layout}
|
||||
kind = {
|
||||
"id": f"k{uuid.uuid4().hex[:8]}",
|
||||
"name": text,
|
||||
"pieces": chosen,
|
||||
"layout": layout,
|
||||
"scale": chosen_scale,
|
||||
}
|
||||
atomic_write_json(_kinds_dir(project_root) / f"{kind['id']}.json", _document(kind, len(kinds)))
|
||||
return kind
|
||||
|
||||
|
||||
def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str]) -> dict[str, Any]:
|
||||
"""종류 이름 · 조각을 고친다(차례 · 양식은 그대로). 그 종류의 확정본은 풀어 다시 그린다."""
|
||||
def update_kind(
|
||||
project_root: Path, kind_id: str, name: str, pieces: list[str], scale: int | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""종류 이름 · 조각 · 축척을 고친다(차례 · 양식은 그대로). 그 종류 확정본은 풀어 다시 그린다."""
|
||||
if not KIND_ID.fullmatch(kind_id):
|
||||
raise ValueError("올바르지 않은 종류 id 입니다.")
|
||||
kinds = _materialize(project_root)
|
||||
@@ -205,10 +249,17 @@ def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str])
|
||||
if current is None or not path.is_file():
|
||||
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
|
||||
text, chosen = _checked(name, pieces, current["layout"])
|
||||
chosen_scale = _checked_scale(current["layout"], scale, current["scale"])
|
||||
if any(kind["name"] == text and kind["id"] != kind_id for kind in kinds):
|
||||
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
|
||||
order = int(_read_json(path).get("order") or 0)
|
||||
kind = {"id": kind_id, "name": text, "pieces": chosen, "layout": current["layout"]}
|
||||
kind = {
|
||||
"id": kind_id,
|
||||
"name": text,
|
||||
"pieces": chosen,
|
||||
"layout": current["layout"],
|
||||
"scale": chosen_scale,
|
||||
}
|
||||
atomic_write_json(path, _document(kind, order))
|
||||
manifest = _read_manifest(project_root)
|
||||
released = False
|
||||
@@ -383,12 +434,14 @@ def build_pieces(
|
||||
designs: dict[int | float, dict[str, Any]],
|
||||
number: int,
|
||||
wanted: list[str],
|
||||
scale: int = DRAWING_SCALE_PLAN,
|
||||
) -> tuple[dict[str, dict[str, Any]], tuple[float, float, float, float], dict[str, Any], int]:
|
||||
"""그 장의 조각들을 계산해 파일로 쓰고 (조각들, 도곽 범위, 장 구간, 전체 장수)를 낸다."""
|
||||
route_xy, chunk, total = _chunk_route(context, longitudinal, number)
|
||||
route_xy, chunk, total = _chunk_route(context, longitudinal, number, scale)
|
||||
if not route_xy:
|
||||
raise FileNotFoundError("계획평면도에 그릴 노선이 없습니다.")
|
||||
box = _sheet_box(route_xy)
|
||||
box = _sheet_box(route_xy, scale)
|
||||
mm = 1000.0 / scale
|
||||
start_m, end_m = float(chunk["start_m"]), float(chunk["end_m"])
|
||||
project_root = Path(context.project_root)
|
||||
route = sorted(
|
||||
@@ -399,9 +452,7 @@ def build_pieces(
|
||||
pieces: dict[str, dict[str, Any]] = {}
|
||||
|
||||
if {"contour", "stream"} & set(wanted):
|
||||
background = map_background(
|
||||
project_root, context.crs, DRAWING_SCALE_PLAN, plan_area_mm(), route_xy
|
||||
)
|
||||
background = map_background(project_root, context.crs, scale, plan_area_mm(), route_xy)
|
||||
pieces["contour"] = {"lines": background["contours"]}
|
||||
pieces["stream"] = {"lines": background["streams"]}
|
||||
if "centerline" in wanted:
|
||||
@@ -473,13 +524,13 @@ def build_pieces(
|
||||
if set(LANDUSE_PIECES) & set(wanted):
|
||||
# 라벨은 이 장 구간이 아니라 노선 전체를 비켜 간다 — 도곽 안에 다른 구간이 들 수 있다.
|
||||
pieces.update(
|
||||
landuse_pieces(project_root, context.crs, box, [(x, y) for _c, x, y in route])
|
||||
landuse_pieces(project_root, context.crs, box, [(x, y) for _c, x, y in route], mm)
|
||||
)
|
||||
if "parcel_info" in wanted:
|
||||
clip_lines(pieces, label_rects(pieces["parcel_info"]["items"]), LABEL_CLIPPED)
|
||||
clip_lines(pieces, label_rects(pieces["parcel_info"]["items"], mm), LABEL_CLIPPED)
|
||||
if "ip_table" in wanted:
|
||||
pieces["ip_table"] = ip_table_piece(
|
||||
pieces, _load_design_curves(project_root), route, box, chunk, total
|
||||
pieces, _load_design_curves(project_root), route, box, chunk, total, mm
|
||||
)
|
||||
|
||||
folder = _design_root(project_root) / "plan_pieces" / str(number)
|
||||
@@ -510,19 +561,21 @@ def plan_kind_source(
|
||||
return {"layout": WATERSHED_LAYOUT, "label": kind["name"], **watershed_source(context)}
|
||||
number = int(match.group(2)) if match.group(2) else 1
|
||||
pieces, box, chunk, total = build_pieces(
|
||||
context, longitudinal, longitudinal_path, designs, number, kind["pieces"]
|
||||
context, longitudinal, longitudinal_path, designs, number, kind["pieces"], kind["scale"]
|
||||
)
|
||||
title = plan_title(kind["name"])
|
||||
return {
|
||||
"label": title if total <= 1 else f"{title} {chunk['number']}장",
|
||||
"box": box,
|
||||
"pieces": pieces,
|
||||
"scale": kind["scale"],
|
||||
}
|
||||
|
||||
|
||||
def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""도면 목록에 실을 종류 × 장 [{id, label, plan_kind}]."""
|
||||
sheets = plan_chunks(plan_stations(longitudinal))
|
||||
stations = plan_stations(longitudinal)
|
||||
by_scale: dict[int, list[dict[str, Any]]] = {}
|
||||
items = []
|
||||
for kind in list_kinds(project_root):
|
||||
if kind["layout"] == WATERSHED_LAYOUT: # 1/6,000 한 장 — 장 나눔 없음
|
||||
@@ -536,6 +589,9 @@ def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[di
|
||||
)
|
||||
continue
|
||||
title = plan_title(kind["name"])
|
||||
if kind["scale"] not in by_scale:
|
||||
by_scale[kind["scale"]] = plan_chunks(stations, kind["scale"])
|
||||
sheets = by_scale[kind["scale"]]
|
||||
for chunk in sheets:
|
||||
single = len(sheets) <= 1
|
||||
items.append(
|
||||
|
||||
Reference in New Issue
Block a user