From 2a7f11c10202b6ccd298812824c2aaa7c15c7afe Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 26 Jul 2026 19:14:19 +0900 Subject: [PATCH] auto: 2026-07-26 19:14 (ESD_LAPTOP) --- .../B04_wf1_Surface_Engine_GisVector.py | 6 +- B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py | 2 - .../B04_wf1_Surface_UI_MapViewer.ts | 78 +++++++++++++++++-- ui_template/ui_template_locale.ts | 1 + 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/B04_wf1_Surface/B04_wf1_Surface_Engine_GisVector.py b/B04_wf1_Surface/B04_wf1_Surface_Engine_GisVector.py index 454cb8b5..5fcf3a9f 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Engine_GisVector.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Engine_GisVector.py @@ -121,6 +121,10 @@ def crop_national_contours(bounds_wgs84: dict, output_dir: Path) -> bool: logger.warning("국가 등고선 클리핑 결과가 비어 있습니다: bbox=%s", bounds_wgs84) return False + # 라벨·구분에 필요한 속성만 유지 (Timestamp 등 나머지는 JSON 직렬화 불가·전송량 낭비) + keep = [c for c in ("CTRLN_SE", "TPGRPH_SE", "CTRLN_HG") if c in gdf.columns] + gdf = gdf[keep + ["geometry"]] + output_dir.mkdir(parents=True, exist_ok=True) gdf_wgs84 = gdf.to_crs("EPSG:4326") geojson_out = output_dir / "등고선_bounds.geojson" @@ -163,7 +167,7 @@ def download_all_gis_vectors(prj_path: Path, bounds_meter: dict, output_dir: Pat request_vworld_wfs("lt_c_uq111", "용도지역도_bounds.geojson", bounds_wgs84, output_dir) request_vworld_wfs("lt_c_adsigg", "행정구역_시군구_bounds.geojson", bounds_wgs84, output_dir) request_vworld_wfs("lt_c_adri", "행정구역_읍면동_bounds.geojson", bounds_wgs84, output_dir) - request_vworld_wfs("lt_c_gimshydro", "수계망_물줄기_bounds.geojson", bounds_wgs84, output_dir) + # 수계망은 도엽 하천중심선(세류 포함)으로 대체되어 다운로드하지 않음 (2026-07-26 사용자 지시) request_vworld_wfs( "lt_c_kfdrssigugrade", "산사태위험등급_bounds.geojson", bounds_wgs84, output_dir ) diff --git a/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py b/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py index 41abb019..8e517e4a 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py @@ -107,7 +107,6 @@ async def get_project_geojson(project_id: UUID, layer: str) -> dict[str, Any] | "용도지역": "용도지역도_bounds.geojson", "행정구역_시군구": "행정구역_시군구_bounds.geojson", "행정구역_읍면동": "행정구역_읍면동_bounds.geojson", - "수계망": "수계망_물줄기_bounds.geojson", "등고선": "등고선_bounds.geojson", "산사태": "산사태위험등급_bounds.geojson", **_SHEET_GEOJSON_FILES, @@ -182,7 +181,6 @@ async def get_vector_tile(project_id: UUID, layer: str, z: int, x: int, y: int) "용도지역": "용도지역도_bounds.geojson", "행정구역_시군구": "행정구역_시군구_bounds.geojson", "행정구역_읍면동": "행정구역_읍면동_bounds.geojson", - "수계망": "수계망_물줄기_bounds.geojson", "등고선": "등고선_bounds.geojson", "산사태": "산사태위험등급_bounds.geojson", "임도노선": "임도노선.geojson", diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts index 794fda06..cda144d9 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts @@ -21,6 +21,7 @@ type GeoJsonGeometry = { type GeoJsonFeature = { geometry?: GeoJsonGeometry | null; + properties?: Record | null; }; type GeoJsonCollection = { @@ -30,7 +31,6 @@ type GeoJsonCollection = { const BACKGROUND_LAYERS = ["white", "satellite", "hybrid"] as const; const GIS_LAYERS = [ "지적도", - "수계망", "산사태", "행정구역_시군구", "행정구역_읍면동", @@ -47,19 +47,24 @@ type GisLayer = (typeof GIS_LAYERS)[number]; const GIS_LAYER_COLORS: Record = { 지적도: "#f97316", - 수계망: "#0ea5e9", 산사태: "#ef4444", 행정구역_시군구: "#7c3aed", 행정구역_읍면동: "#22c55e", - 등고선: "#a16207", - 도엽_등고선: "#57534e", + 등고선: "#fdba74", + 도엽_등고선: "#a5b4fc", 도엽_하천중심선: "#2563eb", - 도엽_표고점: "#334155", + 도엽_표고점: "#f9a8d4", 도엽_성절토: "#f43f5e", 도엽_옹벽석축: "#0f766e", 도엽_유수방향: "#0891b2", }; +// 등고 라벨 표기 대상 레이어와 표고 속성 키 (gpkg=CTRLN_HG, 도엽=등고수치) +const CONTOUR_LABEL_KEYS: Partial> = { + 등고선: ["CTRLN_HG"], + 도엽_등고선: ["등고수치"], +}; + function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } @@ -133,6 +138,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { const geoJsonLayers = new Map(); const activeBackgrounds = new Set(BACKGROUND_LAYERS); const activeGisLayers = new Set(GIS_LAYERS); + let showContourLabels = false; let scale = 1; let offsetX = 0; let offsetY = 0; @@ -176,7 +182,6 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { const gisLabels: Record = { 지적도: L("B04_Surface_Map_Cadastral"), - 수계망: L("B04_Surface_Map_Water"), 산사태: L("B04_Surface_Map_Landslide"), 행정구역_시군구: L("B04_Surface_Map_Sigungu"), 행정구역_읍면동: L("B04_Surface_Map_Eupmyeondong"), @@ -194,6 +199,20 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { ); }); + // 등고 라벨 보기/숨기기 (기본 숨김 — 등고선·도엽 등고선의 계곡선 수치 표기) + const contourLabelButton = document.createElement("button"); + contourLabelButton.type = "button"; + contourLabelButton.className = "b04-map__layer-button"; + contourLabelButton.textContent = L("B04_Surface_Map_ContourLabel"); + contourLabelButton.setAttribute("aria-pressed", "false"); + contourLabelButton.addEventListener("click", () => { + showContourLabels = !showContourLabels; + contourLabelButton.classList.toggle("is-active", showContourLabels); + contourLabelButton.setAttribute("aria-pressed", String(showContourLabels)); + drawVectorLayer(); + }); + gisButtons.append(contourLabelButton); + function updateImageTransform(): void { backgroundImages.forEach((image) => { image.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`; @@ -307,6 +326,52 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { context.fill(); } + function contourLabelAnchor(geometry: GeoJsonGeometry): [number, number] | null { + const coords = geometry.coordinates; + if (!Array.isArray(coords)) return null; + const line = + geometry.type === "LineString" + ? coords + : geometry.type === "MultiLineString" + ? coords[0] + : null; + if (!Array.isArray(line) || line.length === 0) return null; + const mid = line[Math.floor(line.length / 2)]; + if (!Array.isArray(mid) || typeof mid[0] !== "number" || typeof mid[1] !== "number") { + return null; + } + return [mid[0], mid[1]]; + } + + function drawContourLabels( + context: CanvasRenderingContext2D, + width: number, + height: number, + ): void { + context.font = "10px sans-serif"; + context.textAlign = "center"; + context.textBaseline = "middle"; + (Object.keys(CONTOUR_LABEL_KEYS) as GisLayer[]).forEach((layer) => { + if (!activeGisLayers.has(layer)) return; + const keys = CONTOUR_LABEL_KEYS[layer] ?? []; + geoJsonLayers.get(layer)?.features?.forEach((feature) => { + if (!feature.geometry) return; + const raw = keys.map((key) => feature.properties?.[key]).find((value) => value != null); + const elevation = typeof raw === "number" ? raw : Number(raw); + // 계곡선(25m 배수)만 라벨 — 전체 표기 시 화면이 숫자로 뒤덮이는 것 방지 + if (!Number.isFinite(elevation) || elevation % 25 !== 0) return; + const anchor = contourLabelAnchor(feature.geometry); + if (!anchor) return; + const [x, y] = toCanvasPoint(anchor[0], anchor[1], width, height); + context.lineWidth = 3; + context.strokeStyle = "rgba(255, 255, 255, 0.9)"; + context.strokeText(String(elevation), x, y); + context.fillStyle = GIS_LAYER_COLORS[layer]; + context.fillText(String(elevation), x, y); + }); + }); + } + function drawGeometry( context: CanvasRenderingContext2D, geometry: GeoJsonGeometry, @@ -371,6 +436,7 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { if (feature.geometry) drawGeometry(context, feature.geometry, width, height); }); }); + if (showContourLabels) drawContourLabels(context, width, height); updateImageTransform(); drawScaleBar(width, height); } diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index 8d426938..952e6a17 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -658,6 +658,7 @@ export const ui_locales = { B04_Surface_Map_Eupmyeondong: ["읍면동", "Town boundary"], B04_Surface_Map_Contour: ["등고선", "Contour lines"], B04_Surface_Map_SheetContour: ["도엽 등고선", "Sheet contours"], + B04_Surface_Map_ContourLabel: ["등고 라벨", "Contour labels"], B04_Surface_Map_SheetStream: ["세류(하천중심선)", "Stream centerline"], B04_Surface_Map_SheetElevPoint: ["표고점", "Spot elevation"], B04_Surface_Map_SheetCutFill: ["성절토", "Cut/fill slope"],