Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_5' and 'origin/sub_laptop_7' into sub_laptop_6
This commit is contained in:
@@ -37,4 +37,47 @@ assert.equal(paintColor('#f5f7fa'), '#000000', '밝은 바탕: 기본색은 검
|
||||
assert.equal(paintColor('#ff0000'), '#ff0000', '밝은 바탕: 빨강 그대로');
|
||||
assert.notEqual(paintColor('#ffff00'), '#ffff00', '밝은 바탕: 노랑 등 밝은 색은 대비 보정(기존)');
|
||||
|
||||
// 커서 입력창 — 칸 · 글 색이 두 테마 모두 달라야(자동색 둘을 쓰면 paintColor 가 같은 색으로 뒤집음)
|
||||
const { inputFieldColors } = await import(
|
||||
'../../../B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts'
|
||||
);
|
||||
const TOKENS: Record<string, Record<string, string>> = {
|
||||
light: {
|
||||
'--cad-chrome-raised': '#ffffff',
|
||||
'--cad-line': '#e6e2e3',
|
||||
'--cad-text': '#333333',
|
||||
'--cad-text-dim': '#615e6e',
|
||||
},
|
||||
dark: {
|
||||
'--cad-chrome-raised': '#251f38',
|
||||
'--cad-line': '#322a45',
|
||||
'--cad-text': '#e4e0f0',
|
||||
'--cad-text-dim': '#b3aec6',
|
||||
},
|
||||
};
|
||||
for (const name of ['light', 'dark']) {
|
||||
theme = name;
|
||||
const colors = inputFieldColors(
|
||||
(token: string, fallback: string) => TOKENS[name][token] ?? fallback
|
||||
);
|
||||
assert.equal(
|
||||
colors.background,
|
||||
TOKENS[name]['--cad-chrome-raised'],
|
||||
`${name}: 칸 = 토큰 색 그대로`
|
||||
);
|
||||
assert.notEqual(colors.background, colors.text, `${name}: 칸 · 글 색 다름`);
|
||||
assert.notEqual(colors.background, colors.dimText, `${name}: 칸 · 안내 글 색 다름`);
|
||||
assert.notEqual(colors.background, colors.border, `${name}: 칸 · 테두리 색 다름`);
|
||||
// 옛 상수 짝(#000000 칸 · #FFF 글)은 paintColor 를 거치면 같은 색 — 입력창은 건너뛰어야
|
||||
assert.equal(
|
||||
paintColor('#000000'),
|
||||
paintColor('#FFF'),
|
||||
`${name}: 자동색 두 개는 같은 색으로 뒤집힘`
|
||||
);
|
||||
}
|
||||
// 토큰 못 읽으면 폴백도 칸 · 글 다름
|
||||
const fallback = inputFieldColors((_token: string, value: string) => value);
|
||||
assert.notEqual(fallback.background, fallback.text, '폴백: 칸 · 글 색 다름');
|
||||
assert.ok(!isAutoColor(fallback.background) && !isAutoColor(fallback.text), '폴백: 자동색 아님');
|
||||
|
||||
console.log('theme 자동색 시험 통과');
|
||||
|
||||
@@ -82,6 +82,12 @@ console.log(
|
||||
frozen: 1,
|
||||
empty: "",
|
||||
}),
|
||||
pick: m.renderDataTableHtml({
|
||||
columns: [{ key: "x", label: "X" }],
|
||||
rows: [{ x: 1 }, { x: 2 }],
|
||||
total: { x: 3 },
|
||||
onRowClick: () => undefined,
|
||||
}),
|
||||
escaped: m.renderDataTableHtml({ columns: [{ key: "x", label: "<b>" }], rows: [{ x: "a&<" }] }),
|
||||
noTotal: m.renderDataTableHtml({ columns: [tc], rows: [] }).includes("<tfoot>"),
|
||||
}),
|
||||
|
||||
@@ -72,9 +72,29 @@ def test_빈_목록():
|
||||
assert out == [[], [], []]
|
||||
|
||||
|
||||
def test_측점_조합들():
|
||||
# 95-3 — 한 측점(1cm 안) 조합 전부 · 본체 → 그룹 차례 · 구조물 id 따라감 · 같은 구조물 같은 조합은 한 건
|
||||
things = [
|
||||
{"structure_id": "s1", "chainage_m": 40.004, "summary_item": _item("SS1"),
|
||||
"group_items": {"g1": _item("SS3"), "g2": _item("SS1")}},
|
||||
{"structure_id": "pipe@40.0", "chainage_m": 40.0, "summary_item": _item("SS1")},
|
||||
{"structure_id": "s2", "chainage_m": 80.0, "summary_item": _item("SS2")},
|
||||
] # fmt: skip
|
||||
out = _run(
|
||||
f"\nconst things = {json.dumps(things, ensure_ascii=False)};"
|
||||
"\nconsole.log(JSON.stringify([combosAt(things, 40).map((c) => [c.id, c.structureId]),"
|
||||
" combosAt(things, 80).length, combosAt(things, 99).length]));"
|
||||
)
|
||||
assert out == [
|
||||
[["system||SS1", "s1"], ["system||SS3", "s1"], ["system||SS1", "pipe@40.0"]],
|
||||
1,
|
||||
0,
|
||||
]
|
||||
|
||||
|
||||
def test_집계표는_공용_표():
|
||||
# PLAN 70-2 — 공용 많은 자료 표 · 측점 열 고정 · 칸은 빈칸(「—」 아님)
|
||||
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
assert "createDataTable(" in ui and "@ui/ui_template_data_table" in ui
|
||||
assert "frozen: 1" in ui and 'empty: ""' in ui
|
||||
assert "frozen: 1" in ui and 'empty: ""' in ui and "onRowClick" in ui
|
||||
assert 'document.createElement("table")' not in ui
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
"""61-2 B08 2탭 구조물도 — 탭에 붙음 · 읽기만 · 집계표와 같은 조합 목록 · 안내 글 키."""
|
||||
"""61-2 · 95-3 B08 구조물도 모달 — 집계표 행에서 열림 · 읽기만 · 집계표와 같은 조합 목록 · 안내 글 키."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
DRAWING = Path("B08_Quantity/B08_Quantity_UI_Drawing.ts").read_text(encoding="utf-8")
|
||||
SUMMARY = Path("B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
PAGE = Path("B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8")
|
||||
LOCALE = Path("ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8")
|
||||
LOCALE_B2 = Path("ui_template/ui_template_locale_b2.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_sheet_tab_wired():
|
||||
assert 'tab === "sheet"' in PAGE and "renderStructureSheets(view, projectId)" in PAGE
|
||||
def test_sheet_tab_gone_modal_wired():
|
||||
# 구조물도 탭 없음 · 집계표 행 누름이 모달을 늦게 불러 엶
|
||||
assert '"sheet"' not in PAGE and "renderStructureSheets" not in PAGE
|
||||
assert "B08_Quantity_Tab_StructureSheet" not in PAGE + LOCALE + LOCALE_B2
|
||||
assert "onRowClick" in SUMMARY and 'import("./B08_Quantity_UI_Drawing")' in SUMMARY
|
||||
assert "combosAt(things, station)" in SUMMARY
|
||||
|
||||
|
||||
def test_modal_own_class_and_cleanup():
|
||||
css = Path("B08_Quantity/B08_Quantity_UI_Drawing.css").read_text(encoding="utf-8")
|
||||
assert "min(1320px, 96vw)" in css and "--modal-max: 94vh" in css
|
||||
assert "beforeClose" in DRAWING and "cards.destroy()" in DRAWING
|
||||
assert 'dialogClass: "b08-sheets"' in DRAWING
|
||||
|
||||
|
||||
def test_read_only():
|
||||
@@ -19,10 +32,12 @@ def test_read_only():
|
||||
assert "editable: () => false" in DRAWING
|
||||
|
||||
|
||||
def test_same_columns_as_summary():
|
||||
assert "summaryColumns(" in DRAWING and "./B08_Quantity_Summary_Data" in DRAWING
|
||||
def test_copy_first_then_master():
|
||||
# 같은 항목의 프로젝트 복사본이 있으면 복사본 · 없으면 M02 원본 층
|
||||
assert "sameItem(own, row)" in DRAWING and "structureFolder(" in DRAWING
|
||||
assert "readTemplate(layer, part, column, null)" in DRAWING
|
||||
|
||||
|
||||
def test_locale_keys_exist():
|
||||
for key in set(re.findall(r'L\("([A-Za-z0-9_]+)"\)', DRAWING)):
|
||||
assert f"{key}:" in LOCALE or key.startswith("B08_Quantity_Summary_"), key
|
||||
assert f"{key}:" in LOCALE or key.startswith(("B08_Quantity_Summary_", "Common_")), key
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""B08 수리 표 ← B05 배수유역 연결값 (PLAN 96-1 · 96-3).
|
||||
|
||||
견본 세부유역 + 관 지점 → 측점마다 linked(A · L · H · 관경 · 박스 내공 · 월류 폭) ·
|
||||
그 값으로 계산한 Qd ≈ B05 설계유량 · 표준값은 B05 배수 설정 하나.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Hydro import compute # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Hydro_Defaults import DEFAULTS, station_rows # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Hydro_Site import BASINS_FILE, read_basins # noqa: E402
|
||||
from common_util.common_util_drainage_detail import size_pipe # noqa: E402
|
||||
from common_util.common_util_drainage_pipes import PipePoint # noqa: E402
|
||||
from common_util.common_util_wamis_station import mononobe_idf # noqa: E402
|
||||
from config import config_system_design as cfg # noqa: E402
|
||||
|
||||
R24 = 231.56
|
||||
BASINS = [
|
||||
# B05 저장 꼴 — 측점 0.01 반올림 · 면적 0.1 · 표고차 0.01 · 길이 0.1
|
||||
{"kind": "detail_basin", "chainage_m": 85.05, "area_m2": 10717.0, "flow_length_m": 163.9,
|
||||
"relief_m": 120.0},
|
||||
{"kind": "detail_basin", "chainage_m": 173.09, "area_m2": 141145.0, "flow_length_m": 603.3,
|
||||
"relief_m": 320.0},
|
||||
{"kind": "detail_basin", "chainage_m": 300.0, "area_m2": 52000.0, "flow_length_m": 410.0,
|
||||
"relief_m": 0.0},
|
||||
] # fmt: skip
|
||||
for _b in BASINS:
|
||||
_out = size_pipe(
|
||||
_b["area_m2"], max(_b["relief_m"], 0.1), _b["flow_length_m"], mononobe_idf(R24)
|
||||
)
|
||||
_b.update(design_flow_m3s=_out["design_flow_m3s"], tc_minutes=_out["tc_minutes"])
|
||||
|
||||
POINTS = [
|
||||
PipePoint(chainage_m=300.004, facility="box_culvert",
|
||||
options={"body_width_m": 2.0, "body_height_m": 1.5}),
|
||||
PipePoint(chainage_m=85.052, options={"pipe_diameter_mm": "1000", "pipe_kind": "파형강관"}),
|
||||
PipePoint(chainage_m=173.09, facility="ford_bridge", options={"ford_width_m": 10}),
|
||||
PipePoint(chainage_m=500.0, options={"pipe_diameter_mm": 800}), # 유역 짝 없음
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def test_측점마다_연결값() -> None:
|
||||
rows = station_rows(POINTS, BASINS)
|
||||
assert [r["chainage_m"] for r in rows] == [85.052, 173.09, 300.004, 500.0]
|
||||
pipe, ford, box, lone = (r["linked"] for r in rows)
|
||||
assert pipe == {
|
||||
"area_km2": 0.010717,
|
||||
"length_m": 163.9,
|
||||
"height_m": 120.0,
|
||||
"diameter_mm": 1000.0,
|
||||
}
|
||||
assert ford == {"area_km2": 0.141145, "length_m": 603.3, "height_m": 320.0,
|
||||
"overflow_width_m": 10.0} # fmt: skip
|
||||
# 표고차 0 은 빈칸(저장 검사 gt 0) · 박스는 내공
|
||||
assert box == {"area_km2": 0.052, "length_m": 410.0, "box_width_m": 2.0, "box_height_m": 1.5}
|
||||
# 유역 짝이 없으면 시설 치수만 · B05 참고값 없음
|
||||
assert lone == {"diameter_mm": 800.0} and rows[3]["b05"] is None
|
||||
assert rows[0]["b05"] == {"design_flow_m3s": BASINS[0]["design_flow_m3s"], "tc_minutes": 5.0}
|
||||
|
||||
|
||||
def test_잇지_않는_칸() -> None:
|
||||
for row in station_rows(POINTS, BASINS):
|
||||
assert not {"slope_denominator", "pipe_kind", "n"} & set(row["linked"])
|
||||
|
||||
|
||||
def test_basins_를_안_주면_linked_없음() -> None:
|
||||
rows = station_rows(POINTS)
|
||||
assert all(r["linked"] is None and r["b05"] is None for r in rows)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("index", [0, 1])
|
||||
def test_연결값으로_계산한_Qd_는_B05_설계유량(index: int) -> None:
|
||||
row = station_rows(POINTS, BASINS)[index]
|
||||
out = compute({**DEFAULTS, **row["linked"], "r24_mm": R24, "sheet": row["sheet"]})
|
||||
assert out["design_flow"] == pytest.approx(row["b05"]["design_flow_m3s"], abs=1e-4)
|
||||
assert out["tc_hr"] * 60 == pytest.approx(row["b05"]["tc_minutes"], abs=0.05)
|
||||
|
||||
|
||||
def test_표준값은_B05_배수_설정() -> None:
|
||||
assert DEFAULTS["runoff"] == cfg.DRAINAGE_RUNOFF_COEFFICIENT == 0.8
|
||||
assert DEFAULTS["factor"] == cfg.DRAINAGE_DESIGN_FLOW_FACTOR == 2.0
|
||||
assert DEFAULTS["return_period"] == cfg.DRAINAGE_DESIGN_RETURN_PERIOD_YR == 100
|
||||
assert DEFAULTS["tc_min_minutes"] == cfg.DRAINAGE_TC_MIN_MINUTES == 5.0
|
||||
|
||||
|
||||
def test_세부유역_파일_읽기(tmp_path: Path) -> None:
|
||||
assert read_basins(tmp_path) == [] # 분석 저장 전
|
||||
features = [{"type": "Feature", "properties": b, "geometry": None} for b in BASINS]
|
||||
features.append({"type": "Feature", "properties": {"kind": "pipe", "chainage_m": 85.05}})
|
||||
path = tmp_path / BASINS_FILE
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(json.dumps({"type": "FeatureCollection", "features": features}), "utf-8")
|
||||
assert [b["chainage_m"] for b in read_basins(tmp_path)] == [85.05, 173.09, 300.0]
|
||||
@@ -101,7 +101,25 @@ def test_소유_측점에만_싣는다():
|
||||
layouts = _LAYOUTS.read_text(encoding="utf-8")
|
||||
assert "pipeOwnerChainage" in layouts, "소유 측점을 안 가리고 있다"
|
||||
assert "section.culvert?.chainage_m" in layouts, "관이 놓인 자리를 안 읽고 있다"
|
||||
py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert.py").read_text(encoding="utf-8")
|
||||
py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
ts = (PROJECT_ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8")
|
||||
assert '"chainage_m": pipe_chainage' in py
|
||||
assert "chainage_m: pipeChainage" in ts
|
||||
|
||||
|
||||
def test_집수정_실제_모양이_같은_길로_정본에_실린다():
|
||||
"""98-2 — 실제로 선 집수정 모양(patch · 자동 지형 규칙 결과)이 다단 벽과 같은 줄로 정본에 감.
|
||||
|
||||
TS 가 줄에 싣고(`ExtraWallRow.inlet_basin`) 서버가 합칠 때 받아야 함 — 한쪽만 있으면 조용히 버려짐.
|
||||
"""
|
||||
layouts = _LAYOUTS.read_text(encoding="utf-8")
|
||||
block = layouts[layouts.index("export interface ExtraWallRow") :]
|
||||
assert "inlet_basin: BasinShape | null" in block[: block.index("}")]
|
||||
assert "inlet_basin: culvert.basin?.shape ?? null" in layouts
|
||||
prebuild = (PROJECT_ROOT / "B06_Section" / "B06_Section_Server_Calc_Prebuild.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
merged = prebuild[prebuild.index("extra_walls = [") :][:900]
|
||||
assert '"inlet_basin": row.get("inlet_basin")' in merged
|
||||
|
||||
@@ -1,48 +1,89 @@
|
||||
"""B07 용지도 — 필지마다 지번·지목·면적·소유자 한 묶음 · 빈 값은 빈 줄 (59-6)."""
|
||||
"""B07 용지도 — 필지마다 지번·지목·면적·소유자 상자 + 지시선 (59-6 · 68-2 · 93-3).
|
||||
|
||||
import pytest
|
||||
① 필지마다 상자 하나 · 빈 값은 빈 줄 · 쪼개진 필지는 큰 조각에 지시선
|
||||
② 구역이 한 필지 안 — 경계 없이 어느 필지인지 한 번
|
||||
③ 상자 · 지시선 끝이 노선 버퍼와 안 겹침 · 상자끼리 안 겹침 · 도면에서도 노선과 안 겹침
|
||||
"""
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Landuse import (
|
||||
JIBUN_LAYER_ID,
|
||||
MM,
|
||||
build_landuse_drawing,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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)]
|
||||
BOX = (0.0, 0.0, 400.0, 300.0)
|
||||
|
||||
|
||||
def test_one_label_group_per_parcel() -> None:
|
||||
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": ""}
|
||||
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임"
|
||||
two_parts = {
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [[_square(250.0, 200.0, 20.0)], [_square(300.0, 150.0, 80.0)]],
|
||||
}
|
||||
assert split_x.pop() == pytest.approx(215.0 * MM)
|
||||
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) -> None:
|
||||
"""작은 구역이 큰 필지 하나 안에 통째로 — 경계선이 없어도 어느 필지인지 한 번 적는다 (68-2)."""
|
||||
import json
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import _clip_parcels
|
||||
|
||||
def test_zone_inside_one_parcel_is_named_once(tmp_path: Path) -> None:
|
||||
props = {
|
||||
"pnu": "P1",
|
||||
"sgg_nm": "봉화군",
|
||||
@@ -53,27 +94,70 @@ def test_zone_inside_one_parcel_is_named_once(tmp_path) -> None:
|
||||
"parea": 1234567,
|
||||
"owner_nm": "국",
|
||||
}
|
||||
big = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0]]
|
||||
geojson = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": props,
|
||||
"geometry": {"type": "Polygon", "coordinates": [big]},
|
||||
}
|
||||
],
|
||||
}
|
||||
path = tmp_path / "parcels.geojson"
|
||||
path.write_text(json.dumps(geojson, ensure_ascii=False), encoding="utf-8")
|
||||
parcels = _clip_parcels(path, "EPSG:4326", (4.0, 4.0, 6.0, 6.0))
|
||||
assert len(parcels) == 1 and parcels[0]["ring"] == []
|
||||
|
||||
route = [(4.5, 4.5), (5.5, 5.5)]
|
||||
drawing = build_landuse_drawing("landuse", "용지도", route, [], parcels, [], [])
|
||||
labels = [
|
||||
e["shapeData"]["label"] for e in drawing["entities"] if e.get("layerId") == JIBUN_LAYER_ID
|
||||
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㎡",
|
||||
"국",
|
||||
]
|
||||
notes = [label for label in labels if label.startswith("구역 전체가 이 필지 안")]
|
||||
assert notes == ["구역 전체가 이 필지 안 (봉화군 재산면 갈산리)"]
|
||||
assert labels[1:] == ["산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])
|
||||
|
||||
@@ -6,21 +6,31 @@
|
||||
④ 고른 조각만 도면 층으로 얹힘
|
||||
⑤ 조각 잘게(67장) — 노견선 · 성토 외곽선 · 절토 외곽선 따로 · 기본 넷 처음 체크 = 옛 그림 ·
|
||||
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 넷 포함) · 확정본 풀림 · 배수시설 기호
|
||||
⑥ IP 표(94장) — 예시 검산 · 도-분-초 올림 · 장마다 그 장 IP 한 번 · 표 밑 선 자름 · 층
|
||||
⑦ 용지도 종류(93장) — 기본 종류 · 옛 종류 폴더에 한 번 더함(지우면 안 살아남) · 용지 조각 층
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import ezdxf
|
||||
import pytest
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import (
|
||||
curve_values,
|
||||
dms,
|
||||
ip_blocks,
|
||||
ip_table_piece,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
DEFAULT_KINDS,
|
||||
build_plan_kind_drawing,
|
||||
layer_id,
|
||||
slope_pieces,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import drawing_to_dxf
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
_kinds_dir,
|
||||
@@ -123,7 +133,13 @@ def test_outline_width_equals_cross_design_at_two_stations(tmp_path: Path) -> No
|
||||
|
||||
def test_kinds_add_delete_persist(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
assert [k["name"] for k in list_kinds(root)] == ["지형", "노선배치도", "대피소", "라이다"]
|
||||
assert [k["name"] for k in list_kinds(root)] == [
|
||||
"지형",
|
||||
"노선배치도",
|
||||
"대피소",
|
||||
"라이다",
|
||||
"용지도",
|
||||
]
|
||||
added = add_kind(root, "내 평면도", ["centerline", "station", "nope"])
|
||||
assert added["pieces"] == ["centerline", "station"]
|
||||
with pytest.raises(ValueError):
|
||||
@@ -132,14 +148,14 @@ 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"}
|
||||
assert {k["id"] for k in DEFAULT_KINDS} == {"terrain", "route", "layout", "lidar", "landuse"}
|
||||
|
||||
|
||||
def test_refuge_marks_same_spot(tmp_path: Path) -> None:
|
||||
@@ -184,16 +200,16 @@ def test_split_runs_switch_cut_fill_and_stay_joined() -> None:
|
||||
def test_default_kinds_keep_old_picture_and_legacy_file(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
outline = {"road_edge", "fill_outline", "cut_outline"}
|
||||
for (
|
||||
kind
|
||||
) in DEFAULT_KINDS: # 옛 slope_outline 자리를 나눈 셋이 그대로 채움 · 배수시설은 옛 그림에 없음
|
||||
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
|
||||
# 옛 slope_outline 자리를 나눈 셋이 그대로 채움 · 배수시설은 옛 그림에 없음(용지도는 따로)
|
||||
for kind in DEFAULT_KINDS:
|
||||
if kind["id"] != "landuse":
|
||||
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "old.json").write_text(
|
||||
json.dumps({"name": "옛것", "pieces": ["slope_outline", "station"], "order": 0}), "utf-8"
|
||||
)
|
||||
(kind,) = list_kinds(root)
|
||||
kind = next(k for k in list_kinds(root) if k["id"] == "old") # 용지도는 이관으로 더해짐
|
||||
assert set(kind["pieces"]) == outline | {"station"}
|
||||
|
||||
|
||||
@@ -230,3 +246,101 @@ def test_drainage_symbol_piece() -> None:
|
||||
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("drainage")]
|
||||
assert {e["type"] for e in mine} >= {"Text"} and len(mine) >= 2
|
||||
assert any(e["type"] == "Text" and e["shapeData"]["label"] == "D800" for e in mine)
|
||||
|
||||
|
||||
def test_ip_table_example_values_and_dms() -> None:
|
||||
ia = 94 + 59 / 60 + 58.38 / 3600 # 예시 그림 IP1
|
||||
values = curve_values(15.0, ia)
|
||||
assert dms(ia) == "94-59-58.38"
|
||||
assert [f"{values[k]:.5f}" for k in ("tl", "cl", "sl")] == ["16.36950", "24.87082", "7.20271"]
|
||||
assert dms(1 + 59 / 60 + 59.9999 / 3600) == "2-00-00.00" # 초 60.00 올림
|
||||
assert dms(5 + 3 / 60 + 7.2 / 3600) == "5-03-07.20"
|
||||
|
||||
|
||||
# ㄱ자 노선 — (0,0)→(100,0)→(100,100) · 모서리 R 15 곡선(가운데 누가거리 100) · 앞에 노선 밖 옛 곡선
|
||||
_ROUTE = [(0.0, 0.0, 0.0), (85.0, 85.0, 0.0), (115.0, 100.0, 15.0), (200.0, 100.0, 100.0)]
|
||||
_CURVES = [
|
||||
{"apex": [500.0, 500.0], "start": [490.0, 500.0], "end": [500.0, 510.0], "radius_m": 10.0},
|
||||
{"apex": [100.0, 0.0], "start": [85.0, 0.0], "end": [100.0, 15.0], "radius_m": 15.0},
|
||||
]
|
||||
|
||||
|
||||
def test_ip_blocks_one_sheet_each_and_ends() -> None:
|
||||
first = ip_blocks(_CURVES, _ROUTE, 1, 2, 0.0, 100.0)
|
||||
second = ip_blocks(_CURVES, _ROUTE, 2, 2, 100.0, 200.0)
|
||||
assert [b["name"] for b in first] == ["BP"] # 경계 IP 는 뒤 장에만
|
||||
assert [b["name"] for b in second] == ["2", "EP"] # 번호 = 곡선표 차례(옛 곡선은 빠짐)
|
||||
ip = second[0]
|
||||
assert dms(ip["ia"]) == "90-00-00.00" and ip["tl"] == pytest.approx(15.0)
|
||||
assert [b["name"] for b in ip_blocks(_CURVES, _ROUTE, 1, 1, 0.0, 200.0)] == ["BP", "2", "EP"]
|
||||
|
||||
|
||||
def test_ip_table_drawing_clips_lines_under_table(tmp_path: Path) -> None:
|
||||
box = (-20.0, -20.0, 300.0, 200.0)
|
||||
contour = [[-20.0, 0.0], [300.0, 0.0]] # 표(왼쪽 아래 · 높이 48 m) 밑을 지나는 등고선
|
||||
pieces = {
|
||||
"contour": {"lines": [contour]},
|
||||
"centerline": {"lines": [[[0.0, 100.0], [9.0, 100.0]]]},
|
||||
}
|
||||
pieces["ip_table"] = ip_table_piece(
|
||||
pieces, _CURVES, _ROUTE, box, {"number": 1, "start_m": 0.0, "end_m": 200.0}, 1
|
||||
)
|
||||
# IP 표 칸(x 29.2 ~ 72.4 · y -14 ~ 34)만 빠짐 · BP · EP 칸(3행)은 y 0 위라 안 자름
|
||||
assert pieces["contour"]["lines"] == [[[-20.0, 0.0], [29.2, 0.0]], [[72.4, 0.0], [300.0, 0.0]]]
|
||||
assert pieces["centerline"]["lines"] == [[[0.0, 100.0], [9.0, 100.0]]] # 표 밖은 그대로
|
||||
drawing = build_plan_kind_drawing("plan_ip", "시험", box, pieces)
|
||||
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("ip_table")]
|
||||
assert [e["type"] for e in mine] == ["Table"] * 3
|
||||
bp, ip, _ep = (e["shapeData"] for e in mine)
|
||||
assert len(bp["rowHeights"]) == 3 and len(ip["rowHeights"]) == 8
|
||||
assert bp["cells"][1][2]["text"] == "0.00000" and ip["cells"][1][2]["text"] == "0.00000"
|
||||
assert ip["cells"][2][2]["text"] == "100.00000" # Y = 동(x)
|
||||
assert ip["cells"][3][1]["text"] == "90-00-00.00"
|
||||
assert {layer["id"] for layer in drawing["layers"]} >= {layer_id("ip_table")}
|
||||
assert "ip_table" in next(k for k in DEFAULT_KINDS if k["id"] == "route")["pieces"]
|
||||
# DXF — 표 → 선 + 글(IP 묶음: 바깥 4 · 세로 10 · 가로 20 · 글 17)
|
||||
raw, skipped = drawing_to_dxf({"entities": [mine[1]], "layers": drawing["layers"]})
|
||||
(tmp_path / "ip.dxf").write_bytes(raw)
|
||||
space = ezdxf.readfile(tmp_path / "ip.dxf").modelspace()
|
||||
assert skipped == {} and len(space.query("LINE")) == 34 and len(space.query("TEXT")) == 17
|
||||
assert {"90-00-00.00", "15.00000"} <= {t.dxf.text for t in space.query("TEXT")}
|
||||
|
||||
|
||||
def test_landuse_kind_added_once_to_old_folder(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
for order, kind in enumerate(DEFAULT_KINDS[:4]): # 93장 앞에 내려 둔 기본 넷
|
||||
(folder / f"{kind['id']}.json").write_text(
|
||||
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 # 두 번 읽어도 한 번
|
||||
delete_kind(root, "landuse")
|
||||
assert "landuse" not in [k["id"] for k in list_kinds(root)] # 지운 것은 안 살아남
|
||||
|
||||
|
||||
def test_landuse_pieces_become_layers() -> None:
|
||||
pieces = {
|
||||
"parcel": {"lines": [[[0.0, 0.0], [10.0, 0.0]]]},
|
||||
"emd": {"lines": [[[0.0, 5.0], [10.0, 5.0]]]},
|
||||
"sgg": {"lines": [[[0.0, 9.0], [10.0, 9.0]]]},
|
||||
"parcel_info": {
|
||||
"items": [
|
||||
{"lines": ["1-1", "임야", "", "국"], "anchor": [5.0, 2.0], "center": [20.0, 20.0]}
|
||||
]
|
||||
},
|
||||
}
|
||||
drawing = build_plan_kind_drawing(
|
||||
"plan_landuse", "평면도(용지도)", (-50.0, -10.0, 50.0, 50.0), pieces
|
||||
)
|
||||
names = {layer["id"] for layer in drawing["layers"]}
|
||||
assert {layer_id(p) for p in ("parcel", "parcel_info", "emd", "sgg")} <= names
|
||||
labels = [
|
||||
e["shapeData"]["label"]
|
||||
for e in drawing["entities"]
|
||||
if e.get("layerId") == layer_id("parcel_info") and e["type"] == "Text"
|
||||
]
|
||||
assert labels == ["1-1", "임야", "국"]
|
||||
|
||||
@@ -92,3 +92,12 @@ def test_빈_칸_글_풍선_꾸밈_옵션(r):
|
||||
assert '<td class="is-empty"></td>' in o # empty="" — 「-」 대신 빈칸
|
||||
assert '<th scope="col" rowspan="1" class="is-missing" title="풍선">메쌓기</th>' in o
|
||||
assert 'rowspan="2" class="is-frozen is-edge" style="left:0px" title="측점">측점</th>' in o
|
||||
|
||||
|
||||
def test_줄_누름_옵션일_때만_data_i(r):
|
||||
# 95-3 — onRowClick 을 주면 본문 줄에만 data-i · is-clickable(합계 줄 제외) · 안 주면 글 그대로
|
||||
pick = r["pick"]
|
||||
assert '<tr class="is-clickable" data-i="0">' in pick
|
||||
assert '<tr class="is-alt is-clickable" data-i="1">' in pick
|
||||
assert pick.count("data-i=") == 2
|
||||
assert "data-i" not in r["opts"] and "is-clickable" not in r["foot"]
|
||||
|
||||
Reference in New Issue
Block a user