질문답변

제목[re]경사향 모양 변경 및 개별 객체 선택2026-09-02 18:15
카테고리GeOnDT for JS
작성자

2번 문의에 대한 구현법을 포함한 기능 사용 안내 드립니다.


문의해 주신 기능 구현을 위해,

UAnalySlopeAspect (U3dApp.getAnalysis('SlopeAspect') 로 가져오는 객체)의 setArrowStyleFunction을 호출시, 분석 화살표 형상 하나하나 순회하며 파라메터로 입력한 사용사 함수를 호출함으로,

이 API를 이용하여 관련 기능을 구현할 수 있을 것이라 생각 됩니다.


1. 화면 클릭시, 3D 공간 좌표로 변환.

2. 이미 표출 중인 분석결과가 있으면, 변환된 3D 공간 좌표가 분석영역에 포함되는지 확인

3. 포함된다면 클릭한 지점의 특정 범위내 화살표를 찾거나 가장 근처의 화살표를 찾은 함수를 setArrowStyleFunction 의 파라메터로 입력

4. 검색된 화살표를 강조표시하는 함수를 setArrowStyleFunction 의 파라메터로 입력


이 흐름이 될것입니다.


관련 내용은 https://3d.geon.kr/doc/tutorial-official/analysisSlopeAspect.html (경사향 분석 예제) 에 적용되어있습니다.

최초 화면을 클릭하여 분석 형상이 출력된후, 분석 형상의 특정 화살표를 클릭하면 강조 표시가 됩니다.

 


아래는 구현에 사용된 소스 입니다.

const analy = app.getAnalysis('SlopeAspect'); //+ 앱에 기본으로 등록된 경사향 분석 객체를 가져온다.

analy.setAnalysisOption({ //+ 분석 옵션 설정. 아래 값은 STEP 5의 패널 입력으로 언제든 바꿀 수 있다.
width: 250, //+ 분석 영역 가로 길이(m). 내부에서 분석 중심 위도의 3857 길이로 변환된다.
height: 250, //+ 분석 영역 세로 길이(m)
nx: 25, //+ 가로 방향 표면 샘플(화살표) 수
ny: 25, //+ 세로 방향 표면 샘플(화살표) 수
arrowStyleFunction: arrowStyleFunction //+ 화살표(셀)마다 호출되어 스타일을 돌려주는 콜백
});

// 화살표가 화면에서 몇 픽셀 크기로 보이게 할지 정하는 배율.
const ARROW_PIXEL_SIZE = 8;

// 기본 화살표 스타일 콜백.
// 분석 객체는 화살표를 그릴 때마다 셀 정보(celInfo)와 인덱스(index)를 넘겨 이 콜백을 호출하고,
// 반환한 {visible, scale, height, color, opacity} 중 있는 값만 그 화살표에 적용한다.
function arrowStyleFunction(celInfo) {
// (1) 경사가 0인 평지는 방향이 없으므로 화살표를 그리지 않는다.
if (celInfo.slope === 0) {
return {visible: false};
}

// (2) 화살표를 카메라 거리와 무관하게 "항상 같은 화면 픽셀 크기"로 보이게 한다.
// 3D 객체는 카메라에서 멀어질수록 작게 보이므로, 거리에 비례해 월드 크기(scale)를 키워 보정한다.
const camera = app.getCamera(); //+ 현재 카메라
const resolutionY = GeOnDT.UDEF.PIXEL_RESOLUTION.QHD[1]; //+ 기준 세로 해상도(픽셀)
const distance = camera.position.distanceTo(celInfo.position); // 카메라에서 화살표 위치까지의 거리
const denominator = camera.getDenominator() || 1; //+ 카메라 투영 보정 계수
const pixelWorldSize = (distance * denominator) / resolutionY; // 화면 1픽셀이 차지하는 월드 크기

return {
scale: ARROW_PIXEL_SIZE * pixelWorldSize // 원하는 픽셀 크기 x 1픽셀당 월드 크기
};
}

