TransWikia.com

First project in Javascript: NearestNeighbor Calculator

Code Review Asked by Herbo on October 27, 2021

I just finished my first project in Javascript. It takes an array of locations (with coordinates on position 6 and 7) as Input, then the first location is checked against all other locations. After that it takes the closest neighbor and adds it to the route. Then it takes the coordinates of the neighbor which it just found and does same for it (excluding locations that already have a connection). In the end it returns to the starting point. Please give me some hints how i can write better, cleaner and more readable code. Any feedback is appreciated.

var listCoordinates = [
  [1, "München", "Robert-Bürkle-Straße", "1", 85737, "Ismaning", 48.229035, 11.686153, 524, 854],
  [2, "Berlin", "Wittestraße", "30", 13509, "Berlin", 52.580911, 13.293884, 648, 302],
  [3, "Braunschweig", "Mittelweg", "7", 38106, "Braunschweig", 52.278748, 10.524797, 434, 341],
  [4, "Bretten", "Edisonstraße", "2", 75015, "Bretten", 49.032767, 8.698372, 276, 747],
  [5, "Chemnitz", "Zwickauer Straße", "16a", 9122, "Chemnitz", 50.829383, 12.914737, 622, 525],
  [6, "Düsseldorf", "Gladbecker Straße", "3", 40472, "Düsseldorf", 51.274774, 6.794912, 138, 455]
];
//Haversine formula: calulate distance between two locations
const haversineDistance = ([lat1, lon1], [lat2, lon2], isMiles = false) => {
  const toRadian = angle => (Math.PI / 180) * angle;
  const distance = (a, b) => (Math.PI / 180) * (a - b);
  const RADIUS_OF_EARTH_IN_KM = 6371;
  const dLat = distance(lat2, lat1);
  const dLon = distance(lon2, lon1);
  lat1 = toRadian(lat1);
  lat2 = toRadian(lat2);
  //Haversine Formula
  const a = Math.pow(Math.sin(dLat / 2), 2) + Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1) * Math.cos(lat2);
  const c = 2 * Math.asin(Math.sqrt(a));
  let finalDistance = RADIUS_OF_EARTH_IN_KM * c;
  if (isMiles) {
    finalDistance /= 1.60934;
  }
  return finalDistance;
};
//heuristic strict nearest neighbor (points that already have a connection do not get calculated)
function nearestNeighbor(coordinates) {
  var route = {
    msg: '',
    distance: 0,
    route: [0]
  }
  var currentPoint = 0;
  for (var i = 0; i <= coordinates.length - 2; i++) {
    var nearestNeighbor = {
      key: 0,
      distance: 0
    }
    var pointaLat = coordinates[currentPoint][6],
      pointaLon = coordinates[currentPoint][7];
    for (var j = 0; j <= coordinates.length - 1; j++) {
      if (j != currentPoint) {
        if (!route.route.includes(j)) {
          var pointbLat = coordinates[j][6],
            pointbLon = coordinates[j][7];
          var currentDistance = haversineDistance([pointaLat, pointaLon], [pointbLat, pointbLon], 0);
          if (nearestNeighbor.distance === 0 || nearestNeighbor.distance > currentDistance) {
            nearestNeighbor.key = j;
            nearestNeighbor.distance = currentDistance;
          }
        }
      }
    }
    route.distance += nearestNeighbor.distance;
    route.route.push(nearestNeighbor.key)
    currentPoint = nearestNeighbor.key;
  }
  //return to start point
  var pointaLat = coordinates[0][6],
    pointaLon = coordinates[0][7],
    pointbLat = coordinates[route.route[route.route.length - 1]][6],
    pointbLon = coordinates[route.route[route.route.length - 1]][7];
  route.distance += haversineDistance([pointaLat, pointaLon], [pointbLat, pointbLon], 0);
  route.route.push(0);
  route.distance = Math.round((route.distance + Number.EPSILON) * 100) / 100;
  route.msg = "Die kürzeste Strecke beträgt " + route.distance + " km";
  return route;
}

console.log(nearestNeighbor(listCoordinates));

3 Answers

Please give me some hints how i can write better, cleaner and more readable code

.

Objects: anything else is merely arranging the deck chairs on the Titanic

var listCoordinates = [
  [1, "München", "Robert-Bürkle-Straße", "1", 85737, "Ismaning", 48.229035, 11.686153, 524, 854],
  [2, "Berlin", "Wittestraße", "30", 13509, "Berlin", 52.580911, 13.293884, 648, 302],
  [3, "Braunschweig", "Mittelweg", "7", 38106, "Braunschweig", 52.278748, 10.524797, 434, 341],
  [4, "Bretten", "Edisonstraße", "2", 75015, "Bretten", 49.032767, 8.698372, 276, 747],
  [5, "Chemnitz", "Zwickauer Straße", "16a", 9122, "Chemnitz", 50.829383, 12.914737, 622, 525],
  [6, "Düsseldorf", "Gladbecker Straße", "3", 40472, "Düsseldorf", 51.274774, 6.794912, 138, 455]
];


// if I'm making lots of mistakes identifying the array elements, 
// its because the reader starts off not knowing that the heck the
// this array data is, then it's ripped apart several times, named and
// then renamed. 
// I cannot tell without very careful study and tracing what the heck
// is going on.

var places = []; 
listCoordinates.forEach(place => {
   places.push(
     { order          : place[0],
       city           : place[1],
       street         : place[2],
       doNotKnow      : place[3],
       whatIsThis     : place[4],
       nearestNeighbor : place[5],
       latitude       : place[6],
       longatude      : place[7],
       something      : place[8],
       somethingElse  : place[9]
    }
  ) );


//heuristic strict nearest neighbor (points that already have a connection do not get calculated)

// pass in a place object and use its properties.  
// I'm not going to rewrite this because I cannot tell how/where all these independent
// variables match to the place object.
// The stark contrast after *you* rewrite using coherent, understandable objects will be a huge "a-ha" moment
// in your understanding of readability, understandability,
// and the potential of object oriented coding in general.

P.S. write toString methods for objects that output the properties - great for debugging.

Answered by radarbob on October 27, 2021

Below, I have tried to refactor your code with some inline comments about what and why, I have done it. The overall design and workflow is preserved, so it's in the detail, I have tried to improve and optimize:

var locations = [
  {id: 1, region: "Ismaning/München (Hauptsitz)", street: "Robert-Bürkle-Straße", streetnumber: "1", postcode: 85737, city: "Ismaning", latitude: 48.229035, longitude: 11.686153, xaxis: 524, yaxis: 854},
  {id: 2, region: "Berlin", street: "Wittestraße", streetnumber: "30", postcode: 13509, city: "Berlin", latitude: 52.580911, longitude: 13.293884, xaxis: 648, yaxis: 302},
  {id: 3, region: "Braunschweig", street: "Mittelweg", streetnumber: "7", postcode: 38106, city: "Braunschweig", latitude: 52.278748, longitude: 10.524797, xaxis: 434, yaxis: 341},
  {id: 4, region: "Bretten", street: "Edisonstraße", streetnumber: "2", postcode: 75015, city: "Bretten", latitude: 49.032767, longitude: 8.698372, xaxis: 276, yaxis: 747},
  {id: 5, region: "Chemnitz", street: "Zwickauer Straße", streetnumber: "16a", postcode: 9122, city: "Chemnitz", latitude: 50.829383, longitude: 12.914737, xaxis: 622, yaxis: 525}
];

// HH: I have moved these functions outside of haversineDistance
// in order to keep that function more clear
const RADIUS_OF_EARTH_IN_KM = 6371;
function toRadian(angle) { return Math.PI / 180.0 * angle; }
function distance(a, b) { return (Math.PI / 180) * (a - b); }

//Haversine formula: calulate distance between two locations
// HH: This function now takes two point objects as argument
// instead of two arrays/tuples (see also below)
function haversineDistance(ptA, ptB, isMiles = false) {
  const dLat = distance(ptB.lat, ptA.lat);
  const dLon = distance(ptB.lng, ptA.lng);
  const lat1 = toRadian(ptA.lat);
  const lat2 = toRadian(ptB.lat);

  //Haversine Formula
  const a = Math.pow(Math.sin(dLat / 2), 2) + Math.pow(Math.sin(dLon / 2), 2) * Math.cos(lat1) * Math.cos(lat2);
  const c = 2 * Math.asin(Math.sqrt(a));

  let finalDistance = RADIUS_OF_EARTH_IN_KM * c;
  if (isMiles) {
    finalDistance /= 1.60934;
  }
  return finalDistance;
};

// HH: Name the function after what it does - not after how it does it.
function findShortestPath(coordinates) {
  var route = {
    msg: '',
    distance: 0,
    route: [0]
  }

  // HH: Instead of having pairs of lat, lon an object representing a location/point/position is more readable
      function getPoint(index) {
        return {
          lat: coordinates[index].latitude,
          lng: coordinates[index].longitude
        };
      }



  var currentPoint = 0;
  // HH: You iterate over and over again a lot of indices that has already been handled.
  // Instead it would be more efficient to maintain a set of remaining indices
  let indices = Array.from({ length: coordinates.length - 1 }, (_, i) => i + 1); // we don't want 0 included as it is the first currentPoint

  while (indices.length > 0) {
    var nearestNeighbor = {
      key: 0,
      distance: 0
    };

    let ptA = getPoint(currentPoint);
    for (let j of indices) {
      let ptB = getPoint(j);
      // HH: Because indices only contains not handled indices, you can omit guarding against current point and points in the route
      var currentDistance = haversineDistance(ptA, ptB, 0);
      if (nearestNeighbor.distance === 0 || nearestNeighbor.distance > currentDistance) {
        nearestNeighbor.key = j;
        nearestNeighbor.distance = currentDistance;
      }
    }
    route.distance += nearestNeighbor.distance;
    route.route.push(nearestNeighbor.key);
    // HH: The nearest neighbor is removed from indices
    indices.splice(indices.indexOf(nearestNeighbor.key), 1);
    currentPoint = nearestNeighbor.key;
  }

  //return to start point
  ptA = getPoint(0);
  let ptB = getPoint(route.route[route.route.length - 1]);
  route.distance += haversineDistance(ptA, ptB, 0);

  route.route.push(0);
  route.distance = Math.round((route.distance + Number.EPSILON) * 100) / 100;
  route.msg = "Die kürzeste Strecke beträgt " + route.distance + " km";
  return route;
}

console.log(findShortestPath(locations));

Answered by user73941 on October 27, 2021

Are you hoping for the arc length around a sphere or the Earth? What about going through the earth in a straight line? Currently your math solves the sphere but not Earth.

The earth has 2 radi that are used to solve the arc length.

R = √ [ (r1² * cos(B))² + (r2² * sin(B))² ] / [ (r1 * cos(B))² + (r2 * sin(B))² ]
B = Latitude
R = Radius
r1 = radius at equator
r2 = radius at the poles

But your Haversine method is otherwise correct =D

Answered by David Fisher on October 27, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP