- Cambios en el Algoritmo Genetico.

- Se agregó el HASH al JSON que regresa cuando se genera el ruteo.
This commit is contained in:
2024-05-07 05:46:32 -06:00
parent 433fc32b9a
commit b44f28abc9
18 changed files with 2353 additions and 35 deletions

21
Files/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Muyang Ye
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1033
Files/OSMTools.js Normal file

File diff suppressed because it is too large Load Diff

60
Files/README.md Normal file
View File

@@ -0,0 +1,60 @@
![Header](./cover.png)
# 🌎 Travelnetics ([demo](https://muyangye.github.io/Traveling_Salesman_Solver_Google_Maps/))
The Traveling Salesman Problem statement is as follows:
```Given a list of cities and the distances between each pair of cities, what is the shortest possible route that visits each city exactly once and returns to the origin city?```
This problem is **NP-hard** because it is as hard as a NP-Complete problem Hamilton Cycle and doesn't have an efficient certifier (not necessarily need this to be NP-Hard though). It can not be solved within polynomial time. The reason is this:
Suppose there are 19 cities in total, then I have 19 choices of which city should I travel first, 18 choices of which
city should I travel second, ..., 1 choice of which city should I travel at last. In total, that is **19!** possibilities,
out of the **19!** possibilities, I pick the one that has the shortest total distance.
If my computer can test **one billion** tours per second. It is going to take **19!/1,000,000,000** seconds ~ **3.85 years**
to finish. Therefore, it is unfeasible to enumerate all possibilities. This project proposes a partial solution using
`Genetic Algorithm` and calls `Google Maps API` to visualize. You can also utilize this project to plan your travel over 100+ places with ease.
### You can see a demo [here](https://muyangye.github.io/Traveling_Salesman_Solver_Google_Maps/)
(please note that I am using my personal Google Maps API key to host the demo. So I've set up restrictions of daily usage limit.
If you see Google Map does not load correctly. It means the daily limit was exceeded. The settings for the demo site are
`population` of 128, `numIterations` of 10000, and `mutChance` of 0.2)
## ▶️ Steps to Run Locally
1. Replace `apiKey` attribute in `config.js` with your own Google Maps API Key. If you do not have
one, here is the [link](https://developers.google.com/maps/documentation/javascript/get-api-key)
to create one (❗❗❗ Note: Fees charged by Google may apply ❗❗❗)
2. Open `index.html`, type an address in the search bar and Google Maps' Autocomplete API will
show you a list of addresses. click on one will add a waypoint, the **first** waypoint added is the origin
3. Check `Return To Origin?` or not, which means whether the solution should include going back to the origin
3. Click `Calculate Best Route!` at the bottom of `index.html`, enjoy!
## ⚙️ Customize Yourself
### Edit `config.js`, which contains the following fields:
- `popSize`: An `integer` == Population size == The total number of individual routes
- `numIterations`: A `number` > `0` == How many iterations the Genetic Algorithm should run. Generally the
more iterations, the more GA converges
- `mutChance`: A `float` between `0` and `1` == Mutation chance, as explained in `How Does It Work?`
## 💡 How Does It Work?
### [Medium Article](https://medium.com/@realymyplus/introduction-to-genetic-algorithm-with-a-website-to-watch-it-solve-traveling-salesman-problem-live-a21105a3251a)
## ⚠Known Defects
- This project solely calculates the distance between 2 waypoints using **Haversine distance**.
However, this approach has 2 major disadvantages:
- **Shortest distance** is not always equal to **shortest time**
- **Haversine distance** calculates the distance of a straight line between 2 waypoints,
whereas there are other factors involved in the **shortest distance** such as the
**existence/straightness of a road** and/or **elevation**
- All of the above 2 problems can be solved by simply querying [Google Maps' Directions API](https://developers.google.com/maps/documentation/directions/overview),
but again, Google Maps charges very high for this API. In future versions, will add
support to let the user decide whether to use [Google Maps' Directions API](https://developers.google.com/maps/documentation/directions/overview)
or **Haversine distance** for calculating distances
- Genetic Algorithm **does not gurantee** to generate the **global optimal solution** since
Genetic Algorithm may converge fairly quickly. This is why we want `mutChance` for mutation
to add a little bit of randomness here
## 🏆Acknowledgments && Disclaimers
- This project's idea originates from `ITP 435-Professional C++` taught at the
`University of Southern California` designed by [Sanjay Madhav](https://viterbi.usc.edu/directory/faculty/Madhav/Sanjay)
- This is the first time I ever touched Javascript. I am a lifelong C++|Python|Java|PHP developer.
So please bear with me if my Javascript coding style is a mess. Any suggestions are
more than welcome!

6
Files/config.js Normal file
View File

@@ -0,0 +1,6 @@
config = {
"apiKey": "AIzaSyDYUOIGHUNDHrkdsU2B-WT7loUGdqGgCfg", // REPLACE WITH YOUR_API_KEY HERE
"popSize": 128,
"numIterations": 5000,
"mutChance": 0.4,
};

BIN
Files/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

270
Files/genetic-algorithm.js Normal file
View File

@@ -0,0 +1,270 @@
const CONVERT_TO_RADIAN_CONST = 1; // 0.0174533;
function loadScript() {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://maps.googleapis.com/maps/api/js?key=" + config["apiKey"] + "&callback=initMap&v=weekly";
script.defer = true;
document.body.appendChild(script);
}
function initMap() {
let waypointsJSON = localStorage.getItem("waypoints");
// let waypointsJSON = [{"name":"Lago Chalco 47, Anáhuac I Secc, Miguel Hidalgo, 11320 Ciudad de México, CDMX, Mexico","lat":0.33943361448716003,"lon":-1.73094628692019},{"name":"C. Lago Chapala 47, Anáhuac I Secc., Anáhuac I Secc, Miguel Hidalgo, 11320 Ciudad de México, CDMX, Mexico","lat":0.33939502873152005,"lon":-1.73093370832688},{"name":"Lago Texcoco, Anáhuac I Secc, Ciudad de México, CDMX, Mexico","lat":0.33941341578307005,"lon":-1.73099749490239},{"name":"Lago Cuitzeo, Anáhuac I Secc., 11320 Ciudad de México, CDMX, Mexico","lat":0.33936825362399003,"lon":-1.73091535792726}]
let returnToOrigin = localStorage.getItem("returnToOrigin");
let waypoints = JSON.parse(waypointsJSON);
// console.log(">>>>>> ga/waypoints", waypointsJSON);
// console.log(waypoints);
const map = new google.maps.Map(document.getElementById("map"), {
center: { lat: waypoints[0].lat / CONVERT_TO_RADIAN_CONST, lng: waypoints[0].lon / CONVERT_TO_RADIAN_CONST},
zoom: 8,
});
class Waypoint{
constructor(name, location) {
this.name = name;
this.lat = location.lat();
this.lon = location.lng();
}
}
var poly = new google.maps.Polyline({
editable: true,
path: []
});
let popSize = config["popSize"];
let numIterations = config["numIterations"];
let mutChance = config["mutChance"];
// Fisher-Yates shuffle algorithm
function shuffle(individual) {
let i = individual.length;
while (--i > 0) {
let temp = Math.floor(Math.random() * (i + 1));
[individual[temp], individual[i]] = [individual[i], individual[temp]];
}
}
// Generate initial population
function genInitialPopulation(population) {
let individual = []; //Arrego con los puntos a visitar.
let zero = [0];
for(i=0; i < waypoints.length; ++i){
individual.push(i); // Agregamos el primer individuo con el orden original de los puntos.
}
population.push(individual)
for (let i = 0; i < popSize; ++i) { // Agregamos a la poblacion el numero de individuos establecido (popSize).
individual = [...Array(waypoints.length - 1).keys()].map(j => ++j); // Quitamos el 0 (punto de origen) porque es el inicio de la ruta, solo barajeamos los demas y luego lo regresamos.
shuffle(individual); // Barajeamos los puntos del individuo para tener un individuo random.
//console.log(">>>>> ga/individual ", i, [...Array(waypoints.length - 1).keys()].map(j => ++j), individual)
population.push(zero.concat(individual)); // Regresamos el 0 (punto de origen) al inicio de la ruta.
}
// console.log(">>>>> ga/population ", population)
}
// Calculate the Haversine distance between two waypoints
function getHaversineDistance(waypoint1, waypoint2) {
let dlon = waypoint2.lon - waypoint1.lon;
let lat1 = waypoint1.lat;
let lat2 = waypoint2.lat;
let dlat = lat2 - lat1;
let a = Math.pow(Math.sin(dlat/2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon/2), 2);
let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return 3961 * c;
}
function calcTotalDistance(waypoints, individual) {
let totalDistance = 0;
for (let i = 0; i < individual.length - 1; ++i) {
totalDistance += getDistanceFromLatLonInKm(waypoints[individual[i]], waypoints[individual[i+1]]);
}
// Add distance back to origin if returnToOrigin is set to true
return returnToOrigin === "true" ? totalDistance + getHaversineDistance(waypoints[0], waypoints[individual[individual.length - 1]]) : totalDistance;
}
function getDistanceFromLatLonInKm(waypoint1, waypoint2) {
let lat1 = waypoint1.lat;
let lon1 = waypoint1.lon;
let lat2 = waypoint2.lat;
let lon2 = waypoint2.lon;
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2-lat1); // deg2rad below
var dLon = deg2rad(lon2-lon1);
var 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)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c; // Distance in km
return d;
}
function deg2rad(deg) {
return deg * (Math.PI/180)
}
function normalize(probabilities) {
let sum = probabilities.reduce(function(a, b) { //Suma todos los valores del arreglo.
return a + b;
}, 0);
// let x = 0;
// probabilities.forEach((probability, index) => {
// x += probabilities[index];
// });
// console.log("##### SUM: ",sum, x);
probabilities.forEach((probability, index) => { // Cambia cada valor del arreglo con su valor dividido entre SUM.
probabilities[index] /= sum;
});
}
function getRandomInclusive() { // Genera un numero ramdom entro 0 y 1, PERO si resulta 0, entonces regresa 1.
return Math.random() == 0 ? 1 : Math.random();
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min); // Redondeamos hacia arriba.
max = Math.floor(max); // Redondeamos hacia abajo.
return Math.floor(Math.random() * (max - min + 1)) + min; // Redondeamos hacia abajo (random * (max - min + 1)).
}
function genNewPopulation(newPopulation, crossoverIndex, individual1, individual2) {
let newIndividual = [];
++crossoverIndex;
for (let i = 0; i < crossoverIndex; ++i) {
newIndividual.push(individual1[i]);
}
for (let i = 0; i < individual2.length; ++i) {
if (!newIndividual.includes(individual2[i])) {
newIndividual.push(individual2[i]);
}
}
let random = getRandomInclusive();
//console.log(random);
if (random <= mutChance) {
let index1 = getRandomIntInclusive(1, newIndividual.length - 1);
let index2 = getRandomIntInclusive(1, newIndividual.length - 1);
[newIndividual[index1], newIndividual[index2]] = [newIndividual[index2], newIndividual[index1]];
}
newPopulation.push(newIndividual);
}
function addToPath(polyPath, latlng, count) {
polyPath.push(latlng);
if (count != waypoints.length+1) {
new google.maps.Marker({
position: latlng,
label: {text: count.toString(), color: "#00FF00"},
animation: google.maps.Animation.DROP,
map: map,
});
}
}
function startNewCalculation() {
window.location.href = "index.html";
}
document.getElementById("goto-index").addEventListener("click", startNewCalculation);
let waypointsList = document.getElementById("waypoints-list");
/*
INICIAMOS LOS CALCULOS
*/
let population = [];
// let fitTemp = [];
let sortedIndexTemp = [];
genInitialPopulation(population); // Mandamos population en blanco y regresamos 128 (popSize) variaciones.
for (let i = 0; i <= numIterations; ++i) {
// fitness[i] <==> the ith route's total distance
let fitness = [];
population.forEach(individual => {
fitness.push(calcTotalDistance(waypoints, individual)); // Ponemos en fitness la distancia total entre los puntos de este individuo en particular.
});
// fitTemp = fitness;
// console.log(">>>>> ga/fitness", fitness)
let sortedIndexes = [...Array(popSize).keys()].sort((index1, index2) => {
return fitness[index1] < fitness[index2] ? -1 : 1;
});
// console.log(sortedIndexes);
let probabilities = new Array(popSize).fill(1.0 / popSize); // No se que haga este arreglo de probabilidades, pero se usa en algoritmos geneticos.
probabilities[sortedIndexes[0]] *= 6; // Al parecer tiene que ver con las posibiliddes de que un individuo se escoja para la siguiente generación.
probabilities[sortedIndexes[1]] *= 6;
for (let j = 0; j < popSize / 2; ++j) {
probabilities[sortedIndexes[j]] *= 3;
}
// console.log(">>>>> ga/probabilities", probabilities);
// let probs = [] = probabilities;
normalize(probabilities);
// console.log(calcTotalDistance(waypoints, population[sortedIndexes[0]]))
// console.log(">>>>> ga/probabilities normalizadas", probabilities);
if (i == numIterations) { // Si ya completamos el numero de iteraciones especificadas (numIterations), entonces salimos.
let solution = population[sortedIndexes[0]];
// console.log(">>>>> POPULATION: ", population)
console.log(">>>>> FITNESS: ", fitness)
// console.log(">>>>> SORTED INDEXES: ", sortedIndexes)
// console.log(">>>>> PROBS: ", probs);
// console.log(">>>>> PROBABILITIES: ", probabilities)
console.log(">>>>> GA/SOLUTION:", solution);
console.log(calcTotalDistance(waypoints, solution))
let polyPath = [];
let count = 0;
let waypointElement = null;
solution.forEach(waypointIndex => { // Generamos la polilinea para Google Maps.
waypoint = waypoints[waypointIndex];
waypointElement = document.createElement("li");
waypointElement.append(waypoint.name);
waypointsList.appendChild(waypointElement);
addToPath(polyPath, new google.maps.LatLng(waypoint.lat / CONVERT_TO_RADIAN_CONST, waypoint.lon / CONVERT_TO_RADIAN_CONST), ++count);
});
if (returnToOrigin === "true") {
addToPath(polyPath, new google.maps.LatLng(waypoints[0].lat / CONVERT_TO_RADIAN_CONST, waypoints[0].lon / CONVERT_TO_RADIAN_CONST), ++count);
}
poly.setPath(polyPath);
poly.setMap(map);
break;
}
let index1 = 0;
let index2 = 0;
let random = 0;
let currSum = 0;
let crossoverIndex = 0;
let aGoesFirst = 0;
let newPopulation = [];
for (let j = 0; j < popSize; ++j) {
currSum = 0;
random = getRandomInclusive();
for (let k = 0; k < popSize; ++k) { // Generamos index1.
currSum += probabilities[k];
if (currSum >= random) {
index1 = k;
break;
}
}
currSum = 0;
random = getRandomInclusive();
for (let k = 0; k < popSize; ++k) { // Generamos index2.
currSum += probabilities[k];
if (currSum >= random) {
index2 = k;
break;
}
}
crossoverIndex = getRandomIntInclusive(1, waypoints.length - 2);
aGoesFirst = getRandomIntInclusive(0, 1);
//console.log("****************** ",crossoverIndex, aGoesFirst)
// Si aGoesFirst = 0 o 1, entonces obtenemos nueva poblacion de una u otra forma!
aGoesFirst ? genNewPopulation(newPopulation, crossoverIndex, population[index1], population[index2])
: genNewPopulation(newPopulation, crossoverIndex, population[index2], population[index1]);
}
population = newPopulation;
}
//console.log(">>>>> FITNESS TEMP: ", fitTemp)
//console.log(">>>>> SORTED INDEXES TEMP: ", sortedIndexTemp)
}

51
Files/index.html Normal file
View File

@@ -0,0 +1,51 @@
<html>
<head>
<title>Travelnetics</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
</head>
<body>
<div class="pac-card" id="pac-card">
<div>
<div id="title">Search for Waypoints</div>
<br />
</div>
<div id="pac-container">
<input id="pac-input" type="text" placeholder="Enter a location" />
</div>
</div>
<div id="map"></div>
<!-- Searched data -->
<ul id="waypoints-list">
</ul>
<div id="infowindow-content">
<span id="place-name" class="title"></span><br />
<span id="place-address"></span>
</div>
<div id="return-to-origin-div">
<label>Return to Origin?
<input id="return-to-origin" type="checkbox" checked>
</label>
</div>
<div id="button-div">
<button id="goto-result">Calculate Best Route!</button>
</div>
<!--
The `defer` attribute causes the callback to execute after the full HTML
document has been parsed. For non-blocking uses, avoiding race conditions,
and consistent behavior across browsers, consider loading using Promises
with https://www.npmjs.com/package/@googlemaps/js-api-loader.
-->
<script src="./config.js"></script>
<script src="./map.js"></script>
<script defer>
window.initMap = initMap;
window.onload = loadScript;
</script>
</body>
</html>

140
Files/map.js Normal file

File diff suppressed because one or more lines are too long

9
Files/osm.html Normal file
View File

@@ -0,0 +1,9 @@
<script type="text/javascript" src="OSMTools.js"></script>
<head>
<script type="text/javascript">
at.quelltextlich.osm.embedMapMarkedLocation( 47.07655, 15.43683, 11, 500, 300 );
</script>
</head>
<body>
test
</body>

24
Files/r2.html Normal file
View File

@@ -0,0 +1,24 @@
<html>
<head>
<title>Travelnetics</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
</head>
<body>
<div id="map"></div>
<h2 style="text-align: center;">Results:</h2>
<ol style="text-align: center; list-style-position: inside;" id="waypoints-list">
</ol>
<div id="button-div">
<button id="goto-index" style="cursor: pointer;">Start a New Calculation!</button>
</div>
<script src="./config.js"></script>
<script src="./genetic-algorithm.js"></script>
<script defer>
window.onload = loadScript;
window.initMap = initMap;
</script>
</body>
</html>

24
Files/result.html Normal file
View File

@@ -0,0 +1,24 @@
<html>
<head>
<title>Travelnetics</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
</head>
<body>
<div id="map"></div>
<h2 style="text-align: center;">Results:</h2>
<ol style="text-align: center; list-style-position: inside;" id="waypoints-list">
</ol>
<div id="button-div">
<button id="goto-index" style="cursor: pointer;">Start a New Calculation!</button>
</div>
<script src="./config.js"></script>
<script src="./genetic-algorithm.js"></script>
<script defer>
window.onload = loadScript;
window.initMap = initMap;
</script>
</body>
</html>

95
Files/style.css Normal file
View File

@@ -0,0 +1,95 @@
/*
* Always set the map height explicitly to define the size of the div element
* that contains the map.
*/
#map {
height: 80%;
}
/*
* Optional: Makes the sample page fill the window.
*/
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#infowindow-content .title {
font-weight: bold;
}
#infowindow-content {
display: none;
}
#map #infowindow-content {
display: inline;
}
.pac-card {
background-color: #fff;
border: 0;
border-radius: 2px;
box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3);
margin: 10px;
padding: 0 0.5em;
font: 400 18px Roboto, Arial, sans-serif;
overflow: hidden;
font-family: Roboto;
padding: 0;
}
#pac-container {
padding-bottom: 12px;
margin-right: 12px;
}
#pac-input {
background-color: #fff;
font-family: Roboto;
font-size: 15px;
font-weight: 300;
margin-left: 12px;
padding: 0 11px 0 13px;
text-overflow: ellipsis;
width: 400px;
}
#pac-input:focus {
border-color: #4d90fe;
}
#title {
color: #fff;
background-color: #4d90fe;
font-size: 25px;
font-weight: 500;
padding: 6px 12px;
}
#button-div {
text-align: center;
}
#return-to-origin-div {
text-align: center;
font-size: large;
}
#goto-result {
border: 2px solid coral;
height: 50px;
width: 200px;
margin-bottom: 20px;
cursor: pointer;
}
#goto-index {
border: 2px solid coral;
height: 50px;
width: 200px;
margin-bottom: 20px;
cursor: pointer;
}