SQlite En yakın konumlara ulaşma (enlem ve boylam ile)


87

SQLite veritabanımda depolanan enlem ve boylam verilerim var ve koyduğum parametrelere en yakın konumları almak istiyorum (örn. Mevcut konumum - enlem / boylam, vb.).

Bunun MySQL'de mümkün olduğunu biliyorum ve SQLite'nin Haversine formülü için özel bir harici fonksiyona ihtiyaç duyduğuna dair epeyce araştırma yaptım (bir küredeki mesafeyi hesaplama), ancak Java'da yazılmış ve işe yarayan hiçbir şey bulamadım .

Ayrıca, özel işlevler eklemek istersem, org.sqlite.jar (for org.sqlite.Function) dosyasına ihtiyacım var ve bu, uygulamaya gereksiz boyut katıyor.

Bunun diğer yanı, SQL'den Sıralama işlevine ihtiyacım var çünkü tek başına mesafeyi görüntülemek o kadar da sorun değil - bunu zaten özel SimpleCursorAdapter'ımda yaptım, ancak verileri sıralayamıyorum çünkü veritabanımda mesafe sütunu yok. Bu, konum her değiştiğinde veritabanının güncellenmesi anlamına gelir ve bu, pil ve performans kaybıdır. Yani eğer birinin imleci veritabanında olmayan bir sütunla sıralama konusunda herhangi bir fikri varsa, ben de minnettar olurum!

Bu işlevi kullanan tonlarca Android uygulaması olduğunu biliyorum, ancak birileri lütfen sihri açıklayabilir.

Bu arada, şu alternatifi buldum: SQLite'de Radius'a dayalı kayıtları almak için sorgulama?

Enlem ve lng'nin cos ve sin değerleri için 4 yeni sütun yapmayı öneriyor, ancak başka, çok fazla olmayan başka bir yol var mı?


Org.sqlite.Function'ın sizin için çalışıp çalışmadığını kontrol ettiniz mi (formül doğru olmasa bile)?
Thomas Mueller

Hayır, uygulamada 2,6 MB .jar eklemekten daha iyi görünen bir (gereksiz) alternatif (düzenlenmiş gönderi) buldum. Ama yine de daha iyi bir çözüm arıyorum. Teşekkürler!
Jure

Dönüş mesafesi birimi türü nedir?

Burada, konumunuz ile nesnenin konumu arasındaki mesafeye dayalı olarak Android'de bir SQlite sorgusu oluşturmak için tam bir uygulama verilmiştir.
EricLarch

Yanıtlar:


113

1) Öncelikle SQLite verilerinizi iyi bir yaklaşımla filtreleyin ve java kodunuzda değerlendirmeniz gereken veri miktarını azaltın. Bu amaçla aşağıdaki prosedürü kullanın:

Belirleyici bir eşik ve veriler üzerinde daha doğru bir filtreye sahip olmak için , java kodunuzda merkez noktanızın kuzey, batı, doğu ve güneyindeki metre cinsinden 4 konumu hesaplamak ve ardından kolayca ve daha azını kontrol etmek daha iyidir. Veritabanındaki noktalarınızın o dikdörtgende olup olmadığını belirlemek için SQL operatörleri (>, <) .radius

Yöntem calculateDerivedPosition(...)bu noktaları sizin için hesaplar (resimde p1, p2, p3, p4).

görüntü açıklamasını buraya girin

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

    }

Ve şimdi sorgunuzu oluşturun:

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_Xveritabanındaki enlem değerlerini depolayan ve COL_Yboylam için olan sütunun adıdır .

Yani, iyi bir yaklaşımla merkez noktanıza yakın bazı verileriniz var.

2) Artık bu filtrelenmiş veriler üzerinde döngü oluşturabilir ve aşağıdaki yöntemleri kullanarak gerçekten noktanıza yakın (daire içinde) olup olmadıklarını belirleyebilirsiniz:

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

Zevk almak!

Bu referansı kullandım, özelleştirdim ve tamamladım.


Yukarıdaki bu kavramın Javascript uygulamasını arıyorsanız, Chris Veness'in harika web sayfasına göz atın. movable-type.co.uk/scripts/latlong.html
barneymc

@Menma x enlem ve y boylamdır. yarıçap: Resimde gösterilen dairenin yarıçapı.
Bob'lar

yukarıda verilen çözüm doğru ve işe yarıyor. Deneyin ... :)
YS

1
Bu yaklaşık bir çözümdür! Hızlı , dizin dostu bir SQL sorgusu aracılığıyla size oldukça yaklaşık sonuçlar verir . Bazı aşırı durumlarda yanlış sonuç verecektir. Birkaç kilometrelik alan içinde az sayıda yaklaşık sonuç aldığınızda , bu sonuçları filtrelemek için daha yavaş, daha kesin yöntemler kullanın . Çok büyük yarıçaplı filtrelemek için veya uygulamanız sıklıkla ekvatorda kullanılacaksa kullanmayın!
user1643723

1
Ama anlamıyorum. CalculateDerivedPosition, enlem, lng koordinatını kartezyen olana dönüştürüyor ve sonra SQL sorgusunda, bu kartezyen değerleri enlem, uzun değerlerle karşılaştırıyorsunuz. İki farklı geometrik koordinat mı? Bu nasıl çalışıyor? Teşekkürler.
Misgevolution

70

