Commits

Bruno Renié committed 0a50798

Added KML and GPX rendering to the template library.

  • Participants
  • Parent commits dcd6dfe

Comments (0)

Files changed (5)

+2010-05-09 Bruno Renil <bruno@renie.fr>
+
+    * Added KML and GPX rendering to the template library
+    * Tests passes with Django 1.2
+
 2010-05-08 Bruno Renié <bruno@renie.fr>
 
+    * Zooming to the correct bounds when features are set to 'invisible'
+    * Fixed the 'as map_var' behaviour in the templates
     * Added transitionEffect: 'resize' to all widgets for better UX
     * Fixed setup.py again
     * Removed hardcoded SRID in the admin widget

docs/source/templates.rst

 This has to be done **before** the ``geoportal_map`` tag is called: the
 javascript library has to be loaded synchronously.
 
-The {% geoportal_map %} template tag
-------------------------------------
+Rendering a map: the {% geoportal_map %} template tag
+-----------------------------------------------------
 
 Syntax:
 
 .. code-block:: jinja
 
-    {% geoportal_map field option1=value, option2=other_value [as var_name] %}
+    {% geoportal_map field [option1=value, option2=other_value] [as var_name] %}
 
 Basic usage
 ```````````
 
 .. image:: img/template-custom.png
    :align: center
+
+Adding external features
+------------------------
+
+In this section, we assume that you have displayed a map on your page using
+the ``geoportal_map`` template tag, and you've added it to your template
+context:
+
+.. code-block:: html+django
+
+    {% geoportal_map my_field as var_name %}
+
+Now, you can render KML and GPX filed with the corresponding template tags.
+
+KML: the {% geoportal_kml %} tag
+````````````````````````````````
+
+Syntax:
+
+.. code-block:: html+django
+
+    {% geoportal_kml map_var kml_url [option1=value1, option2=value2 ...] %}
+
+The two required arguments are:
+
+* ``map_var``: this is the template variable containing the map name.
+
+* ``kml_url``: the URL to the KML file. It can be a template variable or a raw
+  string.
+
+Options are comma-separated, using the key=value syntax described in the
+``geoportal_map`` section. The values can be raw strings or template
+variables. The available options are:
+
+* ``width``: the width of the border of the feature. Default: 2.
+
+* ``opacity``: the opacity of the innner part of the ferature. Default: 0.4 or
+  the value of ``GEOPORTAL_DEFAULT_OPACITY`` if you've overriden it.
+
+* ``color``: the color of the feature. Default: OpenLayers' default, or the
+  value of ``GEOPORTAL_DEFAULT_COLOR`` if you've overriden it.
+
+* ``extract``: a boolean specifying whether the KML attributes and styles
+  should be extracted from the feature. Default is 1 (true), you may want to
+  switch it off for better performance if you render a lot (hundreds) of
+  features or if you want to style the features yourself.
+
+.. note:: On style extraction
+
+    If you don't specify any options, the feature will be styled according to
+    the KML file you're loading. If you specify any styling option manually,
+    no style will be extracted from the feature. That means you can't specify
+    ``extract=1`` and ``width=4`` at the same time for example. Setting
+    **any** other option will autmatically set ``extract`` to 0 and the
+    remaning ones to to their default values.
+
+GPX: the {% geoportal_gpx %} tag
+````````````````````````````````
+
+Syntax:
+
+.. code-block:: html+django
+
+    {% geoportal_gpx map_var gpx_url [option1=value1, option2=value2 ...] %}
+
+Like with the ``geoportal_kml`` tag, you need to specify the map variable and
+the URL of the GPX file.
+
+The available options are:
+
+* ``color``: the color of the GPX feature. Default: OpenLayers's default or
+  the value of ``GEOPORTAL_DEFAULT_COLOR``.
+
+* ``opacity``: the opacity to apply to the **stroke** of the feature, unlike
+  the ``opacity`` parameter of the KML tag. Default: 1.
+
+* ``width``: the width of the stroke, in pixels. Default: 2.

geoportal/templates/geoportal/gml.html

+<script type="text/javascript">
+	var extract = {{ extract|yesno:"true,false" }};
+	{% ifequal format 'kml' %}
+	var format = new OpenLayers.Format.KML({
+		extractStyles: extract,
+		extractAttributes: extract,
+		maxDepth: 2
+	});
+	{% endifequal %}{% ifequal format 'gpx' %}
+	var format = new OpenLayers.Format.GPX({
+		extractWaypoints: extract,
+		extractTracks: extract,
+		extractRoutes: extract,
+		extractAttributes: extract
+	});{% endifequal %}
+
+	var protocol = new OpenLayers.Protocol.HTTP({
+		url: '{{ url }}',
+		format: format,
+	});
+
+	{% if style or not extract %}
+	var styleMap = new OpenLayers.StyleMap(OpenLayers.Util.applyDefaults({
+			fillColor: '#{{ color }}',
+			strokeColor: '#{{ color }}',
+			fillOpacity: {{ opacity }},
+			{% if style %}strokeOpacity: {{ opacity }},{% endif %}
+			strokeWidth: {{ width }},
+		},
+		OpenLayers.Feature.Vector.style['default'])
+	);{% endif %}
+
+	{% with format|upper as type %}
+	var {{ format }} = new OpenLayers.Layer.Vector('{{ type }}', {
+		projection: new OpenLayers.Projection('EPSG:4326'),
+		strategies: [new OpenLayers.Strategy.Fixed()],
+		protocol: protocol,{% if style or not extract %}
+		styleMap: styleMap{% endif %}
+	});
+	{% endwith %}
+
+	{{ map_var }}.viewer.map.addLayer({{ format }});
+</script>

geoportal/templatetags/geoportal_tags.py

             return self.var
 
 
-class MapNode(template.Node):
+class OptionsNode(template.Node):
+    """A template node with a nice option parsing bahaviour."""
+    args = 0  # Number of required arguments
+    available_options = ()  # List of options the parser should recognize
+
+    def parse_options(self, args):
+        self.options = {}
+        if len(args) > self.args:
+            # Eating empty options, u''
+            options = [o for o in ''.join(args[self.args:]).split(',') if o]
+            for o in options:
+                key, value = o.split('=')
+                if not key in self.available_options:
+                    raise template.TemplateSyntaxError('"%s" option is not su'
+                            'pported. Available options are: %s' % (key,
+                            ', '.join(self.available_options)))
+                self.options[key] = SafeVariable(value)
+
+    def to_boolean(self, var_name):
+        try:
+            self.options[var_name] = bool(int(self.options[var_name]))
+        except ValueError:
+            raise template.TemplateSyntaxError('"%s" can be either 0 or 1 (was'
+                ': "%s")' % (var_name, self.options[var_name]))
+
+
+class MapNode(OptionsNode):
+    args = 2
+    available_options = ('width', 'height', 'visible', 'color',
+                         'opacity', 'zoom', 'navigation')
 
     def __init__(self, args, var_name=None):
         self.geo_field = template.Variable(args[1])
         self.var_name = var_name
-        self.options = {}
-        if len(args) > 2:
-            # Eating empty options, u''
-            options = [o for o in ''.join(args[2:]).split(',') if o]
-            for o in options:
-                key, value = o.split('=')
-                available = ('width', 'height', 'visible', 'color',
-                             'opacity', 'zoom', 'navigation')
-                if not key in available:
-                    raise template.TemplateSyntaxError('"%s" option is not su'
-                            'pported. Available options are: %s' % (key,
-                            ', '.join(available)))
-                self.options[key] = SafeVariable(value)
+        self.parse_options(args)
 
     def render(self, context):
         # Generate a probably unique name for javascript variables -- in case
         else:
             collection_type = 'None'
 
-        # Resolving the options
         for (key, value) in self.options.items():
             self.options[key] = value.resolve(context)
 
 
         self.check_booleans()
 
-        # Completely isolated context
         isolated_context = template.Context({
             'options': self.options,
             'api_key': settings.GEOPORTAL_API_KEY,
             context[self.var_name] = map_var
         return rendered
 
-    def to_boolean(self, var_name):
-        try:
-            self.options[var_name] = bool(int(self.options[var_name]))
-        except ValueError:
-            raise template.TemplateSyntaxError('"%s" can be either 0 or 1 (was'
-                ': "%s")' % (var_name, self.options[var_name]))
-
     def check_booleans(self):
         if not 'visible' in self.options:
             self.options['visible'] = 1
 def geoportal_js():
     return mark_safe('<script type="text/javascript" src=' + \
                      '"%sGeoportalExtended.js"></script>' % utils.MEDIA_URL)
+
+
+class GmlNode(OptionsNode):
+    args = 3
+    available_options = ('color', 'opacity', 'width', 'extract')
+    format_type = None
+    force_style = False
+
+    def __init__(self, args):
+        self.map_var = template.Variable(args[1])
+        self.kml_url = SafeVariable(args[2])
+        self.parse_options(args)
+
+    def render(self, context):
+        for key, value in self.options.items():
+            self.options[key] = value.resolve(context)
+
+        if self.force_style:
+            self.options['extract'] = True
+            self.options['style'] = True
+        else:
+            if 'extract' in self.options:
+                self.to_boolean('extract')
+            else:
+                self.options['extract'] = True
+
+            for key in ('width', 'opacity', 'color'):
+                # Setting any of those disables style extraction
+                if key in self.options:
+                    self.options['extract'] = False
+                    break
+
+        if not 'width' in self.options:
+            self.options['width'] = 2
+
+        if not 'opacity' in self.options:
+            if self.format_type == 'gpx':
+                self.options['opacity'] = 1
+            else:
+                self.options['opacity'] = utils.DEFAULT_OPACITY
+
+        if not 'color' in self.options:
+            self.options['color'] = utils.DEFAULT_COLOR
+
+        self.options['url'] = self.kml_url.resolve(context)
+        self.options['map_var'] = self.map_var.resolve(context)
+        self.options['format'] = self.format_type
+        return template.loader.render_to_string('geoportal/gml.html',
+                                                self.options)
+
+
+class KmlNode(GmlNode):
+    format_type = 'kml'
+
+
+@register.tag
+def geoportal_kml(parser, token):
+    """Adds a layer to an existing map from a KML file
+    {% geoportal_kml map_var kml_url color=..., width=..., opacity=... %}
+
+    Options all have default values:
+        color: utils.DEFAULT_COLOR
+        width: 2
+        opacity: utils.DEFAULT_OPACITY
+        extract: 1
+    """
+    bits = token.split_contents()
+    if len(bits) < 3:
+        raise template.TemplateSyntaxError('geoportal_kml takes at least two '
+                                           'arguments')
+    return KmlNode(bits)
+
+
+class GpxNode(GmlNode):
+    format_type = 'gpx'
+    force_style = True
+    available_options = ('color', 'opacity', 'width')
+
+
+@register.tag
+def geoportal_gpx(parser, token):
+    """Same as KML, renders a GPX file.
+    Options (defaut value):
+        width: width of the feature (2)
+        opacity: stroke opacity (1)
+        color: feature color (utils.DEFAULT_COLOR)
+    """
+    bits = token.split_contents()
+    if len(bits) < 3:
+        raise template.TemplateSyntaxError('geoportal_gpx takes at least two '
+                                           'arguments')
+    return GpxNode(bits)

geoportal/tests.py

 {% geoportal_js %}
 """
 
+KML_TEMPLATE = """
+{%% load geoportal_tags %%}
+{%% geoportal_map geo_field as var_name %%}
+{%% geoportal_kml var_name %s %%}
+"""
+
+GPX_TEMPLATE = """
+{%% load geoportal_tags %%}
+{%% geoportal_map geo_field as var_name %%}
+{%% geoportal_gpx var_name %s %%}
+"""
+
 
 class GeoportalUtilsTest(TestCase):
 
         self.assertRaises(TemplateSyntaxError,
                 lambda: Template(BASE_TEMPLATE % 'some_option=some_value'))
 
+        self.assertRaises(TemplateSyntaxError,
+                lambda: Template(KML_TEMPLATE % ''))
+
+        self.assertRaises(TemplateSyntaxError,
+                lambda: Template(GPX_TEMPLATE % ''))
+
     def test_template_with_variable(self):
         context = Context({'geo_field': self.geo_model.polygon,
                            'map_width': 200,
         rendered = Template(JS_TEMPLATE).render(Context({}))
         self.assertTrue('<script type="text/javascript" src="' in rendered)
 
+    def test_kml(self):
+        context = Context({'geo_field': self.geo_model.polygon})
+        rendered = Template(KML_TEMPLATE % '/kml_url.kml').render(context)
+        self.assertTrue('var extract = true;' in rendered)
+        self.assertTrue('new OpenLayers.Format.KML' in rendered)
+        self.assertTrue("url: '/kml_url.kml'," in rendered)
+
+        tmpl = Template(KML_TEMPLATE % '/kml_url.kml width=5')
+        rendered = tmpl.render(context)
+        self.assertTrue('var extract = false;' in rendered)
+
+    def test_gpx(self):
+        context = Context({'geo_field': self.geo_model.polygon})
+        rendered = Template(GPX_TEMPLATE % '/gpx_url.gpx').render(context)
+        self.assertTrue('var extract = true;' in rendered)
+        self.assertTrue('new OpenLayers.Format.GPX' in rendered)
+        self.assertTrue("url: '/gpx_url.gpx'," in rendered)
+
+        tmpl = Template(GPX_TEMPLATE % '/gpx_url.gpx opacity=0.7')
+        rendered = tmpl.render(context)
+        self.assertTrue('var extract = true;' in rendered)
+
 
 class TestForm(geoportal.forms.Form):
     name = geoportal.forms.CharField(max_length=255)