SQlite Ottenere le posizioni più vicine (con latitudine e longitudine)


86

Ho dati con latitudine e longitudine memorizzati nel mio database SQLite e voglio ottenere le posizioni più vicine ai parametri che ho inserito (es. La mia posizione corrente - lat / lng, ecc.).

So che questo è possibile in MySQL e ho fatto alcune ricerche sul fatto che SQLite necessita di una funzione esterna personalizzata per la formula Haversine (calcolo della distanza su una sfera), ma non ho trovato nulla che sia scritto in Java e funzioni .

Inoltre, se voglio aggiungere funzioni personalizzate, ho bisogno di org.sqlite.jar (per org.sqlite.Function) e questo aggiunge dimensioni non necessarie all'app.

L'altro lato di questo è che ho bisogno della funzione Order by da SQL, perché la visualizzazione della distanza da sola non è un grosso problema: l'ho già fatto nel mio SimpleCursorAdapter personalizzato, ma non posso ordinare i dati, perché ho non ho la colonna della distanza nel mio database. Ciò significherebbe aggiornare il database ogni volta che la posizione cambia e questo è uno spreco di batteria e prestazioni. Quindi, se qualcuno ha qualche idea su come ordinare il cursore con una colonna che non è nel database, anch'io sarei grato!

So che ci sono tonnellate di app Android là fuori che usano questa funzione, ma qualcuno può spiegare la magia.

A proposito, ho trovato questa alternativa: Query per ottenere record basati su Radius in SQLite?

Si suggerisce di creare 4 nuove colonne per i valori cos e sin di lat e lng, ma esiste un altro modo non così ridondante?


Hai controllato se org.sqlite.Function funziona per te (anche se la formula non è corretta)?
Thomas Mueller,

No, ho trovato un'alternativa (ridondante) (post modificato) che suona meglio rispetto all'aggiunta di 2,6 MB .jar nell'app. Ma sto ancora cercando una soluzione migliore. Grazie!
Jure

Qual è il tipo di unità per la distanza di ritorno?

Ecco un'implementazione completa per la creazione di una query SQlite su Android basata sulla distanza tra la tua posizione e la posizione dell'oggetto.
EricLarch

Risposte:


112

1) Per prima cosa filtra i tuoi dati SQLite con una buona approssimazione e diminuisci la quantità di dati che devi valutare nel tuo codice java. Utilizzare la seguente procedura per questo scopo:

Per avere una soglia deterministica e un filtro più accurato sui dati, è meglio calcolare 4 posizioni che sono in radiusmetri del nord, ovest, est e sud del tuo punto centrale nel tuo codice java e quindi controllare facilmente da meno di e più di Operatori SQL (>, <) per determinare se i tuoi punti nel database sono in quel rettangolo o no.

Il metodo calculateDerivedPosition(...)calcola questi punti per te (p1, p2, p3, p4 nell'immagine).

inserisci qui la descrizione dell'immagine

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

    }

E ora crea la tua 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è il nome della colonna nel database che memorizza i valori di latitudine ed COL_Yè per la longitudine.

Quindi hai alcuni dati che sono vicini al tuo punto centrale con una buona approssimazione.

2) Ora puoi eseguire il ciclo su questi dati filtrati e determinare se sono davvero vicini al tuo punto (nel cerchio) o meno utilizzando i seguenti metodi:

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

Godere!

Ho usato e personalizzato questo riferimento e l'ho completato.


Dai un'occhiata alla fantastica pagina web di Chris Veness se stai cercando un'implementazione Javascript di questo concetto sopra. movable-type.co.uk/scripts/latlong.html
barneymc

@Menma x è la latitudine ey è la longitudine. raggio: il raggio del cerchio mostrato nell'immagine.
Bobs

la soluzione data sopra è corretta e funziona. Provalo ... :)
YS

1
Questa è una soluzione approssimativa! Fornisce risultati altamente approssimativi tramite una query SQL rapida e indicizzata. Darà un risultato sbagliato in alcune circostanze estreme. Una volta ottenuto un numero limitato di risultati approssimativi entro un'area di diversi chilometri, utilizzare metodi più lenti e precisi per filtrare tali risultati . Non usarlo per filtrare con un raggio molto ampio o se la tua app verrà utilizzata frequentemente all'equatore!
user1643723

1
Ma non lo capisco. CalculateDerivedPosition sta trasformando la coordinata lat, lng in una cartesiana e quindi nella query SQL, stai confrontando questi valori cartesiani con i valori lat, long. Due coordinate geometriche differenti? Come funziona? Grazie.
Misgevolution

70

