How to Efficiently Calculate Nearest 2D points in JavaScript? - javascript

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?

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!

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

Related

How do I make this function more precise?

I'm a beginner and I made a function to calculate the length of a semi-circular infinite snake figure. I took in two arguments; one of the radius the initial circle, and the next to be the precision (which is just the number of semi-circles).
Here's a diagram of the snake I'm talking about.
Here's what I wrote:
function snake(radius, precision) {
var pi = 3.14159265359
var exp = 1 - precision;
var sub = Math.pow(2, exp);
var product = 2 - sub;
var length = pi * radius * product
return length
}
I'm noticing that at one point the precision doesn't matter when I go really high as the value it return is the same. Is there a way to make it more precise?
You may use the constant Math.PI instead of your pi variable.
Feels like Number.toPrecision() is what you are looking for.
Below is slightly cleaned up version of your code snippet.
For the return value I'm using length.toPrecision(50):
function snake(radius, precision) {
const exp = 1 - precision;
const sub = Math.pow(2, exp);
const product = 2 - sub;
const length = Math.PI * radius * product;
return length.toPrecision(50);
}
console.log(snake(5, 55));
Yo can find out more about toPrecision() here.
The max precision value is 100.
This will be my method,
Length can be get by sum of the below series according to the screen shot,
PI X R (1 + 1/2 + 1/4 + ...)
S(n) = PIxR(1-0.5^n)/(1-0.5)
So the JavaScript function is,
function length(r, n) {
const ln = Math.PI * r * (1 - Math.pow(0.5, n))/(1 - 0.5);
return ln.toPrecision(50);
}

Calculate roll angle for Google Maps Street View

