如何使用django创建一个基于地理位置的应用?


之前有个网站叫brightkite,是个基于地理位置的服务,用户可以checkin一个地方。请问使用django该如何实现?

django 技术 地理位置

魔法G达拉然 10 years ago

As we know Brightkite is a location based social network where users can checkin into a location and make posts. I always had a thought on how we can acheive this functionality using django framework. Last week I started off this project with an eye on contributing it to Pinax project. I talked to Jtauber and I was lucky enough to get a chance before the feature freeze of pinax.

To start this project we need to recollect the basic requirements.

  1. Registration for users. This can be acheived by django registration
  2. Search for a location and get its proper place name, latitude and longitude data
  3. Ability to checkin to that location. In simple words store the userid, place name, latitude, longitude and datetime of the checkin.

Implementation:

I am not going to start a brand new django project and build it upon it. Instead I recommend you to check out pinax project source code. Or you start a new project and add django-registration app to INSTALLED_APPS and set the urls. There are a bunch of tutorials on web on how to do that. Now we can create a new application called "locations".

$ python manage.py startapp locations

You now have the basic skeleton of the location application. Lets start creating the model "Location". As discussed in the basic requirements above,

from django.db import models
from django.contrib.auth.models import User

class Location(models.Model):
    user = models.ForeignKey(User)
    time_checkin = models.DateTimeField()
    place = models.CharField(max_length=100)
    latitude = models.FloatField()
    longitude = models.FloatField()

    class Meta:
        ordering            = ('-time_checkin',)
        get_latest_by       = 'time_checkin'

The model is pretty self explanatory. It has a foriegn key to User, so that each user can have his own checkins. After the model is created our objective is to search for a location and get the geo data for that location. For this we will take help of GeoPy a geocoding toolbox for python. We can query web services like Yahoo, Google, Geocoders to get geo data for a location.

We create an entry in urls.py for this lcoations search.

urlpatterns = patterns('',  
    (r'new/$', 'locations.views.new'),
)

To create a form, just create a file forms.py in locations and

class LocationForm(forms.Form):
    place = forms.CharField()

This means when somebody tries to access the url '/new/' the request is routed to the view 'new'. Lets create a definition for the 'new'. Open view.py

@login_required
def new(request):
    if request.method == 'POST':
         location_form = LocationForm(request.POST)
         if location_form.is_valid():
               y = geocoders.Yahoo('yahoo_map_api')
               p = location_form.cleaned_data['place']
               place, (lat, lng) = y.geocode(p)
               location = {'place': place, 'latitude': lat, 'longitude': lng}
               checkin_form = CheckinForm()
               return render_to_response("locations/checkin.html", {"location": location, "checkin_form": checkin_form})
    else:
         location_form = LocationForm()
         return render_to_response("locations/new.html", {"location_form": location_form})

First of all I am protecting this view, so that only logged in users(@loginrequired) will be able to search locations. Next if the request of the type POST only the loop gets executed. If some tries to access with a GET/anything the else part will be executed there by rendering an empty form again. I'm extracting the data from request.POST and putting in locationform variable.

Next we import geocoders from geopy and then instantiate its Yahoo class. We can use Google or Geocoders as well, but I chose Yahoo. Pass your YAHOOAPIKEY as argument to Yahoo class. I am extracting the form data into a variable 'p' and then performing a geocode using the yahoo object. A request is made to Yahoo Geocoding service and geo data is returned. I created 3 variables place, lat & lng which contains place name, latitude & longitude. I pass all these variables into a dictionary and pass them to a template(checkin.html). In this process I am also creating a blank checkin_form which is also passed to the tempalte. I will discuss those details in the next part. You can check out the checkin.html which is pretty simple. It contains just a form.

The checkin.html template displays results of the search performed. We can use simple map embed methods to show the location on a map. I used Yahoo maps in this case. Here is the checkin.html

<script type="text/javascript" src="http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=enter_your_yahoo_map_id"></script>
<body onload="initialize_ymap()">
 <div id="ymap"></div><h2>{{ location.place }}</h2>
