feat(512-1 B07 유역도 등고선 간격): 유역도 등고선 5 m 기본 · 평면도 관리 모달에서 10 m 고름(1 m 없음 · B05 · B06 전용) — 요구 326
- 원인 확인: 유역도 23 MB = 1 m 아님 · 도엽 N3L 5 m 주곡선 그대로(등고선 층이 응답 대부분) - 유역도 종류 파일 contour_interval_m(5 · 10) · 배경선 = 간격 배수 높이만 · 10 m = 23.17 → 11.91 MB - 지문: 종류 목록 지문에서 간격 뺌 → 간격 바꾸면 유역도만 다시 받음 · 기본 종류 조각 차례 맞춤(파일로 내려도 지문 그대로) - 시험: test_512_1_b07_basin_contour 새로 · test_b07_plan_kinds · helper_130 새 칸 · test_cad_host_tabs 508-1f 뒤 ☰ 자리 단언 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -279,6 +279,8 @@ export interface PlanKind {
|
||||
layout: PlanKindLayout;
|
||||
/** 축척 분모 — 평면도 양식은 `scales` 중 하나(기본 1,200) · 유역도는 6,000 고정. */
|
||||
scale: number;
|
||||
/** 유역도 양식만 — 등고선 간격(m · `contour_intervals` 중 하나 · 기본 5). */
|
||||
contour_interval_m?: number;
|
||||
}
|
||||
export type PlanKindLayout = "plan" | "watershed";
|
||||
export interface PlanMark {
|
||||
@@ -292,6 +294,9 @@ export interface PlanKindsState {
|
||||
scales: number[];
|
||||
default_scale: number;
|
||||
watershed_scale: number;
|
||||
/** 유역도 등고선 간격(m) 고름(서버 `BASIN_CONTOUR_INTERVAL_CHOICES`) · 기본. */
|
||||
contour_intervals: number[];
|
||||
default_contour_interval: number;
|
||||
kinds: PlanKind[];
|
||||
marks: PlanMark[];
|
||||
/** 종단 측점 간격(m) — 측점 NO + 거리 입력용(GET 만 · 종단이 없으면 null). */
|
||||
@@ -308,10 +313,11 @@ export function addPlanKind(
|
||||
pieces: string[],
|
||||
layout: PlanKindLayout = "plan",
|
||||
scale?: number,
|
||||
contourIntervalM?: number,
|
||||
): Promise<PlanKindsState> {
|
||||
return requestJson(`/projects/${projectId}/plan-kinds`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name, pieces, layout, scale }),
|
||||
body: JSON.stringify({ name, pieces, layout, scale, contour_interval_m: contourIntervalM }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,10 +327,11 @@ export function updatePlanKind(
|
||||
name: string,
|
||||
pieces: string[],
|
||||
scale?: number,
|
||||
contourIntervalM?: number,
|
||||
): Promise<PlanKindsState> {
|
||||
return requestJson(`/projects/${projectId}/plan-kinds/${encodeURIComponent(kindId)}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ name, pieces, scale }),
|
||||
body: JSON.stringify({ name, pieces, scale, contour_interval_m: contourIntervalM }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
write_marks,
|
||||
)
|
||||
from config.config_system import (
|
||||
BASIN_CONTOUR_INTERVAL_CHOICES,
|
||||
BASIN_CONTOUR_INTERVAL_M,
|
||||
DRAWING_SCALE_BASIN,
|
||||
DRAWING_SCALE_PLAN,
|
||||
DRAWING_SCALE_PLAN_CHOICES,
|
||||
@@ -44,6 +46,8 @@ class PlanKindRequest(BaseModel):
|
||||
layout: str = "plan"
|
||||
# 축척 분모(`scales` 중 하나) — 없으면 추가는 1/1,200 · 업데이트는 지금 값 · 유역도 1/6,000.
|
||||
scale: int | None = None
|
||||
# 유역도 등고선 간격(m · `contour_intervals` 중 하나) — 없으면 추가는 5 m · 업데이트는 지금 값.
|
||||
contour_interval_m: int | None = None
|
||||
|
||||
|
||||
class PlanKindsOrderRequest(BaseModel):
|
||||
@@ -67,6 +71,8 @@ def _state(root: Any) -> dict[str, Any]:
|
||||
"scales": list(DRAWING_SCALE_PLAN_CHOICES),
|
||||
"default_scale": DRAWING_SCALE_PLAN,
|
||||
"watershed_scale": DRAWING_SCALE_BASIN,
|
||||
"contour_intervals": list(BASIN_CONTOUR_INTERVAL_CHOICES),
|
||||
"default_contour_interval": BASIN_CONTOUR_INTERVAL_M,
|
||||
"kinds": list_kinds(root),
|
||||
"marks": read_marks(root),
|
||||
}
|
||||
@@ -100,7 +106,13 @@ 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, request.layout, request.scale
|
||||
add_kind,
|
||||
root,
|
||||
request.name,
|
||||
request.pieces,
|
||||
request.layout,
|
||||
request.scale,
|
||||
request.contour_interval_m,
|
||||
)
|
||||
return await asyncio.to_thread(_state, root)
|
||||
except ValueError as exc:
|
||||
@@ -120,11 +132,17 @@ async def put_plan_kinds_order(project_id: UUID, request: PlanKindsOrderRequest)
|
||||
|
||||
@router.put("/{project_id}/plan-kinds/{kind_id}")
|
||||
async def put_plan_kind(project_id: UUID, kind_id: str, request: PlanKindRequest) -> Any:
|
||||
"""종류 업데이트 — 이름 · 조각을 고친다(기본 넷도)."""
|
||||
"""종류 업데이트 — 이름 · 조각 · 축척 · 유역도 등고선 간격을 고친다(기본 넷도)."""
|
||||
try:
|
||||
root = await _project_root(project_id)
|
||||
await asyncio.to_thread(
|
||||
update_kind, root, kind_id, request.name, request.pieces, request.scale
|
||||
update_kind,
|
||||
root,
|
||||
kind_id,
|
||||
request.name,
|
||||
request.pieces,
|
||||
request.scale,
|
||||
request.contour_interval_m,
|
||||
)
|
||||
return await asyncio.to_thread(_state, root)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -25,7 +25,11 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
|
||||
plan_drawing_label,
|
||||
)
|
||||
from common_util.common_util_drainage_pipes import detail_basins_path
|
||||
from config.config_system import DRAWING_SCALE_BASIN, DRAWING_SCALE_PLAN
|
||||
from config.config_system import (
|
||||
BASIN_CONTOUR_INTERVAL_M,
|
||||
DRAWING_SCALE_BASIN,
|
||||
DRAWING_SCALE_PLAN,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -188,7 +192,7 @@ def _too_far_from_route(
|
||||
@lru_cache(maxsize=8)
|
||||
def _metric_lines_cached(
|
||||
path_str: str, mtime_ns: int, crs: str
|
||||
) -> tuple[tuple[np.ndarray, ...], np.ndarray]:
|
||||
) -> tuple[tuple[np.ndarray, ...], np.ndarray, np.ndarray]:
|
||||
"""도엽 GeoJSON 한 벌을 사업지 좌표계(m) 선 목록으로 돌려 **캐시**한다.
|
||||
|
||||
같은 배경을 유역도와 계획평면도가 나눠 쓴다 — 도면마다 다시 읽고 다시 투영하면
|
||||
@@ -197,31 +201,44 @@ def _metric_lines_cached(
|
||||
|
||||
선은 (n, 2) 실수 배열로 쥔다 — 파이썬 튜플이면 등고선 한 벌이 수백 MB 를 붙든다.
|
||||
둘째 값은 선마다 (min_x, min_y, max_x, max_y) — 도곽 밖 선을 자르기 전에 거른다.
|
||||
셋째 값은 선마다 높이(`등고수치` · 없으면 nan) — 유역도 등고선 간격 거르기.
|
||||
"""
|
||||
to_metric = Transformer.from_crs("EPSG:4326", crs, always_xy=True)
|
||||
lines: list[np.ndarray] = []
|
||||
levels: list[float] = []
|
||||
for feature in _geojson_features(Path(path_str)):
|
||||
level = (feature.get("properties") or {}).get("등고수치")
|
||||
for part in _geometry_lines(feature.get("geometry")):
|
||||
if len(part) < 2:
|
||||
continue
|
||||
lonlat = np.array([(point[0], point[1]) for point in part], dtype=float)
|
||||
x, y = to_metric.transform(lonlat[:, 0], lonlat[:, 1])
|
||||
lines.append(np.column_stack((x, y)))
|
||||
levels.append(float(level) if isinstance(level, (int, float)) else math.nan)
|
||||
bounds = np.array(
|
||||
[(*line.min(axis=0), *line.max(axis=0)) for line in lines], dtype=float
|
||||
).reshape(-1, 4)
|
||||
return tuple(lines), bounds
|
||||
return tuple(lines), bounds, np.array(levels, dtype=float)
|
||||
|
||||
|
||||
def _metric_lines(
|
||||
path: Path, crs: str, box: tuple[float, float, float, float] | None = None
|
||||
path: Path,
|
||||
crs: str,
|
||||
box: tuple[float, float, float, float] | None = None,
|
||||
interval_m: float | None = None,
|
||||
) -> list[list[tuple[float, float]]]:
|
||||
"""캐시된 배경 선 중 box 에 걸치는 것만 낸다. 파일이 없으면 빈 목록."""
|
||||
"""캐시된 배경 선 중 box 에 걸치는 것만 낸다. 파일이 없으면 빈 목록.
|
||||
|
||||
interval_m = 그 간격의 배수 높이 선만(높이 없는 선은 남김) · None = 전부.
|
||||
"""
|
||||
if not path.is_file():
|
||||
return []
|
||||
lines, bounds = _metric_lines_cached(str(path), path.stat().st_mtime_ns, crs)
|
||||
lines, bounds, levels = _metric_lines_cached(str(path), path.stat().st_mtime_ns, crs)
|
||||
keep = np.ones(len(lines), dtype=bool)
|
||||
if interval_m:
|
||||
keep = np.isnan(levels) | np.isclose(np.mod(levels, interval_m), 0.0)
|
||||
if box is None:
|
||||
picked = range(len(lines))
|
||||
picked = np.flatnonzero(keep)
|
||||
else:
|
||||
min_x, min_y, max_x, max_y = box
|
||||
hit = (
|
||||
@@ -230,7 +247,7 @@ def _metric_lines(
|
||||
& (bounds[:, 3] >= min_y)
|
||||
& (bounds[:, 1] <= max_y)
|
||||
)
|
||||
picked = np.flatnonzero(hit)
|
||||
picked = np.flatnonzero(hit & keep)
|
||||
return [list(map(tuple, lines[index].tolist())) for index in picked]
|
||||
|
||||
|
||||
@@ -240,6 +257,7 @@ def map_background(
|
||||
scale: int,
|
||||
area_mm: tuple[float, float],
|
||||
extent_points: list[tuple[float, float]],
|
||||
contour_interval_m: float | None = None,
|
||||
) -> dict[str, list[list[tuple[float, float]]]]:
|
||||
"""도엽 등고선·세류선을 사업지 좌표계로 읽어 **그 도면의 도곽 크기로 절취**한다.
|
||||
|
||||
@@ -248,6 +266,7 @@ def map_background(
|
||||
|
||||
`extent_points` 는 그 도면의 주제(노선·유역 등) 좌표다. 도곽보다 크면 그쪽을
|
||||
우선한다 — 배경만 잘리고 주제는 다 보인다.
|
||||
`contour_interval_m` = 등고선을 그 간격 배수 높이만(유역도 5 · 10 m) · None = 도엽 그대로.
|
||||
"""
|
||||
area_w_mm, area_h_mm = area_mm
|
||||
half_w_m = area_w_mm / 2.0 * scale / 1000.0
|
||||
@@ -266,9 +285,12 @@ def map_background(
|
||||
|
||||
sheet_dir = Path(project_root) / "B04_PreProcess" / "processed"
|
||||
background: dict[str, list[list[tuple[float, float]]]] = {}
|
||||
for key, filename in (("contours", CONTOUR_FILE), ("streams", STREAM_FILE)):
|
||||
for key, filename, interval in (
|
||||
("contours", CONTOUR_FILE, contour_interval_m),
|
||||
("streams", STREAM_FILE, None),
|
||||
):
|
||||
lines: list[list[tuple[float, float]]] = []
|
||||
for line in _metric_lines(sheet_dir / filename, crs, box):
|
||||
for line in _metric_lines(sheet_dir / filename, crs, box, interval):
|
||||
lines.extend(clip_line_to_box(line, box))
|
||||
background[key] = lines
|
||||
return background
|
||||
@@ -490,9 +512,13 @@ def _hillshade_for_box(
|
||||
)
|
||||
|
||||
|
||||
def watershed_source(context: Any) -> dict[str, Any]:
|
||||
def watershed_source(
|
||||
context: Any, contour_interval_m: float = BASIN_CONTOUR_INTERVAL_M
|
||||
) -> dict[str, Any]:
|
||||
"""유역도 입력(노선·세부유역·등고선·세류선)을 사업지 CRS(m)로 모은다.
|
||||
|
||||
등고선 = 그 종류 간격(5 · 10 m) 배수 높이만 — 1 m 는 B05 · B06 전용.
|
||||
|
||||
저장본은 전부 WGS84라 여기서 미터 좌표로 되돌린다(B04가 저장할 때와 반대 방향).
|
||||
|
||||
되돌리는 좌표계가 둘이다. **배경(도엽 등고선·세류선)은 사업지 좌표계**로 돌린다 —
|
||||
@@ -513,6 +539,7 @@ def watershed_source(context: Any) -> dict[str, Any]:
|
||||
DRAWING_SCALE_BASIN,
|
||||
map_area_mm(),
|
||||
[*route_xy, *(point for basin in basins for point in basin["ring"])],
|
||||
contour_interval_m,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -138,12 +138,19 @@ def code_stamp() -> str:
|
||||
|
||||
|
||||
def _settings(project_root: Path) -> dict[str, Any]:
|
||||
"""B07 설정 파일(축척 · 장 배치 · 표식 · 평면도 종류) — manifest · 그린 결과 폴더는 뺌."""
|
||||
"""B07 설정 파일(축척 · 장 배치 · 표식 · 평면도 종류) — manifest · 그린 결과 폴더는 뺌.
|
||||
|
||||
평면도 종류 = 읽은 종류 목록(기본 종류를 파일로 내려도 같음) · 유역도 등고선 간격은 뺌 —
|
||||
그 도면만 바뀜(`_plan_extra` 의 종류 칸).
|
||||
"""
|
||||
root = _design_root(project_root)
|
||||
files = sorted(root.glob("*.json")) if root.is_dir() else []
|
||||
return {
|
||||
"files": {path.name: stamp(path) for path in files if path.name != "manifest.json"},
|
||||
"plan_kinds": tree_stamp(root / "plan_kinds"),
|
||||
"plan_kinds": [
|
||||
{key: value for key, value in kind.items() if key != "contour_interval_m"}
|
||||
for kind in list_kinds(project_root)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
저장 자리(프로젝트 저장소 `B07_DesignDetail/`):
|
||||
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout, scale} — 파일 하나씩
|
||||
(유역도 양식은 + contour_interval_m 등고선 간격 5 · 10 m)
|
||||
plan_pieces/{장}/{조각}.json 정보 조각 하나 = 그 장 범위의 기하(사업지 좌표 m)
|
||||
plan_marks.json 사용자가 정한 대피소 · 사토장 자리
|
||||
|
||||
@@ -73,6 +74,8 @@ 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 (
|
||||
BASIN_CONTOUR_INTERVAL_CHOICES,
|
||||
BASIN_CONTOUR_INTERVAL_M,
|
||||
DRAWING_SCALE_BASIN,
|
||||
DRAWING_SCALE_PLAN,
|
||||
DRAWING_SCALE_PLAN_CHOICES,
|
||||
@@ -109,6 +112,14 @@ def _scale(layout: str, value: Any) -> int:
|
||||
return value if value in DRAWING_SCALE_PLAN_CHOICES else DRAWING_SCALE_PLAN
|
||||
|
||||
|
||||
def _contour(layout: str, value: Any) -> dict[str, int]:
|
||||
"""유역도 양식만 등고선 간격(m) 칸 — 없거나 목록 밖이면 5 m(옛 파일)."""
|
||||
if layout != WATERSHED_LAYOUT:
|
||||
return {}
|
||||
chosen = value if value in BASIN_CONTOUR_INTERVAL_CHOICES else BASIN_CONTOUR_INTERVAL_M
|
||||
return {"contour_interval_m": chosen}
|
||||
|
||||
|
||||
def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
|
||||
layout = kind.get("layout", "plan")
|
||||
return {
|
||||
@@ -117,6 +128,7 @@ def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
|
||||
"order": order,
|
||||
"layout": layout,
|
||||
"scale": _scale(layout, kind.get("scale")),
|
||||
**_contour(layout, kind.get("contour_interval_m")),
|
||||
}
|
||||
|
||||
|
||||
@@ -149,8 +161,10 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
{
|
||||
"layout": "plan",
|
||||
**kind,
|
||||
"pieces": list(kind["pieces"]),
|
||||
# 조각 차례 = 파일로 내린 뒤와 같게(지문이 안 흔들림)
|
||||
"pieces": [piece for piece in PIECE_IDS if piece in kind["pieces"]],
|
||||
"scale": _scale(kind.get("layout", "plan"), None),
|
||||
**_contour(kind.get("layout", "plan"), None),
|
||||
}
|
||||
for kind in DEFAULT_KINDS
|
||||
]
|
||||
@@ -178,6 +192,7 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
|
||||
"pieces": pieces,
|
||||
"layout": layout,
|
||||
"scale": _scale(layout, document.get("scale")),
|
||||
**_contour(layout, document.get("contour_interval_m")),
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -221,15 +236,26 @@ def _checked_scale(layout: str, scale: int | None, current: int = DRAWING_SCALE_
|
||||
return scale
|
||||
|
||||
|
||||
def _checked_contour(contour: int | None, current: int = BASIN_CONTOUR_INTERVAL_M) -> int:
|
||||
"""고른 유역도 등고선 간격 — 없으면 지금 값 · 목록 밖이면 거절(1 m = B05 · B06 전용)."""
|
||||
if contour is None:
|
||||
return current
|
||||
if contour not in BASIN_CONTOUR_INTERVAL_CHOICES:
|
||||
raise ValueError("고를 수 없는 등고선 간격입니다.")
|
||||
return contour
|
||||
|
||||
|
||||
def add_kind(
|
||||
project_root: Path,
|
||||
name: str,
|
||||
pieces: list[str],
|
||||
layout: str = "plan",
|
||||
scale: int | None = None,
|
||||
contour: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
text, chosen = _checked(name, pieces, layout)
|
||||
chosen_scale = _checked_scale(layout, scale)
|
||||
chosen_contour = _checked_contour(contour)
|
||||
kinds = _materialize(project_root)
|
||||
if any(kind["name"] == text for kind in kinds):
|
||||
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
|
||||
@@ -239,15 +265,22 @@ def add_kind(
|
||||
"pieces": chosen,
|
||||
"layout": layout,
|
||||
"scale": chosen_scale,
|
||||
**_contour(layout, chosen_contour),
|
||||
}
|
||||
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], scale: int | None = None
|
||||
project_root: Path,
|
||||
kind_id: str,
|
||||
name: str,
|
||||
pieces: list[str],
|
||||
scale: int | None = None,
|
||||
contour: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""종류 이름 · 조각 · 축척을 고친다(차례 · 양식은 그대로). 그 종류 확정본은 풀어 다시 그린다."""
|
||||
"""종류 이름 · 조각 · 축척 · 유역도 등고선 간격을 고친다(차례 · 양식은 그대로).
|
||||
그 종류 확정본은 풀어 다시 그린다."""
|
||||
if not KIND_ID.fullmatch(kind_id):
|
||||
raise ValueError("올바르지 않은 종류 id 입니다.")
|
||||
kinds = _materialize(project_root)
|
||||
@@ -257,6 +290,9 @@ def update_kind(
|
||||
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
|
||||
text, chosen = _checked(name, pieces, current["layout"])
|
||||
chosen_scale = _checked_scale(current["layout"], scale, current["scale"])
|
||||
chosen_contour = _checked_contour(
|
||||
contour, current.get("contour_interval_m", BASIN_CONTOUR_INTERVAL_M)
|
||||
)
|
||||
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)
|
||||
@@ -266,6 +302,7 @@ def update_kind(
|
||||
"pieces": chosen,
|
||||
"layout": current["layout"],
|
||||
"scale": chosen_scale,
|
||||
**_contour(current["layout"], chosen_contour),
|
||||
}
|
||||
atomic_write_json(path, _document(kind, order))
|
||||
_release_kinds(project_root, {kind_id})
|
||||
@@ -609,7 +646,11 @@ def plan_kind_source(
|
||||
raise FileNotFoundError("계획평면도 종류를 찾을 수 없습니다.")
|
||||
if kind["layout"] == WATERSHED_LAYOUT:
|
||||
# 유역도 양식 — B04 배수유역 산출물 + 도엽 배경(사업지 CRS) 한 장.
|
||||
return {"layout": WATERSHED_LAYOUT, "label": kind["name"], **watershed_source(context)}
|
||||
return {
|
||||
"layout": WATERSHED_LAYOUT,
|
||||
"label": kind["name"],
|
||||
**watershed_source(context, kind["contour_interval_m"]),
|
||||
}
|
||||
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"], kind["scale"]
|
||||
|
||||
@@ -99,6 +99,15 @@ export function kindForm(
|
||||
// 축척 — 목록은 서버 `scales` 한 곳 · 유역도 양식은 1/6,000 고정(잠금).
|
||||
const scaleText = (value: number): string => `1/${value.toLocaleString("en-US")}`;
|
||||
const scale = createSelectField({ options: [], compact: true, ariaLabel: "축척" });
|
||||
// 등고선 간격 — 유역도 양식만(5 m 기본 · 10 m) · 1 m 는 B05 · B06 전용이라 없음.
|
||||
const contour = createSelectField({
|
||||
options: state.contour_intervals.map((value) => ({
|
||||
value: String(value),
|
||||
text: `등고선 ${value} m`,
|
||||
})),
|
||||
compact: true,
|
||||
ariaLabel: "등고선 간격",
|
||||
});
|
||||
const name = el("input", "b07-plan-tools__name");
|
||||
name.type = "text";
|
||||
name.placeholder = "종류 이름";
|
||||
@@ -128,6 +137,8 @@ export function kindForm(
|
||||
);
|
||||
}
|
||||
scale.select.disabled = watershed;
|
||||
contour.root.hidden = !watershed;
|
||||
contour.select.value = String(kind?.contour_interval_m ?? state.default_contour_interval);
|
||||
};
|
||||
const show = (): void => {
|
||||
const kind = state.kinds.find((item) => item.id === pick.select.value);
|
||||
@@ -155,17 +166,18 @@ export function kindForm(
|
||||
const label = name.value.trim();
|
||||
const chosen = layout.select.value as PlanKindLayout;
|
||||
const denominator = Number(scale.select.value);
|
||||
const interval = chosen === "watershed" ? Number(contour.select.value) : undefined;
|
||||
void run(
|
||||
() =>
|
||||
kindId
|
||||
? updatePlanKind(controls.projectId, kindId, label, pieces, denominator)
|
||||
: addPlanKind(controls.projectId, label, pieces, chosen, denominator),
|
||||
? updatePlanKind(controls.projectId, kindId, label, pieces, denominator, interval)
|
||||
: addPlanKind(controls.projectId, label, pieces, chosen, denominator, interval),
|
||||
controls,
|
||||
);
|
||||
});
|
||||
const actions = el("div", "b07-plan-tools__actions");
|
||||
actions.append(action, remove);
|
||||
form.append(pick.root, layout.root, scale.root, name, pieceList, actions);
|
||||
form.append(pick.root, layout.root, scale.root, contour.root, name, pieceList, actions);
|
||||
return {
|
||||
root: form,
|
||||
pick: (kindId) => {
|
||||
|
||||
@@ -182,6 +182,9 @@ DRAWING_SCALE_MASSHAUL_H_CANDIDATES = (
|
||||
DRAWING_MASSHAUL_USABLE_WIDTH_MM = 700.0
|
||||
DRAWING_SCALE_MASSHAUL_V_M3_MM = 50.0 # 유토곡선 세로 — 종이 1 mm 당 토량(㎥)
|
||||
DRAWING_SCALE_BASIN = 6000 # 유역도 평면 축척 분모 (실거리 1 m = 1/6 mm)
|
||||
# 유역도 등고선 간격(m) — 기본 5 · 종류 옵션으로 10 만(1 m = B05 · B06 전용)
|
||||
BASIN_CONTOUR_INTERVAL_M = 5
|
||||
BASIN_CONTOUR_INTERVAL_CHOICES = (5, 10)
|
||||
# 계획평면도·용지도 평면 축척 분모 — 지식DB 「설계제원_총괄」 측량·도면 기준 1/1,200.
|
||||
# 횡단면도와 같은 원칙: 축척은 줄이지 않고, 한 장에 안 들어가면 **장을 나눈다**.
|
||||
DRAWING_SCALE_PLAN = 1200
|
||||
|
||||
@@ -159,6 +159,8 @@ const state = {
|
||||
scales: [1000, 1200],
|
||||
default_scale: 1200,
|
||||
watershed_scale: 6000,
|
||||
contour_intervals: [5, 10], // 512-1 유역도 등고선 간격(서버 `_state` 와 같은 칸)
|
||||
default_contour_interval: 5,
|
||||
kinds: [
|
||||
{ id: "k_terrain", name: "지형", pieces: ["terrain"], layout: "plan", scale: 1200 },
|
||||
{ id: "k_route", name: "노선배치도", pieces: ["route"], layout: "plan", scale: 1200 },
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""512-1 B07 유역도 등고선 간격 5 m 기본 · 종류 옵션 10 m(임시 폴더 · DB · 실제 마스터 안 씀).
|
||||
|
||||
- 배경선 = 간격 배수 높이만(높이 없는 선은 남김 · None = 전부)
|
||||
- 유역도 종류 = contour_interval_m 5 기본 · 10 고름 · 1 거절 · 평면도 양식엔 칸 없음
|
||||
- 지문 = 간격 바꾸면 유역도만 · 기본 종류를 파일로 내려도 그대로 · 같은 값 = 같은 지문
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_512_1_b07_basin_contour.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail import B07_DesignDetail_Router_Support_Fingerprint as fp # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import _metric_lines # noqa: E402
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import ( # noqa: E402
|
||||
list_kinds,
|
||||
update_kind,
|
||||
)
|
||||
|
||||
|
||||
def _line(level: float | None, x: float) -> dict:
|
||||
props = {} if level is None else {"등고수치": level}
|
||||
coords = [[x, 0.0], [x, 1.0]]
|
||||
return {"properties": props, "geometry": {"type": "LineString", "coordinates": coords}}
|
||||
|
||||
|
||||
def test_간격_배수_높이만(tmp_path):
|
||||
path = tmp_path / "contour.geojson"
|
||||
features = [_line(775.0, 0), _line(780.0, 1), _line(785.0, 2), _line(None, 3)]
|
||||
path.write_text(json.dumps({"type": "FeatureCollection", "features": features}), "utf-8")
|
||||
xs = lambda lines: [line[0][0] for line in lines] # noqa: E731
|
||||
assert xs(_metric_lines(path, "EPSG:4326")) == [0, 1, 2, 3]
|
||||
assert xs(_metric_lines(path, "EPSG:4326", None, 5)) == [0, 1, 2, 3]
|
||||
assert xs(_metric_lines(path, "EPSG:4326", None, 10)) == [1, 3]
|
||||
assert xs(_metric_lines(path, "EPSG:4326", (-1, -1, 1.5, 2), 10)) == [1]
|
||||
|
||||
|
||||
def _watershed(root: Path) -> dict:
|
||||
return next(kind for kind in list_kinds(root) if kind["layout"] == "watershed")
|
||||
|
||||
|
||||
def test_유역도_종류_간격(tmp_path):
|
||||
root = tmp_path / "proj"
|
||||
assert _watershed(root)["contour_interval_m"] == 5
|
||||
assert all("contour_interval_m" not in k for k in list_kinds(root) if k["layout"] == "plan")
|
||||
kind = _watershed(root)
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 10)
|
||||
assert _watershed(root)["contour_interval_m"] == 10
|
||||
update_kind(root, kind["id"], kind["name"], []) # 간격 안 보냄 = 지금 값
|
||||
assert _watershed(root)["contour_interval_m"] == 10
|
||||
with pytest.raises(ValueError):
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 1)
|
||||
|
||||
|
||||
def test_지문_유역도만(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(fp, "detail_basins_path", lambda stored: tmp_path / "basins.geojson")
|
||||
root = tmp_path / "proj"
|
||||
longitudinal = root / "B06_Section" / "longitudinal" / "longitudinal.json"
|
||||
longitudinal.parent.mkdir(parents=True)
|
||||
longitudinal.write_text(json.dumps({"stations": [{"chainage_m": 0}]}), encoding="utf-8")
|
||||
context = SimpleNamespace(
|
||||
vertices=[SimpleNamespace(x=0.0, y=0.0), SimpleNamespace(x=10.0, y=0.0)],
|
||||
crs="EPSG:5186",
|
||||
surface_params={},
|
||||
stored_path="x",
|
||||
)
|
||||
items = [
|
||||
SimpleNamespace(id="cover", kind="cover"),
|
||||
SimpleNamespace(id="plan_route", kind="plan"),
|
||||
SimpleNamespace(id="plan_watershed", kind="plan"),
|
||||
]
|
||||
sources = {"title": {}, "designs": {}, "context": context}
|
||||
|
||||
def prints() -> dict:
|
||||
found = fp.compute_fingerprints(root, longitudinal, items, sources)
|
||||
return {key: value["fingerprint"] for key, value in found.items()}
|
||||
|
||||
def changed(before: dict, after: dict) -> set:
|
||||
return {key for key in before if before[key] != after[key]}
|
||||
|
||||
first = prints()
|
||||
kind = _watershed(root)
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 5) # 기본 종류를 파일로 내림 · 같은 값
|
||||
assert changed(first, prints()) == set()
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 10)
|
||||
ten = prints()
|
||||
assert changed(first, ten) == {"plan_watershed"}
|
||||
update_kind(root, kind["id"], kind["name"], [], None, 5)
|
||||
assert prints() == first
|
||||
@@ -379,6 +379,7 @@ def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
|
||||
"pieces": [],
|
||||
"layout": "watershed",
|
||||
"scale": 6000,
|
||||
"contour_interval_m": 5, # 512-1 유역도 등고선 간격 칸(기본 5 m)
|
||||
}
|
||||
# 측점 0 ~ 2,000 m — 1/1,200 평면도는 여러 장 · 유역도는 한 장
|
||||
stations = [
|
||||
|
||||
@@ -238,7 +238,10 @@ def test_CAD_머리줄_없음_탭줄_빈자리_변수():
|
||||
assert "--cad-title-height: 0px;" in css and ".cad-titlebar" not in css
|
||||
assert ".cad-qtable" not in css and not (src / "components/QuantityPanel.tsx").exists()
|
||||
assert "QuantityPanel" not in (src / "App.tsx").read_text("utf-8")
|
||||
assert "html .cad-ribbon-tabs {" in (src / "integration/host-burger.css").read_text("utf-8")
|
||||
# 508-1f — ☰ 자리 비움이 탭 줄 + 리본 도구 줄 한 묶음(같은 변수)
|
||||
burger = (src / "integration/host-burger.css").read_text("utf-8")
|
||||
assert "html .cad-ribbon-tabs,\nhtml .cad-ribbon-panels {" in burger
|
||||
assert "var(--host-burger-pad, 0px)" in burger
|
||||
|
||||
|
||||
def test_여러_장_문서_장_도번_백업_칸(got):
|
||||
|
||||
Reference in New Issue
Block a user