summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
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
commit0ccc30e337bb046000fb76d2337c2fb9792bc71f (patch)
treebbf2307438389f91fdae1e618d0816238b0a015a
parent5833aa61d039bf3d1f4372e0390ffc53b7d63eb9 (diff)
downloadChimè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.py25
-rw-r--r--chimere/main/models.py46
-rw-r--r--chimere/main/views.py52
-rw-r--r--chimere/main/widgets.py5
-rw-r--r--chimere/settings.py.example5
-rw-r--r--chimere/static/base.js10
-rw-r--r--chimere/static/edit_route_map.js10
-rw-r--r--chimere/urls.py2
-rw-r--r--docs/en/INSTALL.t2t2
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: