SQlite Pobieranie najbliższych lokalizacji (wraz z szerokością i długością geograficzną)

86

Mam dane z szerokością i długością geograficzną przechowywane w mojej bazie danych SQLite i chcę uzyskać najbliższe lokalizacje do parametrów, które wprowadziłem (np. Moja aktualna lokalizacja - szerokość / lng itp.).

Wiem, że jest to możliwe w MySQL i przeprowadziłem sporo badań, że SQLite potrzebuje niestandardowej funkcji zewnętrznej dla formuły Haversine (obliczanie odległości na kuli), ale nie znalazłem niczego, co jest napisane w Javie i działa .

Ponadto, jeśli chcę dodać funkcje niestandardowe, potrzebuję org.sqlitepliku .jar (dla org.sqlite.Function), co powoduje niepotrzebny rozmiar aplikacji.

Z drugiej strony potrzebuję funkcji Order by z SQL, ponieważ samo wyświetlanie odległości nie stanowi większego problemu - zrobiłem to już w moim niestandardowym SimpleCursorAdapter, ale nie mogę sortować danych, ponieważ nie mam kolumny odległości w mojej bazie danych. Oznaczałoby to aktualizację bazy danych za każdym razem, gdy zmienia się lokalizacja, a to strata baterii i wydajności. Więc jeśli ktoś ma pomysł na sortowanie kursora za pomocą kolumny, której nie ma w bazie danych, też byłbym wdzięczny!

Wiem, że istnieje mnóstwo aplikacji na Androida, które używają tej funkcji, ale czy ktoś może wyjaśnić tę magię.

Nawiasem mówiąc, znalazłem tę alternatywę: Zapytanie o pobranie rekordów na podstawie Radius w SQLite?

Sugeruje utworzenie 4 nowych kolumn dla wartości cos i sin lat i lng, ale czy istnieje inny, niezbyt zbędny sposób?

Jure
źródło
Czy sprawdziłeś, czy funkcja org.sqlite.Function działa dla Ciebie (nawet jeśli formuła jest nieprawidłowa)?
Thomas Mueller,
Nie, znalazłem (nadmiarową) alternatywę (zredagowany post), która brzmi lepiej niż dodanie pliku .jar o pojemności 2,6 MB w aplikacji. Ale wciąż szukam lepszego rozwiązania. Dzięki!
Jure,
Jaki jest typ jednostki odległości powrotnej?
Oto pełna implementacja tworzenia zapytania SQlite w systemie Android na podstawie odległości między Twoją lokalizacją a lokalizacją obiektu.
EricLarch

Odpowiedzi:

112

1) Najpierw przefiltruj dane SQLite z dobrym przybliżeniem i zmniejsz ilość danych, które musisz przeanalizować w kodzie java. W tym celu użyj następującej procedury:

Aby mieć deterministyczny próg i dokładniejszy filtr danych, lepiej jest obliczyć 4 lokalizacje, które są w radiusmetrach od północy, zachodu, wschodu i południa od centralnego punktu w kodzie java, a następnie łatwo sprawdzić o mniej niż i więcej niż Operatory SQL (>, <) określające, czy punkty w bazie danych znajdują się w tym prostokącie, czy nie.

Metoda calculateDerivedPosition(...)oblicza te punkty za Ciebie (p1, p2, p3, p4 na rysunku).

wprowadź opis obrazu tutaj

