2009-12-16 11 views
10

Sto cercando di gestire la ricerca di prossimità per un negozio di base in Django. Invece di trascinare PostGIS in giro con la mia app solo per poter utilizzare il filtro della distanza di GeoDjango, mi piacerebbe utilizzare la formula della distanza della sfera dei costi sferici in una query modello. Mi piacerebbe che tutti i calcoli fossero fatti nel database in una query, per efficienza.Filtra i codici di avviamento postale in prossimità di Django con la legge sferica dei coseni.

Un esempio di query MySQL da Internet attuazione della legge sferica di Coseni come questo:

SELECT id, ( 
    3959 * acos(cos(radians(37)) * cos(radians(lat)) * 
    cos(radians(lng) - radians(-122)) + sin(radians(37)) * 
    sin(radians(lat))) 
) 
AS distance FROM stores HAVING distance < 25 ORDER BY distance LIMIT 0 , 20; 

La query deve fare riferimento al Codice Postale ForeignKey per i valori/LNG lat di ogni negozio. Come posso fare tutto questo in una query di modello Django?

+0

Potresti pubblicare i tuoi modelli? – cethegeek

+0

(a) Questa NON è la formula 'haversine'; è la formula della "legge sferica del coseno"; vedi (per esempio) 'http: // www.movable-type.co.uk/scripts/latlong.html' e guarda i rispettivi articoli di Wikipedia. (b) Confido che sostituirai le coordinate hard-coded dell'utente con le variabili :-) (c) Il gentile lettore dovrebbe essere avvisato che la tua unità di distanza è in qualche modo arcaica (qualcosa che ha a che fare con 1000 * (lunghezza di Roman ritmo standard della legione), credo) :-) –

+0

Jeez, cosa c'è di così difficile in GeoDjango? Basta installarlo :) –

risposta

8

È possibile eseguire raw SQL queries in Django.

Il mio suggerimento è scrivere la query per estrarre un elenco di ID (che sembra che tu stia facendo ora), quindi utilizzare gli ID per estrarre i modelli associati (in una normale query Django non raw SQL). Cerca di mantenere il tuo SQL il più indipendente possibile dai dialetti, in modo da non doverti preoccupare di un'altra cosa se dovessi cambiare i database.

Per chiarire, ecco un esempio di come farlo:

def get_models_within_25 (self): 
    from django.db import connection, transaction 
    cursor = connection.cursor() 

    cursor.execute("""SELECT id, ( 
     3959 * acos(cos(radians(37)) * cos(radians(lat)) * 
     cos(radians(lng) - radians(-122)) + sin(radians(37)) * 
     sin(radians(lat)))) 
     AS distance FROM stores HAVING distance < 25 
     ORDER BY distance LIMIT 0 , 20;""") 
    ids = [row[0] for row in cursor.fetchall()] 

    return MyModel.filter(id__in=ids) 

Come un disclaimer, non posso garantire per questo codice, come è stato un paio di mesi da quando ho scritto qualsiasi Django, ma dovrebbe essere lungo le linee giuste.

+0

Funziona benissimo, ha solo bisogno di virgolette (o di essere trasformato in una singola stringa). – Tom

+0

Solo un seguito a questo. La query originale restituisce un campo "distanza" (che mostra la distanza tra i 2 set di log/lat). Come eseguirò la seconda parte, ma con questo campo extra 'distanza'? – dotty

+0

Cos'è "la seconda parte"? – jakeboxer

4

Basta dare seguito alla risposta di jboxer, ecco il tutto come parte di un manager personalizzato con alcune delle cose hard-coded trasformati in variabili:

