1

I have a set of locations that I want to display to a user in proximity order - closest to farthest - based on their current coordinates. Assume we have ~100 data points of locations which includes each one's latitude and longitude (in some kind of object or array), and we know the user's lat and long. The goal is to display an ordered list of locations -- and it is beneficial if we can get and display to the user the nearest 8-10 locations while we calculate then display the remaining distances.

I know the brute-force solution is to loop through all locations, calculate the distance from the user, put them in order, then display them all to the user. But this is too slow.

A better solution is this one: https://stackoverflow.com/a/2466908/1766230 where you check within a bounded box first, expanding if necessary, then do the rest.

I've also seen there are other algorithms out there - like FLANN and other methods - but I haven't seen any examples written in JavaScript.

So the question: What is the fastest way to calculate (and display in order) nearest points in JavaScript?

Community
  • 1
  • 1
Luke
  • 18,811
  • 16
  • 99
  • 115
  • With ~100 points it is unlikely that you will see much, if any, benefit of the "divide and conquer" method over the "naive (brute force)" method. The naive method is very simple to implement. – Xotic750 Mar 06 '14 at 21:55
  • It's actually for a mobile app using Titanium. Have to do more digging to know *why* it appears to be such a bottleneck at the moment... but regardless, I'd like to find the optimal solution if it's not too difficult rather than just brute-force it. – Luke Mar 06 '14 at 22:00
  • From what I understand the "divide and conquer" method is optimal O(n log n) - depending on sort algorithm (merge sort required, I think) – Xotic750 Mar 06 '14 at 22:06
  • Do you have any examples of "divide and conquer" in JavaScript? – Luke Mar 06 '14 at 22:10
  • Nope, only a wiki reference that describes the algorithm. http://en.wikipedia.org/wiki/Closest_pair_of_points_problem#Planar_case – Xotic750 Mar 06 '14 at 22:12
  • For just sorting, check out quicksort or radix sort. Both will be faster than merge sort! – Brent Echols Mar 06 '14 at 22:32
  • quicksort is O(n log n) at best case, O(n2) at worst case but is not a stable sort, mergesort is O(n log n) at worst case but is a stable sort. Unsure whether stability is a requirement of "divide and conquer". Radix sort is O(n log n) at best case and O(kN) at worst case, it can be made a stable sort. This is information that I got from wikipedia about them. – Xotic750 Mar 06 '14 at 22:46
  • Can you show us your brute-force routine? – Xotic750 Mar 07 '14 at 00:15
  • do you have the ability to pre-process the locations before you are given the user's location? Or are the locations coming from some outside source or from a process involving the user/user's location? – גלעד ברקן Mar 08 '14 at 01:11
  • @גלעדברקן That's a very good point! I've been thinking of the problem as the ~100 points and the current location are previously unknown. – Xotic750 Mar 08 '14 at 07:17
  • I also didn't take into account that the earth is not flat! :P – Xotic750 Mar 08 '14 at 07:42

2 Answers2

3

So, if you are starting out with that list of points, drawing a small bounding box won't cut down very much, because you still do an O(n) check against all points for their location.

I would advise using a max-length heap or some other form of partial sort while iterating through all of the points. This lets you keep track of a small subset of approximately maximum/minimal points (as described by the length), so you can render those quickly before dealing with the rest. If you need more explanation about what I'm saying precisely, let me know.

Also what are you making this for that has such stringent performance issues? Typically computation like this shouldn't be a stress point, barring that you have 100k+ points. DOM manipulation is usually the most expensive spot

var points = [];
for (i = 0; i < 100; i++) {
    var point = [getRandomInt(0, 999), getRandomInt(0, 999)];
    point.len = distanceBetweenPoints(point, [499,499]);
    points.push(point);
}

console.log(Heap.nsmallest(points, 10, function (a, b) {
  return a.len < b.len;
}));

Here is the performance for it compared to bruteforce

Heap Code

js fiddle

