Skip to content

Request for comments #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ Installation
pip install django-easy-maps

Then add 'easy_maps' to INSTALLED_APPS and run ``./manage.py syncdb``
(or ``./manage.py migrate easy_maps`` if South is in use)
(or ``./manage.py migrate easy_maps`` if South is in use). Since there are
some media files needed to be used, you have to collect the static files
distributed with this application (using ``./manage collectstatic``).

Settings
========
Expand All @@ -27,6 +29,13 @@ then create a EASY_MAPS_GOOGLE_KEY in your settings.py file::

EASY_MAPS_GOOGLE_KEY = "your-google-maps-api-key"

If you need a place where center the map when no address is inserted yet add the
latitudine and longitude to the EASY_MAPS_CENTER_* variables in your settings.py
like the following::

EASY_MAPS_CENTER_LAT = -41.3
EASY_MAPS_CENTER_LON = 15.2

Usage
=====

Expand Down
55 changes: 49 additions & 6 deletions easy_maps/admin.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,59 @@
from django.contrib import admin
from django import forms
from django.conf.urls.defaults import patterns, url
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt

from .models import Address
from .widgets import AddressWithMapWidget
from .geo import geolocalize

import simplejson

class AddressAdminForm(forms.ModelForm):
class Meta:
widgets = {
'address': AddressWithMapWidget({'class': 'vTextField'})
}

class AddressAdmin(admin.ModelAdmin):
list_display = ['address', 'computed_address', 'latitude', 'longitude', 'geocode_error']
list_filter = ['geocode_error']
search_fields = ['address']
form = AddressAdminForm
class Media:
js = (
'https://maps.google.com/maps/api/js?sensor=false',
'js/easy_maps.js',
)

def get_urls(self):
"""Add a view that serves geolocalized data on POST request
"""
urls = super(AddressAdmin, self).get_urls()
my_urls = patterns('',
url(r'^geo/$', self.admin_site.admin_view(self.get_geo), name='address_json'),
)
return my_urls + urls