La risposta di Chris è davvero utile (grazie!), Ma funzionerà solo se stai usando coordinate rettilinee (es. Riferimenti a griglia UTM o OS). Se si utilizzano i gradi per lat / lng (ad esempio WGS84), quanto sopra funziona solo all'equatore. Ad altre latitudini, è necessario ridurre l'impatto della longitudine sull'ordinamento. (Immagina di essere vicino al polo nord ... un grado di latitudine è sempre lo stesso di ovunque, ma un grado di longitudine può essere solo di pochi piedi. Ciò significa che l'ordinamento non è corretto).

Se non sei all'equatore, calcola in anticipo il fattore di fusione, in base alla tua latitudine attuale:

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

Quindi ordina per:

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

È ancora solo un'approssimazione, ma molto meglio del primo, quindi le imprecisioni nell'ordinamento saranno molto più rare.


3
Questo è un punto davvero interessante sulle linee longitudinali che convergono ai poli e distorcono i risultati più ci si avvicina. Bella soluzione.
Chris Simpson

1
questo sembra funzionare cursore = db.getReadableDatabase (). rawQuery ("Seleziona nome, id come _id," + "(" + latitudine + "- lat) * (" + latitudine + "- lat) + (" + longitudine + "- lon) * (" + longitude + "- lon) *" + fudge + "as distanza" + "from cliente" + "order by distanza asc", null);
max4ever

dovrebbe ((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)essere minore di distanceo distance^2?
Bob

Il fattore di fusione non è espresso in radianti e le colonne in gradi? Non dovrebbero essere convertiti nella stessa unità?
Rangel Reale

1
No, il fattore fudge è un fattore di scala che è 0 ai poli e 1 all'equatore. Non è né in gradi né in radianti, è solo un numero senza unità. La funzione Java Math.cos richiede un argomento in radianti e ho assunto che <lat> fosse in gradi, da cui la funzione Math.toRadians. Ma il coseno risultante non ha unità.
Teasel

68

So che è stato risposto e accettato, ma ho pensato di aggiungere le mie esperienze e la mia soluzione.

Sebbene fossi felice di eseguire una funzione haversine sul dispositivo per calcolare la distanza precisa tra la posizione corrente dell'utente e qualsiasi particolare posizione di destinazione, era necessario ordinare e limitare i risultati della query in ordine di distanza.

La soluzione meno che soddisfacente è restituire il lotto e ordinare e filtrare dopo il fatto, ma ciò comporterebbe un secondo cursore e molti risultati non necessari restituiti e scartati.

La mia soluzione preferita era passare in un ordine ordinario i valori delta al quadrato di long e lat:

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

Non è necessario eseguire l'intera haversine solo per un ordinamento e non è necessario applicare la radice quadrata ai risultati, pertanto SQLite può gestire il calcolo.

MODIFICARE:

Questa risposta sta ancora ricevendo amore. Funziona bene nella maggior parte dei casi, ma se hai bisogno di un po 'più di precisione, controlla la risposta di @Teasel di seguito che aggiunge un fattore "fudge" che corregge le imprecisioni che aumentano man mano che la latitudine si avvicina a 90.


Bella risposta. Potresti spiegare come ha funzionato e come si chiama questo algoritmo?
iMatoria

3
@iMatoria - Questa è solo una versione ridotta del famoso teorema di Pitagora. Dati due serie di coordinate, la differenza tra i due valori X rappresenta un lato di un triangolo rettangolo e la differenza tra i valori Y è l'altro. Per ottenere l'ipotenusa (e quindi la distanza tra i punti) si sommano i quadrati di questi due valori insieme e poi si radica il risultato. Nel nostro caso non facciamo l'ultimo bit (il radicamento quadrato) perché non possiamo. Fortunatamente questo non è necessario per un ordinamento.
Chris Simpson

6
Nella mia app BostonBusMap ho utilizzato questa soluzione per mostrare le fermate più vicine alla posizione corrente. Tuttavia è necessario scalare la distanza di longitudine cos(latitude)per avere latitudine e longitudine più o meno uguali. Vedi en.wikipedia.org/wiki/…
noisecapella

0

Al fine di aumentare le prestazioni il più possibile, suggerisco di migliorare l'idea di @Chris Simpson con la seguente ORDER BYclausola:

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

In questo caso dovresti passare i seguenti valori dal codice:

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

E dovresti anche memorizzare LAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2come colonna aggiuntiva nel database. Popolarlo inserendo le tue entità nel database. Ciò migliora leggermente le prestazioni durante l'estrazione di grandi quantità di dati.


-3

Prova qualcosa di simile:

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

Dopodiché, id contiene l'elemento che desideri dal database in modo da poterlo recuperare:

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

Spero che aiuti!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.