Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added initial geotagging application

  • Loading branch information...
commit 0e54663440923a9c2891d41413e52fe5461a5b4d 1 parent bca7377
@nicolaslara nicolaslara authored
View
4 .gitignore
@@ -0,0 +1,4 @@
+*.pyc
+*.swp
+local_settings.py
+*.egg-info
View
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) Lincoln Loop, LLC and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Lincoln Loop nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
View
70 README.rst
@@ -0,0 +1,70 @@
+This app should eventually be stripped off from the project and be
+packaged as a reusable app.
+
+==============
+ Installation
+==============
+
+TODO: complete this after packaging
+pip install.. etc, etc...
+
+ * add `geottagging` to settings.INSTALLED_APPS
+
+
+=======
+ Usage
+=======
+
+Models
+======
+
+To add geotagging to a model make it inherit from the GeoTag model
+that you need to use. Currently the following GeoTagging types are
+available::
+
+ PointGeoTag
+
+Example::
+
+ from geottaging.models import PointGeoTag
+
+ class Attraction(PointGeoTag):
+ name = models.CharField(max_length=100)
+
+The model should now have a field called `geotagging_point` that
+stores the point information. Also, a new manager is added to the
+model under `Model.geo_objects` so the geographical queries can be
+made.
+
+Example::
+
+ >>> Attraction.objects.all()
+ [<Attraction>, ...]
+ >>> Attraction.geo_objects.all()
+ [<Attraction>, ...]
+ >>> Attraction.objects.filter(geotagging_point__intersects=an_area)
+ ... error
+ >>> Attraction.geo_objects.filter(geotagging_point__intersects=an_area)
+ [<Attraction>, ...]
+
+Admin
+=====
+
+To add the geotagging to the admin, just use the admin provided at::
+
+ geotagging.admin.GeoTaggedModelAdmin
+
+example::
+
+ from istorbyen.apps.geotagging import admin
+ from istorbyen.apps.attractions.models import Attraction
+
+ admin.site.register(Attraction, admin.GeoTaggedModelAdmin)
+
+
+
+======
+ ToDo
+======
+
+ * add tests
View
0  README → geotagging/__init__.py
File renamed without changes
View
4 geotagging/admin.py
@@ -0,0 +1,4 @@
+from django.contrib.gis.admin import *
+
+class GeoTaggedModelAdmin(OSMGeoAdmin):
+ pass
View
16 geotagging/json_utils.py
@@ -0,0 +1,16 @@
+from django.core.serializers import json, serialize
+from django.db.models.query import QuerySet
+from django.db.models.base import ModelBase
+from django.http import HttpResponse
+from django.utils import simplejson
+
+class JsonResponse(HttpResponse):
+ def __init__(self, object):
+ if isinstance(object, QuerySet):
+ content = serialize('json', object)
+ else:
+ content = simplejson.dumps(
+ object, indent=2, cls=json.DjangoJSONEncoder,
+ ensure_ascii=False)
+ super(JsonResponse, self).__init__(
+ content, content_type='application/json')
View
52 geotagging/models.py
@@ -0,0 +1,52 @@
+"""
+This app should eventually be stripped off from the project and be
+packaged as a reusable app.
+"""
+
+from django.contrib.gis.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.db import connection
+
+HAS_GEOGRAPHY = False
+try:
+ # You need Django 1.2 and PostGIS > 1.5
+ # http://code.djangoproject.com/wiki/GeoDjango1.2#PostGISGeographySupport
+ if connection.ops.geography:
+ HAS_GEOGRAPHY = True
+except AttributeError:
+ pass
+
+def field_kwargs(verbose_name):
+ """
+ Build kwargs for field based on the availability of geography fields
+ """
+ kwargs = {
+ 'blank': True,
+ 'null': True,
+ 'verbose_name': _(verbose_name),
+ }
+ if HAS_GEOGRAPHY:
+ kwargs['geography'] = True
+ return kwargs
+
+class GeoTag(models.Model):
+ objects = models.Manager()
+ geo_objects = models.GeoManager()
+
+ class Meta:
+ abstract = True
+
+class PointGeoTag(GeoTag):
+ geotagging_point = models.PointField(**field_kwargs('point'))
+
+ def get_point_coordinates(self, as_string=False, inverted=False):
+ if inverted:
+ xy = (self.geotagging_point.y, self.geotagging_point.x)
+ else:
+ xy = (self.geotagging_point.x, self.geotagging_point.y)
+ if as_string:
+ return '%s,%s' % xy
+ return xy
+
+ class Meta:
+ abstract = True
View
0  geotagging/templatetags/__init__.py
No changes.
View
29 geotagging/templatetags/geotagging_maps.py
@@ -0,0 +1,29 @@
+from classytags.core import Options
+from classytags.arguments import Argument
+from classytags.helpers import InclusionTag
+from django import template
+
+register = template.Library()
+
+class Javascript(InclusionTag):
+ name = 'include_maps_js'
+ template = 'geotagging/map_scripts.html'
+
+register.tag(Javascript)
+
+"""
+Notes:
+
+Static:
+ {% include_map_js %} - Adds the necessary javascript for the maps to load
+ {% map_objects queryset %} - Creates a map showing all objects in the queryset
+Interactive:
+ {% map_objects Model %} - Creates a map showing all Model objects within the map section
+ {% map_objects Model filter %} - Same as above, but adds filtering based on a function
+
+Queue:
+ ???
+ {% include_queue_js element_id %} - Inncludes the javascript for the element queue
+ {% show_queue %}
+
+"""
View
54 geotagging/tests.py
@@ -0,0 +1,54 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+
+"""
+Info for the tests:
+
+ lund = '55.705, 13.200'
+ lomma = '55.671, 13.063'
+ malmo = '55.603, 12.986'
+ kastrup = '55.634, 12.640'
+ kopenhamn = '55.6931, 12.548'
+
+ waypoints = [lund, kastrup, lomma, malmo, kopenhamn]
+
+ response = google_TSP(waypoints)
+
+should work
+
+
+ lund = '55.705, 13.200'
+ lomma = '55.671, 13.063'
+ malmo = '55.603, 12.986'
+ kastrup = '55.634, 12.640'
+ kopenhamn = '55.6931, 12.548'
+
+ waypoints = [lund, kastrup, lomma, malmo, kopenhamn, malmo, kopenhamn, kastrup, lomma, malmo, kopenhamn]
+
+ response = google_TSP(waypoints)
+
+max waypoints exeded
+"""
+
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
View
7 geotagging/urls.py
@@ -0,0 +1,7 @@
+from django.conf.urls.defaults import *
+
+import views
+
+urlpatterns = patterns('',
+ ('^optimize/$', views.optimize),
+)
View
82 geotagging/utils.py
@@ -0,0 +1,82 @@
+import json
+import httplib, urllib
+import itertools
+
+from django.db import models
+
+class GoogleApiException(Exception):
+ error_map = {'NOT_FOUND': "indicates at least one of the locations specified in the requests's origin, destination, or waypoints could not be geocoded.",
+ 'ZERO_RESULTS': "indicates no route could be found between the origin and destination.",
+ 'MAX_WAYPOINTS_EXCEEDED': "indicates that too many waypointss were provided in the request The maximum allowed waypoints is 8, plus the origin, and destination. ( Google Maps Premier customers may contain requests with up to 23 waypoints.)",
+ 'INVALID_REQUEST': "indicates that the provided request was invalid.",
+ 'OVER_QUERY_LIMIT': "indicates the service has received too many requests from your application within the allowed time period.",
+ 'REQUEST_DENIED': "indicates that the service denied use of the directions service by your application.",
+ 'UNKNOWN_ERROR': "indicates a directions request could not be processed due to a server error. The request may succeed if you try again."
+ }
+ def __init__(self, response):
+ self.response = response
+ def __str__(self):
+ status = self.response.get('status', None)
+ detail = self.error_map.get(status, False)
+ if detail:
+ return '%s. %s' % (status, detail)
+ return repr(self.response)
+
+def google_TSP_call(waypoints):
+ """
+ This is the function that does the calling. It expects the
+ number of waypoints to always be within the allowed range.
+ """
+ origin = w_origin = waypoints.pop(0)
+ destination = w_destination = waypoints.pop()
+ coordinates = waypoints
+
+ if isinstance(waypoints[0].__class__, models.base.ModelBase):
+ coordinates = [i.get_point_coordinates(as_string=True, inverted=True)
+ for i in waypoints]
+
+ origin = w_origin.get_point_coordinates(as_string=True, inverted=True)
+ destination = w_destination.get_point_coordinates(as_string=True, inverted=True)
+
+ conn = httplib.HTTPConnection("maps.googleapis.com")
+ params = urllib.urlencode({'origin':origin, 'destination':destination,
+ 'waypoints':'|'.join(['optimize:true']+coordinates),
+ 'sensor':'false'})
+ conn.request("GET", '/maps/api/directions/json?%s' % params)
+ response = conn.getresponse()
+
+ routes = json.loads(response.read())
+
+ if routes['status'] != 'OK':
+ raise GoogleApiException(routes)
+
+ sorted_points = ([w_origin] +
+ [waypoints[i] for i in routes['routes'][0]['waypoint_order']] +
+ [w_destination])
+
+ return sorted_points
+
+def split_num(k, l):
+ nk=k
+ while l%nk<2:
+ nk-=1
+ return nk
+
+def google_TSP(waypoints=[], max_waypoints=8):
+ """
+ Queries google for a the optimal order in which the points should
+ be visited.
+ """
+ n = len(waypoints)
+ if n>max_waypoints+2:
+ #use some sort of heuristic to order and split
+ #for now the heuristic is: no heuristic
+ k = split_num(max_waypoints+2, n)
+ waypoint_iter = itertools.izip_longest(*[iter(waypoints)]*k)
+ else:
+ waypoint_iter = [waypoints]
+
+ return list(itertools.chain.from_iterable(
+ [google_TSP_call([i for i in waypoints if i])
+ for waypoints in waypoint_iter]))
+
View
59 geotagging/views.py
@@ -0,0 +1,59 @@
+from django.db.models.loading import get_model
+
+from utils import google_TSP
+from json_utils import JsonResponse
+
+"""
+Notes:
+
+We need views for:
+ * Getting display information about a geotagged point.
+ * Sorting (TSP)
+ * Save queue
+"""
+
+def optimize(request):
+ """
+ Parameters:
+ It could be either:
+ * model: a string representing which geotagged model is being
+ queried. i.e.: 'attractions.Attraction'
+ * ids: a list of comma-separated ids of the waypoints to optimize
+
+ or:
+ * locations: a list of |-separated locations to be geocoded and
+ optimized. You can also use LatLong pairs.
+ """
+ model = request.GET.get('model', None)
+ if model:
+ klass = get_model(*model.split('.'))
+ ids = request.GET.get('ids', '').split(',')
+
+ if len(ids) < 3:
+ return JsonResponse({
+ 'success': False,
+ 'message': 'At least three objects are needed for optimization'})
+
+ #This is not explicitly taking initial and last point into account
+ waypoints = klass.objects.filter(pk__in=ids)
+
+ #if we want to take repeated objects into account:
+ #object_dict = dict((i.pk, i) for i in klass.objects.filter(pk__in=ids))
+ #waypoints = [object_dict[int(i)] for i in ids]
+ else:
+ waypoints = request.GET.get('locations', None)
+ if not waypoints:
+ return JsonResponse({'success':False, 'message':'Locations not provided'})
+
+ waypoints = waypoints.split('|')
+
+ if len(waypoints) < 3:
+ return JsonResponse({
+ 'success': False,
+ 'message': 'At least three objects are needed for optimization'})
+
+
+ response = google_TSP(waypoints)
+ json_response = map(repr, response)
+
+ return JsonResponse({'success':True, 'optimal_order':json_response})
View
22 setup.py
@@ -0,0 +1,22 @@
+from setuptools import setup, find_packages
+
+setup(
+ name='django-geotagging-new',
+ version='0.0.2',
+ description='This is a geotagging application. It can be used to localize your content.',
+ author='Nicolas Lara',
+ author_email='nicolaslara@gmail.com',
+ url='https://github.com/lincolnloop/django-geotagging-new',
+ packages=find_packages(),
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Framework :: Django',
+ ],
+ include_package_data=True,
+ zip_safe=False,
+)
Please sign in to comment.
Something went wrong with that request. Please try again.