SQlite Mendapatkan lokasi terdekat (dengan lintang dan bujur)

87

Saya memiliki data dengan lintang dan bujur yang disimpan dalam database SQLite saya, dan saya ingin mendapatkan lokasi terdekat dengan parameter yang saya masukkan (mis. Lokasi saya saat ini - lat / lng, dll.).

Saya tahu bahwa ini dimungkinkan di MySQL, dan saya telah melakukan cukup banyak penelitian bahwa SQLite memerlukan fungsi eksternal khusus untuk rumus Haversine (menghitung jarak pada bola), tetapi saya belum menemukan apa pun yang tertulis di Java dan berfungsi .

Selain itu, jika saya ingin menambahkan fungsi khusus, saya memerlukan org.sqlite.jar (untuk org.sqlite.Function), dan itu menambahkan ukuran yang tidak perlu ke aplikasi.

Sisi lain dari ini adalah, saya memerlukan Urutan berdasarkan fungsi dari SQL, karena menampilkan jarak saja tidak terlalu menjadi masalah - Saya sudah melakukannya di SimpleCursorAdapter kustom saya, tetapi saya tidak dapat mengurutkan data, karena saya tidak memiliki kolom jarak di database saya. Itu berarti memperbarui database setiap kali lokasi berubah dan itu hanya membuang-buang baterai dan kinerja. Jadi, jika seseorang memiliki ide untuk mengurutkan kursor dengan kolom yang tidak ada di database, saya juga akan berterima kasih!

Saya tahu ada banyak sekali aplikasi Android di luar sana yang menggunakan fungsi ini, tetapi bisakah seseorang menjelaskan keajaibannya.

Omong-omong, saya menemukan alternatif ini: Kueri untuk mendapatkan catatan berdasarkan Radius di SQLite?

Ini menyarankan untuk membuat 4 kolom baru untuk nilai cos dan sin dari lat dan lng, tetapi adakah cara lain yang tidak terlalu berlebihan?

Jure
sumber
Apakah Anda memeriksa apakah org.sqlite.Fungsi berfungsi untuk Anda (meskipun rumusnya salah)?
Thomas Mueller
Tidak, saya menemukan alternatif (berlebihan) (posting yang diedit) yang terdengar lebih baik daripada menambahkan 2,6MB .jar di aplikasi. Tapi saya masih mencari solusi yang lebih baik. Terima kasih!
Jure
Apa tipe unit jarak kembali?
Berikut adalah implementasi lengkap untuk membuat kueri SQlite di Android berdasarkan jarak antara lokasi Anda dan lokasi objek.
EricLarch

Jawaban:

113

1) Pertama-tama filter data SQLite Anda dengan perkiraan yang baik dan kurangi jumlah data yang perlu Anda evaluasi dalam kode java Anda. Gunakan prosedur berikut untuk tujuan ini:

Untuk memiliki ambang deterministik dan filter yang lebih akurat pada data, lebih baik menghitung 4 lokasi yang berada dalam radiusmeter dari utara, barat, timur dan selatan titik pusat Anda dalam kode java dan kemudian memeriksa dengan mudah dengan kurang dari dan lebih dari Operator SQL (>, <) untuk menentukan apakah poin Anda dalam database berada dalam persegi panjang itu atau tidak.

Metode calculateDerivedPosition(...)menghitung poin tersebut untuk Anda (p1, p2, p3, p4 dalam gambar).

masukkan deskripsi gambar di sini

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

    }

Dan sekarang buat kueri Anda:

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 adalah nama kolom dalam database yang menyimpan nilai garis lintang dan COL_Y untuk bujur.

Jadi, Anda memiliki beberapa data yang mendekati titik pusat Anda dengan perkiraan yang baik.

2) Sekarang Anda dapat mengulang data yang difilter ini dan menentukan apakah data tersebut benar-benar dekat dengan titik Anda (dalam lingkaran) atau tidak menggunakan metode berikut:

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

Nikmati!

Saya menggunakan dan menyesuaikan referensi ini dan menyelesaikannya.

Bobs
sumber
Lihat halaman web hebat Chris Veness jika Anda mencari implementasi Javascript dari konsep ini di atas. movable-type.co.uk/scripts/latlong.html
barneymc
@Menma x adalah lintang dan y adalah bujur. radius: jari-jari lingkaran yang ditunjukkan pada gambar.
Bobs
solusi yang diberikan di atas benar dan berfungsi. Cobalah ... :)
YS
1
Ini adalah solusi perkiraan! Ini memberi Anda hasil yang sangat mendekati melalui kueri SQL yang cepat dan ramah indeks. Ini akan memberikan hasil yang salah dalam beberapa keadaan ekstrim. Setelah Anda mendapatkan sejumlah kecil hasil perkiraan dalam area beberapa kilometer, gunakan metode yang lebih lambat dan lebih tepat untuk memfilter hasil tersebut . Jangan gunakan untuk memfilter dengan radius yang sangat besar atau jika aplikasi Anda akan sering digunakan di ekuator!
pengguna1643723
1
Tapi saya tidak mengerti. CalculateDerivedPosition mengubah koordinat lat, lng menjadi kartesian, lalu Anda dalam kueri SQL, membandingkan nilai kartesian ini dengan nilai lintang dan bujur. Dua koordinat geometris yang berbeda? Bagaimana cara kerjanya? Terima kasih.
Misgevolution
70

