260715_0
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { RENDER_OPTIONS } from "@config/config_frontend";
|
||||
import type { SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch";
|
||||
import {
|
||||
fetchVWorldMeta,
|
||||
getVWorldMapUrl,
|
||||
fetchGisGeoJson,
|
||||
type SurfacePointCloudSampleResponse,
|
||||
} from "./B04_wf1_Surface_Api_Fetch";
|
||||
|
||||
export interface SurfacePointCloudViewer {
|
||||
root: HTMLElement;
|
||||
@@ -9,6 +14,8 @@ export interface SurfacePointCloudViewer {
|
||||
optionsGroup: HTMLElement;
|
||||
statusSpan: HTMLElement;
|
||||
render: (data: SurfacePointCloudSampleResponse | null) => void;
|
||||
updateBgMap: (projectId: string, bgLayer: string) => void;
|
||||
updateGisLayer: (projectId: string, gisLayer: string) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
@@ -85,7 +92,53 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
densityInput.value = "10";
|
||||
densityLabel.append(densityInput);
|
||||
|
||||
optionsDiv.append(sizeLabel, densityLabel);
|
||||
const bgLabel = document.createElement("label");
|
||||
bgLabel.textContent = "배경 지도 ";
|
||||
bgLabel.style.display = "flex";
|
||||
bgLabel.style.flexDirection = "column";
|
||||
bgLabel.style.gap = "var(--spacing-4)";
|
||||
bgLabel.style.marginTop = "var(--spacing-8)";
|
||||
const bgSelect = document.createElement("select");
|
||||
bgSelect.className = "b04-surface__select";
|
||||
const bgOptions = [
|
||||
{ value: "none", label: "없음" },
|
||||
{ value: "satellite", label: "위성사진" },
|
||||
{ value: "hybrid", label: "하이브리드 지도" },
|
||||
{ value: "white", label: "백지도" },
|
||||
];
|
||||
bgOptions.forEach((opt) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = opt.value;
|
||||
o.textContent = opt.label;
|
||||
bgSelect.append(o);
|
||||
});
|
||||
bgLabel.append(bgSelect);
|
||||
|
||||
const gisLabel = document.createElement("label");
|
||||
gisLabel.textContent = "국가 GIS 레이어 ";
|
||||
gisLabel.style.display = "flex";
|
||||
gisLabel.style.flexDirection = "column";
|
||||
gisLabel.style.gap = "var(--spacing-4)";
|
||||
gisLabel.style.marginTop = "var(--spacing-8)";
|
||||
const gisSelect = document.createElement("select");
|
||||
gisSelect.className = "b04-surface__select";
|
||||
const gisOptions = [
|
||||
{ value: "none", label: "없음" },
|
||||
{ value: "지적도", label: "연속지적도" },
|
||||
{ value: "수계망", label: "수계망" },
|
||||
{ value: "산사태", label: "산사태위험등급" },
|
||||
{ value: "행정구역_시군구", label: "시군구 경계" },
|
||||
{ value: "행정구역_읍면동", label: "읍면동 경계" },
|
||||
];
|
||||
gisOptions.forEach((opt) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = opt.value;
|
||||
o.textContent = opt.label;
|
||||
gisSelect.append(o);
|
||||
});
|
||||
gisLabel.append(gisSelect);
|
||||
|
||||
optionsDiv.append(sizeLabel, densityLabel, bgLabel, gisLabel);
|
||||
|
||||
const optionsGroup = document.createElement("section");
|
||||
optionsGroup.className = "b04-surface__group";
|
||||
@@ -151,6 +204,8 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
scene.add(axes);
|
||||
|
||||
let pointsObject: THREE.Points | null = null;
|
||||
let bgPlane: THREE.Mesh | null = null;
|
||||
let gisObjects: THREE.Object3D[] = [];
|
||||
let animationFrame = 0;
|
||||
let currentData: SurfacePointCloudSampleResponse | null = null;
|
||||
|
||||
@@ -343,15 +398,204 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
statusSpan.textContent = ` ${renderPoints.length.toLocaleString()}개 점 표시 중 [높이범위: ~${nearestMin10}m ... ${nearestMax10}m~]`;
|
||||
}
|
||||
|
||||
function clearBgMap(): void {
|
||||
if (bgPlane) {
|
||||
bgPlane.geometry.dispose();
|
||||
if (Array.isArray(bgPlane.material)) {
|
||||
bgPlane.material.forEach((mat) => mat.dispose());
|
||||
} else {
|
||||
bgPlane.material.dispose();
|
||||
}
|
||||
scene.remove(bgPlane);
|
||||
bgPlane = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearGisLayers(): void {
|
||||
gisObjects.forEach((obj) => {
|
||||
if (obj instanceof THREE.Line) {
|
||||
obj.geometry.dispose();
|
||||
if (Array.isArray(obj.material)) {
|
||||
obj.material.forEach((mat) => mat.dispose());
|
||||
} else {
|
||||
obj.material.dispose();
|
||||
}
|
||||
}
|
||||
scene.remove(obj);
|
||||
});
|
||||
gisObjects = [];
|
||||
}
|
||||
|
||||
function updateBgMap(projectId: string, bgLayer: string): void {
|
||||
clearBgMap();
|
||||
if (bgLayer === "none" || !currentData) return;
|
||||
|
||||
const bounds = currentData.bounds;
|
||||
const xMin = bounds.x_min ?? 0;
|
||||
const xMax = bounds.x_max ?? 0;
|
||||
const yMin = bounds.y_min ?? 0;
|
||||
const yMax = bounds.y_max ?? 0;
|
||||
const zMin = bounds.z_min ?? 0;
|
||||
const zMax = bounds.z_max ?? 0;
|
||||
|
||||
const xMid = (xMin + xMax) / 2;
|
||||
const yMid = (yMin + yMax) / 2;
|
||||
const zMid = (zMin + zMax) / 2;
|
||||
|
||||
const xSpan = Math.max(xMax - xMin, 1e-9);
|
||||
const ySpan = Math.max(yMax - yMin, 1e-9);
|
||||
const zSpan = Math.max(zMax - zMin, 1e-9);
|
||||
const scale = 180 / Math.max(xSpan, ySpan, zSpan);
|
||||
|
||||
const targetLayer = bgLayer === "gray" ? "white" : bgLayer;
|
||||
|
||||
fetchVWorldMeta(projectId, targetLayer)
|
||||
.then((meta) => {
|
||||
const planeW = meta.width_meters * scale;
|
||||
const planeH = meta.height_meters * scale;
|
||||
const planeGeo = new THREE.PlaneGeometry(planeW, planeH);
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
textureLoader.load(getVWorldMapUrl(projectId, targetLayer), (texture) => {
|
||||
const planeMat = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.85,
|
||||
});
|
||||
bgPlane = new THREE.Mesh(planeGeo, planeMat);
|
||||
bgPlane.rotation.x = -Math.PI / 2;
|
||||
|
||||
const planeCenterX = (meta.center_x - xMid) * scale;
|
||||
const planeCenterY = -(meta.center_y - yMid) * scale;
|
||||
const planeCenterZ = (zMin - zMid) * scale - 0.5;
|
||||
|
||||
bgPlane.position.set(planeCenterX, planeCenterZ, planeCenterY);
|
||||
scene.add(bgPlane);
|
||||
});
|
||||
})
|
||||
.catch((e) => console.log("VWorld 배경 로드 실패:", e.message));
|
||||
}
|
||||
|
||||
function updateGisLayer(projectId: string, gisLayer: string): void {
|
||||
clearGisLayers();
|
||||
if (gisLayer === "none" || !currentData) return;
|
||||
|
||||
const bounds = currentData.bounds;
|
||||
const xMin = bounds.x_min ?? 0;
|
||||
const xMax = bounds.x_max ?? 0;
|
||||
const yMin = bounds.y_min ?? 0;
|
||||
const yMax = bounds.y_max ?? 0;
|
||||
const zMin = bounds.z_min ?? 0;
|
||||
const zMax = bounds.z_max ?? 0;
|
||||
|
||||
const xMid = (xMin + xMax) / 2;
|
||||
const yMid = (yMin + yMax) / 2;
|
||||
const zMid = (zMin + zMax) / 2;
|
||||
|
||||
const xSpan = Math.max(xMax - xMin, 1e-9);
|
||||
const ySpan = Math.max(yMax - yMin, 1e-9);
|
||||
const zSpan = Math.max(zMax - zMin, 1e-9);
|
||||
const scale = 180 / Math.max(xSpan, ySpan, zSpan);
|
||||
|
||||
Promise.all([fetchGisGeoJson(projectId, gisLayer), fetchVWorldMeta(projectId, "white")])
|
||||
.then(([geoData, meta]) => {
|
||||
const lonRange = meta.lon_max - meta.lon_min;
|
||||
const latRange = meta.lat_max - meta.lat_min;
|
||||
const xRange = meta.x_max - meta.x_min;
|
||||
const yRange = meta.y_max - meta.y_min;
|
||||
|
||||
const toLocal = (lon: number, lat: number): [number, number] => {
|
||||
const localX_m = meta.x_min + ((lon - meta.lon_min) / lonRange) * xRange;
|
||||
const localY_m = meta.y_min + ((lat - meta.lat_min) / latRange) * yRange;
|
||||
return [(localX_m - xMid) * scale, -(localY_m - yMid) * scale];
|
||||
};
|
||||
|
||||
const features = geoData.features || [];
|
||||
const colors: Record<string, number> = {
|
||||
지적도: 0xff5500,
|
||||
수계망: 0x00aaff,
|
||||
산사태: 0xff0055,
|
||||
행정구역_시군구: 0x333333,
|
||||
행정구역_읍면동: 0x666666,
|
||||
};
|
||||
const layerColor = colors[gisLayer] || 0x00aa00;
|
||||
const groundZ = (zMin - zMid) * scale + 0.1;
|
||||
|
||||
features.forEach((feat: any) => {
|
||||
const geom = feat.geometry;
|
||||
if (!geom) return;
|
||||
|
||||
const drawRing = (ring: number[][]) => {
|
||||
const pts: THREE.Vector3[] = ring.map((pt) => {
|
||||
const [lx, ly] = toLocal(pt[0], pt[1]);
|
||||
return new THREE.Vector3(lx, groundZ, ly);
|
||||
});
|
||||
if (pts.length === 0) return;
|
||||
const lineGeo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
const lineMat = new THREE.LineBasicMaterial({ color: layerColor });
|
||||
const line = new THREE.Line(lineGeo, lineMat);
|
||||
scene.add(line);
|
||||
gisObjects.push(line);
|
||||
};
|
||||
|
||||
const drawCoordinates = (coords: any[], type: string) => {
|
||||
if (type === "Polygon") {
|
||||
coords.forEach((ring: number[][]) => drawRing(ring));
|
||||
} else if (type === "MultiPolygon") {
|
||||
coords.forEach((poly: any[]) => drawCoordinates(poly, "Polygon"));
|
||||
} else if (type === "LineString") {
|
||||
drawRing(coords as number[][]);
|
||||
} else if (type === "MultiLineString") {
|
||||
(coords as number[][][]).forEach((line) => drawRing(line));
|
||||
}
|
||||
};
|
||||
|
||||
drawCoordinates(geom.coordinates, geom.type);
|
||||
});
|
||||
})
|
||||
.catch((err) => console.error("GIS 벡터 로드 실패:", err));
|
||||
}
|
||||
|
||||
const getProjectIdLocal = () => {
|
||||
return localStorage.getItem("current_project_id");
|
||||
};
|
||||
|
||||
bgSelect.addEventListener("change", () => {
|
||||
const projId = getProjectIdLocal();
|
||||
if (projId) {
|
||||
updateBgMap(projId, bgSelect.value);
|
||||
}
|
||||
});
|
||||
|
||||
gisSelect.addEventListener("change", () => {
|
||||
const projId = getProjectIdLocal();
|
||||
if (projId) {
|
||||
updateGisLayer(projId, gisSelect.value);
|
||||
}
|
||||
});
|
||||
|
||||
function render(data: SurfacePointCloudSampleResponse | null): void {
|
||||
currentData = data;
|
||||
clearPoints();
|
||||
clearBgMap();
|
||||
clearGisLayers();
|
||||
if (!data) {
|
||||
statusSpan.textContent = "데이터가 존재하지 않습니다.";
|
||||
return;
|
||||
}
|
||||
renderPointCloudInternal(data);
|
||||
setCameraView("top");
|
||||
|
||||
const projId = getProjectIdLocal();
|
||||
if (projId) {
|
||||
if (bgSelect.value !== "none") {
|
||||
updateBgMap(projId, bgSelect.value);
|
||||
}
|
||||
if (gisSelect.value !== "none") {
|
||||
updateGisLayer(projId, gisSelect.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
@@ -377,6 +621,11 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
if (valSpan) valSpan.textContent = `${val * 10}%`;
|
||||
if (currentData) {
|
||||
renderPointCloudInternal(currentData);
|
||||
const projId = getProjectIdLocal();
|
||||
if (projId) {
|
||||
if (bgSelect.value !== "none") updateBgMap(projId, bgSelect.value);
|
||||
if (gisSelect.value !== "none") updateGisLayer(projId, gisSelect.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -388,9 +637,13 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
optionsGroup,
|
||||
statusSpan,
|
||||
render,
|
||||
updateBgMap,
|
||||
updateGisLayer,
|
||||
dispose: () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
clearPoints();
|
||||
clearBgMap();
|
||||
clearGisLayers();
|
||||
controls.dispose();
|
||||
renderer.dispose();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user