feat(760-1): 암상 지질도 겹 · 노선 따라 바위 종류 구간 · 밖 겹 출처 줄
- 서버: 지오빅데이터(KIGAM) 수치지질도 1:50,000 — WFS 암상 면(깨진 한글 칸은 기호별 점 조회로 채움) · 받은 범위 ∪ 노선으로 자름 · 저장본 · 노선 따라 1 m 표본 구간 · 키는 서버만(.env GEOBIGDATA_API_KEY · 쿼리 key) - 그림 조각 대리에 geology(키 쿼리 이름 칸 key_param · 옛 겹 serviceKey 그대로) - 화면(D03 · B05): 「제한·재해」에 「암상 지질도」 — 그림 조각 + 갈래 색 면 · 「기호 · 암상」 글 · 범례 · 노선 구간 표(참고 — 토사 · 암 판정 아님) - 켠 밖 겹마다 범례 맨 아래 출처 줄(브이월드 · 국토지리정보원 · 산림청 · 환경부 · 생활안전지도 공공누리 · 한국지질자원연구원 CC BY-NC) - 범례 칸 높이 한도 · 밀기(낮은 화면) - 시험: test_760_1 새로 · test_634_2 · test_553_4 겹 차례 표에 geology 한 줄 더함(새 겹 · 낡은 표) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
"""D 노선분석 지도 암상 지질도 겹(PLAN 760-1 · 요구 597-3 · 598-3 · 근거 `_분석_597-3`).
|
||||
|
||||
- 출처 = 한국지질자원연구원 지오빅데이터 오픈API(수치지질도 1:50,000 · CC BY-NC) · 키 = `.env`
|
||||
`GEOBIGDATA_API_KEY`(쿼리 `key` · 없으면 `MissingKeyError` · 값은 어떤 로그 · 응답 · 오류 글에도 안 냄)
|
||||
- 그림 = WMS `L_50K_Geology_Map` 조각 대리(`common_util_wms_tiles` 「geology」)
|
||||
- 도형 = WFS `geoOpen:l_50k_geology_litho_latest`(GeoJSON · 경위도로 받아 작업 좌표계로 옮김) ·
|
||||
한글 칸이 「?」로 깨짐 → 기호(`symnum`)마다 그 면 안 점 하나 GetFeatureInfo 로 시대 · 지층 ·
|
||||
대표암상 · 기호를 채움(대문자 전체 인자 · 1.1.1)
|
||||
- 받은 범위로 자르고 기호별 넓이 · 바위 갈래(`ROCK_GROUPS` · 색 = 화면 `--draw-map-geology-*`) ·
|
||||
저장 = `<받은 겹 자리>/암상지질도_bounds.json`(상자가 덮으면 다시 안 받음)
|
||||
- 노선 따라 구간 = `route_segments`(1 m 걸음 표본 · 참고 표시 — 토사 · 암 판정 아님)
|
||||
|
||||
계산 자리 = ③ 서버 단독(브라우저는 KIGAM 을 부르지 않음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Box = tuple[float, float, float, float]
|
||||
|
||||
KEY_ENV = "GEOBIGDATA_API_KEY"
|
||||
WMS_URL = "https://data.kigam.re.kr/openapi/wms"
|
||||
WFS_URL = "https://data.kigam.re.kr/openapi/wfs"
|
||||
MAP_LAYER = "L_50K_Geology_Map"
|
||||
LITHO_TYPE = "geoOpen:l_50k_geology_litho_latest"
|
||||
FILE = "암상지질도_bounds.json"
|
||||
VERSION = 1
|
||||
MAX_FEATURES = 1000
|
||||
TIMEOUT_S = 30
|
||||
STEP_M = 1.0
|
||||
SOURCE = "한국지질자원연구원 지오빅데이터 · 수치지질도 1:50,000 (CC BY-NC)"
|
||||
|
||||
# 바위 갈래(차례 = 화면 `--draw-map-geology-1..6`) — 대표암상의 첫 「…암」 낱말로 고름(없으면 글 통째)
|
||||
ROCK_GROUPS: list[tuple[str, tuple[str, ...]]] = [
|
||||
("심성암(화강암류)", ("화강", "섬록", "반려", "섬장", "몬조", "페그마")),
|
||||
("화산암 · 맥암", ("안산", "유문", "현무", "응회", "조면", "반암", "맥암", "화산", "석영맥")),
|
||||
("퇴적암", ("사암", "셰일", "세일", "이암", "역암", "석회", "미사", "이질", "처트", "백운")),
|
||||
(
|
||||
"변성암",
|
||||
("편마", "편암", "규암", "천매", "대리", "혼펠", "각섬암", "점판", "미그마", "결정질"),
|
||||
),
|
||||
("미고결 퇴적층", ("충적", "자갈", "모래", "점토", "퇴적층", "붕적", "선상지")),
|
||||
]
|
||||
OTHER = len(ROCK_GROUPS) # 그 밖
|
||||
META = 3 # 변성암 — 글 어디든 낱말이 있으면 먼저(「화강편마암」 · 「결정질 석회암」)
|
||||
ROCK_GROUP_NAMES = [name for name, _ in ROCK_GROUPS] + ["그 밖"]
|
||||
SYMBOL = re.compile(r"^[A-Z][A-Za-z0-9-]*$")
|
||||
|
||||
|
||||
class MissingKeyError(RuntimeError):
|
||||
"""`.env` 에 키 없음."""
|
||||
|
||||
|
||||
def rock_group(litho: str) -> int:
|
||||
"""대표암상 → 바위 갈래 번호(0 ~ `OTHER`)."""
|
||||
words = litho.split()
|
||||
head = next((w for w in words if w.endswith(("암", "층")) and not w.endswith("암질")), litho)
|
||||
if any(key in litho for key in ROCK_GROUPS[META][1]):
|
||||
return META
|
||||
for text in (head, litho):
|
||||
for index, (_, keys) in enumerate(ROCK_GROUPS):
|
||||
if any(key in text for key in keys):
|
||||
return index
|
||||
return OTHER
|
||||
|
||||
|
||||
def _get(url: str, params: dict[str, Any]) -> bytes:
|
||||
"""KIGAM 요청 — 키 없음 = `MissingKeyError` · 실패 = `RuntimeError`(오류 글에 주소 · 키 없음)."""
|
||||
key = os.environ.get(KEY_ENV, "").strip()
|
||||
if not key:
|
||||
raise MissingKeyError(f"암상 지질도 키 없음 — .env {KEY_ENV}")
|
||||
query = urllib.parse.urlencode({"key": key, **params})
|
||||
request = urllib.request.Request(f"{url}?{query}", headers={"User-Agent": "Mozilla/5.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=TIMEOUT_S) as response:
|
||||
return response.read()
|
||||
except OSError as error:
|
||||
raise RuntimeError(f"암상 지질도를 받지 못했습니다 — {type(error).__name__}") from None
|
||||
|
||||
|
||||
def _json(body: bytes) -> Any:
|
||||
"""JSON 몸(같은 이름 칸이 겹쳐도 다 남김 — 깨진 한글 칸 「??」) · 아니면 `RuntimeError`."""
|
||||
try:
|
||||
return json.loads(body.decode("utf-8"), object_pairs_hook=list)
|
||||
except ValueError:
|
||||
raise RuntimeError("암상 지질도를 받지 못했습니다 — JSON 이 아닌 응답") from None
|
||||
|
||||
|
||||
def _pairs_get(pairs: Any, name: str) -> Any:
|
||||
return next((v for k, v in pairs if k == name), None) if isinstance(pairs, list) else None
|
||||
|
||||
|
||||
def parse_litho(body: bytes) -> list[dict[str, Any]]:
|
||||
"""WFS GeoJSON → [{symnum, symbol, geometry(경위도 GeoJSON)}] — 기호 = 깨진 칸 중 영문 기호 값."""
|
||||
features = _pairs_get(_json(body), "features") or []
|
||||
out = []
|
||||
for feature in features:
|
||||
props = _pairs_get(feature, "properties") or []
|
||||
geometry = _pairs_get(feature, "geometry")
|
||||
if not geometry:
|
||||
continue
|
||||
symnum = str(_pairs_get(props, "symnum") or "")
|
||||
symbol = next(
|
||||
(
|
||||
str(v)
|
||||
for k, v in props
|
||||
if k not in ("symnum", "mapname", "mapidx") and SYMBOL.match(str(v))
|
||||
),
|
||||
"",
|
||||
)
|
||||
out.append({"symnum": symnum or symbol, "symbol": symbol, "geometry": _plain(geometry)})
|
||||
return out
|
||||
|
||||
|
||||
def _plain(value: Any) -> Any:
|
||||
"""`object_pairs_hook=list` 로 읽은 값 → 보통 dict · list."""
|
||||
if (
|
||||
isinstance(value, list)
|
||||
and value
|
||||
and all(isinstance(p, tuple) and len(p) == 2 for p in value)
|
||||
):
|
||||
return {k: _plain(v) for k, v in value}
|
||||
if isinstance(value, list):
|
||||
return [_plain(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def fetch_litho(bounds: dict[str, float]) -> list[dict[str, Any]]:
|
||||
"""받은 범위(경위도 상자) 안 암상 면."""
|
||||
body = _get(
|
||||
WFS_URL,
|
||||
{
|
||||
"service": "WFS",
|
||||
"version": "1.1.0",
|
||||
"request": "GetFeature",
|
||||
"typeName": LITHO_TYPE,
|
||||
"maxFeatures": MAX_FEATURES,
|
||||
"outputFormat": "application/json",
|
||||
"srsName": "EPSG:4326",
|
||||
"bbox": f"{bounds['min_lon']},{bounds['min_lat']},{bounds['max_lon']},{bounds['max_lat']},EPSG:4326",
|
||||
},
|
||||
)
|
||||
return parse_litho(body)
|
||||
|
||||
|
||||
def fetch_names(lon: float, lat: float) -> list[dict[str, Any]]:
|
||||
"""점 하나의 암상 칸(한글 정상) — 그 점을 덮는 면들의 속성."""
|
||||
d = 1e-4
|
||||
body = _get(
|
||||
WMS_URL,
|
||||
{
|
||||
"SERVICE": "WMS",
|
||||
"VERSION": "1.1.1",
|
||||
"REQUEST": "GetFeatureInfo",
|
||||
"LAYERS": MAP_LAYER,
|
||||
"QUERY_LAYERS": MAP_LAYER,
|
||||
"SRS": "EPSG:4326",
|
||||
"BBOX": f"{lon - d},{lat - d},{lon + d},{lat + d}",
|
||||
"WIDTH": 11,
|
||||
"HEIGHT": 11,
|
||||
"X": 5,
|
||||
"Y": 5,
|
||||
"INFO_FORMAT": "application/json",
|
||||
"FORMAT": "image/png",
|
||||
"STYLES": "",
|
||||
},
|
||||
)
|
||||
got = _plain(_json(body))
|
||||
return [f.get("properties") or {} for f in (got or {}).get("features", [])]
|
||||
|
||||
|
||||
def _names_for(symnum: str, symbol: str, lon: float, lat: float) -> dict[str, str]:
|
||||
"""기호 하나의 이름 칸 — 못 받으면 기호만."""
|
||||
try:
|
||||
rows = fetch_names(lon, lat)
|
||||
except RuntimeError as error:
|
||||
logger.warning("%s", error)
|
||||
rows = []
|
||||
row = next((r for r in rows if str(r.get("symnum")) == symnum), rows[0] if rows else {})
|
||||
return {
|
||||
"symbol": str(row.get("기호") or symbol),
|
||||
"litho": str(row.get("대표암상") or ""),
|
||||
"strata": str(row.get("지층") or ""),
|
||||
"age": str(row.get("시대") or ""),
|
||||
"sheet": str(row.get("mapname") or ""),
|
||||
}
|
||||
|
||||
|
||||
def build_layer(
|
||||
raw: list[dict[str, Any]], box: Box, epsg: int, names: Any = _names_for
|
||||
) -> dict[str, Any]:
|
||||
"""받은 면 → 받은 범위로 자른 겹(경위도 지물 · 기호별 요약 · 작업 좌표계 면)."""
|
||||
from pyproj import Transformer
|
||||
from shapely.geometry import box as shape_box
|
||||
from shapely.geometry import mapping, shape
|
||||
from shapely.ops import transform
|
||||
|
||||
to_m = Transformer.from_crs("EPSG:4326", f"EPSG:{epsg}", always_xy=True).transform
|
||||
to_ll = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True).transform
|
||||
frame = shape_box(*box)
|
||||
by_sym: dict[str, dict[str, Any]] = {}
|
||||
parts: list[tuple[str, Any]] = []
|
||||
for item in raw:
|
||||
try:
|
||||
geom = transform(to_m, shape(item["geometry"])).buffer(0).intersection(frame)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
continue
|
||||
if geom.is_empty or geom.area <= 0:
|
||||
continue
|
||||
parts.append((item["symnum"], geom))
|
||||
unit = by_sym.setdefault(
|
||||
item["symnum"], {"symnum": item["symnum"], "symbol": item["symbol"], "big": geom}
|
||||
)
|
||||
if geom.area > unit["big"].area:
|
||||
unit["big"] = geom
|
||||
summary = []
|
||||
for symnum, unit in by_sym.items():
|
||||
lon, lat = to_ll(*unit["big"].representative_point().coords[0])
|
||||
info = names(symnum, unit["symbol"], lon, lat)
|
||||
area = sum(g.area for s, g in parts if s == symnum)
|
||||
count = sum(1 for s, _ in parts if s == symnum)
|
||||
summary.append(
|
||||
{
|
||||
"symnum": symnum,
|
||||
**info,
|
||||
"group": rock_group(info["litho"]),
|
||||
"count": count,
|
||||
"area_m2": round(area, 1),
|
||||
}
|
||||
)
|
||||
summary.sort(key=lambda s: -s["area_m2"])
|
||||
index = {s["symnum"]: i for i, s in enumerate(summary)}
|
||||
features = []
|
||||
for symnum, geom in parts:
|
||||
unit = summary[index[symnum]]
|
||||
features.append(
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"i": index[symnum],
|
||||
"symbol": unit["symbol"],
|
||||
"litho": unit["litho"],
|
||||
"group": unit["group"],
|
||||
"area_m2": round(geom.area, 1),
|
||||
},
|
||||
"geometry": mapping(transform(to_ll, geom.simplify(1.0))),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"version": VERSION,
|
||||
"box_m": [float(v) for v in box],
|
||||
"source": SOURCE,
|
||||
"summary": summary,
|
||||
"features": features,
|
||||
"parts_m": [[index[s], mapping(g)] for s, g in parts],
|
||||
}
|
||||
|
||||
|
||||
def ensure_geology(processed: Path, box: Box, epsg: int) -> dict[str, Any]:
|
||||
"""암상 겹 — 저장 상자가 덮으면 그대로 · 아니면 받아 지음."""
|
||||
from D03_PlanAnalysis.D03_PlanAnalysis_Layers import _covers, _lonlat_bounds
|
||||
|
||||
path = processed / FILE
|
||||
try:
|
||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
saved = None
|
||||
if (
|
||||
isinstance(saved, dict)
|
||||
and saved.get("version") == VERSION
|
||||
and _covers(saved.get("box_m"), box)
|
||||
):
|
||||
return saved
|
||||
started = time.perf_counter()
|
||||
raw = fetch_litho(_lonlat_bounds(box, epsg))
|
||||
value = build_layer(raw, box, epsg)
|
||||
processed.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, value)
|
||||
logger.info(
|
||||
"D 암상 지질도 받기: 면 %d · 기호 %d · %.2f s",
|
||||
len(raw),
|
||||
len(value["summary"]),
|
||||
time.perf_counter() - started,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def route_segments(
|
||||
parts_m: list[list[Any]], points: list[list[float]], step: float = STEP_M
|
||||
) -> list[dict[str, Any]]:
|
||||
"""노선(작업 좌표계 꺾은선) 따라 암상 구간 — [{from_m, to_m, i}] · i = 요약 차례 · -1 = 자료 없음.
|
||||
|
||||
ponytail: 1 m 걸음 표본 — 경계 오차 ≤ 1 m(1:50,000 지질 경계 어긋남 수십 m 보다 작음)."""
|
||||
from shapely import contains_xy
|
||||
from shapely.geometry import LineString, shape
|
||||
|
||||
if len(points) < 2:
|
||||
return []
|
||||
line = LineString([(float(p[0]), float(p[1])) for p in points])
|
||||
length = line.length
|
||||
if length <= 0:
|
||||
return []
|
||||
count = max(1, int(np.ceil(length / step)))
|
||||
edges = np.minimum(np.arange(count + 1) * step, length)
|
||||
mids = (edges[:-1] + edges[1:]) / 2
|
||||
xy = np.array([line.interpolate(float(m)).coords[0] for m in mids])
|
||||
owner = np.full(count, -1)
|
||||
for i, geometry in parts_m:
|
||||
hit = (owner < 0) & contains_xy(shape(geometry), xy[:, 0], xy[:, 1])
|
||||
owner[hit] = int(i)
|
||||
segments: list[dict[str, Any]] = []
|
||||
for k in range(count):
|
||||
if segments and segments[-1]["i"] == owner[k]:
|
||||
segments[-1]["to_m"] = round(float(edges[k + 1]), 1)
|
||||
else:
|
||||
segments.append(
|
||||
{
|
||||
"from_m": round(float(edges[k]), 1),
|
||||
"to_m": round(float(edges[k + 1]), 1),
|
||||
"i": int(owner[k]),
|
||||
}
|
||||
)
|
||||
return segments
|
||||
|
||||
|
||||
def project_routes(root: Path) -> list[dict[str, Any]]:
|
||||
"""프로젝트 노선 — 예정노선 = 세트마다 이은 선 · 그 밖 = 계획노선(수정본 → 초기 폴리라인)."""
|
||||
from common_util.common_util_route_geometry import (
|
||||
planned_route_initial_path,
|
||||
planned_route_working_path,
|
||||
read_planned_route_csv,
|
||||
)
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Store as store
|
||||
|
||||
plan = store.read_plan(root)
|
||||
if plan and plan.get("lines"):
|
||||
try:
|
||||
_, units = store.plan_parts(plan)
|
||||
except Exception as error: # 그물 오류 — 구간 표만 빠짐
|
||||
logger.warning("암상 구간 — 예정노선을 못 읽음: %s", error)
|
||||
return []
|
||||
return [
|
||||
{"name": f"{u['line_name']} {u['id']}", "points": u["planned"]}
|
||||
for u in units
|
||||
if len(u["planned"]) >= 2
|
||||
]
|
||||
for path in (planned_route_working_path(root), planned_route_initial_path(root)):
|
||||
if path.is_file():
|
||||
route = read_planned_route_csv(path)
|
||||
if route is not None and len(route.vertices) >= 2:
|
||||
return [{"name": "계획노선", "points": [[v.x, v.y] for v in route.vertices]}]
|
||||
return []
|
||||
|
||||
|
||||
def cover_box(box: Box, routes: list[dict[str, Any]], pad: float = 100.0) -> Box:
|
||||
"""받을 상자 = 받은 범위 ∪ 노선(+ 여유) — 노선 끝이 지형 범위 밖이어도 구간이 빈칸 안 됨."""
|
||||
xs = [float(p[0]) for r in routes for p in r["points"]]
|
||||
ys = [float(p[1]) for r in routes for p in r["points"]]
|
||||
if not xs:
|
||||
return box
|
||||
return (
|
||||
min(box[0], min(xs) - pad),
|
||||
min(box[1], min(ys) - pad),
|
||||
max(box[2], max(xs) + pad),
|
||||
max(box[3], max(ys) + pad),
|
||||
)
|
||||
|
||||
|
||||
def layer_response(value: dict[str, Any], routes: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""화면 답 — 경위도 지물 · 요약 · 노선마다 구간(작업 좌표계 면은 안 내보냄)."""
|
||||
parts = value.get("parts_m", [])
|
||||
return {
|
||||
"source": value.get("source", SOURCE),
|
||||
"summary": value["summary"],
|
||||
"groups": ROCK_GROUP_NAMES,
|
||||
"type": "FeatureCollection",
|
||||
"features": value["features"],
|
||||
"routes": [
|
||||
{
|
||||
"name": r["name"],
|
||||
"segments": route_segments(parts, r["points"]),
|
||||
}
|
||||
for r in routes
|
||||
],
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
- `GET …/plan/progress` — 일의 진행률
|
||||
- `GET …/plan/tile/{z}/{x}/{y}` — 위성 조각 대리 · 서버 기억(586 GL 바탕)
|
||||
- `GET …/plan/layers/sheet?key=` · `…/cadastral` · `…/raster?kind=slope|hillshade|landslide` ·
|
||||
`…/zones` · `…/soils` · `…/bands` · `…/lines?kind=` · `…/zero-order` ·
|
||||
`…/zones` · `…/soils` · `…/geology`(760-1) · `…/bands` · `…/lines?kind=` · `…/zero-order` ·
|
||||
`…/forest-roads`(GET · POST 올리기 · `/bundles` 목록 · DELETE `/<번호>`) ·
|
||||
`…/landslide-zones` — 지도 겹 (PLAN 569-1 · 581-1 · 581-2 · 584-1 · 584-3 · 605-1)
|
||||
- `GET …/plan/sheet-tiles?bbox=` · `…/sheet-tiles/{도엽}/{겹}` — 보이는 도엽 · 도엽 조각(606)
|
||||
@@ -41,6 +41,7 @@ from common_util.common_util_http_cache import cached_file_response
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_ForestRoad as forest_road
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Geology as geology
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Landslide as landslide
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_LandslideZone as landslide_zone
|
||||
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
|
||||
@@ -392,6 +393,21 @@ async def plan_layer_soils(project_id: UUID, request: Request) -> Response:
|
||||
return cached_file_response(request, got["path"], "application/geo+json")
|
||||
|
||||
|
||||
@router.get("/{project_id}/plan/layers/geology", response_model=None)
|
||||
async def plan_layer_geology(project_id: UUID) -> dict[str, Any]:
|
||||
"""암상 지질도(KIGAM 5만 · 760-1) — 기호별 요약 · 경위도 면 · 노선마다 암상 구간(참고)."""
|
||||
root, epsg, box, processed, *_ = await _layer_root(project_id)
|
||||
try:
|
||||
routes = await asyncio.to_thread(geology.project_routes, root)
|
||||
box = geology.cover_box(box, routes)
|
||||
value = await asyncio.to_thread(geology.ensure_geology, processed, box, epsg)
|
||||
except geology.MissingKeyError as error:
|
||||
raise HTTPException(status_code=503, detail=str(error)) from error
|
||||
except RuntimeError as error:
|
||||
raise HTTPException(status_code=502, detail=str(error)) from error
|
||||
return {"status": "success", **geology.layer_response(value, routes)}
|
||||
|
||||
|
||||
@router.get("/{project_id}/plan/layers/bands", response_model=None)
|
||||
async def plan_layer_bands(project_id: UUID) -> dict[str, Any]:
|
||||
"""계류 100 m · 도로 300 m 띠(넓이 · 받은 범위 비율) — 두 도엽 겹 지문 같으면 저장본."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- `GET /api/projects/{id}/plan/vt/{name}/{z}/{x}/{y}.pbf` — 벡터 조각(forest_road · forest_soil ·
|
||||
eco_forest · 공용 창고 · `common_util_vector_tiles`)
|
||||
- `GET …/plan/wmt/{name}/{z}/{x}/{y}.png` — 그림 조각 대리(ecology · landslide · 처음 받고 기억 ·
|
||||
- `GET …/plan/wmt/{name}/{z}/{x}/{y}.png` — 그림 조각 대리(ecology · landslide · geology 760-1 · 처음 받고 기억 ·
|
||||
`common_util_wms_tiles`) · 빈 · 오류 조각 = 투명 PNG + `X-Tile-Empty` · 짧게 기억
|
||||
권한 = 다른 plan 길과 같음(`main.py` 등록 때 회사 보호).
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
PlanBand,
|
||||
} from "./D03_PlanAnalysis_Api";
|
||||
import { LINE_KEYS, type LineLayerKey } from "../A00_Common/map/map_layer_list";
|
||||
import type { PlanGeology } from "../A00_Common/map/map_geology";
|
||||
import { bandRatios, type SlopeBand } from "../A00_Common/map/logic/map_slope_bands";
|
||||
|
||||
/** 범례 줄 = 공용(624-3 · B05 평면도 씀) — 옛 이름으로 다시 내놓음. */
|
||||
@@ -58,6 +59,8 @@ export type Prepared = Record<LineLayerKey, PreparedLines> & {
|
||||
bands: { stream: PreparedLayer; road: PreparedLayer; info: { stream: PlanBand; road: PlanBand } };
|
||||
building: PreparedLayer;
|
||||
place: Array<{ lon: number; lat: number; name: string }>;
|
||||
/** 암상 지질도(760-1) — 받은 그대로(GL 만 그림). */
|
||||
geology: PlanGeology;
|
||||
};
|
||||
|
||||
/** 검증 창구 — 겹별 수(지물 · 넓이 m² · 길이 m). */
|
||||
|
||||
@@ -35,6 +35,7 @@ import { bandCollection } from "../A00_Common/map/map_layer_data";
|
||||
import { LAYERS, LINE_LAYERS, ZONE_COLORS, type RoadPick } from "../A00_Common/map/map_layer_list";
|
||||
import type { TerrainSource } from "../A00_Common/map/map_terrain_chip";
|
||||
import { createMapLegend } from "../A00_Common/map/map_legend";
|
||||
import { fetchPlanGeology, geologyCollection, geologyTail } from "../A00_Common/map/map_geology";
|
||||
import { fetchVWorldMeta, type VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import {
|
||||
createNormalizer,
|
||||
@@ -207,7 +208,8 @@ export function createPlanLayers(opts: {
|
||||
for (const def of LAYERS) gl.setVisible(def.key, on[def.key]);
|
||||
gl.setData("hillshade", hillshade && (on.satellite ? hillshade.shade : hillshade.plain));
|
||||
const light = meta && glOn && on.satellite; // 옛 캔버스 = 위성 없음
|
||||
legendBox.show(meta ? legendRows(on, data) : [], light ? [lightRow] : []);
|
||||
const tail = [...(meta ? geologyTail(on, data.geology) : []), ...(light ? [lightRow] : [])];
|
||||
legendBox.show(meta ? legendRows(on, data) : [], tail);
|
||||
if (meta && on.slope && data.slope) legend.firstElementChild?.append(gear);
|
||||
}
|
||||
|
||||
@@ -543,6 +545,9 @@ export function createPlanLayers(opts: {
|
||||
} finally {
|
||||
window.clearTimeout(slow);
|
||||
}
|
||||
} else if (key === "geology") {
|
||||
got.geology = await fetchPlanGeology(projectId); // 암상(760-1) — GL 만 그림
|
||||
put("geology", geologyCollection(got.geology));
|
||||
} else if (key === "bands" || key === "roadBands") {
|
||||
const info = await fetchPlanBands(projectId);
|
||||
got.bands = {
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
max-width: calc(100% - 2 * var(--spacing-8));
|
||||
max-height: 60%; /* 760-1 암상 구간 표 · 출처 줄 — 낮은 화면에서 밀기 */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.d03-plan__legend[hidden] {
|
||||
|
||||
Reference in New Issue
Block a user