Using the method I described, and a prebuilt heap another person wrote, I compared our methods. I think you will be happy! It performed 8,586 ops/sec compared to the 566 in the brute force technique!

Brent Echols
  • 590
  • 2
  • 9
  • Could you elaborate on the max-length heap solution? Wouldn't this still require searching through all locations before displaying the closest 8-10? – Luke Mar 06 '14 at 21:59
  • So there is no way around searching through x points, unless you have a server take care of some of the filtering for you. What this does however, is instead of sorting 'n' terms, you maintain of heap of say, length 10. The maximal element is at the top, w/e you check another value you compare it to that. If your current is smaller than the max, add it to the heap and (if filled), push the top off. If its bigger, just go to the next element. Fundamentally, you can't really NOT check through all of them, so this should be a good way to get the top 10 elements in a timely manner! – Brent Echols Mar 06 '14 at 22:28
  • This technically has around the same worst case as maintaining a sorted list, but the average should be dramatically better, esp. @ larger values. Hopefully this explains everything more in depth everything! Let me know if I can be of more help :D – Brent Echols Mar 06 '14 at 22:29
  • Hey! I updated everything and added code for it, if you were interested! – Brent Echols Mar 07 '14 at 23:46
1

Well this is my attempt at sorting an array of points by distance to a given point. This is brute-force as far as I understand. Then I slice the array to give you the 10 closest points.

Javascript

function distanceBetweenPoints(p1, p2) {
    return Math.abs(Math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1])));
}

function sortByDistance(location, arrayOfPoints) {
    arrayOfPoints.sort(function (a, b) {
        a.distance = distanceBetweenPoints(location, a);
        b.distance = distanceBetweenPoints(location, b);

        return a.distance - b.distance;
    });

    return arrayOfPoints;
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

var points = [];

for (i = 0; i < 100; i += 1) {
    points.push([getRandomInt(-90, 90), getRandomInt(-180, 180)]);
}

console.log(sortByDistance([0, 0], points).slice(0, 10));

On jsFiddle

This will at least give you something to test algorithms against. And here is a jsPerf for the above, so you can add other routines to it and do some real performance comparisons.

Note: This does not take into consideration that the Earth is a sphere! This is calculating Euclidean distance and not Geodesic distance. This is fine if the points, are for example, in the same town (or close proximity) but not if they are in different countries/continents. It also assumes that you have converted your longitude and latitude to a decimal representation.

Otherwise you will need to look at things like Great-circle distance and Haversine formula

In fact, the earth is very slightly ellipsoidal; using a spherical model gives errors typically up to 0.3%

Javascript

function toRadians(degrees) {
    return (degrees * Math.PI) / 180;
}

// Haversine formula
function distanceBetweenPoints(p1, p2) {
    var R = 6371, // mean earth radius in km
        lat1 = toRadians(p1[0]),
        lon1 = toRadians(p1[1]),
        lat2 = toRadians(p2[0]),
        lon2 = toRadians(p2[1]),
        dLat = lat2 - lat1,
        dLon = lon2 - lon1,
        a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2),
        c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)),
        d = R * c;

    return d;
}

function sortByDistance(location, arrayOfPoints) {
    arrayOfPoints.sort(function (a, b) {
        a.distance = distanceBetweenPoints(location, a);
        b.distance = distanceBetweenPoints(location, b);

        return a.distance - b.distance;
    });

    return arrayOfPoints;
}

function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

var points = [];

for (i = 0; i < 100; i += 1) {
    points.push([getRandomInt(-90, 90), getRandomInt(-180, 180)]);
}

console.log(sortByDistance([0, 0], points).slice(0, 10));

On jsFiddle

Xotic750
  • 22,914
  • 8
  • 57
  • 79
  • Great answer, but a small word of caution @Xotic750 adding `.distance` in the first code block in `arrayOfPoints.sort(...)` to an array is not JS best practice imo and would cause a lot of confusion for other developers ex: `[ 100, 95, distance: 19025.000000000004 ]` – Jake.JS Feb 15 '20 at 20:44