[OSRM] Chat GPT와 함께하는 TSP - 지도 표시

2024. 12. 9. 23:42프로그래밍/GIS

728x90

내 맘대로 지도에 한 번 표시를 해 보았습니다.

일단 입력으로 들어간 input.json 의 경우 다음과 같은 형식이었습니다.

뭐 그냥 느낌상으로는 탈것 1번에 대해서 id를 무작위로 부여 하였다 정도로 해석을 해 봅시다.

{
  "vehicles": [
    {
      "id": 1,
      "start": [127.027621, 37.497942],  
      "end": [127.027621, 37.497942]     
    }
  ],
  "jobs": [
    { "id": 1, "location": [127.035212, 37.495477] }, 
    { "id": 2, "location": [127.045126, 37.498431] }, 
    { "id": 3, "location": [127.027436, 37.510205] }, 
    { "id": 4, "location": [127.025221, 37.516245] }, 
    { "id": 5, "location": [127.066409, 37.499513] }, 
    { "id": 6, "location": [127.062032, 37.492522] }, 
    { "id": 7, "location": [127.046882, 37.489915] }, 
    { "id": 8, "location": [127.040508, 37.498161] }, 
    { "id": 9, "location": [127.051553, 37.514139] }, 
    { "id": 10, "location": [127.061707, 37.496634] }, 
    { "id": 11, "location": [127.034794, 37.507503] }, 
    { "id": 12, "location": [127.021602, 37.513866] }, 
    { "id": 13, "location": [127.037316, 37.495522] }, 
    { "id": 14, "location": [127.042979, 37.492641] }, 
    { "id": 15, "location": [127.057222, 37.507457] }, 
    { "id": 16, "location": [127.059778, 37.482721] }, 
    { "id": 17, "location": [127.051248, 37.476564] }, 
    { "id": 18, "location": [127.039653, 37.498994] }, 
    { "id": 19, "location": [127.034097, 37.488837] }, 
    { "id": 20, "location": [127.046734, 37.486172] }, 
    { "id": 21, "location": [127.041748, 37.509745] }, 
    { "id": 22, "location": [127.038188, 37.494878] }, 
    { "id": 23, "location": [127.048871, 37.498221] }, 
    { "id": 24, "location": [127.041187, 37.505308] }, 
    { "id": 25, "location": [127.022919, 37.517817] }, 
    { "id": 26, "location": [127.049905, 37.496708] }, 
    { "id": 27, "location": [127.053617, 37.487403] }, 
    { "id": 28, "location": [127.058739, 37.496972] }, 
    { "id": 29, "location": [127.051709, 37.508669] }, 
    { "id": 30, "location": [127.063031, 37.503671] }, 
    { "id": 31, "location": [127.045371, 37.487391] }, 
    { "id": 32, "location": [127.039102, 37.489985] }, 
    { "id": 33, "location": [127.056914, 37.509118] }, 
    { "id": 34, "location": [127.064212, 37.488129] }, 
    { "id": 35, "location": [127.047619, 37.487612] }, 
    { "id": 36, "location": [127.048714, 37.506603] }, 
    { "id": 37, "location": [127.033219, 37.497418] }, 
    { "id": 38, "location": [127.039881, 37.489802] }, 
    { "id": 39, "location": [127.034607, 37.507405] }, 
    { "id": 40, "location": [127.028774, 37.512234] }, 
    { "id": 41, "location": [127.023587, 37.516785] }, 
    { "id": 42, "location": [127.046448, 37.491074] }, 
    { "id": 43, "location": [127.049489, 37.498019] }, 
    { "id": 44, "location": [127.041298, 37.498793] }, 
    { "id": 45, "location": [127.054103, 37.507996] }, 
    { "id": 46, "location": [127.065716, 37.490374] }, 
    { "id": 47, "location": [127.034502, 37.492107] }, 
    { "id": 48, "location": [127.032810, 37.490583] }, 
    { "id": 49, "location": [127.045891, 37.488392] }, 
    { "id": 50, "location": [127.048809, 37.507305] }  
  ]
}

 

그리고 TSP의 결과물은 다음과 같은 형식으로 나왔습니다.

