Sklepy najbliżej ciebie - geografia z GeoDjango
Boom na urządzenia mobilne jak i powszechny dostęp do internetu wywołał spore zapotrzebowanie na usługi związane z nawigacją, geolokalizacją i wyszukiwaniem spersonalizowanym pod kątem obecnego położenia geograficznego. Obecnie da się już dość "łatwo" tworzyć aplikacje oferujące takie funkcjonalności. Django oferuje cały ogromny wewnętrzny framework - geodjango.
W tym artykule zaprezentuję aplikację Django wykorzystującą kilka elementów geodjango do wyszukiwania najbliższych sklepów - na mapie i po odległości od podanego adresu. Aplikacja ta zaprezentuje podstawowe elementy GeoDjango (i całkiem ciekawe zastosowanie).
Baza danych
GeoDjango zaczyna się od bazy danych. Do obsługi geograficznych typów danych w bazach danych potrzebny jest odpowiedni silnik (GEOS), który trzeba zainstalować. Dokumentacja Django opisuje przypadki dla różnych baz danych. Ja wykorzystam PostgreSQL, które chyba najlepiej sobie z tym radzi.
W systemie musimy mieć zainstalowany serwer baz danych PostgreSQL oraz dodatek - postgis. Dla Ubuntu/Debiana i podobnych będzie to pakiet postgresql-*-postgis (gdzie * to numer wersji, w chwili pisania artykułu, w Ubuntu 12.04 postgresql-9.1-postgis).
Po zainstalowaniu pakietów trzeba skonfigurować postgresa i stworzyć postgisową bazę danych dla naszej aplikacji www. Jest to opisane w dokumentacji podlinkowanej powyżej. Ja przedstawię szybką ścieżkę dla Ubuntu/Debiana.
- Odpalamy konsolę i przechodzimy na użytkownika postgres:
sudo -i su postgres cd
- Pobieramy (wget) skrypt konfiguracyjny - create_template_postgis-debian.sh z dokumentacji Django.
- Odpalamy go:
chmod 755 ./create_template_postgis-debian.sh ./create_template_postgis-debian.sh
- Tworzymy postgisową bazę danych:
createdb -T template_postgis NAZWA_BAZY_DANYCH
- Ustawiamy hasło dla użytkownika postgres (jeżeli nie ustawione):
psql NAZWA_BAZY_DANYCHI odpalamy:\password postgresPodajemy nowe hasło i gotowe.
Konfiguracja Django
Konfiguracja projektu Django jest dość prosta - trzeba aby podać silnik dla baz postgisowych - django.contrib.gis.db.backends.postgis. W moim testowym projekcie wygląda to tak:DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'postgis_test',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
}
}
GISowa aplikacja Django
Mamy specjalny typ bazy danych, mamy specjalną konfigurację Django - teraz trzeba to do czegoś wykorzystać. Zaprezentuję prostą aplikację - listę sklepów. Każdy sklep ma swoją nazwę i adres (adres, miasto). W modelu znajdzie się także GISowe pole na współrzędne geograficzne - a te są używane na mapach (Google Maps, OpenStreetMap) by zaznaczyć położenie obiekty. Są też używane przez GISowe bazy danych do wykonywania "geograficznych" zapytań - np. sortowania po odległości od danego punktu - co wykorzystamy do wyszukiwania sklepów po podanym adresie...
Modele
W normalnej aplikacji model "sklep" wyglądałby tak:from django.db import models
class Shop(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
def __unicode__(self):
return self.name
from django.contrib.gis.db import models as gis_models
from django.contrib.gis import geos
from django.db import models
class Shop(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
location = gis_models.PointField(u"longitude/latitude",
geography=True, blank=True, null=True)
gis = gis_models.GeoManager()
objects = models.Manager()
def __unicode__(self):
return self.name
Shop.gis.filter() # GISowe
Shop.objects.filter() # Standardowe
Odpalamy syncdb co powinno stworzyć tabelę. Model jest gotowy.
Wprowadzamy dane
Korzystając z Panelu Admina dodałem kilka testowych sklepów:

Pole PointField pozostawiłem puste. Silniki map oferują usługi geokodowania adresów na współrzędne geograficzne. Ja wykorzystam geopy do geokodowania adresu na współrzędne geograficzne, które następnie trafią do pola PointField.
By dodać "automatyczne" geokodowanie adresu definiuję w modelu własną metodę save (można też użyć sygnału):from django.contrib.gis.db import models as gis_models
from django.contrib.gis import geos
from django.db import models
from geopy.geocoders.google import Google
from geopy.geocoders.google import GQueryError
class Shop(models.Model):
name = models.CharField(max_length=200)
address = models.CharField(max_length=100)
city = models.CharField(max_length=50)
location = gis_models.PointField(u"longitude/latitude",
geography=True, blank=True, null=True)
gis = gis_models.GeoManager()
objects = models.Manager()
def __unicode__(self):
return self.name
def save(self, **kwargs):
if not self.location:
address = u'%s %s' % (self.city, self.address)
address = address.encode('utf-8')
geocoder = Google()
try:
_, latlon = geocoder.geocode(address)
except (URLError, GQueryError, ValueError):
pass
else:
point = "POINT(%s %s)" % (latlon[1], latlon[0])
self.location = geos.fromstr(point)
super(Shop, self).save()
Wykorzystuję geopy do geokodowania adresu, a następnie przypisuję obiekt typu "POINT" do pola PointField :) Trzeba uważać by nie pomylić długości z szerokością geograficzną.
Dla sklepów z poprawnym adresem powinniśmy zobaczyć wartości pojawiające się w polu location, np:Z poziomu kodu Pythona współrzędne dostępne są jako location.x i location.y
GEO-Zapytania
Teraz wykorzystamy to "magiczne" GISowskie pole PointField do pobrania listy sklepów znajdujących się najbliżej podanego przez użytkownika adresu. Stosują tradycyjne rozwiązania jak dwa pola w bazie na długość i szerokość geograficzną nie byłoby to możliwe (a na pewno nie ładnie i wydajnie).
Stworzę teraz widok z formularzem na adres. Jeżeli adres zostanie podany to zgeokoduje go do współrzędnych za pomocą geopy. Mając współrzędne adresu będę mógł wykonać zapytanie do bazy danych.
Całość wygląda tak:from urllib2 import URLError
from django.contrib.gis import geos
from django.contrib.gis import measure
from django.shortcuts import render_to_response
from django.template import RequestContext
from geopy.geocoders.google import Google
from geopy.geocoders.google import GQueryError
from shops import forms
from shops import models
def geocode_address(address):
address = address.encode('utf-8')
geocoder = Google()
try:
_, latlon = geocoder.geocode(address)
except (URLError, GQueryError, ValueError):
return None
else:
return latlon
def get_shops(longitude, latitude):
current_point = geos.fromstr("POINT(%s %s)" % (longitude, latitude))
distance_from_point = {'km': 10}
shops = models.Shop.gis.filter(location__distance_lte=(current_point, measure.D(**distance_from_point)))
shops = shops.distance(current_point).order_by('distance')
return shops.distance(current_point)
def home(request):
form = forms.AddressForm()
shops = []
if request.POST:
form = forms.AddressForm(request.POST)
if form.is_valid():
address = form.cleaned_data['address']
location = geocode_address(address)
if location:
latitude, longitude = location
shops = get_shops(longitude, latitude)
return render_to_response(
'home.html',
{'form': form, 'shops': shops},
context_instance=RequestContext(request))
Mamy tutaj widok home odpowiedzialny za wyświetlanie wszystkiego. Do tego klasyczna obsługa formularza. Nowością jest kod zawarty w funkcji get_shops. Pobieram w niej sklepy, ale za pomocą GISowskiego managera - models.Shop.gis. Co więcej operuję na polu "location" - PointField. Po prostu każę pobrać sklepy o odległości mniejszej lub równej 10 km od podanego punktu - współrzędnych adresu wprowadzonego przez użytkownika. GISowskie pola mają bardzo rozbudowaną paletę metod i możliwych do przeprowadzenia na nich operacji. Tutaj prezentuję tylko kilka. Rozpiskę znajdziemy na chicagodjango.com.
Z pozostałych elementów mamy zwykły szablon:<h1>Shop GEO-Test</h1>
<form method="post" action="./">
{% csrf_token %}
<table>
{{ form }}
</table>
<input type="submit" value="Search" />
</form>
{% if shops %}
<h2>Shops near you</h2>
<ul>
{% for shop in shops %}
<li><b>{{ shop.name }}</b>: distance: {{ shop.distance }}</li>
{% endfor %}
</ul>
{% endif %}
from django import forms
class AddressForm(forms.Form):
address = forms.CharField()

GeoDjango w działaniu
Do uzyskania fajnej funkcjonalności musieliśmy przejść dość długą drogę. Z drugiej strony Django czyni ją czytelną i przyjemną w implementacji.
Mapy Google
Sama lista i jakieś odległości mogą nie wyglądać zbyt efektywnie. Mając współrzędne możemy bez problemu użyć jednego z systemu map, np. Google Maps to zaprezentowania adresu użytkownika i najbliższych sklepów na mapie. Wystarczy że do szablonu przekażę jeszcze współrzędnego podanego adresu i mogę dodać taki oto kod:<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
$(document).ready(function() {
var latlng = new google.maps.LatLng("{{ latitude }}", "{{ longitude }}");
var mapOptions = {
zoom: 15,
center: latlng,
mapTypeControl: false,
navigationControlOptions: {style: google.maps.NavigationControlStyle.SMALL},
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map($('.map')[0], mapOptions);
var marker = new google.maps.Marker({
position: latlng,
map: map,
title:"Jesteś tutaj"
});
{% for shop in shops %}
latlng = new google.maps.LatLng("{{ shop.location.y }}", "{{ shop.location.x }}");
new google.maps.Marker({
position: latlng,
map: map,
title:"{{ shop.name }}"
});
{% endfor %}
});
</script>
<div class="map" style="width: 400px; height: 400px;"></div>

Sklepy i adres użytkownika zaznaczone na mapie
Comment article