// =========================================================================================
// STEP 4. 지도 클릭 - 분석 실행 또는 가장 가까운 화살표 강조
// =========================================================================================
// 클릭 흐름 요약
// ① 이전 강조 해제 → ② 결과가 없거나 영역 밖이면 새 분석 → ③ 영역 안이면 가장 가까운 화살표 검색
// → ④ 검색이 끝나면 그 화살표를 강조하고 값 POI 표시
//
// 검색과 강조는 모두 "스타일 콜백"으로 구현한다. 분석 객체가 렌더마다 모든 셀에 대해 콜백을
// 호출하므로, 콜백 안에서 셀 정보를 읽으면 별도 순회 없이 검색과 셀별 강조가 가능하다.

// 4-0. 상태 변수
let lastClickPoint; // 마지막으로 분석을 실행한 월드 좌표. 옵션을 바꾸면 이 지점을 다시 분석한다.
let highlightIndex; // 강조 중인 화살표의 resultGrid 인덱스
let highlightPOI; // 강조 화살표의 값을 표시하는 POI
let searchGeneration = 0; // 클릭마다 1씩 증가. 늦게 끝난 이전 검색 결과를 버리는 데 쓴다.
const HIGHLIGHT_SCALE = 2; // 강조 화살표 크기 배율

// 4-1. 클릭 핸들러 - 전체 흐름
app.on('click', function (e) { //+ 지도 클릭 이벤트
const point = app.closestPointAtPixel(e); //+ 클릭한 화면 위치를 지도 표면의 월드 좌표로 변환

// ① 매 클릭마다 기본 스타일 콜백으로 되돌려 이전 강조와 POI를 지운다.
clearHighlight();

// ② 표출 중인 결과가 없거나, 클릭 지점이 표출 중인 분석 영역 밖이면 → 그 지점을 중심으로 새 분석
if (analy.resultGrid.length === 0 || !isInsideAnalysisArea(point)) {
lastClickPoint = point;
analy.getSlope(point).then(function () { //+ 경사향 분석 실행(비동기). 완료되면 resolve된다.
console.log('분석완료');
});
return;
}

// ③ 분석 영역 안을 클릭했으면 → 스타일 콜백을 "검색 콜백"으로 바꿔 가장 가까운 화살표를 찾는다.
analy.setArrowStyleFunction(createSearchStyleFunction(point)); //+ 스타일 콜백 교체
});

// 4-2. 클릭 지점이 표출 중인 분석 격자 영역(XY 경계 상자) 안에 있는지 판정한다.
function isInsideAnalysisArea(point) {
const grid = analy.grid; //+ 샘플링한 격자점의 월드 좌표 2차원 배열 [x][y]
const first = grid[0][0]; // 한쪽 모서리 격자점
const last = grid[grid.length - 1][grid[0].length - 1]; // 반대쪽 모서리 격자점
return point.x >= Math.min(first.x, last.x) && point.x <= Math.max(first.x, last.x)
&& point.y >= Math.min(first.y, last.y) && point.y <= Math.max(first.y, last.y);
}

