91

I have data with latitude and longitude stored in my SQLite database, and I want to get the nearest locations to the parameters I put in (ex. My current location - lat/lng, etc.).

I know that this is possible in MySQL, and I've done quite some research that SQLite needs a custom external function for the Haversine formula (calculating distance on a sphere), but I haven't found anything that is written in Java and works.

Also, if I want to add custom functions, I need the org.sqlite .jar (for org.sqlite.Function), and that adds unnecessary size to the app.

The other side of this is, I need the Order by function from SQL, because displaying the distance alone isn't that much of a problem - I already did it in my custom SimpleCursorAdapter, but I can't sort the data, because I don't have the distance column in my database. That would mean updating the database every time the location changes and that's a waste of battery and performance. So if someone has any idea on sorting the cursor with a column that's not in the database, I'd be grateful too!

I know there are tons of Android apps out there that use this function, but can someone please explain the magic.

By the way, I found this alternative: Query to get records based on Radius in SQLite?

It's suggesting to make 4 new columns for cos and sin values of lat and lng, but is there any other, not so redundant way?

Community
  • 1
  • 1
Jure
  • 3,003
  • 3
  • 25
  • 28
  • Did you check if org.sqlite.Function works for you (even if the formula is not correct)? – Thomas Mueller Sep 12 '10 at 15:50
  • No, I found a (redundant) alternative (edited post) that sounds better than adding a 2,6MB .jar in the app. But I'm still searching for a better solution. Thanks! – Jure Sep 12 '10 at 16:09
  • Whats the return distance unit type? –  Oct 07 '11 at 07:08
  • Here is a [full implementation](http://stackoverflow.com/a/9536914/565798) for building a SQlite query on Android based on distance between your location and the object's location. – EricLarch Mar 02 '12 at 16:37

5 Answers5

115

1) At first filter your SQLite data with a good approximation and decrease amount of data that you need to evaluate in your java code. Use the following procedure for this purpose:

To have a deterministic threshold and more accurate filter on data, It is better to calculate 4 locations that are in radius meter of the north, west, east and south of your central point in your java code and then check easily by less than and more than SQL operators (>, <) to determine if your points in database are in that rectangle or not.

The method calculateDerivedPosition(...) calculates those points for you (p1, p2, p3, p4 in picture).

enter image description here

/**
* Calculates the end-point from a given source at a given range (meters)
* and bearing (degrees). This methods uses simple geometry equations to
* calculate the end-point.
* 
* @param point
*           Point of origin
* @param range
*           Range in meters
* @param bearing
*           Bearing in degrees
* @return End-point from the source given the desired range and bearing.
*/
public static PointF calculateDerivedPosition(PointF point,
            double range, double bearing)
    {
        double EarthRadius = 6371000; // m

        double latA = Math.toRadians(point.x);
        double lonA = Math.toRadians(point.y);
        double angularDistance = range / EarthRadius;
        double trueCourse = Math.toRadians(bearing);

        double lat = Math.asin(
                Math.sin(latA) * Math.cos(angularDistance) +
                        Math.cos(latA) * Math.sin(angularDistance)
                        * Math.cos(trueCourse));

        double dlon = Math.atan2(
                Math.sin(trueCourse) * Math.sin(angularDistance)
                        * Math.cos(latA),
                Math.cos(angularDistance) - Math.sin(latA) * Math.sin(lat));

        double lon = ((lonA + dlon + Math.PI) % (Math.PI * 2)) - Math.PI;

        lat = Math.toDegrees(lat);
        lon = Math.toDegrees(lon);

        PointF newPoint = new PointF((float) lat, (float) lon);

        return newPoint;

    }

And now create your query:

PointF center = new PointF(x, y);
final double mult = 1; // mult = 1.1; is more reliable
PointF p1 = calculateDerivedPosition(center, mult * radius, 0);
PointF p2 = calculateDerivedPosition(center, mult * radius, 90);
PointF p3 = calculateDerivedPosition(center, mult * radius, 180);
PointF p4 = calculateDerivedPosition(center, mult * radius, 270);

strWhere =  " WHERE "
        + COL_X + " > " + String.valueOf(p3.x) + " AND "
        + COL_X + " < " + String.valueOf(p1.x) + " AND "
        + COL_Y + " < " + String.valueOf(p2.y) + " AND "
        + COL_Y + " > " + String.valueOf(p4.y);

COL_X is the name of the column in the database that stores latitude values and COL_Y is for longitude.

So you have some data that are near your central point with a good approximation.

2) Now you can loop on these filtered data and determine if they are really near your point (in the circle) or not using the following methods:

public static boolean pointIsInCircle(PointF pointForCheck, PointF center,
            double radius) {
        if (getDistanceBetweenTwoPoints(pointForCheck, center) <= radius)
            return true;
        else
            return false;
    }

