feat(B07): 배수유역도 종류화 · 평면도 드롭다운 공용 꼴 · IP 표 차례 · 용지도 라벨 2배와 상자 밑 선 자름(109-1~4)
- 배수유역도 = 기본 종류(양식 watershed · 1/6,000 한 장 · 조각 없음) · 도면 id plan_watershed · × 지우기 · 「새 종류」 양식 고르기로 다시 · 옛 폴더에 한 번만 더함 - 평면도 칸 종류 · 양식 고르기 = 공용 createSelectField(compact) - IP 표 첫 줄 맨 위 · 왼 → 오 · 위 → 아래(묶음 왼쪽 아래 기준 그대로) - 용지도 필지 정보 글 3.6(상수 하나) · 상자 밑 등고선 · 계류 · 필지 · 읍면동 · 시군구 선 잘라 냄(clip_lines 일반화) - 시험: 기본 종류 · 유역 한 장 · 지우기 · 다시 만들기 · IP 표 두 줄 · 상자 안 선 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
@@ -180,7 +180,10 @@ export interface PlanKind {
|
||||
id: string;
|
||||
name: string;
|
||||
pieces: string[];
|
||||
/** 양식 — plan(평면도 · 조각 고름) · watershed(유역도 1/6,000 한 장 · 조각 없음). */
|
||||
layout: PlanKindLayout;
|
||||
}
|
||||
export type PlanKindLayout = "plan" | "watershed";
|
||||
export interface PlanMark {
|
||||
chainage_m: number;
|
||||
refuge: boolean;
|
||||
@@ -202,10 +205,11 @@ export function addPlanKind(
|
||||
projectId: string,
|
||||
name: string,
|
||||
pieces: string[],
|
||||
layout: PlanKindLayout = "plan",
|
||||
): Promise<PlanKindsState> {
|
||||
return requestJson(`/projects/${projectId}/plan-kinds`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name, pieces }),
|
||||
body: JSON.stringify({ name, pieces, layout }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -110,33 +110,31 @@ def ip_blocks(
|
||||
|
||||
|
||||
def _place(blocks: list[dict[str, Any]], width_mm: float) -> None:
|
||||
"""묶음마다 표 왼쪽 위 자리(종이 mm) `origin` — 왼쪽 아래부터 나란히 · 넘치면 위로 다음 줄."""
|
||||
"""묶음마다 표 왼쪽 위 자리(종이 mm) `origin` — 왼쪽 → 오른쪽 · 위 → 아래(첫 줄 맨 위).
|
||||
|
||||
묶음 전체는 도곽 왼쪽 아래 기준 — 줄이 늘면 위로 쌓이고 마지막 줄이 맨 아래다.
|
||||
"""
|
||||
block_w = sum(_COLUMNS)
|
||||
per_line = max(1, int((width_mm - 2 * _MARGIN) // block_w))
|
||||
lines = -(-len(blocks) // per_line)
|
||||
for order, block in enumerate(blocks):
|
||||
line, column = divmod(order, per_line)
|
||||
top = _MARGIN + line * (_IP_ROWS * _ROW_H + _LINE_GAP) + _IP_ROWS * _ROW_H
|
||||
top = _MARGIN + (lines - 1 - line) * (_IP_ROWS * _ROW_H + _LINE_GAP) + _IP_ROWS * _ROW_H
|
||||
block["origin"] = [_MARGIN + column * block_w, top]
|
||||
block["rows"] = _END_ROWS if "ia" not in block else _IP_ROWS
|
||||
|
||||
|
||||
def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any) -> None:
|
||||
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
|
||||
min_x, min_y = box[0], box[1]
|
||||
width = sum(_COLUMNS) / MM
|
||||
area = unary_union(
|
||||
[
|
||||
rect(
|
||||
min_x + b["origin"][0] / MM,
|
||||
min_y + b["origin"][1] / MM - b["rows"] * _ROW_H / MM,
|
||||
min_x + b["origin"][0] / MM + width,
|
||||
min_y + b["origin"][1] / MM,
|
||||
)
|
||||
for b in blocks
|
||||
]
|
||||
)
|
||||
def clip_lines(
|
||||
pieces: dict[str, dict[str, Any]],
|
||||
rects: list[tuple[float, float, float, float]],
|
||||
names: tuple[str, ...],
|
||||
) -> None:
|
||||
"""상자들(사업지 m · 최소 x · y · 최대 x · y) 밑 선 조각 `names` 를 상자 밖으로 잘라 낸다."""
|
||||
if not rects:
|
||||
return
|
||||
area = unary_union([rect(*bounds) for bounds in rects])
|
||||
inside = prep(area)
|
||||
for piece in _CLIPPED:
|
||||
for piece in names:
|
||||
lines = (pieces.get(piece) or {}).get("lines")
|
||||
if not lines:
|
||||
continue
|
||||
@@ -155,6 +153,22 @@ def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box:
|
||||
pieces[piece]["lines"] = kept
|
||||
|
||||
|
||||
def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any) -> None:
|
||||
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
|
||||
min_x, min_y = box[0], box[1]
|
||||
width = sum(_COLUMNS) / MM
|
||||
rects = [
|
||||
(
|
||||
min_x + b["origin"][0] / MM,
|
||||
min_y + b["origin"][1] / MM - b["rows"] * _ROW_H / MM,
|
||||
min_x + b["origin"][0] / MM + width,
|
||||
min_y + b["origin"][1] / MM,
|
||||
)
|
||||
for b in blocks
|
||||
]
|
||||
clip_lines(pieces, rects, _CLIPPED)
|
||||
|
||||
|
||||
def ip_table_piece(
|
||||
pieces: dict[str, dict[str, Any]],
|
||||
curves: list[dict[str, Any]],
|
||||
|
||||
@@ -75,7 +75,7 @@ LEGACY_PIECES: dict[str, tuple[str, ...]] = {
|
||||
_OUTLINE = LEGACY_PIECES["slope_outline"]
|
||||
|
||||
_TERRAIN = ("contour", "stream", "centerline", *_OUTLINE, "slope_ticks", "ip", "radius")
|
||||
# 기본 종류 넷 — 지울 수 있다. id 는 옛 도면 id(plan_terrain …)와 같아 확정본이 이어진다.
|
||||
# 기본 종류 — 지울 수 있다. id 는 옛 도면 id(plan_terrain …)와 같아 확정본이 이어진다.
|
||||
DEFAULT_KINDS: tuple[dict[str, Any], ...] = (
|
||||
{"id": "terrain", "name": "지형", "pieces": [*_TERRAIN, "station"]},
|
||||
{
|
||||
@@ -100,6 +100,8 @@ DEFAULT_KINDS: tuple[dict[str, Any], ...] = (
|
||||
"name": "용지도",
|
||||
"pieces": ["contour", "centerline", "parcel", "parcel_info", "emd", "sgg"],
|
||||
},
|
||||
# 배수유역도 = 양식이 다른 종류(1/6,000 한 장 · 정보표) — 조각을 고르지 않는다(109장).
|
||||
{"id": "watershed", "name": "배수유역도", "pieces": [], "layout": "watershed"},
|
||||
)
|
||||
|
||||
_COLORS = {
|
||||
@@ -127,7 +129,7 @@ _BOUNDARY_STYLE: dict[str, tuple[int, list[int] | None]] = {
|
||||
"emd": (2, [8, 4]),
|
||||
"sgg": (3, [14, 5, 3, 5]),
|
||||
}
|
||||
_PARCEL_FONT = 1.8
|
||||
PARCEL_FONT = 3.6 # 필지 정보 글 크기(종이 mm) — 용지도 자리 잡기도 이 값
|
||||
TICK_STEP_M = 2.0 # 비탈 눈금 간격(실거리 m) — 긴 · 짧은 눈금이 번갈아 선다
|
||||
_MAJOR_EVERY = 5 # 측점 NO 큰 글자 · 동그라미 간격(측점 수)
|
||||
_LABEL_FONT = 2.5
|
||||
@@ -518,7 +520,7 @@ def _piece_entities(
|
||||
"rect",
|
||||
layer=layer,
|
||||
color=color,
|
||||
font=_PARCEL_FONT,
|
||||
font=PARCEL_FONT,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
@@ -39,14 +39,12 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
|
||||
CROSS_STANDARD_ID,
|
||||
MASS_HAUL_ID,
|
||||
WATERSHED_ID,
|
||||
_cross_sheet_plan,
|
||||
_drawing_list,
|
||||
_invalidate_drawing,
|
||||
_read_drawing,
|
||||
_read_json,
|
||||
_store_confirmed_drawing,
|
||||
watershed_source,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Overlay import cross_overlay_extents
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import PLAN_KIND_ID, plan_kind_source
|
||||
@@ -322,12 +320,6 @@ async def get_design_drawing(
|
||||
async with pool.acquire() as connection:
|
||||
section = await get_longitudinal_section(connection, project_id, route_id)
|
||||
source_design = ((section or {}).get("data") or {}).get("mass_haul")
|
||||
elif drawing_id == WATERSHED_ID:
|
||||
# 유역도는 B04 배수유역 산출물 + 도엽 배경을 사업지 CRS로 모아 넘긴다.
|
||||
context, reason = await load_drainage_context(project_id)
|
||||
if context is None:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": reason})
|
||||
source_design = await asyncio.to_thread(watershed_source, context)
|
||||
elif drawing_id == CROSS_STANDARD_ID:
|
||||
# 표준 횡단면도는 B06 「표준 횡단면 설정」 저장값으로 그린다(없으면 config 기본값).
|
||||
pool = get_db_pool()
|
||||
|
||||
@@ -34,6 +34,8 @@ router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
|
||||
class PlanKindRequest(BaseModel):
|
||||
name: str
|
||||
pieces: list[str] = Field(default_factory=list)
|
||||
# 양식 — plan(평면도 · 조각 고름) · watershed(유역도). 추가에서만 쓰고 업데이트는 그대로 둔다.
|
||||
layout: str = "plan"
|
||||
|
||||
|
||||
class PlanMark(BaseModel):
|
||||
@@ -79,10 +81,10 @@ async def get_plan_kinds(project_id: UUID) -> Any:
|
||||
|
||||
@router.post("/{project_id}/plan-kinds")
|
||||
async def post_plan_kind(project_id: UUID, request: PlanKindRequest) -> Any:
|
||||
"""종류 추가 — 조각을 골라 이름을 붙인다."""
|
||||
"""종류 추가 — 양식 · 조각을 골라 이름을 붙인다."""
|
||||
try:
|
||||
root = await _project_root(project_id)
|
||||
await asyncio.to_thread(add_kind, root, request.name, request.pieces)
|
||||
await asyncio.to_thread(add_kind, root, request.name, request.pieces, request.layout)
|
||||
return await asyncio.to_thread(_state, root)
|
||||
except ValueError as exc:
|
||||
return _error(400, exc)
|
||||
|
||||
@@ -76,6 +76,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
PLAN_KIND_ID,
|
||||
WATERSHED_LAYOUT,
|
||||
plan_kind_items,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Schema import (
|
||||
@@ -92,7 +93,6 @@ _CROSS_ID = re.compile(rf"^cross_{CROSS_KEY_PATTERN}m$")
|
||||
_LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$")
|
||||
# 노선 전장에 한 장씩만 나오는 도면 — 라우터가 원본 자료를 따로 실어 넘긴다.
|
||||
MASS_HAUL_ID = "mass_haul"
|
||||
WATERSHED_ID = "watershed"
|
||||
COVER_ID = "cover"
|
||||
# 표준 횡단면도 — 노선 자료가 아니라 B06 표준 횡단면 설정값으로 그리는 한 장.
|
||||
CROSS_STANDARD_ID = "cross_standard"
|
||||
@@ -156,7 +156,6 @@ def _drawing_list(
|
||||
(COVER_ID, "cover", "표지"),
|
||||
(CROSS_STANDARD_ID, "cross_standard", STANDARD_LABEL),
|
||||
(MASS_HAUL_ID, "mass_haul", "토적도(유토곡선)"),
|
||||
(WATERSHED_ID, "watershed", "유역도(배수 유역도)"),
|
||||
):
|
||||
drawings.append(
|
||||
DesignDrawingItem(
|
||||
@@ -164,12 +163,6 @@ def _drawing_list(
|
||||
kind=kind,
|
||||
label=label,
|
||||
confirmed=bool(manifest_drawings.get(drawing_id, {}).get("confirmed")),
|
||||
# 배수유역도는 평면도 묶음 안 항목 — 축척(1/6,000 한 장)이 달라 종류는 아니다.
|
||||
**(
|
||||
{"plan_kind": WATERSHED_ID, "plan_kind_name": "배수유역도"}
|
||||
if drawing_id == WATERSHED_ID
|
||||
else {}
|
||||
),
|
||||
)
|
||||
)
|
||||
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — 사용자 확정 ③, PLAN 3장 ④.
|
||||
@@ -405,7 +398,7 @@ def _read_drawing(
|
||||
# 포맷 버전이 다르면(테이블·레이어 구성 변경 전 저장본) 캐시를 버리고
|
||||
# 아래에서 원본 기준으로 재생성한다. 확정 상태도 무효로 응답해 재확정 유도.
|
||||
if saved.get("format") == DRAWING_FORMAT:
|
||||
if drawing_id in (COVER_ID, MASS_HAUL_ID, WATERSHED_ID, CROSS_STANDARD_ID):
|
||||
if drawing_id in (COVER_ID, MASS_HAUL_ID, CROSS_STANDARD_ID):
|
||||
kind = drawing_id # id와 kind가 같은 단장 도면
|
||||
elif PLAN_KIND_ID.fullmatch(drawing_id):
|
||||
kind = "plan"
|
||||
@@ -451,36 +444,22 @@ def _read_drawing(
|
||||
if not isinstance(stored_design, dict):
|
||||
raise FileNotFoundError("계획평면도 자료가 없습니다.")
|
||||
label = str(stored_design.get("label") or drawing_id)
|
||||
return (
|
||||
"plan",
|
||||
label,
|
||||
build_plan_kind_drawing(
|
||||
drawing_id, label, stored_design["box"], stored_design.get("pieces") or {}
|
||||
),
|
||||
False,
|
||||
None,
|
||||
)
|
||||
|
||||
if drawing_id == WATERSHED_ID:
|
||||
# stored_design = watershed_source()가 모아 준 노선·유역·배경 좌표(사업지 CRS).
|
||||
if not isinstance(stored_design, dict):
|
||||
raise FileNotFoundError("배수 유역 산출물이 없습니다.")
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
interval = infer_station_interval(longitudinal.get("stations") or [])
|
||||
return (
|
||||
"watershed",
|
||||
"유역도(배수 유역도)",
|
||||
build_watershed_drawing(
|
||||
if stored_design.get("layout") == WATERSHED_LAYOUT:
|
||||
# 유역도 양식 — watershed_source()가 모아 준 노선·유역·배경 좌표(사업지 CRS).
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
drawing = build_watershed_drawing(
|
||||
drawing_id,
|
||||
stored_design.get("route_xy") or [],
|
||||
stored_design.get("basins") or [],
|
||||
stored_design.get("contours") or [],
|
||||
stored_design.get("streams") or [],
|
||||
interval,
|
||||
),
|
||||
False,
|
||||
None,
|
||||
)
|
||||
infer_station_interval(longitudinal.get("stations") or []),
|
||||
)
|
||||
else:
|
||||
drawing = build_plan_kind_drawing(
|
||||
drawing_id, label, stored_design["box"], stored_design.get("pieces") or {}
|
||||
)
|
||||
return "plan", label, drawing, False, None
|
||||
|
||||
if _LONG_ID.fullmatch(drawing_id):
|
||||
source = _read_json(longitudinal_path)
|
||||
|
||||
@@ -20,7 +20,6 @@ _CROSS_ID = re.compile(r"^cross_(\d+)m$")
|
||||
_LONG_ID = re.compile(r"^longitudinal(?:_(\d+))?$")
|
||||
# 노선 전장에 한 장씩만 나오는 도면 — 라우터가 원본 자료를 따로 실어 넘긴다.
|
||||
MASS_HAUL_ID = "mass_haul"
|
||||
WATERSHED_ID = "watershed"
|
||||
COVER_ID = "cover"
|
||||
|
||||
# ⚠ 빈-도각 목록이 여기 있었다 — 계획평면도·표준 횡단면도·용지도·표준도 일곱 줄.
|
||||
|
||||
@@ -19,16 +19,18 @@ from shapely.validation import make_valid
|
||||
|
||||
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 PARCEL_FONT
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import _clip_rings, _geojson_features
|
||||
|
||||
LANDUSE_PIECES = ("parcel", "parcel_info", "emd", "sgg")
|
||||
# 필지 정보 상자 밑에서 잘라 낼 선 조각 — 배경이 글을 가리지 않게(109-4).
|
||||
LABEL_CLIPPED = ("contour", "stream", "parcel", "emd", "sgg")
|
||||
|
||||
# B04 가 내려받아 저장하는 지적·행정구역 GeoJSON (전부 WGS84).
|
||||
PARCEL_FILE = "연속지적도_bounds.geojson"
|
||||
EMD_FILE = "행정구역_읍면동_bounds.geojson"
|
||||
SGG_FILE = "행정구역_시군구_bounds.geojson"
|
||||
|
||||
PARCEL_FONT = 1.8 # 필지 정보 글 크기(종이 mm)
|
||||
ROUTE_CLEAR_M = 5.0 # 라벨이 비켜 갈 노선 버퍼(중심선에서 m)
|
||||
_GAP_M = 2.0 # 지시선 끝과 상자 사이 틈(m)
|
||||
_RINGS = 5 # 후보 자리 고리 수
|
||||
@@ -187,6 +189,17 @@ def place_labels(
|
||||
return items
|
||||
|
||||
|
||||
def label_rects(items: list[dict[str, Any]]) -> list[tuple[float, float, float, float]]:
|
||||
"""필지 정보 상자 자리(사업지 m) — 그리는 상자(`_balloon_entities`)와 같은 크기."""
|
||||
rects = []
|
||||
for item in items:
|
||||
width_mm, height_mm = balloon_size(item["lines"], PARCEL_FONT)
|
||||
half_w, half_h = width_mm / MM / 2.0, height_mm / MM / 2.0
|
||||
cx, cy = item["center"]
|
||||
rects.append((cx - half_w, cy - half_h, cx + half_w, cy + half_h))
|
||||
return rects
|
||||
|
||||
|
||||
def landuse_pieces(
|
||||
project_root: Path,
|
||||
crs: str,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""B07 계획평면도 종류 · 정보 조각 파일 (PLAN 60장).
|
||||
|
||||
저장 자리(프로젝트 저장소 `B07_DesignDetail/`):
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order} — M02 양식 층처럼 파일 하나씩
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout} — 파일 하나씩
|
||||
plan_pieces/{장}/{조각}.json 정보 조각 하나 = 그 장 범위의 기하(사업지 좌표 m)
|
||||
plan_marks.json 사용자가 정한 대피소 · 사토장 자리
|
||||
|
||||
`plan_kinds/` 가 없으면 기본 넷(지형 · 노선배치도 · 대피소 · 라이다)을 쓴다. 처음 고칠 때
|
||||
기본 넷을 파일로 내려 두고 그 위에서 추가 · 업데이트 · 지운다 — 기본 넷도 고치고 지울 수 있다.
|
||||
`plan_kinds/` 가 없으면 기본 종류(지형 · 노선배치도 · 대피소 · 라이다 · 용지도 · 배수유역도)를
|
||||
쓴다. 양식(layout) = plan(1/1,200 장 나눔 · 조각 고름) · watershed(유역도 1/6,000 한 장 · 조각
|
||||
없음). 처음 고칠 때 기본 종류를 파일로 내려 두고 그 위에서 추가 · 업데이트 · 지운다 — 기본 종류도
|
||||
고치고 지울 수 있다.
|
||||
조각은 서버가 도면을 열 때마다 원자료(B05 노선 · 곡선표 · 구조물, B06 횡단 설계, B04 배경)
|
||||
에서 다시 계산해 파일로 쓰고, 도면은 그 파일을 읽어 고른 조각만 얹는다.
|
||||
"""
|
||||
@@ -24,7 +26,7 @@ from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design, curve_widening_args
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import infer_station_interval, station_no_label
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import ip_table_piece
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import clip_lines, ip_table_piece
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
_structure_label,
|
||||
plan_area_mm,
|
||||
@@ -47,6 +49,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import (
|
||||
_sheet_box,
|
||||
map_background,
|
||||
plan_stations,
|
||||
watershed_source,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
_design_root,
|
||||
@@ -55,7 +58,9 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
|
||||
_write_manifest,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import (
|
||||
LABEL_CLIPPED,
|
||||
LANDUSE_PIECES,
|
||||
label_rects,
|
||||
landuse_pieces,
|
||||
)
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
@@ -65,6 +70,8 @@ from config.config_system import DRAWING_SCALE_PLAN
|
||||
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+))?$")
|
||||
_NAME_MAX = 40
|
||||
WATERSHED_LAYOUT = "watershed"
|
||||
_LAYOUTS = ("plan", WATERSHED_LAYOUT)
|
||||
_REFUGE_TYPES = {"refuge": "대피소", "spoil_bank": "사토장"}
|
||||
_REFUGE_OFFSET_M = 8.0 # 대피소 · 사토장 라벨을 중심선에서 띄우는 거리(좌측)
|
||||
|
||||
@@ -80,16 +87,24 @@ def _kinds_dir(project_root: Path) -> Path:
|
||||
return _design_root(project_root) / "plan_kinds"
|
||||
|
||||
|
||||
# 이관 표시 — 있으면 용지도 종류를 이미 더해 준 폴더(지운 뒤 되살리지 않음).
|
||||
_LANDUSE_MARK = "_landuse_added"
|
||||
# 이관 표시 `_{id}_added` — 있으면 그 기본 종류를 이미 더해 준 폴더(지운 뒤 되살리지 않음).
|
||||
_ADDED_ONCE = ("landuse", "watershed")
|
||||
|
||||
|
||||
def _add_landuse_once(folder: Path) -> None:
|
||||
"""93장 앞에 종류 파일을 내린 프로젝트 — 기본 종류 「용지도」를 한 번 더한다."""
|
||||
mark = folder / _LANDUSE_MARK
|
||||
if mark.exists():
|
||||
def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
|
||||
return {
|
||||
"name": kind["name"],
|
||||
"pieces": list(kind["pieces"]),
|
||||
"order": order,
|
||||
"layout": kind.get("layout", "plan"),
|
||||
}
|
||||
|
||||
|
||||
def _add_defaults_once(folder: Path) -> None:
|
||||
"""기본 종류가 늘기 전에 종류 파일을 내린 프로젝트 — 늘어난 기본 종류를 한 번 더한다."""
|
||||
pending = [kind_id for kind_id in _ADDED_ONCE if not (folder / f"_{kind_id}_added").exists()]
|
||||
if not pending:
|
||||
return
|
||||
landuse = next(kind for kind in DEFAULT_KINDS if kind["id"] == "landuse")
|
||||
documents = []
|
||||
for path in folder.glob("*.json"):
|
||||
try:
|
||||
@@ -97,21 +112,23 @@ def _add_landuse_once(folder: Path) -> None:
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
names = {str(document.get("name")) for document in documents}
|
||||
if not (folder / "landuse.json").exists() and landuse["name"] not in names:
|
||||
order = max((int(document.get("order") or 0) for document in documents), default=-1) + 1
|
||||
atomic_write_json(
|
||||
folder / "landuse.json",
|
||||
{"name": landuse["name"], "pieces": list(landuse["pieces"]), "order": order},
|
||||
)
|
||||
mark.touch()
|
||||
order = max((int(document.get("order") or 0) for document in documents), default=-1) + 1
|
||||
for kind_id in pending:
|
||||
kind = next(kind for kind in DEFAULT_KINDS if kind["id"] == kind_id)
|
||||
if not (folder / f"{kind_id}.json").exists() and kind["name"] not in names:
|
||||
atomic_write_json(folder / f"{kind_id}.json", _document(kind, order))
|
||||
order += 1
|
||||
(folder / f"_{kind_id}_added").touch()
|
||||
|
||||
|
||||
def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""종류 목록 [{id, name, pieces}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
"""종류 목록 [{id, name, pieces, layout}] — 차례대로. 파일이 없으면 기본 종류."""
|
||||
folder = _kinds_dir(project_root)
|
||||
if not folder.is_dir():
|
||||
return [{**kind, "pieces": list(kind["pieces"])} for kind in DEFAULT_KINDS]
|
||||
_add_landuse_once(folder)
|
||||
return [
|
||||
{"layout": "plan", **kind, "pieces": list(kind["pieces"])} for kind in DEFAULT_KINDS
|
||||
]
|
||||
_add_defaults_once(folder)
|
||||
rows: list[tuple[int, str, dict[str, Any]]] = []
|
||||
for path in folder.glob("*.json"):
|
||||
if not KIND_ID.fullmatch(path.stem):
|
||||
@@ -123,67 +140,76 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
stored = set()
|
||||
for piece in document.get("pieces") or []:
|
||||
stored.update(LEGACY_PIECES.get(piece, (piece,)))
|
||||
pieces = [piece for piece in PIECE_IDS if piece in stored]
|
||||
layout = document.get("layout") if document.get("layout") in _LAYOUTS else "plan"
|
||||
pieces = [] if layout == WATERSHED_LAYOUT else [p for p in PIECE_IDS if p in stored]
|
||||
rows.append(
|
||||
(
|
||||
int(document.get("order") or 0),
|
||||
path.stem,
|
||||
{"id": path.stem, "name": str(document.get("name") or path.stem), "pieces": pieces},
|
||||
{
|
||||
"id": path.stem,
|
||||
"name": str(document.get("name") or path.stem),
|
||||
"pieces": pieces,
|
||||
"layout": layout,
|
||||
},
|
||||
)
|
||||
)
|
||||
return [row for _order, _id, row in sorted(rows, key=lambda item: item[:2])]
|
||||
|
||||
|
||||
def _materialize(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""기본 넷을 파일로 내려 둔다(이미 있으면 그대로) — 고치기 전에 한 번."""
|
||||
"""기본 종류를 파일로 내려 둔다(이미 있으면 그대로) — 고치기 전에 한 번."""
|
||||
kinds = list_kinds(project_root)
|
||||
folder = _kinds_dir(project_root)
|
||||
if not folder.is_dir():
|
||||
for order, kind in enumerate(kinds):
|
||||
atomic_write_json(
|
||||
folder / f"{kind['id']}.json",
|
||||
{"name": kind["name"], "pieces": kind["pieces"], "order": order},
|
||||
)
|
||||
(folder / _LANDUSE_MARK).touch()
|
||||
atomic_write_json(folder / f"{kind['id']}.json", _document(kind, order))
|
||||
for kind_id in _ADDED_ONCE:
|
||||
(folder / f"_{kind_id}_added").touch()
|
||||
return kinds
|
||||
|
||||
|
||||
def _checked(name: str, pieces: list[str]) -> tuple[str, list[str]]:
|
||||
def _checked(name: str, pieces: list[str], layout: str = "plan") -> tuple[str, list[str]]:
|
||||
text = str(name or "").strip()
|
||||
if not text or len(text) > _NAME_MAX:
|
||||
raise ValueError(f"종류 이름은 1~{_NAME_MAX}자로 적어 주세요.")
|
||||
if layout not in _LAYOUTS:
|
||||
raise ValueError("올바르지 않은 양식입니다.")
|
||||
if layout == WATERSHED_LAYOUT:
|
||||
return text, [] # 유역도 양식은 그림이 정해져 있다 — 조각을 고르지 않음
|
||||
chosen = [piece for piece in PIECE_IDS if piece in set(pieces or [])]
|
||||
if not chosen:
|
||||
raise ValueError("얹을 정보를 하나 이상 고르세요.")
|
||||
return text, chosen
|
||||
|
||||
|
||||
def add_kind(project_root: Path, name: str, pieces: list[str]) -> dict[str, Any]:
|
||||
text, chosen = _checked(name, pieces)
|
||||
def add_kind(
|
||||
project_root: Path, name: str, pieces: list[str], layout: str = "plan"
|
||||
) -> dict[str, Any]:
|
||||
text, chosen = _checked(name, pieces, layout)
|
||||
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]}"
|
||||
atomic_write_json(
|
||||
_kinds_dir(project_root) / f"{kind_id}.json",
|
||||
{"name": text, "pieces": chosen, "order": len(kinds)},
|
||||
)
|
||||
return {"id": kind_id, "name": text, "pieces": chosen}
|
||||
kind = {"id": f"k{uuid.uuid4().hex[:8]}", "name": text, "pieces": chosen, "layout": layout}
|
||||
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]:
|
||||
"""종류 이름 · 조각을 고친다(차례는 그대로). 그 종류의 확정본은 풀어 새 조각으로 다시 그린다."""
|
||||
"""종류 이름 · 조각을 고친다(차례 · 양식은 그대로). 그 종류의 확정본은 풀어 다시 그린다."""
|
||||
if not KIND_ID.fullmatch(kind_id):
|
||||
raise ValueError("올바르지 않은 종류 id 입니다.")
|
||||
text, chosen = _checked(name, pieces)
|
||||
kinds = _materialize(project_root)
|
||||
current = next((kind for kind in kinds if kind["id"] == kind_id), None)
|
||||
path = _kinds_dir(project_root) / f"{kind_id}.json"
|
||||
if current is None or not path.is_file():
|
||||
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
|
||||
text, chosen = _checked(name, pieces, current["layout"])
|
||||
if any(kind["name"] == text and kind["id"] != kind_id for kind in kinds):
|
||||
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
|
||||
path = _kinds_dir(project_root) / f"{kind_id}.json"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
|
||||
order = int(_read_json(path).get("order") or 0)
|
||||
atomic_write_json(path, {"name": text, "pieces": chosen, "order": order})
|
||||
kind = {"id": kind_id, "name": text, "pieces": chosen, "layout": current["layout"]}
|
||||
atomic_write_json(path, _document(kind, order))
|
||||
manifest = _read_manifest(project_root)
|
||||
released = False
|
||||
for drawing_id, entry in manifest["drawings"].items():
|
||||
@@ -193,7 +219,7 @@ def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str])
|
||||
released = True
|
||||
if released:
|
||||
_write_manifest(project_root, manifest)
|
||||
return {"id": kind_id, "name": text, "pieces": chosen}
|
||||
return kind
|
||||
|
||||
|
||||
def delete_kind(project_root: Path, kind_id: str) -> None:
|
||||
@@ -449,6 +475,8 @@ def build_pieces(
|
||||
pieces.update(
|
||||
landuse_pieces(project_root, context.crs, box, [(x, y) for _c, x, y in route])
|
||||
)
|
||||
if "parcel_info" in wanted:
|
||||
clip_lines(pieces, label_rects(pieces["parcel_info"]["items"]), LABEL_CLIPPED)
|
||||
if "ip_table" in wanted:
|
||||
pieces["ip_table"] = ip_table_piece(
|
||||
pieces, _load_design_curves(project_root), route, box, chunk, total
|
||||
@@ -477,6 +505,9 @@ def plan_kind_source(
|
||||
)
|
||||
if kind is None:
|
||||
raise FileNotFoundError("계획평면도 종류를 찾을 수 없습니다.")
|
||||
if kind["layout"] == WATERSHED_LAYOUT:
|
||||
# 유역도 양식 — B04 배수유역 산출물 + 도엽 배경(사업지 CRS) 한 장.
|
||||
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"]
|
||||
@@ -494,6 +525,16 @@ def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[di
|
||||
sheets = plan_chunks(plan_stations(longitudinal))
|
||||
items = []
|
||||
for kind in list_kinds(project_root):
|
||||
if kind["layout"] == WATERSHED_LAYOUT: # 1/6,000 한 장 — 장 나눔 없음
|
||||
items.append(
|
||||
{
|
||||
"id": f"plan_{kind['id']}",
|
||||
"label": kind["name"],
|
||||
"plan_kind": kind["id"],
|
||||
"plan_kind_name": kind["name"],
|
||||
}
|
||||
)
|
||||
continue
|
||||
title = plan_title(kind["name"])
|
||||
for chunk in sheets:
|
||||
single = len(sheets) <= 1
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DRAWING_GROUPS: readonly {
|
||||
idPrefix?: string;
|
||||
}[] = [
|
||||
{ label: "표지", kind: "cover" },
|
||||
// 종류(지형 · 노선배치도 · 대피소 · 라이다 · 용지도 · 사용자 추가) + 배수유역도를 한 묶음으로.
|
||||
// 종류(지형 · 노선배치도 · 대피소 · 라이다 · 용지도 · 배수유역도 · 사용자 추가)를 한 묶음으로.
|
||||
{ label: "평면도", kind: "plan" },
|
||||
{ label: "종단도", kind: "longitudinal" },
|
||||
// 표준 횡단면도는 횡단도 묶음 맨 위 한 줄(93-5).
|
||||
@@ -89,7 +89,7 @@ export function buildDrawingSidePanel(
|
||||
for (const group of DRAWING_GROUPS) {
|
||||
if (group.kind === "plan") {
|
||||
// 종류를 다 지워도 묶음은 남긴다 — 그 안에서 다시 더한다.
|
||||
const plans = drawings.filter((item) => item.kind === "plan" || item.kind === "watershed");
|
||||
const plans = drawings.filter((item) => item.kind === "plan");
|
||||
panel.append(buildPlanSection(plans, drawingButton, plan));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* 사용자 추가)별 도면 단추, 종류 지우기 · 추가 · 업데이트, 대피소 · 사토장 자리 (PLAN 60 · 67장).
|
||||
*
|
||||
* 종류 = 정보 조각 고르기 목록이다. 고치면 서버 파일이 바뀌고 `onChanged` 로 화면을 다시 세운다.
|
||||
* 체크박스는 공용(`ui_template_checkbox`) · 측점 입력은 B05 구조물 폼과 같은 측점 NO + 거리 두 칸.
|
||||
* 드롭다운 · 체크박스는 공용(`createSelectField` · `ui_template_checkbox`) · 측점 입력은 B05 구조물 폼과 같은 측점 NO + 거리 두 칸.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { createSelectField, showToast } from "@ui/ui_template_elements";
|
||||
import { stationFields } from "../B05_Profile/B05_Profile_UI_Structures_Fields";
|
||||
import {
|
||||
addPlanKind,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
savePlanMarks,
|
||||
updatePlanKind,
|
||||
type DesignDrawingItem,
|
||||
type PlanKindLayout,
|
||||
type PlanKindsState,
|
||||
type PlanMark,
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
@@ -51,15 +52,31 @@ async function run(action: () => Promise<unknown>, controls: PlanKindsControls):
|
||||
}
|
||||
}
|
||||
|
||||
/** 종류 — 공통 항목. 종류를 고르면 든 조각이 체크로 보이고 [업데이트] · 「새 종류」면 [추가]. */
|
||||
/** 양식 — 평면도(조각 고름 · 1/1,200 장 나눔) · 유역도(1/6,000 한 장 · 그림 고정). */
|
||||
const LAYOUTS: { value: PlanKindLayout; text: string }[] = [
|
||||
{ value: "plan", text: "평면도" },
|
||||
{ value: "watershed", text: "유역도" },
|
||||
];
|
||||
|
||||
/** 종류 — 공통 항목. 종류를 고르면 든 조각이 체크로 보이고 [업데이트] · 「새 종류」면 양식 고르고 [추가]. */
|
||||
function kindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLElement {
|
||||
const form = el("div", "b07-plan-tools__block");
|
||||
form.append(el("strong", "", "종류"));
|
||||
const pick = el("select", "b07-plan-tools__kind");
|
||||
pick.append(
|
||||
...state.kinds.map((kind) => new Option(kind.name, kind.id)),
|
||||
new Option("새 종류", ""),
|
||||
);
|
||||
const pick = createSelectField({
|
||||
options: [
|
||||
...state.kinds.map((kind) => ({ value: kind.id, text: kind.name })),
|
||||
{ value: "", text: "새 종류" },
|
||||
],
|
||||
compact: true,
|
||||
ariaLabel: "종류",
|
||||
onChange: () => show(),
|
||||
});
|
||||
const layout = createSelectField({
|
||||
options: LAYOUTS,
|
||||
compact: true,
|
||||
ariaLabel: "양식",
|
||||
onChange: () => showPieces(),
|
||||
});
|
||||
const name = el("input", "b07-plan-tools__name");
|
||||
name.type = "text";
|
||||
name.placeholder = "종류 이름";
|
||||
@@ -69,27 +86,35 @@ function kindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLEleme
|
||||
pieceList.append(...boxes.map((box) => box.root));
|
||||
const action = el("button", "b07-plan-tools__button");
|
||||
action.type = "button";
|
||||
// 유역도 양식은 그림이 정해져 있어 얹을 정보를 고르지 않는다.
|
||||
const showPieces = (): void => {
|
||||
pieceList.hidden = layout.select.value === "watershed";
|
||||
};
|
||||
const show = (): void => {
|
||||
const kind = state.kinds.find((item) => item.id === pick.value);
|
||||
const kind = state.kinds.find((item) => item.id === pick.select.value);
|
||||
name.value = kind?.name ?? "";
|
||||
boxes.forEach(({ input }) => (input.checked = kind?.pieces.includes(input.value) ?? false));
|
||||
// 양식은 새로 만들 때만 고른다 — 있는 종류는 그 양식을 보이기만.
|
||||
layout.select.value = kind?.layout ?? "plan";
|
||||
layout.select.disabled = Boolean(kind);
|
||||
action.textContent = kind ? "업데이트" : "추가";
|
||||
showPieces();
|
||||
};
|
||||
pick.addEventListener("change", show);
|
||||
show();
|
||||
action.addEventListener("click", () => {
|
||||
const pieces = boxes.filter((box) => box.input.checked).map((box) => box.input.value);
|
||||
const kindId = pick.value;
|
||||
const kindId = pick.select.value;
|
||||
const label = name.value.trim();
|
||||
const chosen = layout.select.value as PlanKindLayout;
|
||||
void run(
|
||||
() =>
|
||||
kindId
|
||||
? updatePlanKind(controls.projectId, kindId, label, pieces)
|
||||
: addPlanKind(controls.projectId, label, pieces),
|
||||
: addPlanKind(controls.projectId, label, pieces, chosen),
|
||||
controls,
|
||||
);
|
||||
});
|
||||
form.append(pick, name, pieceList, action);
|
||||
form.append(pick.root, layout.root, name, pieceList, action);
|
||||
return form;
|
||||
}
|
||||
|
||||
@@ -173,8 +198,7 @@ export function buildPlanSection(
|
||||
makeButton(drawing, sheets.length > 1 ? `${name} ${index + 1}장` : name),
|
||||
);
|
||||
head.append(sheets.length > 1 ? el("span", "b07-plan-kind__name", name) : buttons[0]);
|
||||
// × 는 종류만 — 배수유역도는 묶음 안 고정 항목(지우면 서버가 404).
|
||||
if (controls && sheets[0].kind === "plan") {
|
||||
if (controls) {
|
||||
const remove = el("button", "b07-plan-tools__remove", "×");
|
||||
remove.type = "button";
|
||||
remove.title = `「${name}」 종류 지우기`;
|
||||
|
||||
@@ -279,7 +279,10 @@
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
}
|
||||
|
||||
.b07-plan-tools__kind,
|
||||
.b07-plan-tools__pieces[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b07-plan-tools__name {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
① 필지마다 상자 하나 · 빈 값은 빈 줄 · 쪼개진 필지는 큰 조각에 지시선
|
||||
② 구역이 한 필지 안 — 경계 없이 어느 필지인지 한 번
|
||||
③ 상자 · 지시선 끝이 노선 버퍼와 안 겹침 · 상자끼리 안 겹침 · 도면에서도 노선과 안 겹침
|
||||
④ 상자 밑 선(등고선 · 계류 · 필지 · 읍면동 · 시군구)은 잘려 상자 안 선 0 (109-4)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -11,6 +12,7 @@ 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_IpTable import clip_lines
|
||||
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 (
|
||||
@@ -18,9 +20,11 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
layer_id,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import (
|
||||
LABEL_CLIPPED,
|
||||
PARCEL_FONT,
|
||||
ROUTE_CLEAR_M,
|
||||
clip_parcels,
|
||||
label_rects,
|
||||
place_labels,
|
||||
)
|
||||
|
||||
@@ -161,3 +165,44 @@ def _polyline(entity: dict) -> LineString:
|
||||
c["shapeData"]["endPoint"] for c in children
|
||||
]
|
||||
return LineString([(p["x"], p["y"]) for p in points])
|
||||
|
||||
|
||||
def test_lines_under_label_boxes_are_cut(tmp_path: Path) -> None:
|
||||
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)
|
||||
]
|
||||
parcels = clip_parcels(_geojson(tmp_path, features), "EPSG:4326", BOX)
|
||||
route = [(0.0, 140.0), (130.0, 160.0), (260.0, 120.0), (400.0, 170.0)]
|
||||
items = place_labels(parcels, route, BOX)
|
||||
rects = label_rects(items)
|
||||
assert PARCEL_FONT == 3.6 and rects == [_rect(item) for item in items] # 그리는 상자와 같음
|
||||
# 도곽 전체를 촘촘히 가로지르는 배경 선 — 상자마다 반드시 지나감
|
||||
across = [[[0.0, y], [400.0, y]] for y in range(1, 300, 2)]
|
||||
pieces = {
|
||||
"contour": {"lines": [list(line) for line in across]},
|
||||
"stream": {"lines": [[[x, 0.0], [x, 300.0]] for x in range(1, 400, 2)]},
|
||||
"parcel": {"lines": [line for parcel in parcels for line in parcel["lines"]]},
|
||||
"emd": {"lines": [list(line) for line in across]},
|
||||
"sgg": {"lines": [list(line) for line in across]},
|
||||
"centerline": {"lines": [[list(p) for p in route]]},
|
||||
}
|
||||
clip_lines(pieces, rects, LABEL_CLIPPED)
|
||||
inner = [box_polygon(*rect).buffer(-1e-3) for rect in rects]
|
||||
for piece in LABEL_CLIPPED:
|
||||
for points in pieces[piece]["lines"]:
|
||||
line = LineString(points)
|
||||
assert all(line.intersection(area).length < 1e-6 for area in inner), piece
|
||||
# 상자 밖은 남음 · 노선은 그대로
|
||||
assert sum(LineString(p).length for p in pieces["contour"]["lines"]) > 0.5 * 150 * 400
|
||||
assert pieces["centerline"]["lines"] == [[list(p) for p in route]]
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 넷 포함) · 확정본 풀림 · 배수시설 기호
|
||||
⑥ IP 표(94장) — 예시 검산 · 도-분-초 올림 · 장마다 그 장 IP 한 번 · 표 밑 선 자름 · 층
|
||||
⑦ 용지도 종류(93장) — 기본 종류 · 옛 종류 폴더에 한 번 더함(지우면 안 살아남) · 용지 조각 층
|
||||
⑧ 배수유역도 종류(109장) — 양식 watershed · 한 장 목록 · 지우기 · 다시 만들기 · IP 표 두 줄 차례 ·
|
||||
용지도 상자 안 선 0
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -19,6 +21,7 @@ import pytest
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import (
|
||||
_place,
|
||||
curve_values,
|
||||
dms,
|
||||
ip_blocks,
|
||||
@@ -38,6 +41,7 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
add_kind,
|
||||
delete_kind,
|
||||
list_kinds,
|
||||
plan_kind_items,
|
||||
read_marks,
|
||||
slope_records,
|
||||
update_kind,
|
||||
@@ -139,6 +143,7 @@ def test_kinds_add_delete_persist(tmp_path: Path) -> None:
|
||||
"대피소",
|
||||
"라이다",
|
||||
"용지도",
|
||||
"배수유역도",
|
||||
]
|
||||
added = add_kind(root, "내 평면도", ["centerline", "station", "nope"])
|
||||
assert added["pieces"] == ["centerline", "station"]
|
||||
@@ -148,14 +153,28 @@ def test_kinds_add_delete_persist(tmp_path: Path) -> None:
|
||||
add_kind(root, "빈 것", [])
|
||||
delete_kind(root, "terrain") # 기본 넷도 지움
|
||||
again = list_kinds(root) # 다시 읽어 같음
|
||||
assert [k["name"] for k in again] == ["노선배치도", "대피소", "라이다", "용지도", "내 평면도"]
|
||||
assert [k["name"] for k in again] == [
|
||||
"노선배치도",
|
||||
"대피소",
|
||||
"라이다",
|
||||
"용지도",
|
||||
"배수유역도",
|
||||
"내 평면도",
|
||||
]
|
||||
assert again[-1]["pieces"] == ["centerline", "station"]
|
||||
with pytest.raises(FileNotFoundError):
|
||||
delete_kind(root, "terrain")
|
||||
for kind in list(again):
|
||||
delete_kind(root, kind["id"])
|
||||
assert list_kinds(root) == [] # 다 지우면 비어 있음(기본 넷이 되살아나지 않음)
|
||||
assert {k["id"] for k in DEFAULT_KINDS} == {"terrain", "route", "layout", "lidar", "landuse"}
|
||||
assert {k["id"] for k in DEFAULT_KINDS} == {
|
||||
"terrain",
|
||||
"route",
|
||||
"layout",
|
||||
"lidar",
|
||||
"landuse",
|
||||
"watershed",
|
||||
}
|
||||
|
||||
|
||||
def test_refuge_marks_same_spot(tmp_path: Path) -> None:
|
||||
@@ -202,7 +221,7 @@ def test_default_kinds_keep_old_picture_and_legacy_file(tmp_path: Path) -> None:
|
||||
outline = {"road_edge", "fill_outline", "cut_outline"}
|
||||
# 옛 slope_outline 자리를 나눈 셋이 그대로 채움 · 배수시설은 옛 그림에 없음(용지도는 따로)
|
||||
for kind in DEFAULT_KINDS:
|
||||
if kind["id"] != "landuse":
|
||||
if kind["id"] not in ("landuse", "watershed"):
|
||||
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
@@ -227,6 +246,7 @@ def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
|
||||
"id": "terrain",
|
||||
"name": "지형2",
|
||||
"pieces": ["contour", "centerline"],
|
||||
"layout": "plan",
|
||||
}
|
||||
assert [k["id"] for k in list_kinds(root)][0] == "terrain" # 차례 그대로
|
||||
drawings = _read_manifest(root)["drawings"]
|
||||
@@ -315,9 +335,9 @@ def test_landuse_kind_added_once_to_old_folder(tmp_path: Path) -> None:
|
||||
json.dumps({"name": kind["name"], "pieces": kind["pieces"], "order": order}), "utf-8"
|
||||
)
|
||||
kinds = list_kinds(root)
|
||||
assert [k["id"] for k in kinds][-1] == "landuse"
|
||||
assert kinds[-1]["pieces"] == ["contour", "centerline", "parcel", "parcel_info", "emd", "sgg"]
|
||||
assert len(list_kinds(root)) == 5 # 두 번 읽어도 한 번
|
||||
assert [k["id"] for k in kinds][-2:] == ["landuse", "watershed"] # 늘어난 기본 종류 둘
|
||||
assert kinds[-2]["pieces"] == ["contour", "centerline", "parcel", "parcel_info", "emd", "sgg"]
|
||||
assert len(list_kinds(root)) == 6 # 두 번 읽어도 한 번
|
||||
delete_kind(root, "landuse")
|
||||
assert "landuse" not in [k["id"] for k in list_kinds(root)] # 지운 것은 안 살아남
|
||||
|
||||
@@ -344,3 +364,79 @@ def test_landuse_pieces_become_layers() -> None:
|
||||
if e.get("layerId") == layer_id("parcel_info") and e["type"] == "Text"
|
||||
]
|
||||
assert labels == ["1-1", "임야", "국"]
|
||||
|
||||
|
||||
def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
|
||||
"""배수유역도 = 기본 종류 · 양식 watershed · 조각 없음 · 장이 여럿이어도 목록은 한 장."""
|
||||
root = tmp_path / "project"
|
||||
watershed = next(k for k in list_kinds(root) if k["id"] == "watershed")
|
||||
assert watershed == {
|
||||
"id": "watershed",
|
||||
"name": "배수유역도",
|
||||
"pieces": [],
|
||||
"layout": "watershed",
|
||||
}
|
||||
# 측점 0 ~ 2,000 m — 1/1,200 평면도는 여러 장 · 유역도는 한 장
|
||||
stations = [
|
||||
{"chainage_m": float(c), "center_x": 0.0, "center_y": float(c)} for c in range(0, 2001, 20)
|
||||
]
|
||||
items = plan_kind_items(root, {"stations": stations})
|
||||
terrain = [item for item in items if item["plan_kind"] == "terrain"]
|
||||
mine = [item for item in items if item["plan_kind"] == "watershed"]
|
||||
assert len(terrain) > 1
|
||||
assert mine == [
|
||||
{
|
||||
"id": "plan_watershed",
|
||||
"label": "배수유역도",
|
||||
"plan_kind": "watershed",
|
||||
"plan_kind_name": "배수유역도",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_watershed_kind_delete_and_add_again(tmp_path: Path) -> None:
|
||||
"""× 로 지우면 안 살아남 · 「새 종류」 양식 유역도로 다시 만듦 · 업데이트는 양식 그대로."""
|
||||
root = tmp_path / "project"
|
||||
delete_kind(root, "watershed")
|
||||
assert "watershed" not in [k["id"] for k in list_kinds(root)]
|
||||
assert "watershed" not in [k["id"] for k in list_kinds(root)] # 다시 읽어도 안 살아남
|
||||
added = add_kind(root, "배수유역도", ["contour"], "watershed") # 조각은 버림
|
||||
assert added["layout"] == "watershed" and added["pieces"] == []
|
||||
again = next(k for k in list_kinds(root) if k["id"] == added["id"])
|
||||
assert again == added
|
||||
assert any(
|
||||
item["id"] == f"plan_{added['id']}" for item in plan_kind_items(root, {"stations": []})
|
||||
)
|
||||
update_kind(root, added["id"], "유역도2", []) # 유역도 양식은 조각 없이 업데이트
|
||||
assert next(k for k in list_kinds(root) if k["id"] == added["id"])["layout"] == "watershed"
|
||||
with pytest.raises(ValueError):
|
||||
add_kind(root, "이상한 양식", ["contour"], "nope")
|
||||
|
||||
|
||||
def test_watershed_kind_added_once_to_folder_with_landuse(tmp_path: Path) -> None:
|
||||
"""용지도 표시가 있는 옛 폴더 — 배수유역도만 한 번 더해짐 · 지우면 안 살아남."""
|
||||
root = tmp_path / "project"
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
for order, kind in enumerate(DEFAULT_KINDS[:5]):
|
||||
(folder / f"{kind['id']}.json").write_text(
|
||||
json.dumps({"name": kind["name"], "pieces": kind["pieces"], "order": order}), "utf-8"
|
||||
)
|
||||
(folder / "_landuse_added").touch()
|
||||
assert [k["id"] for k in list_kinds(root)][-1] == "watershed"
|
||||
delete_kind(root, "watershed")
|
||||
assert [k["id"] for k in list_kinds(root)][-1] == "landuse"
|
||||
|
||||
|
||||
def test_ip_table_two_lines_left_to_right_top_to_bottom() -> None:
|
||||
"""한 줄 셋까지 들어가는 폭 · 묶음 넷 — 첫 줄(1 · 2 · 3) 맨 위 왼 → 오 · 넷째는 아랫줄 왼쪽."""
|
||||
blocks = [{"name": str(i), "ia": 1.0} for i in range(1, 5)]
|
||||
block_w = 7.0 + 6.0 + 23.0
|
||||
_place(blocks, 2 * 5.0 + 3 * block_w + 1.0)
|
||||
origins = [b["origin"] for b in blocks]
|
||||
tops = [o[1] for o in origins]
|
||||
assert tops[0] == tops[1] == tops[2] > tops[3] # 첫 줄이 위
|
||||
assert origins[0][0] < origins[1][0] < origins[2][0] # 왼 → 오
|
||||
assert origins[3][0] == origins[0][0] # 다음 줄은 다시 왼쪽
|
||||
assert tops[3] == 5.0 + 8 * 5.0 # 마지막 줄이 도곽 왼쪽 아래 기준(옛 한 줄 자리와 같음)
|
||||
assert tops[0] - tops[3] == 8 * 5.0 + 3.0 # 줄 간격 그대로
|
||||
|
||||
Reference in New Issue
Block a user