Chris'in cevabı gerçekten yararlıdır (teşekkürler!), Ancak yalnızca doğrusal koordinatlar kullanıyorsanız işe yarar (örneğin, UTM veya OS ızgara referansları). Enlem / boylam için derece kullanılıyorsa (örn. WGS84), yukarıdakiler yalnızca ekvatorda çalışır. Diğer enlemlerde, boylamın sıralama düzeni üzerindeki etkisini azaltmanız gerekir. (Kuzey kutbuna yakın olduğunuzu hayal edin ... bir enlem derecesi hala herhangi bir yerdeki ile aynıdır, ancak bir boylam derecesi yalnızca birkaç fit olabilir. Bu, sıralama düzeninin yanlış olduğu anlamına gelir).

Ekvatorda değilseniz, mevcut enleminize bağlı olarak geçiştirme faktörünü önceden hesaplayın:

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

Sonra şuna göre sipariş ver:

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

Hala sadece bir tahmin, ancak birincisinden çok daha iyi, bu nedenle sıralama düzeni yanlışlıkları çok daha nadir olacaktır.


3
Bu, kutuplarda birleşen ve yaklaştıkça sonuçları çarpıtan uzunlamasına çizgilerin gerçekten ilginç bir noktası. Güzel düzeltme.
Chris Simpson

1
bu cursor = db.getReadableDatabase (). rawQuery ("nome, id olarak _id olarak seçin," + "(" + enlem + "- enlem) * (" + enlem + "- enlem) + (" + boylam + "- lon) * (" + boylam + "- lon) *" + fudge + "as distanza" + "cliente'den" + "distanza asc tarafından düzen", null);
max4ever

veya ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)daha az olmalıdır ? distancedistance^2
Bob'lar

Fudge faktörü radyan cinsinden ve sütunlar derece cinsinden değil mi? Aynı birime dönüştürülmeleri gerekmez mi?
Rangel Reale

1
Hayır, geçiştirme faktörü kutuplarda 0 ve ekvatorda 1 olan bir ölçeklendirme faktörüdür. Ne derece ne de radyan cinsinden, sadece birimsiz bir sayıdır. Java Math.cos işlevi radyan cinsinden bir argüman gerektirir ve <lat> derecesinin derece cinsinden olduğunu varsaydım, dolayısıyla Math.toRadians işlevi. Ancak ortaya çıkan kosinüsün birimi yoktur.
Teasel

68

Bunun yanıtlandığını ve kabul edildiğini biliyorum ama deneyimlerimi ve çözümümü de ekleyeceğimi düşündüm.

Kullanıcının mevcut konumu ile herhangi bir belirli hedef konum arasındaki doğru mesafeyi hesaplamak için cihazda bir haversine işlevi yapmaktan mutlu olsam da, sorgu sonuçlarını mesafe sırasına göre sıralamak ve sınırlandırmak gerekiyordu.

Tatmin edici olmayan çözüm, partiyi geri döndürmek ve olaydan sonra sıralamak ve filtrelemektir, ancak bu, ikinci bir imleçle sonuçlanacak ve birçok gereksiz sonuç iade edilip atılacaktır.

Tercih ettiğim çözüm, long ve lats'nin kare delta değerlerinin bir sıralama düzeninde geçirilmesiydi:

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

Sadece bir sıralama düzeni için tam haversine yapmaya gerek yoktur ve sonuçların kareköküne gerek yoktur, bu nedenle SQLite hesaplamayı halledebilir.

DÜZENLE:

Bu cevap hala sevgiyi alıyor. Çoğu durumda iyi çalışıyor, ancak biraz daha fazla doğruluğa ihtiyacınız varsa, lütfen aşağıdaki enlem 90'a yaklaştıkça artan yanlışlıkları düzelten bir "geçiştirme" faktörü ekleyen @Teasel tarafından verilen yanıta göz atın.


Mükemmel cevap. Nasıl çalıştığını ve bu algoritmanın adını açıklar mısınız?
iMatoria

3
@iMatoria - Bu, ünlü pisagor teoreminin kısaltılmış bir versiyonudur. İki koordinat kümesi verildiğinde, iki X değeri arasındaki fark dik açılı bir üçgenin bir tarafını temsil eder ve Y değerleri arasındaki fark diğeridir. Hipotenüsü (ve dolayısıyla noktalar arasındaki mesafeyi) elde etmek için bu iki değerin karelerini toplayın ve ardından sonucun karekökünü alın. Bizim durumumuzda son biti (karekökleme) yapmayız çünkü yapamayız. Neyse ki bu bir sıralama için gerekli değildir.
Chris Simpson

6
BostonBusMap uygulamamda, mevcut konuma en yakın durakları göstermek için bu çözümü kullandım. Ancak cos(latitude), enlem ve boylamın kabaca eşit olması için boylam mesafesini ölçeklemeniz gerekir . Bkz en.wikipedia.org/wiki/...
noisecapella

0

Performansı olabildiğince artırmak için @Chris Simpson'ın fikrini aşağıdaki ORDER BYmaddeyle geliştirmenizi öneririm :

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

Bu durumda aşağıdaki değerleri koddan geçirmelisiniz:

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

Ayrıca LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2veritabanında ek sütun olarak depolamalısınız . Varlıklarınızı veritabanına ekleyerek doldurun. Bu, büyük miktarda veri çıkarırken performansı biraz artırır.


-3

Bunun gibi bir şey dene:

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

Bundan sonra, id veritabanından istediğiniz öğeyi içerir, böylece onu getirebilirsiniz:

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

Umarım yardımcı olur!

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.