본문 바로가기
Project/내비게이션

[OSM]OpenStreetMap을 이용한 내비게이션 만들기 - 3

by 알파쿼카 2024. 9. 19.

지난 글에서는 경로 검색 기능까지 제작해 보았다.


이번 글에서는 현위치를 가장 가까운 건물(시설물)과 연동해서 보여주고,
현위치에 관한 세부 기능들을 완성해보자.


 현위치를 건물이름과 연동하기 

        var current_lat = current_point[0];
        var current_lng = current_point[1];
        
		//현재 주소 반환
        getNearestBuilding(current_lat, current_lng);

        // 역지오코딩으로 가까운 건물 찾기
        function getNearestBuilding(lat, lng) {
            var url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`;

            fetch(url)
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok ' + response.statusText);
                    }
                    return response.json();
                })
                .then(data => {
                    if (data && data.display_name) {
                        var city = data.address.city;
                        var borough = data.address.borough;
                        var road = data.address.road;
                        var address = city + ' ' + borough + ' ' + road

                        document.getElementById('start').value = address; // 출발지 입력란에 설정
                    } else {
                        alert('가장 가까운 위치를 찾을 수 없습니다.');
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                    alert('위치를 찾는 중 오류가 발생했습니다.');
                });
        }

 

[결과 화면]


주소 배열이 뒤죽박죽이라서 흔히 쓰는 주소명이 나올 수 있도록 배치했다.


 ISSUE 

1) 찾기 버튼을 여러번 누를 경우 데이터 베이스 요청이 배로 늘어나는 이슈가 있었음

 

[원인]

찾기 버튼을 여러 번 누를 경우 데이터베이스 요청이 배로 늘어나는 문제는 getRoute 함수 내에서 이벤트 리스너가 중복으로 추가되기 때문이다.

control.on('routesfound', function(e) {...}); 블록이 getRoute 함수 내부에서 호출될 때마다 새로운 이벤트 리스너가 추가됨이로 인해 경로를 찾을 때마다 동일한 작업이 여러 번 수행되어 데이터베이스 요청이 중복 발생함

 

[해결]

이벤트 리스너를 추가하기 전에 기존의 리스너를 제거하거나, 리스너를 한 번만 추가하도록 수정해야함

 

        // 경로 찾기 함수
        function getRoute() {
            var start = document.getElementById('start').value;
            var end = document.getElementById('end').value;

            if (start && end) {
                L.Control.Geocoder.nominatim().geocode(start, function(results) {
                    if (results.length > 0) {
                        var startLatLng = results[0].center;

                        L.Control.Geocoder.nominatim().geocode(end, function(results) {
                            if (results.length > 0) {
                                var endLatLng = results[0].center;

                                // 출발지와 도착지 설정
                                control.setWaypoints([
                                    L.latLng(startLatLng.lat, startLatLng.lng),
                                    L.latLng(endLatLng.lat, endLatLng.lng)
                                ]);

                                // 경로 정보 요청
                                // 이벤트 리스너가 중복 추가되지 않도록 설정
                                control.once('routesfound', function(e) {
                                    var routes = e.routes;
                                    var all = routes[0].coordinates.map(coord => `"${coord.lat}, ${coord.lng}"`).join(',');
                                    var distance = routes[0].summary.totalDistance;
                                    var routeTime = routes[0].summary.totalTime;

                                    var index = routes[0].instructions.map(instruction => instruction.index);
                                    var locations = index.map(index => {
                                        var coord = routes[0].coordinates[index];
                                        return coord ? `${coord.lat}, ${coord.lng}` : null;
                                    }).filter(coord => coord !== null);

                                    //경로 발견시 지도 위치 조정
                                    map.on('locationfound', function(e) {
                                        marker.setLatLng(e.latlng);
                                        map.setView(e.latlng, 16);
                                        closePopup(); // 위치가 발견되면 팝업 닫기
                                    });

                                    map.on('locationerror', function(e) {
                                        alert(e.message);
                                    });

                                    // AJAX 요청으로 데이터베이스 업데이트
                                    updateDatabase(routes, start, end, locations, distance, routeTime, index, all);
                                });
                            } else {
                                alert('도착지를 찾을 수 없습니다.');
                            }
                        });
                    } else {
                        alert('출발지를 찾을 수 없습니다.');
                    }
                });
            } else {
                alert('출발지와 도착지를 모두 입력하세요.');
            }
        }

 

2) 경로를 찾았을 때 지도 뷰가 재배치되지 않음

 

[원인]

- 경로가 발견된 후 지도 뷰 설정: control.once('routesfound', ...) 내에서 경로가 발견된 후에 지도 뷰를 설정하도록 변경
- 중복 이벤트 리스너 제거: 경로에 대한 이벤트 리스너를 한 번만 설정하도록 변경

 

[해결]

				// 경로 정보 요청
                                // 이벤트 리스너가 중복 추가되지 않도록 설정
                                control.once('routesfound', function(e) {
                                    var routes = e.routes;

                                    // 경로 정보를 가져온 후 지도 뷰를 설정
                                    var firstRoute = routes[0];
                                    var waypoints = firstRoute.coordinates;
                                    var bounds = L.latLngBounds(waypoints); // 모든 경로의 경계 계산
                                    map.fitBounds(bounds); // 경로에 맞게 지도를 조정

                                    var all = waypoints.map(coord => `"${coord.lat}, ${coord.lng}"`).join(',');
                                    var distance = firstRoute.summary.totalDistance;
                                    var routeTime = firstRoute.summary.totalTime;

                                    var index = firstRoute.instructions.map(instruction => instruction.index);
                                    var locations = index.map(index => {
                                        var coord = waypoints[index];
                                        return coord ? `${coord.lat}, ${coord.lng}` : null;
                                    }).filter(coord => coord !== null);

                                    // AJAX 요청으로 데이터베이스 업데이트
                                    updateDatabase(routes, start, end, locations, distance, routeTime, index, all);
                                });

 

[결과 화면]

 

지도가 경로에 맞게 재배치 되는 것을 볼 수 있다!


 출발지와 도착지에 마커 찍기 

출발지와 도착지에 각각 표시될 마커를 찍어주자.

                                // 출발지와 도착지 설정
                                control.setWaypoints([
                                    L.latLng(startLatLng.lat, startLatLng.lng),
                                    L.latLng(endLatLng.lat, endLatLng.lng)
                                ]);

                                // 출발지 마커 추가
                                var startIcon = L.icon({
                                    iconUrl: '/h/img/start.png', // start.png의 경로
                                    iconSize: [25, 41], // 아이콘 크기
                                    iconAnchor: [12, 41], // 아이콘 기준점
                                });
                                L.marker(startLatLng, {
                                    icon: startIcon
                                }).addTo(map).bindPopup("출발지: " + start);

                                // 도착지 마커 추가
                                var endIcon = L.icon({
                                    iconUrl: '/h/img/end.png', // end.png의 경로
                                    iconSize: [25, 41], // 아이콘 크기
                                    iconAnchor: [12, 41], // 아이콘 기준점
                                });
                                L.marker(endLatLng, {
                                    icon: endIcon
                                }).addTo(map).bindPopup("도착지: " + end);

 

 

대충 PNG 이미지 만들어서 넣어줬다

첫 화면에서도 현재 위치를 마커로 띄워주고, 아이콘 위치도 자연스럽게 조정했다.

 전체 코드 

<?php
include($_SERVER['DOCUMENT_ROOT'] . "/h/db/db.php");

$user_id = $_SESSION['id'];
$sql = "SELECT * FROM `route` WHERE `id` = " . $user_id . "";
$user_id;
$res = mysqli_query($conn, $sql);
$row = mysqli_fetch_array($res);

($row['current_point'] != null) ? $current_point = $row['current_point'] : $current_point = '1';
?>
<!DOCTYPE html>
<html lang="ko">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>노드 확인용</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
    <link rel="stylesheet" href="https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.css" />
    <link rel="stylesheet" href="/h/css/index.css">
</head>

<body>
    <div id="popup">
        <div class="search_left">
            <button class="btn btn_change" onclick="">↑↓</button>
        </div>
        <div class="search">
            <input type="text" id="current" value='<?php echo $current_point; ?>' style='display: none;'>
            <input type="text" id="start" placeholder="출발지 입력">
            <input type="text" id="end" placeholder="도착지 입력">
        </div>
        <div class="search_right">
            <button class="btn btn_current" onclick="test()">현위치</button>
            <button class="btn btn_search" onclick="getRoute()">찾기</button>
        </div>
    </div>
    <div id="map"></div>
    <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
    <script src="https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.js"></script>
    <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
    <script>
        // current_value --> 위도, 경도 (문자열 형식)
        var current_value = document.getElementById('current').value;

        // current_point --> [위도, 경도] (배열로 변환)
        var current_point = current_value.split(',').map(Number); // 문자열을 배열로 변환하고 숫자로 변환

        var current_lat = current_point[0];
        var current_lng = current_point[1];

        // 현재 위치 마커 변수
        var currentMarker;
        // 출발지 마커 변수
        var startMarker;
        // 도착지 마커 변수
        var endMarker;

        // 지도 초기화
        var map = L.map('map').setView(current_point, 16); // 지도의 맨 처음 위치를 현위치로 설정

        // OpenStreetMap 타일 레이어 추가
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
        }).addTo(map);

        // 현재 위치 마커 추가
        var currentIcon = L.icon({
            iconUrl: '/h/img/current.png', // current.png의 경로
            iconSize: [15, 15], // 아이콘 크기
            iconAnchor: [7.5, 7.5], // 아이콘 기준점
        });
        currentMarker = L.marker([current_lat, current_lng], {
                icon: currentIcon
            })
            .addTo(map)
            .bindPopup("현재 위치");

        var control = L.Routing.control({
            waypoints: [],
            routeWhileDragging: true,
            geocoder: L.Control.Geocoder.nominatim(),
            createMarker: function() {
                return null;
            } // 기본 마커 비활성화
        }).addTo(map);

        //길찾기 팝업 숨기기 !!
        control.getContainer().style.display = 'none';

        // 확대/축소 컨트롤 숨기기
        var zoomControl = map.zoomControl; // 기본 줌 컨트롤 가져오기
        if (zoomControl) {
            zoomControl.remove(); // 줌 컨트롤 제거
        }

        //현재 주소 반환
        getNearestBuilding(current_lat, current_lng);

        // 역지오코딩으로 가까운 건물 찾기
        function getNearestBuilding(lat, lng) {
            var url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`;

            fetch(url)
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok ' + response.statusText);
                    }
                    return response.json();
                })
                .then(data => {
                    if (data && data.display_name) {
                        var city = data.address.city;
                        var borough = data.address.borough;
                        var road = data.address.road;
                        var address = city + ' ' + borough + ' ' + road

                        document.getElementById('start').value = address; // 출발지 입력란에 설정
                    } else {
                        alert('가장 가까운 위치를 찾을 수 없습니다.');
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                    alert('위치를 찾는 중 오류가 발생했습니다.');
                });
        }

        // 경로 찾기 함수
        function getRoute() {
            var start = document.getElementById('start').value;
            var end = document.getElementById('end').value;

            if (start && end) {
                L.Control.Geocoder.nominatim().geocode(start, function(results) {
                    if (results.length > 0) {
                        var startLatLng = results[0].center;

                        L.Control.Geocoder.nominatim().geocode(end, function(results) {
                            if (results.length > 0) {
                                var endLatLng = results[0].center;

                                // 출발지와 도착지 설정
                                control.setWaypoints([
                                    L.latLng(startLatLng.lat, startLatLng.lng),
                                    L.latLng(endLatLng.lat, endLatLng.lng)
                                ]);

                                // 기존 마커 제거
                                if (startMarker) {
                                    map.removeLayer(startMarker);
                                }
                                if (endMarker) {
                                    map.removeLayer(endMarker);
                                }

                                // 출발지 마커 추가
                                var startIcon = L.icon({
                                    iconUrl: '/h/img/start.png', // start.png의 경로
                                    iconSize: [30, 41], // 아이콘 크기
                                    iconAnchor: [15, 41], // 아이콘 기준점
                                });
                                startMarker = L.marker(startLatLng, {
                                        icon: startIcon
                                    })
                                    .addTo(map)
                                    .bindPopup("출발지: " + start);

                                // 도착지 마커 추가
                                var endIcon = L.icon({
                                    iconUrl: '/h/img/end.png', // end.png의 경로
                                    iconSize: [30, 41], // 아이콘 크기
                                    iconAnchor: [15, 41], // 아이콘 기준점
                                });
                                endMarker = L.marker(endLatLng, {
                                    icon: endIcon
                                }).addTo(map).bindPopup("도착지: " + end);

                                // 경로 정보 요청
                                // 이벤트 리스너가 중복 추가되지 않도록 설정
                                control.once('routesfound', function(e) {
                                    var routes = e.routes;

                                    // 경로 정보를 가져온 후 지도 뷰를 설정
                                    var firstRoute = routes[0];
                                    var waypoints = firstRoute.coordinates;
                                    var bounds = L.latLngBounds(waypoints); // 모든 경로의 경계 계산
                                    map.fitBounds(bounds); // 경로에 맞게 지도를 조정

                                    var all = waypoints.map(coord => `"${coord.lat}, ${coord.lng}"`).join(',');
                                    var distance = firstRoute.summary.totalDistance;
                                    var routeTime = firstRoute.summary.totalTime;

                                    var index = firstRoute.instructions.map(instruction => instruction.index);
                                    var locations = index.map(index => {
                                        var coord = waypoints[index];
                                        return coord ? `${coord.lat}, ${coord.lng}` : null;
                                    }).filter(coord => coord !== null);

                                    // AJAX 요청으로 데이터베이스 업데이트
                                    updateDatabase(routes, start, end, locations, distance, routeTime, index, all);
                                });
                            } else {
                                alert('도착지를 찾을 수 없습니다.');
                            }
                        });
                    } else {
                        alert('출발지를 찾을 수 없습니다.');
                    }
                });
            } else {
                alert('출발지와 도착지를 모두 입력하세요.');
            }
        }

        // 데이터베이스 업데이트 함수
        function updateDatabase(routes, start, end, locations, distance, routeTime, index, all) {
            var xhr = new XMLHttpRequest();
            xhr.open("POST", "update_route.php", true);
            xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4) {
                    if (xhr.status == 200) {
                        console.log('데이터베이스 업데이트 성공:', xhr.responseText);
                    } else {
                        console.error('업데이트 실패:', xhr.status, xhr.statusText);
                    }
                }
            };
            xhr.send(JSON.stringify({
                start_point: start,
                end_point: end,
                locations: JSON.stringify(locations).slice(1, -1),
                distance: distance,
                route_time: routeTime,
                all: all
            }));

            // console.log(routes);
            // console.log(index);
            // console.log(all);
            // console.log(JSON.stringify(locations).slice(1, -1));
        }
    </script>
</body>

</html>


짜잔ㅎㅎ 경로가 아주 만족스럽다! 

다음 게시물에서는 현재 위치를 실시간으로 업데이트 해보자