<script type="text/javascript">
function initialize_ymap()
{
   var map = new YMap(document.getElementById('ymap'));
   var yPoint = new YGeoPoint({{ location.latitude }},
                         {{ location.longitude }});
   map.drawZoomAndCenter(yPoint, 15);
   var myMarker = new YMarker(yPoint);
   var myMarkerContent = "This is {{location.place}}";
   YEvent.Capture(myMarker, EventsList.MouseClick,
      function() {  
          myMarker.openSmartWindow(myMarkerContent);
       }
   map.addOverlay(myMarker);
 }
 </script></body>

So when you search for a location you can see that location on a map and proper name of the location. This data is returned by yahoo geo data search. Now our next objective would be to to make a user checkin in to that location. So how can we do that? Pretty simple all we need to do is add a checkin button to the page which would send all the location data and logged in user to a view. Hmm sounds pretty simple, but how would you send the data to a view?

  1. Through the url as arguments to a view.
  2. Through a form

The 2nd option sounds great. But adding a form and making user fill in the data sounds stupid. So we need to populate the form with data while rendering the form. How about make the form invisible too? Yay!! that even sounds better. Ok a quick search on Django hidden forms showed up a djangosnippet. I could have used a widget with hidden input field. But I chose to use this method. Now lets open the forms.py and create a checkin form which inherits the 'HiddenBaseForm'.

class HiddenBaseForm(forms.BaseForm):
    def as_hidden(self):
        output = []
        for name, field in self.fields.items():
            bf = BoundField(self, field, name)
            output.append(bf.as_hidden())
        return u'
'.join(output)

class CheckinForm(HiddenBaseForm, forms.Form):
    place = forms.CharField()
    latitude = forms.FloatField()
    longitude = forms.FloatField()

Now add the following snippet to the checkin.html file. This form is going to be invisible to the user except for the 'Checkin' button. When a user clicks checkin the form data is sent to a view. In this case it is 'checkin' method. Observe the url in form action, it decides which view is going to handle this.

<form action="/locations/checkin/" method="POST">
<input type="hidden" name="place" value="{{ location.place }}" id="id_place" /> 
<input type="hidden" name="latitude" value="{{ location.latitude }}"  id="id_latitude" /> 
<input type="hidden" name="longitude" value="{{ location.longitude }}" id="id_longitude" />
<p><input type="submit" value="Checkin &rarr;"></p></form>

Ok now open up the urls.py and make an entry for '/checkin/' url. Route this url to the view 'checkin'.

(r'checkin/$', 'locations.views.checkin'),

Now fire up the views.py and create the checkin view.

@login_required
def checkin(request):
    if request.method == 'POST':
        checkin_form = CheckinForm(request.POST)
        if checkin_form.is_valid():
              c = Location(place=checkin_form.cleaned_data['place'], 
                  latitude=checkin_form.cleaned_data['latitude'],
                  longitude=checkin_form.cleaned_data['longitude'], 
                  user=request.user, time_checkin= datetime.datetime.now())
              c.save()
              return HttpResponseRedirect(reverse('locations.views.your_locations'))
    else:
         return HttpResponseRedirect(reverse('locations.views.new'))

The view is pretty self-explanatory. I am protecting this view to logged in users. I am extracting the data from request.POST and then checking if the form.is_valid(). If it is valid I am creating an object which is an instance of the Location model. Then I populate the instance with userid, location name, latitude, longitude and current datatime. I save the object and redirect the user to show all the checkins of that user.

Now create a view 'your_locations' which shows all the checkins of the user.

def your_locations(request):
    user = request.user
    locations = Location.objects.filter(user=user)
    return render_to_response("locations/your_locations.html", {"locations": locations},  
      context_instance=RequestContext(request))

I am filtering all the location objects by the logged in user and then passing all the location objects of that user to the template your_locations.html. Do create an entry in urls.py to serve this view.

(r'^$', 'locations.views.your_locations'),

Now the your_locations.html will iterate over the set of locations and print the attributes. If you want to display the current checkin of the user. You can create if loop in the parent for loop and check if it is the first iteration of the for loop.

{{ "{% for location in locations "}}%}
{{ "{% if forloop.first "}}%}
     // Put all the Yahoo map javascript code here with 
     // {{ location.latitude }} and {{ location.longitude }}

I guess that's it you now have a working skeleton of brightkite. You can enhance this app and submit it to me.

gunwlwl answered 10 years ago

Your Answer