{
	"code":0,
	"summary":
	{"cost":4622,"routes":1,"unassigned":0,"setup":0,"service":0,"duration":4622,"waiting_time":0,"priority":0,"violations":[],
		"computing_times":{"loading":2089,"solving":8,"routing":0}},
		"unassigned":[],
		"routes":
		[
			{"vehicle":1,"cost":4622,"setup":0,"service":0,"duration":4622,"waiting_time":0,"priority":0,
				"steps":
				[
					{"type":"start","location":[127.123289,37.538416],"setup":0,"service":0,"waiting_time":0,"arrival":0,"duration":0,"violations":[]},
					{"type":"job","location":[127.123289,37.538416],"id":1,"setup":0,"service":0,"waiting_time":0,"job":1,"arrival":0,"duration":0,"violations":[]},
					{"type":"job","location":[127.1275,37.5375],"id":18,"setup":0,"service":0,"waiting_time":0,"job":18,"arrival":122,"duration":122,"violations":[]},
					{"type":"job","location":[127.123789,37.539789],"id":28,"setup":0,"service":0,"waiting_time":0,"job":28,"arrival":151,"duration":151,"violations":[]},
					{"type":"job","location":[127.12389,37.54089],"id":45,"setup":0,"service":0,"waiting_time":0,"job":45,"arrival":158,"duration":158,"violations":[]},
					{"type":"job","location":[127.123345,37.541234],"id":33,"setup":0,"service":0,"waiting_time":0,"job":33,"arrival":188,"duration":188,"violations":[]},
					{"type":"job","location":[127.1235,37.5405],"id":17,"setup":0,"service":0,"waiting_time":0,"job":17,"arrival":208,"duration":208,"violations":[]},
					{"type":"job","location":[127.123678,37.540678],"id":39,"setup":0,"service":0,"waiting_time":0,"job":39,"arrival":211,"duration":211,"violations":[]},
					{"type":"job","location":[127.130789,37.540789],"id":29,"setup":0,"service":0,"waiting_time":0,"job":29,"arrival":285,"duration":285,"violations":[]},
					{"type":"job","location":[127.1335,37.5435],"id":20,"setup":0,"service":0,"waiting_time":0,"job":20,"arrival":350,"duration":350,"violations":[]},
					{"type":"job","location":[127.135,37.545],"id":12,"setup":0,"service":0,"waiting_time":0,"job":12,"arrival":417,"duration":417,"violations":[]},
					{"type":"job","location":[127.12989,37.54589],"id":46,"setup":0,"service":0,"waiting_time":0,"job":46,"arrival":489,"duration":489,"violations":[]},
					{"type":"job","location":[127.129678,37.545678],"id":40,"setup":0,"service":0,"waiting_time":0,"job":40,"arrival":491,"duration":491,"violations":[]},
					{"type":"job","location":[127.129234,37.546123],"id":34,"setup":0,"service":0,"waiting_time":0,"job":34,"arrival":509,"duration":509,"violations":[]},
					{"type":"job","location":[127.13,37.55],"id":11,"setup":0,"service":0,"waiting_time":0,"job":11,"arrival":570,"duration":570,"violations":[]},
					{"type":"job","location":[127.129153,37.551052],"id":2,"setup":0,"service":0,"waiting_time":0,"job":2,"arrival":597,"duration":597,"violations":[]},
					{"type":"job","location":[127.1295,37.5505],"id":19,"setup":0,"service":0,"waiting_time":0,"job":19,"arrival":610,"duration":610,"violations":[]},
					{"type":"job","location":[127.135234,37.551234],"id":35,"setup":0,"service":0,"waiting_time":0,"job":35,"arrival":701,"duration":701,"violations":[]},
					{"type":"job","location":[127.135678,37.550678],"id":41,"setup":0,"service":0,"waiting_time":0,"job":41,"arrival":726,"duration":726,"violations":[]},
					{"type":"job","location":[127.13589,37.55089],"id":47,"setup":0,"service":0,"waiting_time":0,"job":47,"arrival":763,"duration":763,"violations":[]},
					{"type":"job","location":[127.1385,37.5485],"id":21,"setup":0,"service":0,"waiting_time":0,"job":21,"arrival":846,"duration":846,"violations":[]},
					{"type":"job","location":[127.140789,37.545789],"id":30,"setup":0,"service":0,"waiting_time":0,"job":30,"arrival":899,"duration":899,"violations":[]},
					{"type":"job","location":[127.150789,37.550789],"id":31,"setup":0,"service":0,"waiting_time":0,"job":31,"arrival":1007,"duration":1007,"violations":[]},
					{"type":"job","location":[127.16,37.555],"id":15,"setup":0,"service":0,"waiting_time":0,"job":15,"arrival":1103,"duration":1103,"violations":[]},
					{"type":"job","location":[127.160789,37.555789],"id":32,"setup":0,"service":0,"waiting_time":0,"job":32,"arrival":1109,"duration":1109,"violations":[]},
					{"type":"job","location":[127.166023,37.556111],"id":8,"setup":0,"service":0,"waiting_time":0,"job":8,"arrival":1146,"duration":1146,"violations":[]},
					{"type":"job","location":[127.176654,37.565231],"id":9,"setup":0,"service":0,"waiting_time":0,"job":9,"arrival":1278,"duration":1278,"violations":[]},
					{"type":"job","location":[127.1685,37.5785],"id":27,"setup":0,"service":0,"waiting_time":0,"job":27,"arrival":1639,"duration":1639,"violations":[]},
					{"type":"job","location":[127.1635,37.5735],"id":26,"setup":0,"service":0,"waiting_time":0,"job":26,"arrival":1817,"duration":1817,"violations":[]},
					{"type":"job","location":[127.16589,37.56589],"id":50,"setup":0,"service":0,"waiting_time":0,"job":50,"arrival":2333,"duration":2333,"violations":[]},
					{"type":"job","location":[127.165678,37.565678],"id":44,"setup":0,"service":0,"waiting_time":0,"job":44,"arrival":2334,"duration":2334,"violations":[]},
					{"type":"job","location":[127.165234,37.566234],"id":38,"setup":0,"service":0,"waiting_time":0,"job":38,"arrival":2534,"duration":2534,"violations":[]},
					{"type":"job","location":[127.17,37.565],"id":16,"setup":0,"service":0,"waiting_time":0,"job":16,"arrival":2598,"duration":2598,"violations":[]},
					{"type":"job","location":[127.1585,37.5685],"id":25,"setup":0,"service":0,"waiting_time":0,"job":25,"arrival":2785,"duration":2785,"violations":[]},
					{"type":"job","location":[127.156823,37.561231],"id":7,"setup":0,"service":0,"waiting_time":0,"job":7,"arrival":2901,"duration":2901,"violations":[]},
					{"type":"job","location":[127.15589,37.56089],"id":49,"setup":0,"service":0,"waiting_time":0,"job":49,"arrival":2936,"duration":2936,"violations":[]},
					{"type":"job","location":[127.155678,37.560678],"id":43,"setup":0,"service":0,"waiting_time":0,"job":43,"arrival":2940,"duration":2940,"violations":[]},
					{"type":"job","location":[127.155234,37.561234],"id":37,"setup":0,"service":0,"waiting_time":0,"job":37,"arrival":2961,"duration":2961,"violations":[]},
					{"type":"job","location":[127.1535,37.5635],"id":24,"setup":0,"service":0,"waiting_time":0,"job":24,"arrival":3005,"duration":3005,"violations":[]},
					{"type":"job","location":[127.1485,37.5585],"id":23,"setup":0,"service":0,"waiting_time":0,"job":23,"arrival":3098,"duration":3098,"violations":[]},
					{"type":"job","location":[127.14589,37.55589],"id":48,"setup":0,"service":0,"waiting_time":0,"job":48,"arrival":3175,"duration":3175,"violations":[]},
					{"type":"job","location":[127.145678,37.555678],"id":42,"setup":0,"service":0,"waiting_time":0,"job":42,"arrival":3177,"duration":3177,"violations":[]},
					{"type":"job","location":[127.145234,37.556234],"id":36,"setup":0,"service":0,"waiting_time":0,"job":36,"arrival":3184,"duration":3184,"violations":[]},
					{"type":"job","location":[127.1435,37.5535],"id":22,"setup":0,"service":0,"waiting_time":0,"job":22,"arrival":3228,"duration":3228,"violations":[]},
					{"type":"job","location":[127.143545,37.548152],"id":4,"setup":0,"service":0,"waiting_time":0,"job":4,"arrival":3274,"duration":3274,"violations":[]},
					{"type":"job","location":[127.140395,37.538827],"id":5,"setup":0,"service":0,"waiting_time":0,"job":5,"arrival":3333,"duration":3333,"violations":[]},
					{"type":"job","location":[127.14,37.535],"id":13,"setup":0,"service":0,"waiting_time":0,"job":13,"arrival":3382,"duration":3382,"violations":[]},
					{"type":"job","location":[127.15,37.525],"id":14,"setup":0,"service":0,"waiting_time":0,"job":14,"arrival":3836,"duration":3836,"violations":[]},
					{"type":"job","location":[127.137241,37.528131],"id":3,"setup":0,"service":0,"waiting_time":0,"job":3,"arrival":4220,"duration":4220,"violations":[]},
					{"type":"job","location":[127.127874,37.537267],"id":6,"setup":0,"service":0,"waiting_time":0,"job":6,"arrival":4336,"duration":4336,"violations":[]},
					{"type":"job","location":[127.1205,37.54],"id":10,"setup":0,"service":0,"waiting_time":0,"job":10,"arrival":4603,"duration":4603,"violations":[]},
					{"type":"end","location":[127.123289,37.538416],"setup":0,"service":0,"waiting_time":0,"arrival":4622,"duration":4622,"violations":[]}
				],
				"violations":[]
			}
		]
}

 

위에서 정해 진 순번의 배열과 함께 id가 어디로 가 있는 지를 굳이 추적해 보고 싶었습니다.

우선 input.json에 대해서 순번과 ID를 표시하는 코드는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Leaflet Map with Steps and IDs</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <style>
    #map {
      height: 100vh;
    }
    .step-label {
      background-color: rgba(255, 255, 255, 0.8); /* White with transparency */
      border: 2px solid blue;                     /* Blue border */
      border-radius: 5px;                         /* Rounded corners */
      padding: 5px 10px;                          /* Padding for label */
      font-size: 10px;                            /* Font size */
      color: blue;                                /* Blue text */
      font-weight: bold;                          /* Bold text */
    }
  </style>
</head>
<body>
  <div id="map"></div>

  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <script>
    // Initialize the map
    const map = L.map('map').setView([37.497942, 127.027621], 13); // Default center

    // Add a tile layer to the map
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
    }).addTo(map);

    // Load the output.json file
    fetch('output.json')
      .then(response => response.json())
      .then(data => {
        const jobLocations = [];
        let stepOrder = 0;

        // Process steps in the routes array
        data.routes.forEach(route => {
          route.steps.forEach((step, index) => {
            let labelText;

            // Determine the label text for the step
            if (step.type === 'start') {
              labelText = `Start (0)`;
            } else if (step.type === 'end') {
              labelText = `End (${route.steps.length - 1})`;
            } else {
              labelText = `Step ${stepOrder} (ID: ${step.id})`;
              stepOrder++;
            }

            // Create a label for each step
            const label = L.divIcon({
              className: 'step-label',
              html: labelText, // Display the step order and ID
            });

            // Add a marker to the map
            L.marker([step.location[1], step.location[0]], { icon: label }).addTo(map);

            // Save the step location for polyline
            jobLocations.push([step.location[1], step.location[0]]);
          });
        });

        // Draw connecting lines between steps
        L.polyline(jobLocations, {
          color: 'blue',
          weight: 3,
          opacity: 0.7,
        }).addTo(map);

        // Adjust the map bounds to fit all locations
        const bounds = L.latLngBounds(jobLocations);
        map.fitBounds(bounds);
      })
      .catch(err => console.error('Error loading output.json:', err));
  </script>