class form(forms.ModelForm):
class Meta:
widgets = {
'address': AddressWithMapWidget({'class': 'vTextField'})
# FIXME: add CSRF protection look at https://docs.djangoproject.com/en/1.4/ref/contrib/csrf/#ajax
# for example in passing a CSRF token
@csrf_exempt
def get_geo(self, request):
"""Return a json that will be used to insert correct value
into the model form.
"""
if request.method != "POST" or not request.POST.has_key('address') or request.POST['address'] == '':
return HttpResponseBadRequest()

computed_address, latitude, longitude, geocode_error = geolocalize(request.POST["address"])
return HttpResponse(simplejson.dumps(
{
'computed_address': computed_address,
'latitude': latitude,
'longitude': longitude,
'geocode_error': geocode_error,
}
), content_type='application/json')

class AddressInlineAdmin(admin.StackedInline):
extra = 1
form = AddressAdminForm
20 changes: 20 additions & 0 deletions easy_maps/geo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.conf import settings
from django.utils.encoding import smart_str

from geopy import geocoders

def geolocalize(address):
"""From an address return the values needed to fullify an Address model form
"""
try:
if hasattr(settings, "EASY_MAPS_GOOGLE_KEY") and settings.EASY_MAPS_GOOGLE_KEY:
g = geocoders.Google(settings.EASY_MAPS_GOOGLE_KEY)
else:
g = geocoders.Google(resource='maps')
s_address = smart_str(address)
computed_address, (latitude, longitude,) = g.geocode(s_address, exactly_one=False)[0]
geocode_error = False
except (UnboundLocalError, ValueError,geocoders.google.GQueryError):
geocode_error = True

return computed_address, latitude, longitude, geocode_error
35 changes: 20 additions & 15 deletions easy_maps/models.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
from django.conf import settings
from django.db import models
from django.utils.encoding import smart_str
from geopy import geocoders

from .geo import geolocalize
from . import settings


class Address(models.Model):
address = models.CharField(max_length=255, db_index=True)
computed_address = models.CharField(max_length=255, null=True, blank=True)
latitude = models.FloatField(null=True, blank=True)
longitude = models.FloatField(null=True, blank=True)
latitude = models.FloatField(default=settings.EASY_MAPS_CENTER_LAT, null=True, blank=True)
longitude = models.FloatField(default=settings.EASY_MAPS_CENTER_LON, null=True, blank=True)
geocode_error = models.BooleanField(default=False)

def fill_geocode_data(self):
if not self.address:
self.geocode_error = True
return
try:
if hasattr(settings, "EASY_MAPS_GOOGLE_KEY") and settings.EASY_MAPS_GOOGLE_KEY:
g = geocoders.Google(settings.EASY_MAPS_GOOGLE_KEY)
else:
g = geocoders.Google(resource='maps')
address = smart_str(self.address)
self.computed_address, (self.latitude, self.longitude,) = g.geocode(address, exactly_one=False)[0]
self.geocode_error = False
except (UnboundLocalError, ValueError,geocoders.google.GQueryError):
self.geocode_error = True

self.computed_address, self.latitude, self.longitude, self.geocode_error = geolocalize(self.address)

def save(self, *args, **kwargs):
# fill geocode data if it is unknown
Expand All @@ -38,3 +31,15 @@ class Meta:
verbose_name = "EasyMaps Address"
verbose_name_plural = "Address Geocoding Cache"

def json(self):
"""Returns a JSON representation of the address data to be used
with the javascript in a template.
"""
import simplejson
dic = {
'address': self.address,
'computed_address': self.computed_address,
'latitude': self.latitude,
'longitude': self.longitude,
}
return simplejson.dumps(dic)
4 changes: 4 additions & 0 deletions easy_maps/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.conf import settings

EASY_MAPS_CENTER_LAT = getattr(settings, 'EASY_MAPS_CENTER_LAT', -34.397)
EASY_MAPS_CENTER_LON = getattr(settings, 'EASY_MAPS_CENTER_LON', 150.644)
59 changes: 59 additions & 0 deletions easy_maps/static/js/easy_maps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// will contain the boundary of the map.
var g_lat_long_bound = new google.maps.LatLngBounds();

function easy_maps_set_form_value(id_prefix) {
return function (computed_address, lat, lng, error) {
document.getElementById(id_prefix + 'computed_address').value = computed_address;
document.getElementById(id_prefix + 'latitude').value = lat
document.getElementById(id_prefix + 'longitude').value = lng;
document.getElementById(id_prefix + 'geocode_error').value = error;
};
}

function easy_maps_bind_button (id_prefix) {
django.jQuery.post(
// FIXME: this is hardcoded
'/admin/easy_maps/address/geo/', {
//'{% url admin:address_json %}', {
'address': document.getElementById(id_prefix + 'address').value
},
function(data) {
easy_maps_set_form_value(id_prefix)(
data["computed_address"],
data["latitude"],
data["longitude"],
data["geocode_error"]
);
var center = new google.maps.LatLng(data["latitude"], data["longitude"]);
marker.setPosition(center);
map.setCenter(center);
}
);

return false;
}

function easy_maps_add_listener(id_prefix, marker) {
// update the coordinate on marker dragging
google.maps.event.addListener(marker, 'dragend', function(evt) {
var ll = marker.getPosition();
// FIXME: fix id names
document.getElementById(id_prefix + 'latitude').value = ll.lat();
document.getElementById(id_prefix + 'longitude').value = ll.lng();
});
}

function easy_maps_add_marker(map, marker) {
var latlng = new google.maps.LatLng(marker.latitude, marker.longitude);
var marker = new google.maps.Marker({
position: latlng,
map: map,
draggable: true,
title: marker.address
});

// add marker's coordinate to the boundary
g_lat_long_bound.extend(latlng);

return marker;
}
6 changes: 6 additions & 0 deletions easy_maps/templates/admin/easy_maps/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% extends "admin/change_list.html" %}
{% load easy_maps_tags %}
{% block result_list %}
{{block.super}}
{% easy_map cl.query_set 900 700 %}
{% endblock %}
47 changes: 25 additions & 22 deletions easy_maps/templates/easy_maps/map.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
{% with map.latitude|stringformat:"f" as lat %}
{% with map.longitude|stringformat:"f" as long %}

{% block api_js %}
<!-- Google Maps API javascript -->
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
{% endblock %}
{% load easy_maps_tags %}
{% with latitude|stringformat:"f" as lat %}
{% with longitude|stringformat:"f" as long %}

