feat: draft main-line centreline stations from plan sheets (Task 3, HITL)
extract_alignment_plan.py takes the midpoint of each station's two side ticks (CA-ALGN-STSM) as the centreline point. Sheet 001 gives 4+700..5+500 with consecutive residual <= 0.02 m. Sheet 002 gives identical output, so the duplicate question is left to HITL. Profile EL is not yet extracted. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A5Cun3RHQKna9qxE6ybZ4A
This commit is contained in:
1 parent
14a4c4a37a
commit
f144d53f79
5 files changed
+193
No files matched your search
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
Reference in new issue
Block a user