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.sqlite
pliku .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?
źródło
Odpowiedzi:
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
radius
metrach 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)./** * 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_X
to nazwa kolumny w bazie danych, która przechowuje wartości szerokości geograficznej iCOL_Y
dotyczy 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.
źródło
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.
źródło
((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)
być mniejsze niżdistance
lubdistance^2
?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.
źródło
cos(latitude)
aby szerokość i długość geograficzna były w przybliżeniu równe. Zobacz en.wikipedia.org/wiki/…Aby maksymalnie zwiększyć wydajność, proponuję ulepszyć pomysł @Chrisa Simpsona za pomocą następującej
ORDER BY
klauzuli: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^2
jako 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.źródło
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!
źródło