auto: 2026-07-26 19:14 (ESD_LAPTOP)

This commit is contained in:
2026-07-26 19:14:19 +09:00
parent e064f88856
commit 2a7f11c102
4 changed files with 78 additions and 9 deletions
@@ -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
)
@@ -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",
@@ -21,6 +21,7 @@ type GeoJsonGeometry = {
type GeoJsonFeature = {
geometry?: GeoJsonGeometry | null;
properties?: Record<string, unknown> | 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<GisLayer, string> = {
: "#f97316",
: "#0ea5e9",
: "#ef4444",
_시군구: "#7c3aed",
_읍면동: "#22c55e",
: "#a16207",
_등고선: "#57534e",
: "#fdba74",
_등고선: "#a5b4fc",
_하천중심선: "#2563eb",
_표고점: "#334155",
_표고점: "#f9a8d4",
_성절토: "#f43f5e",
_옹벽석축: "#0f766e",
_유수방향: "#0891b2",
};
// 등고 라벨 표기 대상 레이어와 표고 속성 키 (gpkg=CTRLN_HG, 도엽=등고수치)
const CONTOUR_LABEL_KEYS: Partial<Record<GisLayer, string[]>> = {
: ["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<GisLayer, GeoJsonCollection>();
const activeBackgrounds = new Set<BackgroundLayer>(BACKGROUND_LAYERS);
const activeGisLayers = new Set<GisLayer>(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<GisLayer, string> = {
지적도: 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);
}
+1
View File
@@ -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"],