Files
Pier_v1/pierzainer_ground/pierzainer_ground/auto_inputs.py
T

244 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Resolve terrain and coordinate axes from the drawing and pier input alone."""
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
import numpy as np
from ezdxf import recover
from pyproj import CRS
from scipy.spatial import QhullError
from .config import GroundConfig
from .dtm import DTM
from .dxf_terrain import TerrainData, convert_dwg_to_dxf, read_terrain_dxf, read_source_document
from .piers import pier_from_dict, read_piers
from .project_coordinates import workbook_piers
from .survey_terrain import read_survey_block
def pier_controls(path):
piers=([pier_from_dict(p) for p in workbook_piers(path)]
if Path(path).suffix.lower()=='.xlsx' else read_piers(path))
if not piers:raise ValueError('교각 Excel에서 IP1/IP3 좌표를 찾지 못했습니다.')
return piers
def referenced_profile(source,root):
"""Reuse established source conventions only for an identical original file."""
digest=hashlib.sha256(source.read_bytes()).hexdigest()
for folder in sorted((root/'data/input').iterdir()):
config=folder/'project.json'
if not config.is_file():continue
profile=json.loads(config.read_text(encoding='utf-8'))
if not profile.get('terrain_source_available'):continue
for original in folder.iterdir():
if original.suffix.lower()!=source.suffix.lower() or original.stat().st_size!=source.stat().st_size:continue
if hashlib.sha256(original.read_bytes()).hexdigest()==digest:
return profile,original.name
return None,None
def referenced_coordinate_choice(source,workbook,root):
"""Retain a user's earlier CRS choice for exactly the same two inputs."""
path=root/'data/input/coordinate_references.json'
if not path.is_file():return None
drawing_hash=hashlib.sha256(source.read_bytes()).hexdigest()
workbook_hash=hashlib.sha256(Path(workbook).read_bytes()).hexdigest()
for reference in json.loads(path.read_text(encoding='utf-8')).get('references',[]):
if reference.get('drawing_sha256')==drawing_hash and reference.get('pier_sha256')==workbook_hash:
crs=CRS.from_user_input(reference['crs'])
if crs.is_projected and all(abs(a.unit_conversion_factor-1)<1e-8 for a in crs.axis_info[:2]):return reference
return None
def explicit_crs(doc,workbook):
"""Only explicit EPSG/WKT metadata qualifies; coordinates alone do not."""
found=set()
texts=[]
for obj in doc.objects:
if obj.dxftype()=='GEODATA':
definition=getattr(obj,'coordinate_system_definition','')
if definition:
texts.append(definition)
try:
epsg=CRS.from_wkt(definition).to_epsg()
if epsg:found.add(epsg)
except Exception:pass
for block in doc.blocks:
for entity in block:
if entity.dxftype() in ('TEXT','MTEXT'):
text=entity.plain_text() if entity.dxftype()=='MTEXT' else entity.dxf.text
if 'epsg' in text.lower():texts.append(text)
if Path(workbook).suffix.lower()=='.xlsx':
import openpyxl
book=openpyxl.load_workbook(workbook,data_only=True,read_only=True)
try:
for sheet in book:
for row in sheet.iter_rows(values_only=True):
texts.extend(v for v in row if isinstance(v,str) and 'epsg' in v.lower())
finally:book.close()
for text in texts:
found.update(int(v) for v in re.findall(r'EPSG\s*[:=:]?\s*(\d{4,6})',text,re.I))
valid=[]
for code in sorted(found):
try:
crs=CRS.from_epsg(code)
if crs.is_projected and all(abs(a.unit_conversion_factor-1)<1e-8 for a in crs.axis_info[:2]):valid.append(crs.to_string())
except Exception:pass
return (valid[0] if len(valid)==1 else ''),valid
def read_auto_inputs(job,work,log):
from .drawing_job import ROOT
source=Path(job.drawing)
piers=pier_controls(job.piers)
centers=np.array([[p.X,p.Y] for p in piers])
if not np.isfinite(centers).all():raise ValueError('교각 Excel 좌표에 유효하지 않은 수가 있습니다.')
profile,profile_source=referenced_profile(source,ROOT)
coordinate_choice=referenced_coordinate_choice(source,job.piers,ROOT)
if source.suffix.lower()=='.dwg':
log('DWG를 DXF로 변환하는 중... (원본이 같으면 변환 캐시 재사용)')
work.mkdir(parents=True,exist_ok=True)
source=convert_dwg_to_dxf(source,work)
log('도면의 측량 블록·실제 표고점과 Excel 교각 중심을 대조하는 중...')
document,_=read_source_document(source,work)
crs,crs_candidates=explicit_crs(document,job.piers)
job.crs=crs;job.terrain_crs=crs
job.terrain_block='';job.layers='';job.use_3d_lines=False
job.z_min=-500.;job.z_max=9000.
status='explicit_input_metadata' if crs else 'unresolved_no_input_metadata'
if not crs and coordinate_choice:
job.crs=coordinate_choice['crs'];job.terrain_crs=coordinate_choice.get('terrain_crs',job.crs)
status=coordinate_choice['status']
log(f'동일한 두 입력의 기존 사용자 지정 좌표계 적용: {job.crs} (공인 기준점 검증 전)')
if profile:
job.crs=profile['crs'];job.terrain_crs=profile['terrain_crs']
job.z_min=0.;job.z_max=200.
status=profile.get('crs_status','existing_source_reference')
log(f'동일 원본의 기존 좌표 처리 규약 재사용: {profile_source}')
default_applied=False
if not job.crs:
defaults_path=ROOT/'data/input/map_defaults.json'
if defaults_path.is_file():
defaults=json.loads(defaults_path.read_text(encoding='utf-8'))
default_crs=CRS.from_user_input(defaults['crs'])
if not default_crs.is_projected or any(abs(a.unit_conversion_factor-1)>1e-8 for a in default_crs.axis_info[:2]):
raise ValueError('프로젝트 기본 지도 좌표계는 m 단위 평면좌표계여야 합니다.')
job.crs=default_crs.to_string();job.terrain_crs=job.crs
status=defaults['status'];default_applied=True
log(f'원본에 좌표계 표기 없음 → 프로젝트 기본 {job.crs}로 지도 포함 생성 (좌표계 가정)')
job.auto_detection={'crs_status':status,'crs_candidates':crs_candidates,
'crs_assumed':bool(profile) or bool(coordinate_choice and not crs) or default_applied,
'project_default_applied':default_applied,'source_reference':profile_source,
'method':'terrain_TIN_coverage_at_Excel_IP1_IP3_centers',
'arbitrary_offset_applied':False}
cfg=GroundConfig(use_profile_points=False,z_min=job.z_min,z_max=job.z_max,
tin_max_edge=job.max_edge,use_3d_lines=False)
names=[]
for block in document.blocks:
if block.name.lower() in ('*paper_space','*model_space'):
if block.name.lower()!='*model_space':continue
elif block.name.startswith('*'):continue
measured=0
for e in block:
if e.dxftype()=='POINT' and 1e-6<abs(e.dxf.location.z)<9000:measured+=1
elif e.dxftype()=='LWPOLYLINE' and 1e-6<abs(e.dxf.elevation)<9000:measured+=1
elif e.dxftype()=='POLYLINE':
vertices=list(e.points_in_wcs())
if len(vertices)>1 and 1e-6<abs(vertices[0].z)<9000 and max(p.z for p in vertices)-min(p.z for p in vertices)<1e-6:
measured+=1
if measured>=3:break
if measured>=3:names.append(block.name)
candidates=[]
lower=centers.min(0)-job.margin-job.max_edge
upper=centers.max(0)+job.margin+job.max_edge
for name in names:
for axes in ('YX','XY'):
if profile and name!=profile['terrain_block']:continue
try:
points,counts,contours=read_survey_block(source,{
'terrain_block':name,'terrain_crs':job.terrain_crs,'crs':job.crs,
'cad_axes':axes,'contour_step_m':cfg.contour_step,'dedup_m':cfg.dedup_xy,
'z_min':job.z_min,'z_max':job.z_max,'exclude_zero':True},doc=document)
local=points[np.all((points[:,:2]>=lower)&(points[:,:2]<=upper),axis=1)]
if len(local)<3:continue
dtm=DTM(local,job.max_edge); values=dtm.z_many(centers)
covered=int(np.isfinite(values).sum())
if not covered:continue
candidates.append({'name':name,'axes':axes,'points':points,'counts':counts,
'contours':contours,'covered':covered,'values':values,'nearby':len(local)})
log(f'지형 후보 {name} / CAD {axes}: 교각 {covered}/{len(piers)}개에서 실측 TIN 확인')
except (ValueError,KeyError,QhullError):continue
if not candidates:
# Preserve the established text/contour layer reader as a fallback.
for axes in ('YX','XY'):
cfg.cad_axes=axes
terrain=read_terrain_dxf(source,cfg,work,document=document)
points=terrain.points
if len(points)<3:continue
points=points[(np.abs(points[:,2])>1e-6)&np.all((points[:,:2]>=lower)&(points[:,:2]<=upper),axis=1)]
if len(points)<3:continue
try:values=DTM(points,job.max_edge).z_many(centers)
except (ValueError,QhullError):continue
covered=int(np.isfinite(values).sum())
if covered:candidates.append({'name':'*Model_Space','axes':axes,'points':points,
'counts':{},'contours':len(terrain.contours),'covered':covered,'values':values,'nearby':len(points)})
if not candidates:
# A plan may have been rotated/translated for its printed sheet. Recover
# that frame only when original pier rectangles identify every Excel
# midpoint and axis, with independent hold-out residuals <= 3 cm.
# Unknown numeric CAD lines are never used as terrain in this branch.
from .plan_registration import resolve_plan_registration
registration=(resolve_plan_registration(document,workbook_piers(job.piers))
if len(piers)>=3 and Path(job.piers).suffix.lower()=='.xlsx' else {'status':'no_verified_frame'})
if registration['status']=='registered':
cfg.cad_axes='YX'
layers=tuple(name for name in (*cfg.layers.spot_block,*cfg.layers.spot_text,*cfg.layers.contour) if name in document.layers)
if layers:
cfg.allowed_terrain_layers=layers
original=read_terrain_dxf(source,cfg,work,document=document)
transform=registration['solutions'][0]
rotation=np.array(transform['rotation']);translation=np.array(transform['translation'])
xy=original.points[:,[1,0]]@rotation+translation
points=np.column_stack([xy[:,1],xy[:,0],original.points[:,2]])
local=points[np.all((points[:,:2]>=lower)&(points[:,:2]<=upper),axis=1)]
if len(local)>=3:
values=DTM(local,job.max_edge).z_many(centers)
if np.isfinite(values).all():
job.cad_axes='YX';job.terrain_block='*Model_Space';job.layers=','.join(layers)
registration.update(source_sha256=hashlib.sha256(Path(job.drawing).read_bytes()).hexdigest(),
workbook_sha256=hashlib.sha256(Path(job.piers).read_bytes()).hexdigest(),terrain_layers=list(layers))
job.auto_detection.update(plan_registration=registration,covered_piers=len(piers),total_piers=len(piers),
terrain_block=job.terrain_block,cad_axes=job.cad_axes)
original.points=points;original.contours=[];original.bridge_lines=[];original.pier_labels=[]
for bore in original.boreholes:
east,north=np.array([bore['Y'],bore['X']])@rotation+translation
bore.update(X=float(north),Y=float(east))
original.stats.update(source_block='*Model_Space',block_selection='verified_plan_pier_geometry_registration',
plan_registration=registration)
log(f"원본 평면 교각 {len(piers)}개로 배치 좌표 복구: 최대 잔차 {max(transform['residual_m']):.9f} m. 표고 변경 없음.")
return original,cfg
if not candidates:
raise ValueError('두 입력 파일에서 교각 위치와 겹치는 실제 지형을 찾지 못했습니다. 원본 도면에 측량 지형/XREF가 포함되어 있는지 확인이 필요합니다. 임의 좌표 보정이나 지형 생성은 하지 않았습니다.')
candidates.sort(key=lambda c:(c['covered'],c['nearby']),reverse=True)
best=candidates[0]
for other in candidates[1:]:
if other['covered']!=best['covered']:continue
shared=np.isfinite(other['values'])&np.isfinite(best['values'])
if np.any(shared) and np.max(np.abs(other['values'][shared]-best['values'][shared]))>.05:
raise ValueError('교각 위치에서 서로 다른 표고를 가진 지형 원본이 중복됩니다. 원도면의 최신 측량 지형을 확인해야 합니다: '+best['name']+', '+other['name'])
job.terrain_block=best['name'];job.cad_axes=best['axes'];cfg.cad_axes=best['axes']
job.auto_detection.update(terrain_block=best['name'],cad_axes=best['axes'],
covered_piers=best['covered'],total_piers=len(piers))
log(f"자동 선택: {best['name']} / CAD {best['axes']} / 교각 {best['covered']}/{len(piers)}개")
if not job.crs:log('원본에 EPSG/지도 좌표계 표기가 없습니다. 지반면은 Excel과 일치하는 측량 좌표로 생성합니다.')
terrain=TerrainData(best['points'],np.full(len(best['points']),'survey'),stats={
'file':source.name,'points_used':len(best['points']),'removed':0,
'source_block':best['name'],'source_counts':best['counts'],'contours':best['contours'],
'block_selection':'automatic_Excel_coordinate_TIN_coverage',
'source_metadata_repair':getattr(document,'_ground_source_repair',None)})
return terrain,cfg