Programing/JavaScript

javascript 수선의 발 구하는 방법, 직선과 점의 가장 가까운 점 구하는 방법, 폴리라인과 마커간 최단거리

리커니 2021. 9. 28.
반응형

javascript 수선의 발 구하는 방법, 직선과 점의 가장 가까운 점 구하는 방법, 폴리라인과 마커간 최단거리

 

이번 포스팅은 수학적인 지식이 약간은 필요합니다.

 

수선의 발(foot of perpendicular) 이란 수선과 직선 또는 평면이 만나는 점을 뜻하며, 한 점에서 직선 또는 평면에 대해 수선을 그었을 때 만나는 점을 의미합니다.

 

간단히 직선과 점을 이은 수선의 각이 90도가 되는 점을 의미합니다. (점과 직선과의 최단거리)

아래의 이미지 처럼 검은색 두 점을 연결한 직선과 파란색 점을 연결하였을 때 직각이 되는

빨강색 점을 수선의 발이라고 합니다.

 

이제 이 수선의 발을 구하는 공식을 사용하여 카카오 지도에 표시해 보도록 하겠습니다.

 

저의 경우 링크와(폴리라인) 정류소(마커) 사이의 최단거리 지점을 구해달라는 요청이 있었습니다.

그 때 구현한 코드로 설명을 하겠습니다.

 

폴리라인은 그려져 있고, 카카오 맵의 drawing 라이브러리를 활용하여 신규 마커를 생성합니다.

신규로 생성한 마커의 position과 수선의 발을 내릴 폴리라인의ID를 파라메터로 받는 함수 입니다.

 

const setBsPointOnLink = function(bsPos, linkId){
	linkId = Number(linkId);
	if(GV.bsInLink !== null){
		GV.bsInLink.setMap(null);
	}
	const mapPro = GV.map.map.getProjection();
	const bsPnt = mapPro.pointFromCoords(bsPos);
	const lines = GV.map.polyline.link[linkId].getPath();
	let newPos = null, forwardPos = [], reversePos = [];
	/*st, ed 두 점을 지나는 직선에서 정류장 마커와의 수선의 발을 구한다*/
	const getNewPntOnLine = function(st, ed){
		let newPnt = {}, newPos = null;
		const stPnt = mapPro.pointFromCoords(st); /*직선의 시작점*/
		const edPnt = mapPro.pointFromCoords(ed); /*직선의 끝점*/
		const a = (edPnt.y - stPnt.y)/(edPnt.x - stPnt.x); /*두 점을 지나는 직선의 기울기 */
        const b = stPnt.y - a*stPnt.x; /*기울기와 한점의 좌표로 y절편 구하기  y=ax+b */ 
        const c = bsPnt.y + (bsPnt.x/a); /*두 점을 지나는 직선을 직교하는 직선의 y절편c  y=-x/a+c*/
        newPnt.x = (c-b)/(a+1/a);
        newPnt.y = -1/a*newPnt.x + c;
        if(!isNaN(newPnt.x)){
        	if(stPnt.x >= newPnt.x && edPnt.x <= newPnt.x){
        		const po = new kakao.maps.Point(newPnt.x, newPnt.y);
	            if(newPos == null){
	            	newPos = mapPro.coordsFromPoint(po);
	            }else{
	            	const tempPos = mapPro.coordsFromPoint(po);
	            	const posLength = getLengthByCoord(newPos, bsPos);
	            	const tempPosLength = getLengthByCoord(tempPos, bsPos);
	            	if(posLength > tempPosLength){
	            		newPos = tempPos;
	            	}
	            }
        	}
        }
        return newPos;
	}
	/*정방향/역방향 직선들의 수선의 발을 구한다*/
	lines.reduce(function(pre, cur, curIdx){
		if(pre){
			let fPnt = getNewPntOnLine(pre,cur);
			if(fPnt !== null) forwardPos.push(fPnt);
			let rPnt = getNewPntOnLine(cur,pre);
			if(rPnt !== null) reversePos.push(rPnt);
		}
        return cur;
	});
	/*구한 수선의 발들과 정류장 지점의 최단거리 수선의 발을 구한다*/
	const concatPos = forwardPos.concat(reversePos);
	if(concatPos.length > 0){
		let length = null, resultPos = null, tempLenth = null;
		for(let i in concatPos){
			if(length == null){
				length = getLengthByCoord(concatPos[i], bsPos);
				resultPos = concatPos[i];
			}else{
				tempLength = getLengthByCoord(concatPos[i], bsPos);
				if(length > tempLength) {
					length = tempLength;
					resultPos = concatPos[i];
				}
			}
		}
		GV.bsInLink = new kakao.maps.Marker({
			position : new kakao.maps.LatLng(resultPos.getLat(), resultPos.getLng()),
			image : GV.markerImg.bsInLink.select,
			title : "링크 내 정류장 위치로 설정될 지점 (링크 내 정류장 길이 계산 지점)"
		});
	    GV.bsInLink.setMap(GV.map.map);
	    return true;
	}else{
		alert("링크 내 정류장 위치를 확인할 수 없습니다!\n해당 정류장의 위치나 링크를 확인하세요!");
		return false;
	}
}

저는 카카오 지도의 라이브러리를 사용하여 위경도를 평면좌표로 변환하여(pointFromCoords) 수선을 발을 구하고

수선의 발을 다시 위경도로 변환(coordsFromPoint)하였습니다.

변환 없이 위경도로 구현하여도 같은 값이 나올 것으로 생각됩니다.

 

getNewPntOnLine 함수가 실제 수선의 발을 구하는 함수이고, 

폴리라인의 path로 reduce를 돌아 정방향과 역방향 직선의 수선의 발을 구했습니다.

그리고 구한 정방향/역방향의 수선의 발 중에 추가한 점과의 최단거리를 구하여

해당 좌표를 최종 지점으로 설정하였습니다.

 

getLengthByCoord 함수는 두 위경도간 거리(m)를 구하는 함수 입니다.

 

const getLengthByCoord = function(pos1, pos2){
	var array = [
		{lat : pos1.getLat(), lng : pos1.getLng()},
		{lat : pos2.getLat(), lng : pos2.getLng()}
	];
	return AF.getDistanceFromLatlng(array);
}
getDistanceFromLatlng : function(array){
    const lat1 = array[0].lat;
    const lng1 = array[0].lng;
    const lat2 = array[1].lat;
    const lng2 = array[1].lng;
				
    function deg2rad(deg) {
        return deg * (Math.PI/180)
    }
    
    const r = 6371; //지구의 반지름(km)
    const dLat = deg2rad(lat2-lat1);
    const dLon = deg2rad(lng2-lng1);
    const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    const d = r * c; // Distance in km
    return Number((d*1000).toFixed(1));
},

 

결론적으로 폴리라인의 각 정방향 역방향 직선과 신규 추가한 점과의 수선의 발들을 구하여

그 중 최단거리 지점에 마커를 추가하는 코드입니다.

 

 

버스 모양의 마커가 신규 추가 마커이고, 푸른색선이 폴리라인, 빨강색 마커가 최단거리 수선의 발이 됩니다.

신규 추가한 마커에 드래그앤드 이벤트를 주어 해당 이벤트가 탈 때 마다 위의 함수를 실행시키게 하면

신규 마커 이동시마다 최단거리 수선의발을 표출하게 됩니다.

 

 

반응형

댓글

💲 추천 글