Jawaban Chris sangat berguna (terima kasih!), Tetapi hanya akan berfungsi jika Anda menggunakan koordinat bujursangkar (mis. Referensi kisi UTM atau OS). Jika menggunakan derajat untuk lintang / bujur (misalnya WGS84) maka yang di atas hanya berfungsi di ekuator. Di lintang lain, Anda perlu mengurangi pengaruh bujur pada tata urutan. (Bayangkan Anda dekat dengan kutub utara ... derajat garis lintangnya masih sama dengan di mana pun, tetapi derajat garis bujur mungkin hanya beberapa kaki. Ini berarti urutan pengurutannya salah).

Jika Anda tidak berada di ekuator, hitung dahulu faktor fudge, berdasarkan garis lintang Anda saat ini:

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

Kemudian pesan berdasarkan:

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

Ini masih hanya perkiraan, tetapi jauh lebih baik daripada yang pertama, jadi ketidakakuratan urutan urutan akan jauh lebih jarang.

Teasel
sumber
3
Itu adalah poin yang sangat menarik tentang garis longitudal yang bertemu di kutub dan hasil yang miring semakin dekat. Perbaikan yang bagus.
Chris Simpson
1
ini sepertinya bekerja cursor = db.getReadableDatabase (). rawQuery ("Pilih nome, id as _id," + "(" + latitude + "- lat) * (" + latitude + "- lat) + (" + longitude + "- lon) * (" + longitude + "- lon) *" + fudge + "sebagai distanza" + "from cliente" + "order by distanza asc", null);
max4ever
harus ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)kurang dari distanceatau distance^2?
Bobs
Bukankah faktor fudge dalam radian dan kolom dalam derajat? Bukankah seharusnya mereka diubah ke unit yang sama?
Rangel Reale
1
Tidak, faktor fudge adalah faktor skala yaitu 0 di kutub dan 1 di ekuator. Bukan dalam derajat, atau radian, itu hanya bilangan tak bersatuan. Fungsi Java Math.cos membutuhkan argumen dalam radian, dan saya berasumsi <lat> dalam derajat, oleh karena itu fungsi Math.toRadians. Tetapi kosinus yang dihasilkan tidak memiliki satuan.
Teasel
68

Saya tahu ini telah dijawab dan diterima tetapi saya pikir saya akan menambahkan pengalaman dan solusi saya.

Meskipun saya senang melakukan fungsi haversine pada perangkat untuk menghitung jarak akurat antara posisi pengguna saat ini dan lokasi target tertentu, ada kebutuhan untuk mengurutkan dan membatasi hasil kueri dalam urutan jarak.

Solusi yang kurang memuaskan adalah mengembalikan lot, mengurutkan, dan memfilter setelah kejadian tetapi ini akan menghasilkan kursor kedua dan banyak hasil yang tidak perlu dikembalikan dan dibuang.

Solusi yang saya sukai adalah meneruskan dengan urutan nilai delta kuadrat dari long dan lats:

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

Tidak perlu melakukan haversine lengkap hanya untuk pengurutan dan tidak perlu mengakar pangkat dua hasil sehingga SQLite dapat menangani penghitungan.

EDIT:

Jawaban ini tetap menerima cinta. Ini berfungsi dengan baik dalam banyak kasus, tetapi jika Anda memerlukan sedikit lebih banyak akurasi, silakan lihat jawaban @Teasel di bawah ini yang menambahkan faktor "fudge" yang memperbaiki ketidakakuratan yang meningkat saat garis lintang mendekati 90.

Chris Simpson
sumber
Jawaban yang bagus. Bisakah Anda menjelaskan bagaimana cara kerjanya dan apa nama algoritma ini?
iMatoria
3
@iMatoria - Ini hanyalah versi singkat dari teorema terkenal pythagoras. Diberikan dua set koordinat, perbedaan antara dua nilai X mewakili satu sisi segitiga siku-siku dan perbedaan antara nilai Y adalah sisi lainnya. Untuk mendapatkan hipotenusa (dan oleh karena itu jarak antara titik-titik) Anda menambahkan kuadrat dari kedua nilai ini dan kemudian akar kuadrat hasilnya. Dalam kasus kami, kami tidak melakukan bit terakhir (rooting kuadrat) karena kami tidak bisa. Untungnya ini tidak diperlukan untuk tata tertib.
Chris Simpson
6
Dalam aplikasi saya, BostonBusMap, saya menggunakan solusi ini untuk menunjukkan perhentian yang paling dekat dengan lokasi saat ini. Namun Anda perlu mengukur jarak bujur dengan cos(latitude)membuat garis lintang dan garis bujur kira-kira sama. Lihat en.wikipedia.org/wiki/…
noisecapella
0

Untuk meningkatkan performa sebanyak mungkin, saya sarankan untuk meningkatkan ide @Chris Simpson dengan ORDER BYklausa berikut :

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

Dalam hal ini Anda harus meneruskan nilai berikut dari kode:

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

Dan Anda juga harus menyimpan LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2sebagai kolom tambahan di database. Isi itu dengan memasukkan entitas Anda ke dalam database. Ini sedikit meningkatkan kinerja saat mengekstrak data dalam jumlah besar.

Sergey Metlov
sumber
-3

Coba sesuatu seperti ini:

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

Setelah ini, id berisi item yang Anda inginkan dari database sehingga Anda dapat mengambilnya:

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

Semoga membantu!

Scott Helme
sumber