Search code examples
mathgeolocation

Getting limits on latitude and longitude


I have a service that looks for nearby locations(300m) from a user specified point.

I'm using the haversine formula to check if a location is near the point https://en.wikipedia.org/wiki/Haversine_formula

My problem is that it's slow since it's checking against all of the points in my DB.

What I want to do is limit the initial query and apply the haversine formula to a list of points in a smaller bounded area e.g.

results = (SELECT * FROM location WHERE location.latitude BETWEEN 14.223 AND 14.5 )
AND location.longitude BETWEEN 121.5 AND 122

haversine(results, user_point)

Is there a loose way of getting the bounds from a given point? Or basically a dirty conversion of lat/long to meters?


Solution

  • If you can modify your database structure, there's one super-easy way to do it: instead of (or in addition to) storing latitude and longitude, convert your location coordinates into 3D space, with columns for x, y, and z in meters. Then you can just do

    SELECT * FROM location
      WHERE location.x BETWEEN center.x - 300 AND center.x + 300
        AND location.y BETWEEN center.y - 300 AND center.y + 300
        AND location.z BETWEEN center.z - 300 AND center.z + 300
    

    That will trim down your list pretty well, and you can do the haversine calculation on the resulting set.


    If you're stuck with using a database that has only longitude and latitude in it, you can still narrow down the search. It's easy for latitude: one degree of latitude due north or south always corresponds to 111 km of distance, as long as you ignore the complications that arise when you get close to the poles. That means a distance of 300 m is 0.0027... degrees of latitude, although you might as well be a bit conservative and use 0.003 or 0.004.

    Longitude is a bit trickier because the conversion factor changes depending on how far north or south you are, but it's still not too complicated: you just multiply by the cosine of the latitude.

    distance = cos(latitude) * 111.19... km/degree * delta_angle
    

    At the equator, it's the same as with latitude: one degree change in longitude at the equator is 111 km. At 80 degrees north (or south), you multiply by a factor of cos(80 degrees) = 0.17..., with the result that 1 degree change in longitude is only 19.3 km. For your purposes, you could invert this and find the range of longitudes to select as 300 m / cos(latitude) / (111.19... km/degree) = (0.0027... degrees) / cos(latitude). That coefficient is the same quantity from the first paragraph; it's not a coincidence.

    The tricky problems come up near the discontinuities of the coordinate system, for example when you get near the poles. You can see why when you start plugging in latitudes like 89.9996 degrees:

    0.0027... degrees / cos(89.9996 degrees) = 386... degrees
    

    Well, how can that be when there are only 360 degrees in a whole circle? This is an indicator that you've gotten to the point where your 300 m radius extends all the way around the pole and comes back to include your starting location, in a manner of speaking. At that point, you might as well just search all points in your database close enough to the pole. Of course you should really start doing this at 89.999 degrees or so, because that's where the 600 m diameter of the region you're searching just about encircles the pole completely.

    There's another issue at (well, near) the International Date Line, or more precisely the "antimeridian", having to do with the jump from -180 to +180 degrees of longitude. A point at +179.9999 degrees and one at -179.9999 degrees, both on the equator, will have very different coordinates even though they are geographically just a few meters apart. Since you're just doing this as a preliminary filter for a more detailed search, it's probably easiest to just pass through every point within 0.006 degrees (that's roughly the diameter of a 300 m-radius circle) of the antimeridian, and then the haversine calculation will determine whether the points are actually close.

    To sum up, you can use the bounds on latitude and longitude I mentioned above and just add special cases for the poles and the antimeridian. In some kind of pseudo-SQL/code hybrid:

    IF abs(center.latitude) > 89.999
      SELECT * FROM location WHERE abs(location.latitude - center.latitude) < 0.003
    ELSE
      IF abs(center.longitude) > 179.997
        SELECT * FROM location
          WHERE abs(location.latitude - center.latitude) < 0.003
          AND 180 - abs(location.longitude) < (0.006 / cos(center.latitude))
      ELSE
        SELECT * FROM location
          WHERE abs(location.latitude - center.latitude) < 0.003
          AND abs(location.longitude - center.longitude) < (0.003 / cos(center.latitude))
      ENDIF
    ENDIF
    

    If you want a pithy statement at the expense of having to test potentially twice as many points, you can only compare the absolute values of longitude:

    SELECT * FROM location
      WHERE abs(location.latitude - center.latitude) < 0.003
      AND abs(abs(location.longitude) - abs(center.longitude)) <= min(0.003 / cos(center.latitude), 180)