diff options
author | Étienne Loks <etienne.loks@peacefrogs.net> | 2011-09-09 01:26:34 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@peacefrogs.net> | 2011-09-09 01:26:34 +0200 |
commit | 0ccc30e337bb046000fb76d2337c2fb9792bc71f (patch) | |
tree | bbf2307438389f91fdae1e618d0816238b0a015a | |
parent | 5833aa61d039bf3d1f4372e0390ffc53b7d63eb9 (diff) | |
download | Chimère-0ccc30e337bb046000fb76d2337c2fb9792bc71f.tar.bz2 Chimère-0ccc30e337bb046000fb76d2337c2fb9792bc71f.zip |
Association of a GPX (or KML) file to a route (refs #302)
- new table in the model
- new form in route edition
- uploaded gpx file simplification
- automatic route creation from the GPX file
-rw-r--r-- | chimere/main/forms.py | 25 | ||||
-rw-r--r-- | chimere/main/models.py | 46 | ||||
-rw-r--r-- | chimere/main/views.py | 52 | ||||
-rw-r--r-- | chimere/main/widgets.py | 5 | ||||
-rw-r--r-- | chimere/settings.py.example | 5 | ||||
-rw-r--r-- | chimere/static/base.js | 10 | ||||
-rw-r--r-- | chimere/static/edit_route_map.js | 10 | ||||
-rw-r--r-- | chimere/urls.py | 2 | ||||
-rw-r--r-- | docs/en/INSTALL.t2t | 2 |
9 files changed, 141 insertions, 16 deletions
diff --git a/chimere/main/forms.py b/chimere/main/forms.py index 1ecd348..dc1fbad 100644 --- a/chimere/main/forms.py +++ b/chimere/main/forms.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (C) 2008-2010 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> +# Copyright (C) 2008-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as @@ -106,9 +106,9 @@ class MarkerAdminForm(forms.ModelForm): """ # declare properties for property in PropertyModel.objects.filter(available=True): - exec('property_%d_%d = forms.CharField(label="%s", widget=%s, \ -required=False)' % (property.order, property.id, property.name, - PropertyModel.TYPE_WIDGET[property.type])) + exec('property_%d_%d = forms.CharField(label="%s", widget=%s, '\ + 'required=False)' % (property.order, property.id, property.name, + PropertyModel.TYPE_WIDGET[property.type])) class Meta: model = Marker @@ -219,9 +219,9 @@ class RouteForm(RouteAdminForm): exclude = ('status',) # marker properties for property in PropertyModel.objects.filter(available=True): - exec('property_%d_%d = forms.CharField(label="%s", widget=%s, \ -required=False)' % (property.order, property.id, property.name, - PropertyModel.TYPE_WIDGET[property.type])) + exec('property_%d_%d = forms.CharField(label="%s", widget=%s, '\ + 'required=False)' % (property.order, property.id, property.name, + PropertyModel.TYPE_WIDGET[property.type])) def save(self, *args, **keys): """ @@ -246,6 +246,17 @@ required=False)' % (property.order, property.id, property.name, new_marker.saveProperties(properties) return new_route +class FileForm(forms.Form): + raw_file = forms.FileField() + + def clean_raw_file(self): + data = self.cleaned_data['raw_file'] + if '.' not in data.name or \ + data.name.split('.')[-1].lower() not in ('kml', 'gpx'): + raise forms.ValidationError(_(u"Bad file format: this must be a "\ + u"GPX or KML file")) + return data + class AreaAdminForm(forms.ModelForm): """ Admin page to create an area diff --git a/chimere/main/models.py b/chimere/main/models.py index 3897fe9..5ab45f6 100644 --- a/chimere/main/models.py +++ b/chimere/main/models.py @@ -20,7 +20,12 @@ """ Models description """ +import os, string +import lxml.etree as ElementTree from datetime import datetime, timedelta +from subprocess import Popen, PIPE + +from django.core.files import File from django.utils.translation import ugettext_lazy as _ @@ -317,6 +322,46 @@ class RouteFile(models.Model): def __unicode__(self): return self.name + def process(self): + if self.simplified_file: + return + input_name = settings.MEDIA_ROOT + self.raw_file.name + output_name = settings.MEDIA_ROOT + self.raw_file.name[:-4] + \ + "_simplified" + ".gpx" + cli_args = [settings.GPSBABEL, '-i'] + if self.file_type == 'K': + cli_args.append('kml') + elif self.file_type == 'G': + cli_args.append('gpx') + cli_args += ['-f', input_name, '-x', settings.GPSBABEL_OPTIONS, + '-o', 'gpx', '-F', output_name] + p = Popen(cli_args, stderr=PIPE) + p.wait() + if p.returncode: + print p.stderr.read() + #logger.error(p.stderr.read()) + else: + self.simplified_file = File(open(output_name)) + self.save() + os.remove(output_name) + + @property + def route(self): + if not self.simplified_file: + return + mainNS = string.Template("{http://www.topografix.com/GPX/1/0}$tag") + trkpt = mainNS.substitute(tag="trkpt") + file_name = settings.MEDIA_ROOT + self.simplified_file.name + et = ElementTree.parse(open(file_name)) + pts = [] + for pt in et.findall("//" + trkpt): + pts.append((pt.get("lon"), pt.get("lat"))) + geojson_tpl = u'{"type":"Feature", "geometry":{ "type": "LineString", '\ + '"coordinates":[%s]}}' + wkt_tpl = u'LINESTRING(%s)' + return wkt_tpl % u','.join([u'%s %s' % (pt[0], pt[1]) \ + for pt in pts]) + class Route(models.Model): '''Route on the map ''' @@ -385,6 +430,7 @@ class Route(models.Model): return u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": "%(name)s", '\ u'"color":"%(color)s"}}' % attributes + def getTinyUrl(self): parameters = 'current_feature=%d&checked_categories=%s' % (self.id, self.categories[0].id) diff --git a/chimere/main/views.py b/chimere/main/views.py index 2f80ea8..812fff6 100644 --- a/chimere/main/views.py +++ b/chimere/main/views.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (C) 2008-2010 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> +# Copyright (C) 2008-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as @@ -31,16 +31,17 @@ from django.http import HttpResponseRedirect, HttpResponse from django.core import serializers from django.utils.http import urlquote from django.db.models import Q +from django.utils import simplejson from chimere import settings from chimere.main.actions import actions -from chimere.main.models import Category, SubCategory, PropertyModel, Marker, \ - Route, News, SimpleArea, Area, Color, TinyUrl +from chimere.main.models import Category, SubCategory, PropertyModel, \ + Marker, Route, News, SimpleArea, Area, Color, TinyUrl, RouteFile from chimere.main.widgets import getMapJS, PointChooserWidget, \ RouteChooserWidget, URL_OSM_JS, URL_OSM_CSS from chimere.main.forms import MarkerForm, RouteForm, ContactForm, \ - notifySubmission, notifyStaff + FileForm, notifySubmission, notifyStaff def get_base_response(area_name=""): """ @@ -58,6 +59,7 @@ def get_base_response(area_name=""): if settings.CSS_AREAS and area_name: base_response_dct['css_area'] = area_name + ".css" base_response_dct['area_name'] = area_name + base_response_dct['JQUERY_URL'] = settings.JQUERY_URL return base_response_dct def index(request, area_name=None, default_area=None, simple=False): @@ -124,7 +126,7 @@ def edit(request, area_name=""): notifySubmission(marker) response_dct = get_base_response(area_name) return HttpResponseRedirect(response_dct['extra_url'] + \ -'submited/edit') + 'submited/edit') else: # An unbound form form = MarkerForm() @@ -149,6 +151,44 @@ def edit(request, area_name=""): response_dct['current_category'] = int(form.data['subcategory']) return render_to_response('edit.html', response_dct) +def uploadFile(request, area_name=''): + response_dct = get_base_response(area_name) + # If the form has been submited + if request.method == 'POST': + form = FileForm(request.POST, request.FILES) + # All validation rules pass + if form.is_valid(): + raw_file = form.cleaned_data['raw_file'] + name = raw_file.name.split('.')[0] + file_type = raw_file.name.split('.')[-1][0].upper() + routefile = RouteFile(raw_file=raw_file, name=name, + file_type=file_type) + routefile.save() + response_dct = get_base_response(area_name) + response_dct['gpx_id'] = routefile.pk + return render_to_response('uploadFile.html', response_dct) + else: + # An unbound form + form = FileForm() + response_dct.update({'form':form}) + return render_to_response('uploadFile.html', response_dct) + +def processRouteFile(request, area_name='', file_id=None): + if file_id: + try: + route_file = RouteFile.objects.get(pk=file_id) + route_file.process() + route = route_file.route + if not route: + return HttpResponse(status=500) + return HttpResponse(simplejson.dumps({'wkt':route}), + 'application/javascript', status=200) + except: + return HttpResponse(status=500) + else: + return HttpResponse(status=400) + + def editRoute(request, area_name=""): """ Route edition page @@ -165,7 +205,7 @@ def editRoute(request, area_name=""): notifySubmission(route) response_dct = get_base_response(area_name) return HttpResponseRedirect(response_dct['extra_url'] + \ -'submited/edit') + 'submited/edit') else: # An unbound form form = RouteForm() diff --git a/chimere/main/widgets.py b/chimere/main/widgets.py index 06c75c6..caac0ea 100644 --- a/chimere/main/widgets.py +++ b/chimere/main/widgets.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> +# Copyright (C) 2008-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as @@ -212,6 +212,9 @@ initFeature(geometry);""" % val.json tpl += """ // --></script> """ + tpl += u'<a href="#" class="add-button" '\ + u'onclick="open_window(\'%supload_file\');">%s</a>' % ( + settings.BASE_URL, _(u"Upload a route file (GPX or KML)")) return mark_safe(tpl) class RouteField(models.LineStringField): diff --git a/chimere/settings.py.example b/chimere/settings.py.example index 60d4c91..5a1d1fe 100644 --- a/chimere/settings.py.example +++ b/chimere/settings.py.example @@ -12,6 +12,11 @@ BASE_URL = SERVER_URL + EXTRA_URL EMAIL_HOST = 'localhost' TINYMCE_URL = SERVER_URL + 'tinymce/' +JQUERY_URL = SERVER_URL + 'jquery/jquery-1.4.4.min.js' +GPSBABEL = '/usr/bin/gpsbabel' +GPSBABEL_OPTIONS = 'simplify,crosstrack,error=0.005k' # simplify with an + # error of 5 meters +#GPSBABEL_OPTIONS = 'simplify,count=100' ## chimere specific ## # center of the map diff --git a/chimere/static/base.js b/chimere/static/base.js index 91ec310..3f9d39d 100644 --- a/chimere/static/base.js +++ b/chimere/static/base.js @@ -1,5 +1,5 @@ /* base function shared by some pages */ -/* Copyright (C) 2009-2010 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> +/* Copyright (C) 2009-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as @@ -52,6 +52,14 @@ function showHide(id){ } } +/* open a popup window */ +function open_window(url){ + var newwindow = window.open(url, '_blank', + 'height=170,width=400,scrollbars=yes'); + if (window.focus) {newwindow.focus()} + return false; +} + function saveExtent() { /* save the current extent in a cookie */ diff --git a/chimere/static/edit_route_map.js b/chimere/static/edit_route_map.js index 02d3bd9..cb6f148 100644 --- a/chimere/static/edit_route_map.js +++ b/chimere/static/edit_route_map.js @@ -55,9 +55,19 @@ function initFeature(json_geometry){ point_array.push(point); } var linestring = new OpenLayers.Geometry.LineString(point_array); + initFeatureFromGeometry(linestring); +} + +function initFeatureFromWkt(wkt_geometry){ + var linestring = OpenLayers.Geometry.fromWKT(wkt_geometry); + initFeatureFromGeometry(linestring); +} + +function initFeatureFromGeometry(linestring){ linestring.transform(epsg_display_projection, map.getProjectionObject()); currentFeature = new OpenLayers.Feature.Vector(); currentFeature.geometry = linestring; + vectors.removeFeatures(); vectors.addFeatures([currentFeature]); currentControl = pathModify; /*zoom to the route*/ diff --git a/chimere/urls.py b/chimere/urls.py index 8740f85..4d62bac 100644 --- a/chimere/urls.py +++ b/chimere/urls.py @@ -67,6 +67,8 @@ urlpatterns += patterns('chimere.main.views', (BASE + EXTRA + r'contact/$', 'contactus', default_dct), (BASE + EXTRA + r'edit/$', 'edit', default_dct), (BASE + EXTRA + r'edit_route/$', 'editRoute', default_dct), +(BASE + EXTRA + r'upload_file/$', 'uploadFile', default_dct), +(BASE + EXTRA + r'process_route_file/(?P<file_id>\d+)/$', 'processRouteFile', default_dct), (BASE + EXTRA + r'submited/(?P<action>\w+)/$', 'submited', default_dct), (BASE + EXTRA + r'getDetail/(?P<marker_id>\d+)/$', 'getDetail', default_dct), (BASE + EXTRA + r'getDescriptionDetail/(?P<category_id>\d+)/$', diff --git a/docs/en/INSTALL.t2t b/docs/en/INSTALL.t2t index c4e35c5..233737c 100644 --- a/docs/en/INSTALL.t2t +++ b/docs/en/INSTALL.t2t @@ -16,7 +16,7 @@ Last update: %%date(%m-%d-%Y) - [psycopg2 http://freshmeat.net/projects/psycopg/] - [Python Imaging Library http://www.pythonware.com/products/pil/] - [Beautiful Soup http://www.crummy.com/software/BeautifulSoup/] - +- [lxml http://lxml.de/] geodjango is a part of django version 1.0 but it has some specific (geographically related) additionnal dependencies: |