knowledge (구 Aislo-law 독립 저장소 → resources/knowledge 이관, 저장소 폐지): - 법령·행정규칙·표준시방서·교본 원문 + 기술문서 55건 + 실무 분석·종합비교 - 루트 지침 체계: README(지도)·00_운영지침·01_수집지침·02_분석지침· 03_미결_및_확인사항(교본 충돌 리스트 포함)·04_참조_법령기준_목록 - 기술문서 55건 원문 전수 검증 완료 (사방 설계홍수량 법정 기준 등 반영) - 정리: CAD·오피스 잔재 142건, 중복 zip 7건(413MB), 빈 폴더 30개 제거 resources 그룹 재편 (이름순 그룹핑): - app_branding(구 prog_icon.jpg)·app_policies(구 legal)· data_global_contours(구 grobal_contours)·data_rainfall_idf_cache(구 wamis_contours)· template_2dDrawing(구 dwg_analysis/templete — 오타 교정, 상수·경로 동기화) - dwg_analysis(분석 완료 1.8GB)·templates(빈 폴더)·templete_calc_cost.xlsx 삭제 - 참조 코드 5파일 경로 수정 + 프론트 재빌드 (구 경로 잔존 0) - .gitignore: resources 추적 전환, national_contours.gpkg(22GB) 영구 제외 - .env: knowledge 수집용 API 정보 주석 통합 (KCSC·법령센터·조달청 제비율) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
209 lines
7.5 KiB
Python
209 lines
7.5 KiB
Python
"""국가기본도 등고선(TN_CTRLN) → 단일 GeoPackage 변환.
|
|
|
|
기존 convert_to_gpkg.py는 표고 컬럼 후보를 고정 목록으로만 찾아 속성을 통째로
|
|
버렸다. 이 스크립트는 geometry 외 모든 컬럼을 그대로 보존하고, 공간 인덱스와
|
|
조회용 속성 인덱스까지 만들어 크롭 단계에서 바로 쓸 수 있게 한다.
|
|
|
|
원본은 5m 간격 그대로 유지한다. 1m 보간은 프로젝트 크롭 시점에 국소 영역에서만
|
|
수행한다(전국 1m DEM은 계산량이 비현실적).
|
|
|
|
[사전 필수 패키지]
|
|
pip install geopandas pyogrio shapely
|
|
|
|
사용법
|
|
python build_contour_gpkg.py <입력> [-o national_contours.gpkg]
|
|
[--layer contours] [--chunk 50000]
|
|
[--append] [--limit N]
|
|
|
|
<입력> : .zip | .shp | SHP가 든 폴더
|
|
--layer : 저장할 레이어명. 나중에 능선을 --append 로 덧붙일 때 사용
|
|
--append : 기존 gpkg를 지우지 않고 레이어를 추가
|
|
--limit : 소스당 읽을 최대 건수 (동작 확인용)
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import time
|
|
import zipfile
|
|
|
|
try:
|
|
import pyogrio
|
|
except ImportError:
|
|
print("[오류] 필수 라이브러리가 누락되었습니다.")
|
|
print(">>> pip install geopandas pyogrio shapely")
|
|
sys.exit(1)
|
|
|
|
# 레이어별로 조회 인덱스를 걸 컬럼. 없는 컬럼은 조용히 건너뛴다.
|
|
INDEX_COLUMNS = {
|
|
"contours": ("CTRLN_HG", "CTRLN_SE", "TPGRPH_SE", "CONT", "DIVI", "SCLS"),
|
|
"ridges": ("SCLS", "SERV"),
|
|
"streams": ("DIVI", "TYPE", "STAT"),
|
|
}
|
|
|
|
|
|
def collect_sources(path: str) -> list[str]:
|
|
"""입력 경로에서 읽을 SHP 목록을 만든다 (zip은 /vsizip/ 경로로 변환)."""
|
|
path = os.path.abspath(path)
|
|
if path.lower().endswith(".zip"):
|
|
with zipfile.ZipFile(path) as zf:
|
|
inner = sorted(n for n in zf.namelist() if n.lower().endswith(".shp"))
|
|
base = "/vsizip/" + path.replace("\\", "/")
|
|
return [f"{base}/{n}" for n in inner]
|
|
if path.lower().endswith(".shp"):
|
|
return [path]
|
|
if os.path.isdir(path):
|
|
found = sorted(glob.glob(os.path.join(path, "**", "*.shp"), recursive=True))
|
|
if found:
|
|
return found
|
|
out: list[str] = []
|
|
for z in sorted(glob.glob(os.path.join(path, "**", "*.zip"), recursive=True)):
|
|
out.extend(collect_sources(z))
|
|
return out
|
|
return []
|
|
|
|
|
|
def write_source(
|
|
src: str,
|
|
out_path: str,
|
|
layer: str,
|
|
encoding: str,
|
|
chunk: int,
|
|
limit: int | None,
|
|
first: bool,
|
|
) -> int:
|
|
"""SHP 하나를 청크 단위로 읽어 GeoPackage에 이어쓴다.
|
|
|
|
전체를 한 번에 메모리에 올리면 1.8GB SHP에서 수 GB를 쓰게 되므로 나눠 읽는다.
|
|
"""
|
|
info = pyogrio.read_info(src, encoding=encoding)
|
|
total = info["features"] if limit is None else min(info["features"], limit)
|
|
written = 0
|
|
started = time.monotonic()
|
|
|
|
while written < total:
|
|
take = min(chunk, total - written)
|
|
gdf = pyogrio.read_dataframe(
|
|
src, encoding=encoding, skip_features=written, max_features=take
|
|
)
|
|
if not len(gdf):
|
|
break
|
|
# 첫 청크만 새 레이어를 만들고 이후는 append 한다.
|
|
pyogrio.write_dataframe(
|
|
gdf,
|
|
out_path,
|
|
layer=layer,
|
|
driver="GPKG",
|
|
append=not (first and written == 0),
|
|
spatial_index=True,
|
|
promote_to_multi=True,
|
|
)
|
|
written += len(gdf)
|
|
print(
|
|
" %s %s / %s (%.0fs)"
|
|
% (
|
|
os.path.basename(src),
|
|
format(written, ","),
|
|
format(total, ","),
|
|
time.monotonic() - started,
|
|
),
|
|
flush=True,
|
|
)
|
|
if len(gdf) < take:
|
|
break
|
|
return written
|
|
|
|
|
|
def create_attribute_indexes(out_path: str, layer: str) -> list[str]:
|
|
"""GeoPackage는 SQLite이므로 조회용 속성 인덱스를 직접 만든다."""
|
|
made: list[str] = []
|
|
wanted = INDEX_COLUMNS.get(layer, ())
|
|
if not wanted:
|
|
return made
|
|
with sqlite3.connect(out_path) as con:
|
|
cols = {row[1] for row in con.execute(f'PRAGMA table_info("{layer}")')}
|
|
for col in wanted:
|
|
if col not in cols:
|
|
continue
|
|
name = f"idx_{layer}_{col.lower()}"
|
|
con.execute(f'CREATE INDEX IF NOT EXISTS "{name}" ON "{layer}" ("{col}")')
|
|
made.append(col)
|
|
con.commit()
|
|
return made
|
|
|
|
|
|
def summarize(out_path: str) -> None:
|
|
"""생성 결과를 요약 출력한다."""
|
|
print("\n[결과]")
|
|
for name, geom in pyogrio.list_layers(out_path):
|
|
info = pyogrio.read_info(out_path, layer=name)
|
|
print(" 레이어 %-12s %s %s건" % (name, geom, format(info["features"], ",")))
|
|
print(" 컬럼 %s" % list(info["fields"]))
|
|
box = info.get("total_bounds")
|
|
if box is not None:
|
|
print(" 범위 X %.0f~%.0f / Y %.0f~%.0f" % (box[0], box[2], box[1], box[3]))
|
|
size = os.path.getsize(out_path) / (1024**3)
|
|
print(" 파일 %s (%.2f GB)" % (out_path, size))
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="국가 등고선 원본 → GeoPackage 변환")
|
|
parser.add_argument("input", help=".zip | .shp | SHP가 든 폴더")
|
|
parser.add_argument("-o", "--output", default=None, help="출력 gpkg 경로")
|
|
parser.add_argument("--layer", default="contours", help="레이어명 (기본 contours)")
|
|
parser.add_argument("--encoding", default="cp949", help="DBF 인코딩 (기본 cp949)")
|
|
parser.add_argument("--chunk", type=int, default=50000, help="청크 크기 (기본 50000)")
|
|
parser.add_argument("--limit", type=int, default=None, help="소스당 최대 건수 (확인용)")
|
|
parser.add_argument("--append", action="store_true", help="기존 gpkg에 레이어 추가")
|
|
args = parser.parse_args()
|
|
|
|
out_path = args.output or os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "national_contours.gpkg"
|
|
)
|
|
|
|
sources = collect_sources(args.input)
|
|
if not sources:
|
|
print("[오류] 읽을 SHP를 찾지 못했습니다: %s" % args.input)
|
|
sys.exit(1)
|
|
|
|
if os.path.exists(out_path) and not args.append:
|
|
print("[중단] 출력 파일이 이미 있습니다: %s" % out_path)
|
|
print(" 덮어쓰려면 파일을 먼저 옮기거나 --append 로 레이어를 추가하세요.")
|
|
sys.exit(1)
|
|
|
|
info = pyogrio.read_info(sources[0], encoding=args.encoding)
|
|
print("=" * 74)
|
|
print("입력 : %s" % args.input)
|
|
print("소스 : SHP %d개" % len(sources))
|
|
print("컬럼 : %s" % list(info["fields"]))
|
|
print("좌표계 : %s" % info["crs"])
|
|
print("출력 : %s (레이어 %s)" % (out_path, args.layer))
|
|
print("=" * 74)
|
|
|
|
started = time.monotonic()
|
|
total = 0
|
|
for idx, src in enumerate(sources):
|
|
print("\n[%d/%d] %s" % (idx + 1, len(sources), os.path.basename(src)))
|
|
total += write_source(
|
|
src,
|
|
out_path,
|
|
args.layer,
|
|
args.encoding,
|
|
args.chunk,
|
|
args.limit,
|
|
first=(idx == 0 and not args.append),
|
|
)
|
|
|
|
print("\n총 %s건 기록 (%.1f분)" % (format(total, ","), (time.monotonic() - started) / 60))
|
|
|
|
made = create_attribute_indexes(out_path, args.layer)
|
|
print("속성 인덱스: %s" % (", ".join(made) if made else "없음"))
|
|
|
|
summarize(out_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|