{% block html %}
<!-- HTML map container -->
<div id="map-canvas-{{ map.pk }}"
{% if width and map.latitude and not map.geocode_error %}
style="width: {{ width }}px; height: {{ height }}px;"
{% endif %}
<div id="map-canvas-{{ id }}"
style="width: {{ width }}px; height: {{ height }}px;"
class="easy-map-googlemap">
{% block noscript %}
<noscript>
<img alt="Map of {{ map.address }}" src="https://maps.google.com/maps/api/staticmap?center={{ lat }},{{ long }}&zoom={{ zoom }}&markers={{ lat }},{{ long }}&size={{ width }}x{{ height }}&sensor=false">
<img alt="Map of {{ address }}" src="https://maps.google.com/maps/api/staticmap?center={{ lat }},{{ long }}&zoom={{ zoom }}&markers={{ lat }},{{ long }}&size={{ width }}x{{ height }}&sensor=false">
</noscript>
{% endblock noscript %}
</div>
Expand All @@ -24,10 +18,9 @@
{% block map_js %}
<!-- Map creation script -->
<script type="text/javascript">
function initialize_map_{{ map.pk }}() {
function initialize_map_{{ id_safe }}() {
var latlng = new google.maps.LatLng({{ lat }}, {{ long }});
var mapElem = document.getElementById("map-canvas-{{ map.pk }}");

var mapElem = document.getElementById("map-canvas-{{ id }}");
{% block map_options_js %}
var mapOptions = {
zoom: {{ zoom }},
Expand All @@ -39,20 +32,30 @@
var map = new google.maps.Map(mapElem, mapOptions);

{% block extra_js %}
var marker = new google.maps.Marker({
position: latlng,
map: map,
title: "{{ map.address }}"
});
{% if markers %}
{% for marker in markers %}
var marker = easy_maps_add_marker(map, {{marker.json|safe}});
{% endfor %}

{% comment %}use the zoom level passed as argument if there is only one marker{% endcomment %}
{% if markers|length > 1 %}
// display all the markers
// http://blog.shamess.info/2009/09/29/zoom-to-fit-all-markers-on-google-maps-api-v3/
map.fitBounds(g_lat_long_bound);
{% else %}
easy_maps_add_listener("{{ id }}", marker);
{% endif %}
{% endif %}

{% endblock %}
}

{% block map_loading_js %}
// initialize the map after page loading
google.maps.event.addDomListener(window, 'load', initialize_map_{{ map.pk }});
google.maps.event.addDomListener(window, 'load', initialize_map_{{ id_safe }});
{% endblock %}
</script>
{% endblock %}

{% endwith %}
{% endwith %}
{% endwith %}
30 changes: 25 additions & 5 deletions easy_maps/templatetags/easy_maps_tags.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
#coding: utf-8
from django import template
from django.template.loader import render_to_string
from easy_maps.models import Address
from ..models import Address
from .. import settings

register = template.Library()

@register.tag
def easy_map(parser, token):
"""
The syntax:

{% easy_map <address> [<width> <height>] [<zoom>] [using <template_name>] %}

or

{% easy_map <addresses> [<width> <height>] [<zoom>] [using <template_name>] %}

where in the second case you pass a queryset containing the addresses to be
visualized.
"""
width, height, zoom, template_name = None, None, None, None
params = token.split_contents()
Expand Down Expand Up @@ -42,16 +52,26 @@ def __init__(self, address, width, height, zoom, template_name):

def render(self, context):
try:
address = self.address.resolve(context)
address = self.address.resolve(context) or ''
template_name = self.template_name.resolve(context)

map, _ = Address.objects.get_or_create(address=address or '')
if isinstance(address, basestring):
# if not exists the searched address then created an unsaved instance
try:
address = Address.objects.get(address=address)
except:
address = Address(address=address)

address = [address,]

context.update({
'map': map,
'markers': address,
'width': self.width,
'height': self.height,
# FIXME: if the list is empty?
'latitude': address[0].latitude,
'longitude': address[0].longitude,
'zoom': self.zoom,
'template_name': template_name
})
return render_to_string(template_name, context_instance=context)
except template.VariableDoesNotExist:
Expand Down
Loading