Preamble: there's an issue logged with the Google Maps API, requesting the ability to correct the roll angle of street view tiles to compensate for hills. I've come up with a client-side workaround involving some css sorcery on the tile container. Here's my rotate function:
rotate: function() {
var tilesLoaded = setInterval(function() {
var tiles = $('map-canvas').getElementsByTagName('img');
for (var i=0; i<tiles.length; i++) {
if (tiles[i].src.indexOf(maps.panorama.getPano()) > -1) {
if (typeof maps.panorama.getPhotographerPov != 'undefined') {
var pov = maps.panorama.getPhotographerPov(),
pitch = pov.pitch,
cameraHeading = pov.heading;
/**************************
// I need help with my logic here.
**************************/
var yaw = pov.heading - 90;
if (yaw < 0) yaw += 360;
var scale = ((Math.abs(maps.heading - yaw) / 90) - 1) * -1;
pitch = pov.pitch * scale;
tiles[i].parentNode.parentNode.style.transform = 'rotate(' + pitch + 'deg)';
clearInterval(tilesLoaded);
return;
}
}
}
}, 20);
}
A full (and more thoroughly commented) proof-of-concept is at this JSFiddle. Oddly, the horizon is just about perfectly level if I do no calculation at all on the example in the JSFiddle, but that result isn't consistent for every Lat/Lng. That's just a coincidence.
So, I need to calculate the roll at the client's heading, given the client heading, photographer's heading, and photographer's pitch. Assume the photographer is either facing uphill or downhill, and pov.pitch is superlative (at the min or max limit). How can I calculate the desired pitch facing the side at a certain degree?
Edit: I found an equation that seems to work pretty well. I updated the code and the fiddle. While it seems to be pretty close to the answer, my algorithm is linear. I believe the correct equation should be logarithmic, resulting in subtler adjustments closer to the camera heading and opposite, while to the camera's left and right adjustments are larger.
I found the answer I was looking for. The calculation involves spherical trigonometry, which I didn't even know existed before researching this issue. If anyone notices any problems, please comment. Or if you have a better solution than the one I found, feel free to add your answer and I'll probably accept it if it's more reliable or significantly more efficient than my own.
Anyway, if the tile canvas is a sphere, 0 pitch (horizon) is a plane, and camera pitch is another plane intersecting at the photographer, the two planes project a spherical lune onto the canvas. This lune can be used to calculate a spherical triangle where:
polar angle = Math.abs(camera pitch)
base = camera heading - client heading
one angle = 90° (for flat horizon)
With two angles and a side available, other properties of a spherical triangle can be calculated using the spherical law of sines. The entire triangle isn't needed -- only the side opposite the polar angle. Because this is math beyond my skills, I had to borrow the logic from this spherical triangle calculator. Special thanks to emfril!
The jsfiddle has been updated. My production roll getter has been updated as follows:
function $(what) { return document.getElementById(what); }
var maps = {
get roll() {
function acos(what) {
return (Math.abs(Math.abs(what) - 1) < 0.0000000001)
? Math.round(Math.acos(what)) : Math.acos(what);
}
function sin(what) { return Math.sin(what); }
function cos(what) { return Math.cos(what); }
function abs(what) { return Math.abs(what); }
function deg2rad(what) { return what * Math.PI / 180; }
function rad2deg(what) { return what * 180 / Math.PI; }
var roll=0;
if (typeof maps.panorama.getPhotographerPov() != 'undefined') {
var pov = maps.panorama.getPhotographerPov(),
clientHeading = maps.panorama.getPov().heading;
while (clientHeading < 0) clientHeading += 360;
while (clientHeading > 360) clientHeading -= 360;
// Spherical trigonometry method
a1 = deg2rad(abs(pov.pitch));
a2 = deg2rad(90);
yaw = deg2rad((pov.heading < 0 ? pov.heading + 360 : pov.heading) - clientHeading);
b1 = acos((cos(a1) * cos(a2)) + (sin(a1) * sin(a2) * cos(yaw)));
if (sin(a1) * sin(a2) * sin(b1) !== 0) {
roll = acos((cos(a1) - (cos(a2) * cos(b1))) / (sin(a2) * sin(b1)));
direction = pov.heading - clientHeading;
if (direction < 0) direction += 360;
if (pov.pitch < 0)
roll = (direction < 180) ? rad2deg(roll) * -1 : rad2deg(roll);
else
roll = (direction > 180) ? rad2deg(roll) * -1 : rad2deg(roll);
} else {
// Fall back to algebraic estimate to avoid divide-by-zero
var yaw = pov.heading - 90;
if (yaw < 0) yaw += 360;
var scale = ((abs(clientHeading - yaw) / 90) - 1) * -1;
roll = pov.pitch * scale;
if (abs(roll) > abs(pov.pitch)) {
var diff = (abs(roll) - abs(pov.pitch)) * 2;
roll = (roll < 0) ? roll + diff : roll - diff;
}
}
}
return roll;
}, // end maps.roll getter
// ... rest of maps object...
} // end maps{}
After rotating the panorama tile container, the container also needs to be expanded to hide the blank corners. I was originally using the 2D law of sines for this, but I found a more efficient shortcut. Thanks Mr. Tan!
function deg2rad(what) { return what * Math.PI / 180; }
function cos(what) { return Math.cos(deg2rad(what)); }
function sin(what) { return Math.sin(deg2rad(what)); }
var W = $('map-canvas').clientWidth,
H = $('map-canvas').clientHeight,
Rot = Math.abs(maps.originPitch);
// pixels per side
maps.growX = Math.round(((W * cos(Rot) + H * cos(90 - Rot)) - W) / 2);
maps.growY = Math.round(((W * sin(Rot) + H * sin(90 - Rot)) - H) / 2);
There will be no more edits to this answer, as I don't wish to have it converted to a community wiki answer yet. As updates occur to me, they will be applied to the fiddle.

Apply gravity between two or more objects in HTML5 Canvas