</body>
</html>

 

결과는 다음과 같이 나왔습니다.

좀 어지러워 보이지만 잘 찍었네요(??) ㅋ

 

너무 어지러워 보이니 약간 터치를 합니다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Leaflet Map with Steps and IDs</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <style>
    #map {
      height: 100vh;
    }
    .step-label {
      background-color: rgba(255, 255, 255, 0.8); /* White with transparency */
      border: 2px solid blue;                     /* Blue border */
      border-radius: 5px;                         /* Rounded corners */
      padding: 5px 10px;                          /* Padding for label */
      font-size: 10px;                            /* Font size */
      color: blue;                                /* Blue text */
      font-weight: bold;                          /* Bold text */
    }
  </style>
</head>
<body>
  <div id="map"></div>

  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <script>
    // Initialize the map
    const map = L.map('map').setView([37.497942, 127.027621], 13); // Default center

    // Add a tile layer to the map
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
    }).addTo(map);

    // Load the output.json file
    fetch('output.json')
      .then(response => response.json())
      .then(data => {
        const jobLocations = [];
        let stepOrder = 0;

        // Process steps in the routes array
        data.routes.forEach(route => {
          route.steps.forEach((step, index) => {
            let labelText;

            // Determine the label text for the step
            if (step.type === 'start') {
            //  labelText = `Start (0)`;
			  labelText = `S(0)`;
            } else if (step.type === 'end') {
              //labelText = `End (${route.steps.length - 1})`;
			  labelText = `E(${route.steps.length - 1})`;
            } else {
              //labelText = `Step ${stepOrder} (ID: ${step.id})`;
			  labelText = `${stepOrder}(${step.id})`;
              stepOrder++;
            }

            // Create a label for each step
            const label = L.divIcon({
              className: 'step-label',
              html: labelText, // Display the step order and ID
            });

            // Add a marker to the map
            L.marker([step.location[1], step.location[0]], { icon: label }).addTo(map);

            // Save the step location for polyline
            jobLocations.push([step.location[1], step.location[0]]);
          });
        });

        // Draw connecting lines between steps
        L.polyline(jobLocations, {
          color: 'blue',
          weight: 3,
          opacity: 0.7,
        }).addTo(map);

        // Adjust the map bounds to fit all locations
        const bounds = L.latLngBounds(jobLocations);
        map.fitBounds(bounds);
      })
      .catch(err => console.error('Error loading output.json:', err));
  </script>
</body>
</html>

다시 결과를 확인 해 보면,

 

그냥 볼만 하네요

 

TSP 경로 확인

그런 다음 route.json 파일을 만들기 위해서는 위의 id와 순번을 다 가지고 다녀야 합니다.

아래 코드로 TSP 경로를 다시 요청 합니다.

const fs = require('fs');
const axios = require('axios');

// OSRM 서버 URL
const OSRM_BASE_URL = 'http://localhost:5000/route/v1/driving';

// 파일 경로
const OUTPUT_JSON = './output.json';
const ROUTE_JSON = './route_with_id.json';

// 경로 요청 및 처리
async function generateRouteWithIds() {
  try {
    // Step 1: output.json 읽기
    const outputData = JSON.parse(fs.readFileSync(OUTPUT_JSON, 'utf-8'));

    if (!outputData.routes || outputData.routes.length === 0) {
      throw new Error('output.json에 경로 데이터가 없습니다.');
    }

    // Step 2: 작업 좌표와 ID 수집
    const routeSteps = outputData.routes[0].steps; // 첫 번째 경로만 처리
    const coordinates = routeSteps.map((step) => step.location.join(',')).join(';'); // 경도,위도 포맷
    const stepsWithId = routeSteps.map((step, index) => ({
      id: step.id || "SE", // ID가 null이면 "SE"로 설정
      type: step.type,
      location: step.location,
      order: index // 순번 추가
    }));

    console.log('OSRM 요청 좌표:', coordinates);

    // Step 3: OSRM에 요청
    const osrmUrl = `${OSRM_BASE_URL}/${coordinates}?overview=full&geometries=geojson`;
    console.log('OSRM 요청 URL:', osrmUrl);

    const response = await axios.get(osrmUrl);

    if (response.status !== 200) {
      throw new Error(`OSRM 요청 실패: ${response.statusText}`);
    }

    const routeData = response.data;

    // Step 4: 순번 및 ID를 포함한 경로 데이터 작성
    const enhancedRoute = {
      geometry: routeData.routes[0].geometry,
      steps: stepsWithId, // ID와 순번 포함
    };

    // Step 5: 결과를 route.json으로 저장
    fs.writeFileSync(ROUTE_JSON, JSON.stringify(enhancedRoute, null, 2), 'utf-8');
    console.log('route.json 파일 생성 완료!');
  } catch (error) {
    console.error('오류 발생:', error.message);
  }
}

// 실행
generateRouteWithIds();

이런 느낌으로 경로들이 나오는 군요...

 

나온 경로를 확인 하는 코드는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Leaflet Map with Steps and IDs</title>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <style>
    #map {
      height: 100vh;
    }
    .step-label {
      background-color: rgba(255, 255, 255, 0.8);
      border: 2px solid green;
      border-radius: 5px;
      padding: 5px 10px;
      font-size: 14px;
      color: green;
      font-weight: bold;
    }
  </style>
</head>
<body>
  <div id="map"></div>

  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <script>
    // Initialize the map
    const map = L.map('map').setView([37.497942, 127.027621], 13);

    // Add a tile layer to the map
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);

    // Load the route.json file
    fetch('route_with_id.json')
      .then(response => response.json())
      .then(data => {
        const jobLocations = [];

        // Draw route geometry
        if (data.geometry) {
          L.geoJSON(data.geometry, {
            style: { color: 'blue', weight: 3, opacity: 0.7 }
          }).addTo(map);
        }

        // Process steps
        data.steps.forEach(step => {
          // ID가 "SE"인 경우 처리
          const labelText = `${step.type} (${step.order}) ${step.id === "SE" ? "SE" : `ID: ${step.id}`}`;

          const label = L.divIcon({
            className: 'step-label',
            html: labelText
          });

          L.marker([step.location[1], step.location[0]], { icon: label }).addTo(map);
          jobLocations.push([step.location[1], step.location[0]]);
        });

        // Fit map bounds
        const bounds = L.latLngBounds(jobLocations);
        map.fitBounds(bounds);
      })
      .catch(err => console.error('Error loading route.json:', err));
  </script>
</body>
</html>

 

결과는 다음과 같이..

애도 어지러우니 좀 고쳐 줍니다.

 

근데 약간 시작점에서 2번으로 가는 것이 좀 이상한 것 같기는 하네요....

 

이상.

728x90