public static double getDistanceBetweenTwoPoints(PointF p1, PointF p2) {
        double R = 6371000; // m
        double dLat = Math.toRadians(p2.x - p1.x);
        double dLon = Math.toRadians(p2.y - p1.y);
        double lat1 = Math.toRadians(p1.x);
        double lat2 = Math.toRadians(p2.x);

        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2)
                * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        double d = R * c;

        return d;
    }

Enjoy!

I used and customized this reference and completed it.

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
Bob
  • 22,810
  • 38
  • 143
  • 225
  • Check out Chris Veness great webpage if you are looking for a Javascript implementation of this concept above. http://www.movable-type.co.uk/scripts/latlong.html – barneymc Sep 21 '14 at 17:20
  • @Menma x is latitude and y is longitude. radius: the radius of the circle that is shown in the picture. – Bob Sep 09 '15 at 09:19
  • the solution given above is correct and works. Try it ... :) – Yash Sampat Nov 03 '15 at 08:31
  • Sorry, man. The answer is correct, and it works perfectly fine for me. – Yash Sampat Nov 05 '15 at 05:32
  • Works perfectly for me! However, there is also the `android.location.Location. distanceBetween()` method, which basically does the same as the last function in this answer, so no use reinventing it. – Magnus May 26 '16 at 16:40
  • @breceivemail : why do you use PointF containing float values instead of an object containing double values? – jublikon Aug 29 '16 at 16:08
  • It doesn't work for me. Center is not the center. P1 is too close to the center and P3 is too far. Maybe the problem is the current position. I'm close to Mediterranean sea. If I did correct everything (i did copy-paste) If so the solution doesn't work for every one in the world. Unfortunately. – efkan Sep 23 '16 at 07:25
  • This is NOT WORKING! I've put as center point 0,0 and it fives me the following results, with radius set to 30000: 0.269796481776,-8.11266146075 1.65202088404E-17,-8.11266146075 -0.269796481776,-8.11266146075 -4.95606265212E-17,-8.11266146075 I get always the same LONGITUDE! How is this possible! The point 1 and 3 are super correct..the points 2 and 4 are on the same vertical line as the other too!! They should spread! – user1447316 Nov 20 '16 at 15:52
  • 1
    **This is an approximate solution!** It gives you highly approximate results via a **fast**, index-friendly SQL query. It will give wrong result in some extreme circumstances. Once you have gotten small number of approximate result within area of several kilometers, **use slower, more precise methods to filter those results**. Do not use it to filter with very large radius or if your app is going to be frequently used at equator! – user1643723 Mar 28 '18 at 09:46
  • 1
    But I don't get it. CalculateDerivedPosition is transforming the lat,lng coordinate in to a cartesian one and then you in the SQL query, you are comparing these cartesian values with the lat,long values. Two different geometrical coordinates? How does this work? Thanks. – Misgevolution May 17 '19 at 05:45
  • Why are you using PointF instead of lat and lng values. and did you store geometric coordinates in x and y variable of input parameter ?! – Armin Jul 16 '19 at 12:01
74

Chris's answer is really useful (thanks!), but will only work if you are using rectilinear coordinates (eg UTM or OS grid references). If using degrees for lat/lng (eg WGS84) then the above only works at the equator. At other latitudes, you need to decrease the impact of longitude on the sort order. (Imagine you're close to the north pole... a degree of latitude is still the same as it is anywhere, but a degree of longitude may only be a few feet. This will mean that the sort order is incorrect).

If you are not at the equator, pre-calculate the fudge-factor, based on your current latitude:

<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);

Then order by:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)

It's still only an approximation, but much better than the first one, so sort order inaccuracies will be much rarer.

Bob
  • 22,810
  • 38
  • 143
  • 225