class LocationManager(models.Manager): 
    def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True): 
     if use_miles: 
      distance_unit = 3959 
     else: 
      distance_unit = 6371 

     from django.db import connection, transaction 
     cursor = connection.cursor() 

     sql = """SELECT id, (%f * acos(cos(radians(%f)) * cos(radians(latitude)) * 
     cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) 
     AS distance FROM locations_location HAVING distance < %d 
     ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results) 
     cursor.execute(sql) 
     ids = [row[0] for row in cursor.fetchall()] 

     return self.filter(id__in=ids) 
1

seguito la risposta di jboxer

def find_cars_within_miles_from_postcode(request, miles, postcode=0): 

    # create cursor for RAW query 
    cursor = connection.cursor() 

    # Get lat and lon from google 
    lat, lon = getLonLatFromPostcode(postcode) 

    # Gen query 
    query = "SELECT id, ((ACOS(SIN("+lat+" * PI()/180) * SIN(lat * PI()/180) + COS("+lat+" * PI()/180) * COS(lat * PI()/180) * COS(("+lon+" - lon) * PI()/180)) * 180/PI()) * 60 * 1.1515) AS distance FROM app_car HAVING distance<='"+miles+"' ORDER BY distance ASC" 

    # execute the query 
    cursor.execute(query) 

    # grab all the IDS form the sql result 
    ids = [row[0] for row in cursor.fetchall()] 

    # find cars from ids 
    cars = Car.objects.filter(id__in=ids) 

    # return the Cars with these IDS 
    return HttpResponse(cars) 

Questo restituisce le mie auto da x quantità di miglia, questo funziona bene. Tuttavia, la query non elaborata ha restituito la distanza da una determinata posizione, penso che il fieldname fosse "distance".

Come posso restituire questo campo "distanza" con i miei oggetti auto?

8

Per dare seguito alla risposta di Tom, per impostazione predefinita non funzionerà in SQLite a causa della mancanza di funzioni matematiche di SQLite. Nessun problema, è abbastanza semplice per aggiungere:

class LocationManager(models.Manager): 
    def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True): 
     if use_miles: 
      distance_unit = 3959 
     else: 
      distance_unit = 6371 

     from django.db import connection, transaction 
     from mysite import settings 
     cursor = connection.cursor() 
     if settings.DATABASE_ENGINE == 'sqlite3': 
      connection.connection.create_function('acos', 1, math.acos) 
      connection.connection.create_function('cos', 1, math.cos) 
      connection.connection.create_function('radians', 1, math.radians) 
      connection.connection.create_function('sin', 1, math.sin) 

     sql = """SELECT id, (%f * acos(cos(radians(%f)) * cos(radians(latitude)) * 
     cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) 
     AS distance FROM location_location WHERE distance < %d 
     ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results) 
     cursor.execute(sql) 
     ids = [row[0] for row in cursor.fetchall()] 

     return self.filter(id__in=ids) 
+0

Sembra magia, come funziona? Immagino sia piuttosto inefficiente? – nisc

+0

Il 'da impostazioni di importazione di mysite' può essere reso più generico usando 'dalle impostazioni di importazione di django.conf' –

+0

Dove si trova il punto di riferimento? – Gocht

5

di follow-up su Tom, se si vuole avere una query che funziona anche in PostgreSQL, non è possibile utilizzare AS perché si otterrà un errore che dice 'distanza' non esiste.

Si dovrebbe mettere l'intero sferica legge expresion nella clausola WHERE, come questo (funziona anche in mysql):

import math 
from django.db import connection, transaction 
from django.conf import settings 

from django .db import models 

class LocationManager(models.Manager): 
    def nearby_locations(self, latitude, longitude, radius, use_miles=False): 
     if use_miles: 
      distance_unit = 3959 
     else: 
      distance_unit = 6371 

     cursor = connection.cursor() 

     sql = """SELECT id, latitude, longitude FROM locations_location WHERE (%f * acos(cos(radians(%f)) * cos(radians(latitude)) * 
      cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) < %d 
      """ % (distance_unit, latitude, longitude, latitude, int(radius)) 
     cursor.execute(sql) 
     ids = [row[0] for row in cursor.fetchall()] 

     return self.filter(id__in=ids) 

prega di notare che è necessario selezionare la latitudine e la longitudine, altrimenti non è possibile usalo nella clausola WHERE.

0

Utilizzando alcune delle risposte proposte di cui sopra, mi è stato sempre risultati incosistent così ho deciso di controllare di nuovo l'equazione utilizzando [questo link] http://www.movable-type.co.uk/scripts/latlong.html come riferimento, l'equazione è d = acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(lon2-lon1)) * 6371 dove d è la distanza deve essere calcolato,

lat1,lon1 è la coordinata del punto base e lat2,lon2 è la coordinata degli altri punti che nel nostro caso sono punti nel database.

Dalle risposte di cui sopra, il LocationManager classe assomiglia a questo

class LocationManager(models.Manager): 
def nearby_locations(self, latitude, longitude, radius, max_results=100, use_miles=True): 
    if use_miles: 
     distance_unit = 3959 
    else: 
     distance_unit = 6371 

    from django.db import connection, transaction 
    from mysite import settings 
    cursor = connection.cursor() 
    if settings.DATABASE_ENGINE == 'sqlite3': 
     connection.connection.create_function('acos', 1, math.acos) 
     connection.connection.create_function('cos', 1, math.cos) 
     connection.connection.create_function('radians', 1, math.radians) 
     connection.connection.create_function('sin', 1, math.sin) 

    sql = """SELECT id, (acos(sin(radians(%f)) * sin(radians(latitude)) + cos(radians(%f)) 
      * cos(radians(latitude)) * cos(radians(%f-longitude))) * %d) 
    AS distance FROM skills_coveragearea WHERE distance < %f 
    ORDER BY distance LIMIT 0 , %d;""" % (latitude, latitude, longitude,distance_unit, radius, max_results) 
    cursor.execute(sql) 
    ids = [row[0] for row in cursor.fetchall()] 

    return self.filter(id__in=ids) 

Utilizzando il sito [link] http://www.movable-type.co.uk/scripts/latlong.html come controllo, dove i miei risultati coerenti.

Problemi correlati