172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
"""G3 — 시추 시트 읽기 · 검사 · 양식 만들기.
|
||
|
||
엑셀 시트 "시추": 한 행 = 한 지층.
|
||
| 공번 | X | Y | 공구EL | 지층명 | 하단깊이 | 하단EL | N값 | 지하수위깊이 |
|
||
- X, Y, 공구EL, 지하수위깊이는 공마다 첫 행에만 적어도 된다.
|
||
- 하단EL 은 비워 두면 공구EL − 하단깊이 로 계산한다.
|
||
- X, Y 가 비어 있으면 지형 DXF의 시추공 위치(같은 공번)로 채운다.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
|
||
from .config import GroundConfig
|
||
|
||
COLUMNS = ["공번", "X", "Y", "공구EL", "지층명", "하단깊이", "하단EL", "N값", "지하수위깊이"]
|
||
|
||
|
||
@dataclass
|
||
class Stratum:
|
||
name: str
|
||
bottom_depth: float
|
||
bottom_el: float
|
||
n_value: str | None = None
|
||
|
||
|
||
@dataclass
|
||
class Borehole:
|
||
id: str
|
||
X: float | None
|
||
Y: float | None
|
||
top_el: float | None
|
||
strata: list[Stratum] = field(default_factory=list)
|
||
gwl_depth: float | None = None
|
||
|
||
@property
|
||
def gwl_el(self) -> float | None:
|
||
if self.gwl_depth is None or self.top_el is None:
|
||
return None
|
||
return self.top_el - self.gwl_depth
|
||
|
||
@property
|
||
def names(self) -> list[str]:
|
||
return [s.name for s in self.strata]
|
||
|
||
def top_of(self, i: int) -> float:
|
||
return self.top_el if i == 0 else self.strata[i - 1].bottom_el
|
||
|
||
|
||
def _num(v) -> float | None:
|
||
if v is None or (isinstance(v, str) and not v.strip()):
|
||
return None
|
||
try:
|
||
return float(str(v).replace(",", ""))
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def read_boreholes(path: str | Path, cfg: GroundConfig | None = None, sheet: str = "시추",
|
||
dxf_positions: dict[str, tuple[float, float]] | None = None
|
||
) -> tuple[list[Borehole], list[str]]:
|
||
"""시추 시트를 읽고 (시추공 목록, 검사 문제 목록)을 돌려준다. 문제가 있는 공은 빼지 않고 문제만 알린다."""
|
||
from openpyxl import load_workbook
|
||
|
||
cfg = cfg or GroundConfig()
|
||
wb = load_workbook(path, data_only=True)
|
||
if sheet not in wb.sheetnames:
|
||
raise ValueError(f"'{sheet}' 시트가 없습니다. 시트 목록: {wb.sheetnames}")
|
||
ws = wb[sheet]
|
||
rows = list(ws.iter_rows(values_only=True))
|
||
if not rows:
|
||
return [], ["시추 시트가 비어 있습니다."]
|
||
header = [str(h).replace(" ", "").strip() if h is not None else "" for h in rows[0]]
|
||
missing = [c for c in COLUMNS if c not in header]
|
||
if missing:
|
||
raise ValueError(f"시추 시트 머리글에 없는 열: {missing}")
|
||
col = {c: header.index(c) for c in COLUMNS}
|
||
|
||
bores: dict[str, Borehole] = {}
|
||
issues: list[str] = []
|
||
for r_i, r in enumerate(rows[1:], start=2):
|
||
bid = r[col["공번"]]
|
||
if bid is None or not str(bid).strip():
|
||
continue
|
||
bid = str(bid).strip()
|
||
b = bores.setdefault(bid, Borehole(bid, None, None, None))
|
||
for attr, c in (("X", "X"), ("Y", "Y"), ("top_el", "공구EL"), ("gwl_depth", "지하수위깊이")):
|
||
v = _num(r[col[c]])
|
||
if v is not None and getattr(b, attr) is None:
|
||
setattr(b, attr, v)
|
||
name = r[col["지층명"]]
|
||
depth = _num(r[col["하단깊이"]])
|
||
if name is None or not str(name).strip():
|
||
continue
|
||
name = str(name).strip()
|
||
if depth is None:
|
||
issues.append(f"{bid} {r_i}행: 하단깊이가 없습니다.")
|
||
continue
|
||
el = _num(r[col["하단EL"]])
|
||
n = r[col["N값"]]
|
||
b.strata.append(Stratum(name, depth, el if el is not None else float("nan"), None if n is None else str(n)))
|
||
|
||
for b in bores.values():
|
||
if (b.X is None or b.Y is None) and dxf_positions and b.id in dxf_positions:
|
||
b.X, b.Y = dxf_positions[b.id]
|
||
if b.X is None or b.Y is None:
|
||
issues.append(f"{b.id}: 좌표가 없습니다 (지형 도면에도 같은 공번이 없음).")
|
||
if b.top_el is None:
|
||
issues.append(f"{b.id}: 공구EL이 없습니다.")
|
||
prev_d, prev_rank = 0.0, -1
|
||
for s in b.strata:
|
||
if s.bottom_el != s.bottom_el and b.top_el is not None: # NaN → 계산
|
||
s.bottom_el = b.top_el - s.bottom_depth
|
||
elif b.top_el is not None and abs(b.top_el - s.bottom_depth - s.bottom_el) > 0.05:
|
||
issues.append(f"{b.id} {s.name}: 하단EL {s.bottom_el:.2f} ≠ 공구EL − 하단깊이 {b.top_el - s.bottom_depth:.2f}")
|
||
if s.bottom_depth <= prev_d:
|
||
issues.append(f"{b.id} {s.name}: 하단깊이 {s.bottom_depth}가 위 지층({prev_d})보다 깊지 않습니다.")
|
||
prev_d = s.bottom_depth
|
||
if s.name not in cfg.strata_order:
|
||
issues.append(f"{b.id}: 지층명 '{s.name}'이 지층 구분 {list(cfg.strata_order)}에 없습니다.")
|
||
else:
|
||
rank = cfg.strata_order.index(s.name)
|
||
if rank < prev_rank:
|
||
issues.append(f"{b.id}: '{s.name}'이 더 깊은 지층 아래에 있습니다 (지층 순서 확인).")
|
||
prev_rank = max(prev_rank, rank)
|
||
if not b.strata:
|
||
issues.append(f"{b.id}: 지층 행이 없습니다.")
|
||
return list(bores.values()), issues
|
||
|
||
|
||
def write_borehole_template(path: str | Path, bore_positions: list[dict], dtm=None) -> Path:
|
||
"""시추공 위치(지형 DXF)를 미리 채운 '시추' 시트 양식을 만든다. 공구EL은 DTM 값을 참고값으로 넣는다."""
|
||
from openpyxl import Workbook
|
||
from openpyxl.comments import Comment
|
||
from openpyxl.styles import Font, PatternFill
|
||
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "시추"
|
||
ws.append(COLUMNS)
|
||
for c in ws[1]:
|
||
c.font = Font(bold=True)
|
||
c.fill = PatternFill("solid", fgColor="DDE6F0")
|
||
gray = Font(color="7F7F7F")
|
||
for b in bore_positions:
|
||
z = dtm.z_at(b["X"], b["Y"]) if dtm is not None else None
|
||
ws.append([b["id"], round(b["X"], 3), round(b["Y"], 3), None if z is None else round(z, 2),
|
||
None, None, None, None, None])
|
||
cell = ws.cell(row=ws.max_row, column=4)
|
||
if z is not None:
|
||
cell.font = gray
|
||
cell.comment = Comment("지형 DTM에서 구한 참고값입니다. 주상도의 공구 EL로 바꿔 주세요.", "pierzainer")
|
||
for i, w in enumerate([10, 13, 13, 9, 9, 9, 9, 7, 11], start=1):
|
||
ws.column_dimensions[chr(64 + i)].width = w
|
||
ws.freeze_panes = "A2"
|
||
|
||
info = wb.create_sheet("설명")
|
||
for line in [
|
||
["시추 시트 작성 방법"],
|
||
["한 행 = 한 지층. 같은 공번의 지층을 위에서 아래 순서로 적습니다."],
|
||
["X·Y·공구EL·지하수위깊이는 공의 첫 행에만 적어도 됩니다."],
|
||
["하단EL을 비우면 공구EL − 하단깊이로 계산합니다."],
|
||
["지층명: 매립토 / 퇴적토 / 풍화토 / 풍화암 / 연암 / 경암"],
|
||
["좌표: 측량 X(북) · Y(동), m"],
|
||
["회색 공구EL은 지형도에서 구한 참고값입니다."],
|
||
]:
|
||
info.append(line)
|
||
info.column_dimensions["A"].width = 70
|
||
path = Path(path)
|
||
wb.save(path)
|
||
return path
|