// 4-3. 검색 콜백 만들기
// 다음 렌더 한 번 동안 모든 셀을 인덱스 순으로 받아 보며 클릭 지점과 가장 가까운(그려지는) 화살표를 찾는다.
// 화면에는 기본 스타일 그대로 그리고, 마지막 셀까지 보면 강조 단계(4-4)로 넘어간다.
function createSearchStyleFunction(clickPoint) {
const generation = ++searchGeneration;
const lastIndex = analy.resultGrid.length - 1; //+ resultGrid: 계산에 성공한 셀 목록
let minDistance = Infinity;
let nearestIndex;
let finished = false;

return function searchStyleFunction(celInfo, index) {
const style = arrowStyleFunction(celInfo); // 검색 중에도 화살표는 기본 스타일로 그린다.

// 그려지지 않는(평지) 셀은 제외하고, XY 평면 거리가 가장 짧은 셀의 인덱스를 기록한다.
if (style.visible !== false) {
const distance = Math.hypot(celInfo.position.x - clickPoint.x, celInfo.position.y - clickPoint.y);
if (distance < minDistance) {
minDistance = distance;
nearestIndex = index;
}
}

// 마지막 셀까지 검사했으면 렌더 루프가 끝난 뒤(setTimeout) 강조 단계로 넘어간다.
if (index === lastIndex && !finished) {
finished = true;
setTimeout(function () {
if (generation !== searchGeneration) return; // 그 사이 다른 클릭이 있었으면 이 결과는 버린다.
if (nearestIndex === undefined) { // 그려진 화살표가 하나도 없으면 기본 스타일로 복원만 한다.
analy.setArrowStyleFunction(arrowStyleFunction);
return;
}
highlightIndex = nearestIndex;
applyHighlight();
});
}
return style;
};
}

// 4-4. 강조 적용 - 스타일 콜백을 "강조 콜백"으로 바꾸고, 화살표 위치에 값을 표시하는 POI를 만든다.
function applyHighlight() {
const celInfo = analy.resultGrid[highlightIndex]; //+ 셀 정보 - slope(경사도), aspect(경사향), position(월드 좌표) 등

analy.setArrowStyleFunction(highlightStyleFunction); //+ 스타일 콜백 교체

const poiPosition = celInfo.position.clone();
poiPosition.z += getHighlightLift(celInfo); // 띄운 화살표 높이에 맞춰 POI도 올린다.
highlightPOI = new GeOnDT.geom.U3dPOI({ //+ 라벨을 표시하는 POI 생성
position: poiPosition, //+ POI 위치(월드 좌표)
label: '경사도 ' + celInfo.slope.toFixed(1) + '° / 경사향 ' + celInfo.aspect.toFixed(1) + '° (' + toCompass(celInfo.aspect) + ')', //+ 라벨 문자열
depthTest: false //+ 지형이나 화살표에 가려지지 않게 깊이 검사 끔
});
app.addPOI(highlightPOI); //+ 지도에 POI 추가
}

// 4-5. 강조 콜백 - 기록된 인덱스의 화살표만 크기를 키우고 z축으로 띄운다. 나머지는 기본 스타일 그대로.
function highlightStyleFunction(celInfo, index) {
const style = arrowStyleFunction(celInfo);
if (index !== highlightIndex || style.visible === false) return style;

return {
scale: style.scale * HIGHLIGHT_SCALE, //+ scale: 화살표 크기 배율
height: getHighlightLift(celInfo) //+ height: 화살표를 표면에서 z축으로 띄우는 높이
};
}

// 4-6. 강조 해제 - 기본 스타일 콜백으로 되돌리고 값 POI를 제거한다.
function clearHighlight() {
searchGeneration++; // 진행 중이던 검색의 지연 결과를 무효화한다.
highlightIndex = undefined;
analy.setArrowStyleFunction(arrowStyleFunction); //+ 기본(원래) 스타일 콜백으로 복원
if (highlightPOI !== undefined) {
app.removePOI(highlightPOI); //+ 지도에서 POI 제거
highlightPOI = undefined;
}
}

// 4-7. 보조 함수
// 강조 화살표를 표면에서 띄울 높이. 키운 화살표 한 개 길이(꼬리 길이 x 배율)만큼 올려 한눈에 띄게 한다.
function getHighlightLift(celInfo) {
const scale = (arrowStyleFunction(celInfo).scale || 1) * HIGHLIGHT_SCALE;
return analy.getAnalysisOption().arrowTailLength * scale; //+ arrowTailLength: 현재 화살표 꼬리 길이 옵션
}

// 경사향 각도(0~360, 북쪽이 0)를 8방위 문자열로 바꾼다.
function toCompass(aspect) {
const names = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
return names[Math.round(aspect / 45) % 8];
}