I was creating something like a 2d gravity simulator, just for fun, and noticed that I'm a complete idiot in terms of math. I just can't get the gravity to work.
I've tried following the instructions found here but it looks weird and when the distance reaches zero, it goes completely buggy. If I add 1 to the distance as recommended in the question, all objects go upper left. I've even tried not modifying gravity when distances reach zero, but this doesn't change the behavior.
Here's the algorithm I'm using to apply gravity:
var distX = obj1.x - obj2.x,
distY = obj1.y - obj2.y;
if (obj1 != obj2) {
if (distY != 0) {
obj1.vy += -(1 / (distY));
}
if (distX != 0) {
obj1.vx += -(1 / (distX));
}
}
I've tried using other algorithms too, but most of them don't care for the distance between objects.
Note that I want the gravity to affect distant objects less than closer objects.
Instead of solving any equations we could use an approximation. dv/dt = G*M*m/r^2, but for small t we could use the approximation Δv = (G*M*m/r^2)*Δt.
When the objects collide I have implemented perfectly inelastic collision (see Wikipedia). This prevents the distance between two objects from being to small and therefore the maximum force is limited.
I also moved the part of the code where the object's position is changed to a separate loop, so the forces calculated for obj1 and obj2 are equal in size.
Demo
function tick() {
allObjs.forEach(function (obj1) {
allObjs.forEach(function (obj2) {
var diffX = obj2.x - obj1.x,
var diffY = obj2.y - obj1.y;
var distSquare = diffX*diffX + diffY*diffY
var dist = Math.sqrt(distSquare);
if (obj1 != obj2) {
if (dist > obj1.w/2 + obj2.w/2) {
//If you add mass to the objects change to obj2.mass
//instead of 50
var totalForce = 50/distSquare;
obj1.vx += totalForce * diffX / dist;
obj1.vy += totalForce * diffY / dist;
} else {
//Collision has occurred
//If you add mass to the objects change to
//tempX = (obj1.mass*obj1.vx + obj2.mass*obj2.vx)/(obj1.mass+
//obj2.mass);
//tempY = (obj1.mass*obj1.vy + obj2.mass*obj2.vy)/(obj1.mass+
//obj2.mass);
var tempX = (obj1.vx + obj2.vx)/2;
var tempY = (obj1.vy + obj2.vy)/2;
obj1.vx = tempX; obj2.vx = tempX;
obj1.vy = tempY; obj2.vy = tempY;
}
}
});
});
allObjs.forEach(function (obj1) {
obj1.x += obj1.vx / 25;
obj1.y += obj1.vy / 25;
});
stage.update();
}
Try
var distX = obj1.x - obj2.x,
distY = obj1.y - obj2.y;
var rsq = distX *distX + distY * distY;
var r = Math.sqrt(rsq);
var F = 50 / rsq; // constant chosen to be pleasing
var rhat_x = distX / r;
var rhat_y = distY / r;
var Fx = F * rhat_x;
var Fy = F * rhat_y;
obj1.vx += -Fx;
obj1.vy += -Fy;
obj2.vx += Fx;
obj2.vy += Fy;
This is very basic, its not taking mass into account its using the simplest possible way of solving the equations you should really use something like 5th order Runga-Kutta w/ error correction. But it does use the formula for gravitational
F = - G m1 m2 / r^2
where G is the universal gravitational constant, m1 m2 are the two masses (I've all of these to 1!) r^2 is the square of the distance between the objects. The force is in the direction to the other object, let this be a unit vector rhat so the vector version of the force, using 1 for the constants
F = - ( 1 / r^2 ) rhat
The above gives reasonable results it you start out with
createPlanet(50, 200, 2, 0, 1);
createPlanet(400, 200, 2, 0, -1);
you have to take care that the two planets don't get too close or the acceleration goes off to infinity and the velocities get too big.
While playing around I tried
var distX = obj1.x - obj2.x,
distY = obj1.y - obj2.y;
var rsq = distX *distX + distY * distY; // square of the distance
var r = Math.sqrt(rsq);
var Fx = distX / r;
var Fy = distY / r;
obj1.vx += -Fx;
obj1.vy += -Fy;
obj2.vx += Fx;
obj2.vy += Fy;
which gives pleasing but physically incorrect results.
Newton's equations of motion F = ma need to be solved here. You are not doing anything like that in your code. No wonder it isn't matching your intuition.
It would help to understand the physics.
This is a vector equation. The force is gravity, which follows an inverse distance squared law.
You also know how acceleration, velocity, and displacement are related. You have to know calculus.
For your 2D world, that means six equations for each body in the problem. Two bodies means 12 coupled equations.
Solving these equations means integrating all those coupled ordinary differential equations in time. You'll need to know something about numerical methods (e.g. Runga-Kutta 5th order integration w/ error correction).
You'd have a lot to learn to write such a thing yourself. I'd recommend looking into a JavaScript physics library like Box2D or something else that Google might find.

Generating a Regular Polygon with Three.js

I'm using Three.js to procedurally generate a regular N-gon based on a user-provided number of sides. The long-term goal is to use this as the first step in rendering a polyhedral prism.
I'm using the solution discussed here to calculate the vertices of the N-gon.
I'm then using the technique discussed here to generate faces on the N-gon.
My first attempt to produce the necessary Geometry object resulted in the following, which doesn't seem to render anything after being added to a Mesh:
function createGeometry (n, circumradius) {
var geometry = new THREE.Geometry(),
vertices = [],
faces = [],
x;
// Generate the vertices of the n-gon.
for (x = 1; x <= n; x++) {
geometry.vertices.push(new THREE.Vector3(
circumradius * Math.sin((Math.PI / n) + (x * ((2 * Math.PI)/ n))),
circumradius * Math.cos((Math.PI / n) + (x * ((2 * Math.PI)/ n))),
0
));
}
// Generate the faces of the n-gon.
for (x = 0; x < n-2; x++) {
geometry.faces.push(new THREE.Face3(0, x + 1, x + 2));
}
geometry.computeBoundingSphere();
return geometry;
}
After toying with that for too long, I discovered the ShapeGeometry class. This uses the same vertex algorithm as the above example, but this one renders properly after being added to a Mesh:
function createShapeGeometry (n, circumradius) {
var shape = new THREE.Shape(),
vertices = [],
x;
// Calculate the vertices of the n-gon.
for (x = 1; x <= sides; x++) {
vertices.push([
circumradius * Math.sin((Math.PI / n) + (x * ((2 * Math.PI)/ n))),
circumradius * Math.cos((Math.PI / n) + (x * ((2 * Math.PI)/ n)))
]);
}
// Start at the last vertex.
shape.moveTo.apply(shape, vertices[sides - 1]);
// Connect each vertex to the next in sequential order.
for (x = 0; x < n; x++) {
shape.lineTo.apply(shape, vertices[x]);
}
// It's shape and bake... and I helped!
return new THREE.ShapeGeometry(shape);
}
What's wrong with the Geometry example that's resolved with the ShapeGeometry example?
I don't think it's an issue with camera or positioning because replacing the complex vertex calculations with simpler whole numbers produces a polygon without an issue, provided the values make sense.
The reason I'm asking is because, as I mentioned initially, I'd like to eventually use this as the first step in rendering a polyhedron. ShapeGeometry objects can be extruded to give them depth, but even with the options that Three.js makes available, this may not be enough for my needs in the long run as the required polyhedra become more irregular.
Any thoughts?
You can create prisms using THREE.CylinderGeometry; for an n-sided prism, you could use
// radiusAtTop, radiusAtBottom, height, segmentsAroundRadius, segmentsAlongHeight
var nPrism = new THREE.CylinderGeometry( 30, 30, 80, n, 4 );
You can also use CylinderGeometry to create pyramids and frustums; for more examples of built-in shapes, you can check out:
http://stemkoski.github.io/Three.js/Shapes.html
Since you also sound like you may be interested in more general polyhedra, you might also want to check out:
http://stemkoski.github.io/Three.js/Polyhedra.html
which includes models of the Platonic Solids, Archimedean Solids, Prisms, Antiprisms, and Johnson Solids; however, in that program the polyhedra are "thick" from using spheres for vertices and cylinders for edges.
Hope this helps!
Your function works as expected.
Look at this fiddle http://jsfiddle.net/Elephanter/mUah5/
there is a modified threejs fiddle with your createGeometry function
So you have problem in another place, not at createGeometry function

Compute a distance including elevation/terrain data for a KmlLinestring

The Google Earth Desktop Application shows the both the map length and ground length of a line.
In the Google Earth plugin I want to do a similar thing, that is I wish to determine the ground length of a tessellated KmlLineString taking the terrain into account.
Can I do this, and if so, how?
You can certainly get the length pretty easily if you use the earth-api-utility-library. Using that you can do.
var length = (new geo.Path(linestring)).distance();
Granted this method does not take the terrain into account - but there are a number of caveats you should be aware of before trying calculate distances using an elevation gradient.
Firstly any differences between topographic and direct distance are minimal in most cases. Indeed many quality GPS receivers simply don't take any changes in elevation into account when calculating distances.
Secondly ground altitude is one of the most unreliable pieces data. Using a gradient based on elevation to determine distance will often produce greater inaccuracy in distance measurements than using a simple 'as the crow flies' measure.
Bearing that in mind, if you still wanted to do it then one way would be something like the following.
Sample the line string at certain points (say every 10 meters).
Get the ground altitude at each point.
Convert each point to Cartesian coordinates
Calculate the angular distances between each Cartesian point in sequence.
You can improve your precision of this kind of method in two ways, either by increasing the sampling rate (say every meter) or by applying a smoothing procedure to the results.
For a rougher version, you could just loop over the coordinates in the the KmlLinestring itself, rather than resampling at some set distance. You would use the latitude, longitude of the coordinate to get the ground altitude at each point. Then you would construct a Cartesian coordinate from this data (latitude, longitude, elevation => X,Y,Z) and work out the angular distance between it and the next point...and so on.
something like the following idea should work - although it is written here and untested!
var EARTH_RADIUS = 6378135; // approximate in meters
var degreestoRadians = function(degrees) {
return degrees * Math.PI / 180;
}
var arcLength = function(point1 , point2) {
var length = Math.sqrt(Math.pow(point1.X-point2.X, 2)
+ Math.pow(point1.Y-point2.Y, 2)
+ Math.pow(point1.Z-point2.Z, 2));
var angle = 2 * Math.asin(length/2/EARTH_RADIUS);
return EARTH_RADIUS * angle;
}
var sphericalToCartesian = function(latitude, longitude, altitude) {
var phi = degreestoRadians(latitude);
var theta = degreestoRadians(longitude);
var rho = EARTH_RADIUS + altitude;
return {
X: Math.cos(phi) * Math.cos(theta) * rho,
Y: Math.cos(phi) * Math.sin(theta) * rho,
Z: Math.sin(phi) * rho
}
}
var topographicDistance = function(linestring) {
var coordinates = linestring.getCoordinates(); //KmlCoordArray
var last = null;
var distance = 0;
for(var i = 0; i < coordinates.length; i++) {
var coord = coordinates.get(i); //KmlCoord
var lat = coord.getLatitude();
var lng = coord.getLongitude();
var alt = ge.getGlobe().getGroundAltitude(lat, lng);
var latest = sphericalToCartesian(lat, lng, alt);
if(last != null) {
distance += arcLength(last, latest);
}
last = latest;
}
return distance;
}
You would use it like so...
var distance = topographicDistance(yourLinestring);

Categories

Resources