/**
* 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;

    }

A teraz utwórz zapytanie:

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_Xto nazwa kolumny w bazie danych, która przechowuje wartości szerokości geograficznej i COL_Ydotyczy długości geograficznej.

Masz więc pewne dane, które znajdują się blisko twojego centralnego punktu z dobrym przybliżeniem.

2) Teraz możesz zapętlić te przefiltrowane dane i określić, czy są naprawdę blisko Twojego punktu (w kółku), czy też nie, korzystając z następujących metod:

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;
    }

Cieszyć się!

Użyłem i dostosowałem ten numer referencyjny i ukończyłem go.

Bobs
źródło
Sprawdź świetną stronę internetową Chrisa Venessa, jeśli szukasz implementacji tej koncepcji w Javascript. movable-type.co.uk/scripts/latlong.html
barneymc
@Menma x to szerokość geograficzna, a y to długość geograficzna. promień: promień okręgu pokazanego na rysunku.
Bobs
rozwiązanie podane powyżej jest poprawne i działa. Spróbuj ... :)
YS
1
To jest przybliżone rozwiązanie! Daje bardzo przybliżone wyniki za pomocą szybkiego , przyjaznego dla indeksów zapytania SQL. W ekstremalnych okolicznościach da to zły wynik. Po uzyskaniu niewielkiej liczby przybliżonych wyników w obszarze kilku kilometrów użyj wolniejszych, bardziej precyzyjnych metod filtrowania tych wyników . Nie używaj go do filtrowania o bardzo dużym promieniu lub jeśli Twoja aplikacja będzie często używana na równiku!
user1643723
1
Ale nie rozumiem. CalculateDerivedPosition przekształca współrzędną lat, lng na współrzędną kartezjańską, a następnie w zapytaniu SQL porównujesz te wartości kartezjańskie z długimi, szerokimi wartościami. Dwie różne współrzędne geometryczne? Jak to działa? Dzięki.
Misgevolution
70

Odpowiedź Chrisa jest naprawdę przydatna (dzięki!), Ale zadziała tylko wtedy, gdy używasz współrzędnych prostoliniowych (np. Odniesienia do siatki UTM lub OS). Jeśli używasz stopni dla lat / lng (np. WGS84), powyższe działa tylko na równiku. Na innych szerokościach geograficznych należy zmniejszyć wpływ długości geograficznej na kolejność sortowania. (Wyobraź sobie, że jesteś blisko bieguna północnego ... stopień szerokości geograficznej jest nadal taki sam, jak gdziekolwiek, ale stopień długości geograficznej może wynosić tylko kilka stóp. Oznacza to, że kolejność sortowania jest nieprawidłowa).

Jeśli nie jesteś na równiku, oblicz wstępnie współczynnik krówki na podstawie aktualnej szerokości geograficznej:

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

Następnie zamów przez:

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

To wciąż tylko przybliżenie, ale znacznie lepsze niż pierwsze, więc niedokładności sortowania będą znacznie rzadsze.

Oset
źródło
3
To naprawdę interesujący punkt, jeśli chodzi o podłużne linie zbiegające się na biegunach i wypaczające wyniki, im bliżej się znajdujesz. Niezła poprawka.
Chris Simpson,
1
wydaje się, że to działa. kursor = db.getReadableDatabase (). rawQuery ("Wybierz nazwę, id jako _id," + "(" + latitude + "- lat) * (" + latitude + "- lat) + (" + długość + "- lon) * (" + longitude + "- lon) *" + fudge + "as distanza" + "from cliente" + "order by distanza asc", null);
max4ever
powinno ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)być mniejsze niż distancelub distance^2?
Bobs
Czy współczynnik fudge nie jest wyrażony w radianach, a kolumny w stopniach? Czy nie należy ich zamienić na tę samą jednostkę?
Rangel Reale
1
Nie, współczynnik fudge to współczynnik skalujący, który wynosi 0 na biegunach i 1 na równiku. To nie jest ani w stopniach, ani w radianach, to po prostu liczba bez jednostek. Funkcja Java Math.cos wymaga argumentu w radianach, a ja założyłem, że <lat> jest w stopniach, stąd funkcja Math.toRadians. Ale wynikowy cosinus nie ma jednostek.
Teasel
68

Wiem, że na to odpowiedziano i zaakceptowano, ale pomyślałem, że dodam moje doświadczenia i rozwiązanie.

Chociaż z przyjemnością wykonałem funkcję haversine na urządzeniu, aby obliczyć dokładną odległość między aktualną pozycją użytkownika a jakąkolwiek konkretną lokalizacją docelową, konieczne było sortowanie i ograniczanie wyników zapytania według odległości.

Mniej niż zadowalającym rozwiązaniem jest zwrócenie partii oraz posortowanie i przefiltrowanie po fakcie, ale spowodowałoby to drugi kursor i zwrócenie i odrzucenie wielu niepotrzebnych wyników.

Moim preferowanym rozwiązaniem było posortowanie w kolejności kwadratów wartości delta długości i łaty:

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

Nie ma potrzeby wykonywania pełnego haversine tylko dla kolejności sortowania i nie ma potrzeby stosowania pierwiastka kwadratowego wyników, dlatego SQLite może obsłużyć obliczenia.

EDYTOWAĆ:

Ta odpowiedź wciąż otrzymuje miłość. W większości przypadków działa dobrze, ale jeśli potrzebujesz trochę większej dokładności, zapoznaj się z odpowiedzią @Teasel poniżej, która dodaje czynnik „fudge”, który naprawia niedokładności, które zwiększają się, gdy szerokość geograficzna zbliża się do 90.

Chris Simpson
źródło
Świetna odpowiedź. Czy możesz wyjaśnić, jak to działało i jak nazywa się ten algorytm?
iMatoria
3
@iMatoria - to tylko okrojona wersja słynnego twierdzenia Pitagorasa. Biorąc pod uwagę dwa zestawy współrzędnych, różnica między dwiema wartościami X reprezentuje jedną stronę trójkąta prostokątnego, a różnica między wartościami Y jest drugą. Aby uzyskać przeciwprostokątną (a tym samym odległość między punktami), dodaj do siebie kwadraty tych dwóch wartości, a następnie uzyskasz pierwiastek kwadratowy. W naszym przypadku nie robimy ostatniego bitu (rootowanie kwadratowe), ponieważ nie możemy. Na szczęście nie jest to konieczne do sortowania.
Chris Simpson,
6
W mojej aplikacji BostonBusMap wykorzystałem to rozwiązanie, aby pokazać przystanki najbliżej aktualnej lokalizacji. Jednak musisz przeskalować długość geograficzną o, cos(latitude)aby szerokość i długość geograficzna były w przybliżeniu równe. Zobacz en.wikipedia.org/wiki/…
noisecapella
0

Aby maksymalnie zwiększyć wydajność, proponuję ulepszyć pomysł @Chrisa Simpsona za pomocą następującej ORDER BYklauzuli:

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

W takim przypadku należy przekazać z kodu następujące wartości:

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

Powinieneś także przechowywać LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2jako dodatkową kolumnę w bazie danych. Wypełnij go, wstawiając jednostki do bazy danych. To nieznacznie poprawia wydajność podczas wyodrębniania dużej ilości danych.

Siergiej Metłow
źródło
-3

Spróbuj czegoś takiego:

    //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)); 
        } 
    } 

Następnie id zawiera żądany element z bazy danych, abyś mógł go pobrać:

    //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)); 

Mam nadzieję, że to pomoże!

Scott Helme
źródło