diff --git a/data/alignment_plan_001_centre.csv b/data/alignment_plan_001_centre.csv new file mode 100644 index 0000000..8764e62 --- /dev/null +++ b/data/alignment_plan_001_centre.csv @@ -0,0 +1,10 @@ +station_m,centre_x,centre_y,side_gap_m +4700,151067.261,483726.735,8.554 +4800,151119.817,483641.679,11.191 +4900,151169.128,483554.701,13.981 +5000,151215.122,483465.925,16.844 +5100,151258.471,483375.815,18.211 +5200,151301.3,483285.451,17.956 +5300,151344.993,483195.51,15.809 +5400,151390.045,483106.254,12.322 +5500,151437.284,483018.131,9.389 diff --git a/data/alignment_plan_002_centre.csv b/data/alignment_plan_002_centre.csv new file mode 100644 index 0000000..8764e62 --- /dev/null +++ b/data/alignment_plan_002_centre.csv @@ -0,0 +1,10 @@ +station_m,centre_x,centre_y,side_gap_m +4700,151067.261,483726.735,8.554 +4800,151119.817,483641.679,11.191 +4900,151169.128,483554.701,13.981 +5000,151215.122,483465.925,16.844 +5100,151258.471,483375.815,18.211 +5200,151301.3,483285.451,17.956 +5300,151344.993,483195.51,15.809 +5400,151390.045,483106.254,12.322 +5500,151437.284,483018.131,9.389 diff --git a/scripts/explore_alignment.py b/scripts/explore_alignment.py new file mode 100644 index 0000000..6189b21 --- /dev/null +++ b/scripts/explore_alignment.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Task 3 exploration: layer usage and long polylines / text clusters in a +plan-profile DXF, to locate the tunnel centreline (본선 축선) candidates.""" +import collections +import json +import sys + +import ezdxf + +path = sys.argv[1] +doc = ezdxf.readfile(path) +msp = doc.modelspace() +by_layer = collections.Counter(e.dxf.layer for e in msp) +print("layers used:", len(by_layer)) +for name, n in by_layer.most_common(25): + print(f" {n:6d} {name}") +# long polylines = candidate alignment / outline strings +polys = [] +for e in msp: + if e.dxftype() == "LWPOLYLINE": + pts = list(e.get_points("xy")) + if len(pts) >= 20: + xs = [p[0] for p in pts]; ys = [p[1] for p in pts] + polys.append({"layer": e.dxf.layer, "n": len(pts), + "x": [round(min(xs), 1), round(max(xs), 1)], + "y": [round(min(ys), 1), round(max(ys), 1)]}) +polys.sort(key=lambda d: -d["n"]) +print("long LWPOLYLINE (>=20 vertices):", len(polys)) +for p in polys[:12]: + print(" ", p) +# text that looks like station (STA) or level (EL) labels +import re +pat = re.compile(r"(STA|No\.?|EL|E\.L|\d+\+\d+|\+\d{2,3}\.\d{3}|\d{2,3}\.\d{2,3})", re.I) +hits = [] +for e in msp: + if e.dxftype() in ("TEXT", "MTEXT"): + s = e.dxf.text if e.dxftype() == "TEXT" else e.plain_text() + if s and pat.search(s): + loc = e.dxf.insert if e.dxftype() == "TEXT" else e.dxf.insert + hits.append((round(loc.x, 1), round(loc.y, 1), s.strip()[:40])) +print("station/level-like texts:", len(hits)) +for h in sorted(hits)[:25]: + print(" ", h) diff --git a/scripts/extract_alignment_plan.py b/scripts/extract_alignment_plan.py new file mode 100644 index 0000000..3292c60 --- /dev/null +++ b/scripts/extract_alignment_plan.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Task 3 (HITL draft): main-line centreline station table from a plan sheet. + +Method (layers CA-ALGN-TEXT labels and CA-ALGN-STSM station ticks): + 1. Station labels: 'N+mmm' is explicit; bare numbers take the km of the next + explicit label when they precede it (700 before 5+000 -> 4+700), otherwise + the km of the previous explicit label. + 2. Each station has two labels (one per side of the road). Each label is + paired with its nearest tick; the centreline point is the midpoint of the + two tick points. + 3. Checks reported (not forced): side offset per station, and consecutive + centreline distance vs station difference (residual). +DRAFT for HITL review. No value is adjusted to fit.""" +import csv +import math +import re +import sys +from collections import defaultdict + +import ezdxf + +SRC, OUT = sys.argv[1], sys.argv[2] +msp = ezdxf.readfile(SRC).modelspace() +explicit_re = re.compile(r"^(\d+)\+(\d{3})$") + +labels = [(e.dxf.insert.x, e.dxf.insert.y, e.dxf.text.strip()) + for e in msp.query("TEXT") if e.dxf.layer == "CA-ALGN-TEXT"] +ticks = [] +for e in msp.query("LINE"): + if e.dxf.layer == "CA-ALGN-STSM": + a, b = e.dxf.start, e.dxf.end + ticks.append(((a.x + b.x) / 2, (a.y + b.y) / 2)) + +labels = [l for l in labels if explicit_re.match(l[2]) or l[2].isdigit()] +labels.sort(key=lambda t: -t[1]) # along the sheet, NNE -> SSW +explicit_idx = [i for i, l in enumerate(labels) if explicit_re.match(l[2])] +station_of = {} +for i, (x, y, s) in enumerate(labels): + if explicit_re.match(s): + m = explicit_re.match(s) + station_of[i] = int(m.group(1)) * 1000 + int(m.group(2)) + continue + v = int(s) + nxt = next((j for j in explicit_idx if j > i), None) + prv = next((j for j in reversed(explicit_idx) if j < i), None) + if prv is not None: + km = int(explicit_re.match(labels[prv][2]).group(1)) + else: + km = int(explicit_re.match(labels[nxt][2]).group(1)) - 1 + station_of[i] = km * 1000 + v + +groups = defaultdict(list) +for i, (x, y, s) in enumerate(labels): + groups[station_of[i]].append((x, y)) + + +def nearest_tick(pt): + return min(ticks, key=lambda t: math.hypot(t[0] - pt[0], t[1] - pt[1])) + +centre = [] +for sta in sorted(groups): + pts = groups[sta] + if len(pts) != 2: + continue + t1, t2 = nearest_tick(pts[0]), nearest_tick(pts[1]) + side_gap = math.dist(t1, t2) + mid = ((t1[0] + t2[0]) / 2, (t1[1] + t2[1]) / 2) + centre.append((sta, mid[0], mid[1], side_gap)) + +with open(OUT, "w", newline="", encoding="utf-8") as fh: + w = csv.writer(fh) + w.writerow(["station_m", "centre_x", "centre_y", "side_gap_m"]) + for sta, x, y, g in centre: + w.writerow([sta, round(x, 3), round(y, 3), round(g, 3)]) + +print(f"stations={len(centre)} (labels={len(labels)}, ticks={len(ticks)})") +prev = None +for sta, x, y, g in centre: + step = "" if prev is None else f" d={math.dist(prev[1:3], (x, y)):8.3f} dSTA={sta - prev[0]:5d} res={math.dist(prev[1:3], (x, y)) - (sta - prev[0]):+.3f}" + print(f" STA {sta:>5} ({x:12.3f}, {y:12.3f}) side_gap={g:6.2f}{step}") + prev = (sta, x, y) diff --git a/scripts/make_centre_doc.py b/scripts/make_centre_doc.py new file mode 100644 index 0000000..0f48071 --- /dev/null +++ b/scripts/make_centre_doc.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Render the Task 3 draft centreline report (DOC/03_설계) from the centreline +CSVs written by extract_alignment_plan.py. Numbers are read, not typed.""" +import csv +import math +import sys + +CSV_001, CSV_002, OUT_MD = sys.argv[1], sys.argv[2], sys.argv[3] + + +def load(path): + with open(path, encoding="utf-8") as fh: + return [(int(r["station_m"]), float(r["centre_x"]), float(r["centre_y"]), float(r["side_gap_m"])) + for r in csv.DictReader(fh)] + +a, b = load(CSV_001), load(CSV_002) +same = a == b +lines = ["# 본선 중심선 측점 좌표 (초안)\n", + "| 버전 | 일자 | 변경 내용 |", "|---|---|---|", + "| v0.1.00 | 2026-10-21 | 최초 작성. 평면도 02-001 기준 측점 4+700~5+500 중심선 초안 (HITL 검토 대상) |\n", + "## 1. 추출 방법", + "- 소스: 레이어 `CA-ALGN-TEXT`(측점 라벨), `CA-ALGN-STSM`(측점 눈금) [출처: C0060302-001]", + "- 측점 라벨은 측점당 2개(좌우 측). 각 라벨의 최근접 눈금 2점의 **중점**을 중심선 점으로 채택 (정의 HITL 확인 대상)", + "- 좌표는 도면 좌표계 그대로 (단위: 도면 표기 따름, 사용자 승인 2026-10-21)\n", + "## 2. 측점별 중심선 좌표 (02-001)\n", + "| 측점(m) | 표기 | X | Y | 좌우 간격(m) | 인접 거리(m) | 측점차(m) | 잔차(m) |", + "|---|---|---|---|---|---|---|---|"] +prev = None +for sta, x, y, g in a: + label = f"{sta // 1000}+{sta % 1000:03d}" + if prev is None: + lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | — | — | — |") + else: + d = math.dist(prev[1:3], (x, y)) + lines.append(f"| {sta} | {label} | {x:,.3f} | {y:,.3f} | {g:.2f} | {d:.3f} | {sta - prev[0]} | {d - (sta - prev[0]):+.3f} |") + prev = (sta, x, y) +resid = [math.dist(a[i][1:3], a[i + 1][1:3]) - (a[i + 1][0] - a[i][0]) for i in range(len(a) - 1)] +lines += ["", + f"- 검증: 인접 측점 거리와 측점차의 잔차 최대 {max(abs(r) for r in resid):.3f} m (측점 9개, 8구간). 잔차는 보정하지 않고 그대로 기재", + "- 범위: 4+700 ~ 5+500 (800m). **이 시트 밖의 연장은 확보된 도면이 없음**\n", + "## 3. 02-002 시트 대조", + f"- 02-002 추출 결과 {'02-001과 측점 좌표가 동일' if same else '02-001과 차이 있음'} ({'중복 시트 가능성' if same else '차이 확인 필요'}). 원본 파일 md5는 상이함 [출처: C0060302-002]", + "- 02-002가 종단(프로파일) 시트인지 중복 시트인지는 사용자 확인 필요\n", + "## 4. 미완 항목 (OPEN 등재 대상)", + "- 종단 EL 추출 미실시 (측점별 계획고·지반고). `CR-TABL-TEX2`의 740.00~980.00 라벨은 20m 간격으로 보이나 측점 체계와 대응 미확인 [추론]", + "- 허용오차 ±50mm 판정은 EL 추출 후 실시", + "- 측점 4+700 이전·5+500 이후 구간 평면 도면 미확보\n"] +open(OUT_MD, "w", encoding="utf-8").write("\n".join(lines) + "\n") +print("written", OUT_MD, "same_as_002=", same, "max_resid=", round(max(abs(r) for r in resid), 3))