2009-12-16 15 views
10

J'essaie de gérer la recherche de proximité pour trouver un localisateur de base dans Django. Plutôt que d'utiliser PostGIS avec mon application pour utiliser le filtre de distance de GeoDjango, j'aimerais utiliser la formule de la distance sphérique des cosinus dans une requête de modèle. Je voudrais que tous les calculs soient effectués dans la base de données dans une requête, pour plus d'efficacité.Filtrez les codes postaux par proximité dans Django avec la loi sphérique des cosinus

Un exemple MySQL requête à partir d'Internet application de la loi de Cosinus sphérique comme ceci:

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 requête doit faire référence à la ForeignKey pour les valeurs NPA lat/lng de chaque magasin. Comment puis-je faire tout cela dans une requête de modèle Django?

+0

Pourriez-vous publier vos modèles? – cethegeek

+0

(a) Ce n'est PAS la formule «haversine»; c'est la formule «loi sphérique des cosinus»; voyez (par exemple) 'http: // www.movable-type.co.uk/scripts/latlong.html' et regardez les articles respectifs de Wikipedia. (b) Je crois que vous allez remplacer les coordonnées de l'utilisateur codées en dur par des variables :-) (c) Le gentil lecteur devrait être averti que votre unité de distance est quelque peu archaïque (quelque chose à faire avec 1000 * rythme normal de la légion), je crois) :-) –

+0

Jeez, qu'est-ce qui est si dur avec GeoDjango? Il suffit de l'installer :) –

Répondre

8

Il est possible que l'exécution raw SQL queries in Django. Ma suggestion est, écrivez la requête pour tirer une liste d'ID (que vous semblez faire maintenant), puis utilisez les ID pour extraire les modèles associés (dans une requête Django normale, non-raw-SQL)). Essayez de garder votre SQL aussi indépendant que possible du dialecte, afin de ne plus avoir à vous soucier d'une chose si vous devez changer de base de données.

Pour clarifier, voici un exemple de la façon de le faire:

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) 

Comme un avertissement, je ne peux pas garantir ce code, car il a été quelques mois depuis que je l'ai écrit tout Django, mais il devrait être dans le bon sens.

+0

Cela fonctionne très bien, il suffit de tripler les guillemets (ou de les transformer en une seule chaîne). – Tom

+0

Juste un suivi à cela. La requête d'origine renvoie un champ "distance" (qui montre la distance entre les 2 ensembles de log/lat). Comment pourrais-je effectuer la deuxième partie, mais avec ce champ de «distance» supplémentaire? – dotty

+0

Qu'est-ce que "la deuxième partie"? – jakeboxer

4

Juste pour le suivi de la réponse de jboxer, voici le tout dans le cadre d'un gestionnaire personnalisé avec certaines des choses codées en dur transformées en variables:

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

Après la réponse de 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) 

Cela renvoie mes voitures de x nombre de miles, cela fonctionne bien. Cependant la requête brute a retourné à quelle distance ils étaient d'un certain endroit, je pense que le nom de champ était «distance». Comment puis-je retourner ce champ «distance» avec mes objets de voiture?

8

Pour faire suite à la réponse de Tom, cela ne fonctionnera pas dans SQLite par défaut à cause du manque de fonctions mathématiques de SQLite par défaut. Pas de problème, il est assez simple d'ajouter:

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

On dirait de la magie, comment ça marche? Je suppose que c'est assez inefficace? – nisc

+0

Les 'paramètres d'importation mysite' peuvent être rendus plus génériques en utilisant 'depuis les paramètres d'importation de django.conf' –

+0

Où est le point de référence? – Gocht

5

Pour donner suite à Tom, si vous voulez avoir une requête qui fonctionne aussi dans postgresql, vous ne pouvez pas utiliser AS parce que vous obtiendrez une erreur indiquant que la « distance » n'existe pas.

Vous devriez mettre l'ensemble expresion de la loi sphérique dans la clause WHERE, comme celui-ci (il fonctionne aussi dans une base 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) 

S'il vous plaît noter que vous devez sélectionner la latitude et la longitude, sinon vous ne pouvez pas utilisez-le dans la clause WHERE.

0

En utilisant quelques-unes des réponses proposées ci-dessus, je recevais des résultats incosistent alors j'ai décidé de vérifier l'équation à nouveau en utilisant [ce lien] http://www.movable-type.co.uk/scripts/latlong.html comme référence, l'équation est d = acos(sin(lat1)*sin(lat2) + cos(lat1)*cos(lat2)*cos(lon2-lon1)) * 6371d est la distance à calculer,

lat1,lon1 est la coordonnée du point de base et lat2,lon2 est la coordonnée des autres points qui dans notre cas sont des points dans la base de données.

A partir des réponses ci-dessus, la classe LocationManager ressemble à ce

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) 

L'utilisation du site [lien] http://www.movable-type.co.uk/scripts/latlong.html comme chèque, mes résultats lorsque cela est compatible.