Teasel
  • 960
  • 6
  • 6
  • 3
    That's a really interesting point about the longitudal lines converging at the poles and skewing the results the nearer you get. Nice fix. – Chris Simpson Oct 17 '11 at 03:59
  • 1
    this seems to work cursor = db.getReadableDatabase().rawQuery("Select nome, id as _id, " + "( " + latitude + " - lat) * ( " + latitude +"- lat) + ( " + longitude + "- lon) * ( " + longitude + "- lon) * " + fudge + " as distanza " + " from cliente "+ " order by distanza asc", null); – max4ever Aug 31 '12 at 11:08
  • should `(( - LAT_COLUMN) * ( - LAT_COLUMN) + ( - LNG_COLUMN) * ( - LNG_COLUMN) * )` be less than `distance` or `distance^2`? – Bob Oct 21 '12 at 07:32
  • Isn't the fudge factor in radians and the columns in degrees? Shouldn't them be converted to the same unit? – Rangel Reale Dec 11 '12 at 18:46
  • 1
    No, the fudge factor is a scaling factor which is 0 at the poles and 1 at the equator. It's neither in degrees, nor radians, it's just a unitless number. The Java Math.cos function requires an argument in radians, and I assumed was in degrees, hence the Math.toRadians function. But the resulting cosine has no units. – Teasel Jan 16 '13 at 18:24
  • This is just an 'order by' clause for the SQL... it makes sure that the closest records are at the top, but doesn't calculate distances. So, once you've got the result set, you should then calculate the distances in your code, using the Haversine formula. Eg you could use breceivemail's getDistanceBetweenTwoPoints function, posted above. That function hardcodes an Earth radius of 6371000, so the distances it returns will be in m. If you want your results in different units, simply change that number (eg use 31670 for distances in furlongs). – Teasel Feb 25 '16 at 12:07
  • I have added that piece of code to my order by clause and I get a SQL syntax error. The SQL statement is working without the order clause. Where do I have made a mistake? SELECT * FROM locations WHERE latitude > 51.193966 AND latitude < 51.194324 AND longitude < 6.4470406 AND longitude > 6.4464664 ORDER BY ((51.19414666666667 - latitude) * (51.19414666666667 - latitude + (6.446753333333334 - longitude) * (6.446753333333334 - longitude) * 0.39273211543715586) – jublikon Aug 26 '16 at 14:26
73

I know this has been answered and accepted but thought I'd add my experiences and solution.

Whilst I was happy to do a haversine function on the device to calculate the accurate distance between the user's current position and any particular target location there was a need to sort and limit the query results in order of distance.

The less than satisfactory solution is to return the lot and sort and filter after the fact but this would result in a second cursor and many unnecessary results being returned and discarded.

My preferred solution was to pass in a sort order of the squared delta values of the long and lats:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
 (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))

There's no need to do the full haversine just for a sort order and there's no need to square root the results therefore SQLite can handle the calculation.

EDIT:

This answer is still receiving love. It works fine in most cases but if you need a little more accuracy, please check out the answer by @Teasel below which adds a "fudge" factor that fixes inaccuracies that increase as the latitude approaches 90.

Chris Simpson
  • 7,821
  • 10
  • 48
  • 68
  • Great answer. Could you explain how did it work and what is this algorithm called? – iMatoria Oct 16 '11 at 15:47
  • 3
    @iMatoria - This is just a cut down version of pythagoras famous theorem. Given two sets of coordinates, the difference between the two X values represents one side of a right angled triangle and the difference between the Y values is the other. To get the hypotenuse (and therefore the distance between the points) you add the squares of these two values together and then square root the result. In our case we don't do the last bit (the square rooting) because we can't. Fortunately this is not necessary for a sort order. – Chris Simpson Oct 17 '11 at 03:51
  • 6
    In my app BostonBusMap I used this solution to show stops closest to the current location. However you need to scale the longitude distance by `cos(latitude)` to have the latitude and longitude roughly equal. See http://en.wikipedia.org/wiki/Geographical_distance#Spherical_Earth_projected_to_a_plane – noisecapella Sep 17 '12 at 00:38
0

In order to increase performance as much as possible I suggest improve @Chris Simpson's idea with the following ORDER BY clause:

ORDER BY (<L> - <A> * LAT_COL - <B> * LON_COL + LAT_LON_SQ_SUM)

In this case you should pass the following values from code:

<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
<B> = 2 * center_lon

And you should also store LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2 as additional column in database. Populate it inserting your entities into database. This slightly improves performance while extracting large amount of data.

Sergey Metlov
  • 25,747
  • 28
  • 93
  • 153
-3

Try something like this:

    //locations to calculate difference with 
    Location me   = new Location(""); 
    Location dest = new Location(""); 

    //set lat and long of comparison obj 
    me.setLatitude(_mLat); 
    me.setLongitude(_mLong); 

    //init to circumference of the Earth 
    float smallest = 40008000.0f; //m 

    //var to hold id of db element we want 
    Integer id = 0; 

    //step through results 
    while(_myCursor.moveToNext()){ 

        //set lat and long of destination obj 
        dest.setLatitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE))); 
        dest.setLongitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE))); 

        //grab distance between me and the destination 
        float dist = me.distanceTo(dest); 

        //if this is the smallest dist so far 
        if(dist < smallest){ 
            //store it 
            smallest = dist; 

            //grab it's id 
            id = _myCursor.getInt(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_ID)); 
        } 
    } 

After this, id contains the item you want from the database so you can fetch it:

    //now we have traversed all the data, fetch the id of the closest event to us 
    _myCursor = _myDBHelper.fetchID(id); 
    _myCursor.moveToFirst(); 

    //get lat and long of nearest location to user, used to push out to map view 
    _mLatNearest  = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE)); 
    _mLongNearest = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE)); 

Hope that helps!

Scott Helme
  • 4,786
  • 2
  • 23
  • 35