Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #5472 --Added OpenLayers-based widgets in contrib.gis

Largely inspired from django-floppyforms. Designed to not depend
on OpenLayers at code level.
  • Loading branch information...
commit b16b72d415808073da0418de93bf32f71ead959d 1 parent d4d1145
@claudep claudep authored
View
13 django/contrib/gis/db/models/fields.py
@@ -44,6 +44,7 @@ class GeometryField(Field):
# The OpenGIS Geometry name.
geom_type = 'GEOMETRY'
+ form_class = forms.GeometryField
# Geodetic units.
geodetic_units = ('Decimal Degree', 'degree')
@@ -201,11 +202,14 @@ def db_type(self, connection):
return connection.ops.geo_db_type(self)
def formfield(self, **kwargs):
- defaults = {'form_class' : forms.GeometryField,
+ defaults = {'form_class' : self.form_class,
'geom_type' : self.geom_type,
'srid' : self.srid,
}
defaults.update(kwargs)
+ if (self.dim > 2 and not 'widget' in kwargs and
+ not getattr(defaults['form_class'].widget, 'supports_3d', False)):
+ defaults['widget'] = forms.Textarea
return super(GeometryField, self).formfield(**defaults)
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
@@ -267,28 +271,35 @@ def get_placeholder(self, value, connection):
# The OpenGIS Geometry Type Fields
class PointField(GeometryField):
geom_type = 'POINT'
+ form_class = forms.PointField
description = _("Point")
class LineStringField(GeometryField):
geom_type = 'LINESTRING'
+ form_class = forms.LineStringField
description = _("Line string")
class PolygonField(GeometryField):
geom_type = 'POLYGON'
+ form_class = forms.PolygonField
description = _("Polygon")
class MultiPointField(GeometryField):
geom_type = 'MULTIPOINT'
+ form_class = forms.MultiPointField
description = _("Multi-point")
class MultiLineStringField(GeometryField):
geom_type = 'MULTILINESTRING'
+ form_class = forms.MultiLineStringField
description = _("Multi-line string")
class MultiPolygonField(GeometryField):
geom_type = 'MULTIPOLYGON'
+ form_class = forms.MultiPolygonField
description = _("Multi polygon")
class GeometryCollectionField(GeometryField):
geom_type = 'GEOMETRYCOLLECTION'
+ form_class = forms.GeometryCollectionField
description = _("Geometry collection")
View
5 django/contrib/gis/forms/__init__.py
@@ -1,2 +1,5 @@
from django.forms import *
-from django.contrib.gis.forms.fields import GeometryField
+from .fields import (GeometryField, GeometryCollectionField, PointField,
+ MultiPointField, LineStringField, MultiLineStringField, PolygonField,
+ MultiPolygonField)
+from .widgets import BaseGeometryWidget, OpenLayersWidget, OSMWidget
View
35 django/contrib/gis/forms/fields.py
@@ -9,6 +9,7 @@
# While this couples the geographic forms to the GEOS library,
# it decouples from database (by not importing SpatialBackend).
from django.contrib.gis.geos import GEOSException, GEOSGeometry, fromstr
+from .widgets import OpenLayersWidget
class GeometryField(forms.Field):
@@ -17,7 +18,8 @@ class GeometryField(forms.Field):
accepted by GEOSGeometry is accepted by this form. By default,
this includes WKT, HEXEWKB, WKB (in a buffer), and GeoJSON.
"""
- widget = forms.Textarea
+ widget = OpenLayersWidget
+ geom_type = 'GEOMETRY'
default_error_messages = {
'required' : _('No geometry value provided.'),
@@ -31,12 +33,13 @@ def __init__(self, **kwargs):
# Pop out attributes from the database field, or use sensible
# defaults (e.g., allow None).
self.srid = kwargs.pop('srid', None)
- self.geom_type = kwargs.pop('geom_type', 'GEOMETRY')
+ self.geom_type = kwargs.pop('geom_type', self.geom_type)
if 'null' in kwargs:
kwargs.pop('null', True)
warnings.warn("Passing 'null' keyword argument to GeometryField is deprecated.",
DeprecationWarning, stacklevel=2)
super(GeometryField, self).__init__(**kwargs)
+ self.widget.attrs['geom_type'] = self.geom_type
def to_python(self, value):
"""
@@ -98,3 +101,31 @@ def _has_changed(self, initial, data):
else:
# Check for change of state of existence
return bool(initial) != bool(data)
+
+
+class GeometryCollectionField(GeometryField):
+ geom_type = 'GEOMETRYCOLLECTION'
+
+
+class PointField(GeometryField):
+ geom_type = 'POINT'
+
+
+class MultiPointField(GeometryField):
+ geom_type = 'MULTIPOINT'
+
+
+class LineStringField(GeometryField):
+ geom_type = 'LINESTRING'
+
+
+class MultiLineStringField(GeometryField):
+ geom_type = 'MULTILINESTRING'
+
+
+class PolygonField(GeometryField):
+ geom_type = 'POLYGON'
+
+
+class MultiPolygonField(GeometryField):
+ geom_type = 'MULTIPOLYGON'
View
112 django/contrib/gis/forms/widgets.py
@@ -0,0 +1,112 @@
+from __future__ import unicode_literals
+
+import logging
+
+from django.conf import settings
+from django.contrib.gis import gdal
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+from django.forms.widgets import Widget
+from django.template import loader
+from django.utils import six
+from django.utils import translation
+
+logger = logging.getLogger('django.contrib.gis')
+
+
+class BaseGeometryWidget(Widget):
+ """
+ The base class for rich geometry widgets.
+ Renders a map using the WKT of the geometry.
+ """
+ geom_type = 'GEOMETRY'
+ map_srid = 4326
+ map_width = 600
+ map_height = 400
+ display_wkt = False
+
+ supports_3d = False
+ template_name = '' # set on subclasses
+
+ def __init__(self, attrs=None):
+ self.attrs = {}
+ for key in ('geom_type', 'map_srid', 'map_width', 'map_height', 'display_wkt'):
+ self.attrs[key] = getattr(self, key)
+ if attrs:
+ self.attrs.update(attrs)
+
+ def render(self, name, value, attrs=None):
+ # If a string reaches here (via a validation error on another
+ # field) then just reconstruct the Geometry.
+ if isinstance(value, six.string_types):
+ try:
+ value = GEOSGeometry(value)
+ except (GEOSException, ValueError) as err:
+ logger.error(
+ "Error creating geometry from value '%s' (%s)" % (
+ value, err)
+ )
+ value = None
+
+ wkt = ''
+ if value:
+ # Check that srid of value and map match
+ if value.srid != self.map_srid:
+ try:
+ ogr = value.ogr
+ ogr.transform(self.map_srid)
+ wkt = ogr.wkt
+ except gdal.OGRException as err:
+ logger.error(
+ "Error transforming geometry from srid '%s' to srid '%s' (%s)" % (
+ value.srid, self.map_srid, err)
+ )
+ else:
+ wkt = value.wkt
+
+ context = self.build_attrs(attrs,
+ name=name,
+ module='geodjango_%s' % name.replace('-','_'), # JS-safe
+ wkt=wkt,
+ geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
+ STATIC_URL=settings.STATIC_URL,
+ LANGUAGE_BIDI=translation.get_language_bidi(),
+ )
+ return loader.render_to_string(self.template_name, context)
+
+
+class OpenLayersWidget(BaseGeometryWidget):
+ template_name = 'gis/openlayers.html'
+ class Media:
+ js = (
+ 'http://openlayers.org/api/2.11/OpenLayers.js',
+ 'gis/js/OLMapWidget.js',
+ )
+
+
+class OSMWidget(BaseGeometryWidget):
+ """
+ An OpenLayers/OpenStreetMap-based widget.
+ """
+ template_name = 'gis/openlayers-osm.html'
+ default_lon = 5
+ default_lat = 47
+
+ class Media:
+ js = (
+ 'http://openlayers.org/api/2.11/OpenLayers.js',
+ 'http://www.openstreetmap.org/openlayers/OpenStreetMap.js',
+ 'gis/js/OLMapWidget.js',
+ )
+
+ @property
+ def map_srid(self):
+ # Use the official spherical mercator projection SRID on versions
+ # of GDAL that support it; otherwise, fallback to 900913.
+ if gdal.HAS_GDAL and gdal.GDAL_VERSION >= (1, 7):
+ return 3857
+ else:
+ return 900913
+
+ def render(self, name, value, attrs=None):
+ return super(self, OSMWidget).render(name, value,
+ {'default_lon': self.default_lon, 'default_lat': self.default_lat})
View
371 django/contrib/gis/static/gis/js/OLMapWidget.js
@@ -0,0 +1,371 @@
+(function() {
+/**
+ * Transforms an array of features to a single feature with the merged
+ * geometry of geom_type
+ */
+OpenLayers.Util.properFeatures = function(features, geom_type) {
+ if (features.constructor == Array) {
+ var geoms = [];
+ for (var i=0; i<features.length; i++) {
+ geoms.push(features[i].geometry);
+ }
+ var geom = new geom_type(geoms);
+ features = new OpenLayers.Feature.Vector(geom);
+ }
+ return features;
+}
+
+/**
+ * @requires OpenLayers/Format/WKT.js
+ */
+
+/**
+ * Class: OpenLayers.Format.DjangoWKT
+ * Class for reading Well-Known Text, with workarounds to successfully parse
+ * geometries and collections as returnes by django.contrib.gis.geos.
+ *
+ * Inherits from:
+ * - <OpenLayers.Format.WKT>
+ */
+
+OpenLayers.Format.DjangoWKT = OpenLayers.Class(OpenLayers.Format.WKT, {
+ initialize: function(options) {
+ OpenLayers.Format.WKT.prototype.initialize.apply(this, [options]);
+ this.regExes.justComma = /\s*,\s*/;
+ },
+
+ parse: {
+ 'point': function(str) {
+ var coords = OpenLayers.String.trim(str).split(this.regExes.spaces);
+ return new OpenLayers.Feature.Vector(
+ new OpenLayers.Geometry.Point(coords[0], coords[1])
+ );
+ },
+
+ 'multipoint': function(str) {
+ var point;
+ var points = OpenLayers.String.trim(str).split(this.regExes.justComma);
+ var components = [];
+ for(var i=0, len=points.length; i<len; ++i) {
+ point = points[i].replace(this.regExes.trimParens, '$1');
+ components.push(this.parse.point.apply(this, [point]).geometry);
+ }
+ return new OpenLayers.Feature.Vector(
+ new OpenLayers.Geometry.MultiPoint(components)
+ );
+ },
+
+ 'linestring': function(str) {
+ var points = OpenLayers.String.trim(str).split(',');
+ var components = [];
+ for(var i=0, len=points.length; i<len; ++i) {
+ components.push(this.parse.point.apply(this, [points[i]]).geometry);
+ }
+ return new OpenLayers.Feature.Vector(
+ new OpenLayers.Geometry.LineString(components)
+ );
+ },
+
+ 'multilinestring': function(str) {
+ var line;
+ var lines = OpenLayers.String.trim(str).split(this.regExes.parenComma);
+ var components = [];
+ for(var i=0, len=lines.length; i<len; ++i) {
+ line = lines[i].replace(this.regExes.trimParens, '$1');
+ components.push(this.parse.linestring.apply(this, [line]).geometry);
+ }
+ return new OpenLayers.Feature.Vector(
+ new OpenLayers.Geometry.MultiLineString(components)
+ );
+ },
+
+ 'polygon': function(str) {
+ var ring, linestring, linearring;
+ var rings = OpenLayers.String.trim(str).split(this.regExes.parenComma);
+ var components = [];
+ for(var i=0, len=rings.length; i<len; ++i) {
+ ring = rings[i].replace(this.regExes.trimParens, '$1');
+ linestring = this.parse.linestring.apply(this, [ring]).geometry;
+ linearring = new OpenLayers.Geometry.LinearRing(linestring.components);
+ components.push(linearring);
+ }
+ return new OpenLayers.Feature.Vector(
+ new OpenLayers.Geometry.Polygon(components)
+ );
+ },
+
+ 'multipolygon': function(str) {
+ var polygon;
+ var polygons = OpenLayers.String.trim(str).split(this.regExes.doubleParenComma);
+ var components = [];
+ for(var i=0, len=polygons.length; i<len; ++i) {
+ polygon = polygons[i].replace(this.regExes.trimParens, '$1');
+ components.push(this.parse.polygon.apply(this, [polygon]).geometry);
+ }
+ return new OpenLayers.Feature.Vector(
+ new OpenLayers.Geometry.MultiPolygon(components)
+ );
+ },
+
+ 'geometrycollection': function(str) {
+ // separate components of the collection with |
+ str = str.replace(/,\s*([A-Za-z])/g, '|$1');
+ var wktArray = OpenLayers.String.trim(str).split('|');
+ var components = [];
+ for(var i=0, len=wktArray.length; i<len; ++i) {
+ components.push(OpenLayers.Format.WKT.prototype.read.apply(this,[wktArray[i]]));
+ }
+ return components;
+ }
+ },
+
+ extractGeometry: function(geometry) {
+ var type = geometry.CLASS_NAME.split('.')[2].toLowerCase();
+ if (!this.extract[type]) {
+ return null;
+ }
+ if (this.internalProjection && this.externalProjection) {
+ geometry = geometry.clone();
+ geometry.transform(this.internalProjection, this.externalProjection);
+ }
+ var wktType = type == 'collection' ? 'GEOMETRYCOLLECTION' : type.toUpperCase();
+ var data = wktType + '(' + this.extract[type].apply(this, [geometry]) + ')';
+ return data;
+ },
+
+ /**
+ * Patched write: successfully writes WKT for geometries and
+ * geometrycollections.
+ */
+ write: function(features) {
+ var collection, geometry, type, data, isCollection;
+ isCollection = features.geometry.CLASS_NAME == "OpenLayers.Geometry.Collection";
+ var pieces = [];
+ if (isCollection) {
+ collection = features.geometry.components;
+ pieces.push('GEOMETRYCOLLECTION(');
+ for (var i=0, len=collection.length; i<len; ++i) {
+ if (i>0) {
+ pieces.push(',');
+ }
+ pieces.push(this.extractGeometry(collection[i]));
+ }
+ pieces.push(')');
+ } else {
+ pieces.push(this.extractGeometry(features.geometry));
+ }
+ return pieces.join('');
+ },
+
+ CLASS_NAME: "OpenLayers.Format.DjangoWKT"
+});
+
+function MapWidget(options) {
+ this.map = null;
+ this.controls = null;
+ this.panel = null;
+ this.layers = {};
+ this.wkt_f = new OpenLayers.Format.DjangoWKT();
+
+ // Mapping from OGRGeomType name to OpenLayers.Geometry name
+ if (options['geom_name'] == 'Unknown') options['geom_type'] = OpenLayers.Geometry;
+ else if (options['geom_name'] == 'GeometryCollection') options['geom_type'] = OpenLayers.Geometry.Collection;
+ else options['geom_type'] = eval('OpenLayers.Geometry' + options['geom_name']);
+
+ // Default options
+ this.options = {
+ color: 'ee9900',
+ default_lat: 0,
+ default_lon: 0,
+ default_zoom: 4,
+ is_collection: options['geom_type'] instanceof OpenLayers.Geometry.Collection,
+ layerswitcher: false,
+ map_options: {},
+ map_srid: 4326,
+ modifiable: true,
+ mouse_position: false,
+ opacity: 0.4,
+ point_zoom: 12,
+ scale_text: false,
+ scrollable: true
+ };
+
+ // Altering using user-provied options
+ for (var property in options) {
+ if (options.hasOwnProperty(property)) {
+ this.options[property] = options[property];
+ }
+ }
+
+ this.map = new OpenLayers.Map(this.options.map_id, this.options.map_options);
+ if (this.options.base_layer) this.layers.base = this.options.base_layer;
+ else this.layers.base = new OpenLayers.Layer.WMS('OpenLayers WMS', 'http://vmap0.tiles.osgeo.org/wms/vmap0', {layers: 'basic'});
+ this.map.addLayer(this.layers.base);
+
+ var defaults_style = {
+ 'fillColor': '#' + this.options.color,
+ 'fillOpacity': this.options.opacity,
+ 'strokeColor': '#' + this.options.color,
+ };
+ if (this.options.geom_name == 'LineString') {
+ defaults_style['strokeWidth'] = 3;
+ }
+ var styleMap = new OpenLayers.StyleMap({'default': OpenLayers.Util.applyDefaults(defaults_style, OpenLayers.Feature.Vector.style['default'])});
+ this.layers.vector = new OpenLayers.Layer.Vector(" " + this.options.name, {styleMap: styleMap});
+ this.map.addLayer(this.layers.vector);
+ wkt = document.getElementById(this.options.id).value;
+ if (wkt) {
+ var feat = OpenLayers.Util.properFeatures(this.read_wkt(wkt), this.options.geom_type);
+ this.write_wkt(feat);
+ if (this.options.is_collection) {
+ for (var i=0; i<this.num_geom; i++) {
+ this.layers.vector.addFeatures([new OpenLayers.Feature.Vector(feat.geometry.components[i].clone())]);
+ }
+ } else {
+ this.layers.vector.addFeatures([feat]);
+ }
+ this.map.zoomToExtent(feat.geometry.getBounds());
+ if (this.options.geom_name == 'Point') {
+ this.map.zoomTo(this.options.point_zoom);
+ }
+ } else {
+ this.map.setCenter(this.defaultCenter(), this.options.default_zoom);
+ }
+ this.layers.vector.events.on({'featuremodified': this.modify_wkt, scope: this});
+ this.layers.vector.events.on({'featureadded': this.add_wkt, scope: this});
+
+ this.getControls(this.layers.vector);
+ this.panel.addControls(this.controls);
+ this.map.addControl(this.panel);
+ this.addSelectControl();
+
+ if (this.options.mouse_position) {
+ this.map.addControl(new OpenLayers.Control.MousePosition());
+ }
+ if (this.options.scale_text) {
+ this.map.addControl(new OpenLayers.Control.Scale());
+ }
+ if (this.options.layerswitcher) {
+ this.map.addControl(new OpenLayers.Control.LayerSwitcher());
+ }
+ if (!this.options.scrollable) {
+ this.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel();
+ }
+ if (wkt) {
+ if (this.options.modifiable) {
+ this.enableEditing();
+ }
+ } else {
+ this.enableDrawing();
+ }
+}
+
+MapWidget.prototype.get_ewkt = function(feat) {
+ return "SRID=" + this.options.map_srid + ";" + this.wkt_f.write(feat);
+};
+
+MapWidget.prototype.read_wkt = function(wkt) {
+ var prefix = 'SRID=' + this.options.map_srid + ';'
+ if (wkt.indexOf(prefix) === 0) {
+ wkt = wkt.slice(prefix.length);
+ }
+ return this.wkt_f.read(wkt);
+};
+
+MapWidget.prototype.write_wkt = function(feat) {
+ feat = OpenLayers.Util.properFeatures(feat, this.options.geom_type);
+ if (this.options.is_collection) {
+ this.num_geom = feat.geometry.components.length;
+ } else {
+ this.num_geom = 1;
+ }
+ document.getElementById(this.options.id).value = this.get_ewkt(feat);
+};
+
+MapWidget.prototype.add_wkt = function(event) {
+ if (this.options.is_collection) {
+ var feat = new OpenLayers.Feature.Vector(new this.options.geom_type());
+ for (var i=0; i<this.layers.vector.features.length; i++) {
+ feat.geometry.addComponents([this.layers.vector.features[i].geometry]);
+ }
+ this.write_wkt(feat);
+ } else {
+ if (this.layers.vector.features.length > 1) {
+ old_feats = [this.layers.vector.features[0]];
+ this.layers.vector.removeFeatures(old_feats);
+ this.layers.vector.destroyFeatures(old_feats);
+ }
+ this.write_wkt(event.feature);
+ }
+};
+
+MapWidget.prototype.modify_wkt = function(event) {
+ if (this.options.is_collection) {
+ if (this.options.geom_name == 'MultiPoint') {
+ this.add_wkt(event);
+ return;
+ } else {
+ var feat = new OpenLayers.Feature.Vector(new this.options.geom_type());
+ for (var i=0; i<this.num_geom; i++) {
+ feat.geometry.addComponents([this.layers.vector.features[i].geometry]);
+ }
+ this.write_wkt(feat);
+ }
+ } else {
+ this.write_wkt(event.feature);
+ }
+};
+
+MapWidget.prototype.deleteFeatures = function() {
+ this.layers.vector.removeFeatures(this.layers.vector.features);
+ this.layers.vector.destroyFeatures();
+};
+
+MapWidget.prototype.clearFeatures = function() {
+ this.deleteFeatures();
+ document.getElementById(this.options.id).value = '';
+ this.map.setCenter(this.defaultCenter(), this.options.default_zoom);
+};
+
+MapWidget.prototype.defaultCenter = function() {
+ var center = new OpenLayers.LonLat(this.options.default_lon, this.options.default_lat);
+ if (this.options.map_srid) {
+ return center.transform(new OpenLayers.Projection("EPSG:4326"), this.map.getProjectionObject());
+ }
+ return center;
+};
+
+MapWidget.prototype.addSelectControl = function() {
+ var select = new OpenLayers.Control.SelectFeature(this.layers.vector, {'toggle': true, 'clickout': true});
+ this.map.addControl(select);
+ select.activate();
+};
+
+MapWidget.prototype.enableDrawing = function () {
+ this.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate();
+};
+
+MapWidget.prototype.enableEditing = function () {
+ this.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate();
+};
+
+MapWidget.prototype.getControls = function(layer) {
+ this.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'});
+ this.controls = [new OpenLayers.Control.Navigation()];
+ if (!this.options.modifiable && layer.features.length)
+ return;
+ if (this.options.geom_name == 'LineString' || this.options.geom_name == 'Unknown') {
+ this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'}));
+ }
+ if (this.options.geom_name == 'Polygon' || this.options.geom_name == 'Unknown') {
+ this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'}));
+ }
+ if (this.options.geom_name == 'Point' || this.options.geom_name == 'Unknown') {
+ this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'}));
+ }
+ if (this.options.modifiable) {
+ this.controls.push(new OpenLayers.Control.ModifyFeature(layer, {'displayClass': 'olControlModifyFeature'}));
+ }
+};
+window.MapWidget = MapWidget;
+})();
View
17 django/contrib/gis/templates/gis/openlayers-osm.html
@@ -0,0 +1,17 @@
+{% extends "gis/openlayers.html" %}
+{% load l10n %}
+
+{% block map_options %}var map_options = {
+ maxExtend: new OpenLayers.Bounds(-20037508,-20037508,20037508,20037508),
+ maxResolution: 156543.0339,
+ numZoomLevels: 20,
+ units: 'm'
+};{% endblock %}
+
+{% block options %}{{ block.super }}
+options['scale_text'] = true;
+options['mouse_position'] = true;
+options['default_lon'] = {{ default_lon|unlocalize }};
+options['default_lat'] = {{ default_lat|unlocalize }};
+options['base_layer'] = new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)");
+{% endblock %}
View
34 django/contrib/gis/templates/gis/openlayers.html
@@ -0,0 +1,34 @@
+<style type="text/css">{% block map_css %}
+ #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; }
+ #{{ id }}_map .aligned label { float: inherit; }
+ #{{ id }}_div_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; }
+ {% if not display_wkt %}#{{ id }} { display: none; }{% endif %}
+ .olControlEditingToolbar .olControlModifyFeatureItemActive {
+ background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_on.png");
+ background-repeat: no-repeat;
+ }
+ .olControlEditingToolbar .olControlModifyFeatureItemInactive {
+ background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_off.png");
+ background-repeat: no-repeat;
+ }{% endblock %}
+</style>
+
+<div id="{{ id }}_div_map">
+ <div id="{{ id }}_map"></div>
+ <span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a></span>
+ {% if display_wkt %}<p> WKT debugging window:</p>{% endif %}
+ <textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea>
+ <script type="text/javascript">
+ {% block map_options %}var map_options = {};{% endblock %}
+ {% block options %}var options = {
+ geom_name: '{{ geom_type }}',
+ id: '{{ id }}',
+ map_id: '{{ id }}_map',
+ map_options: map_options,
+ map_srid: {{ map_srid }},
+ name: '{{ name }}'
+ };
+ {% endblock %}
+ var {{ module }} = new MapWidget(options);
+ </script>
+</div>
View
189 django/contrib/gis/tests/test_geoforms.py
@@ -1,24 +1,25 @@
from django.forms import ValidationError
from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.tests.utils import HAS_SPATIALREFSYS
+from django.test import SimpleTestCase
from django.utils import six
-from django.utils import unittest
+from django.utils.unittest import skipUnless
if HAS_SPATIALREFSYS:
from django.contrib.gis import forms
from django.contrib.gis.geos import GEOSGeometry
-@unittest.skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, "GeometryFieldTest needs gdal support and a spatial database")
-class GeometryFieldTest(unittest.TestCase):
+@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, "GeometryFieldTest needs gdal support and a spatial database")
+class GeometryFieldTest(SimpleTestCase):
- def test00_init(self):
+ def test_init(self):
"Testing GeometryField initialization with defaults."
fld = forms.GeometryField()
for bad_default in ('blah', 3, 'FoO', None, 0):
self.assertRaises(ValidationError, fld.clean, bad_default)
- def test01_srid(self):
+ def test_srid(self):
"Testing GeometryField with a SRID set."
# Input that doesn't specify the SRID is assumed to be in the SRID
# of the input field.
@@ -34,7 +35,7 @@ def test01_srid(self):
cleaned_geom = fld.clean('SRID=4326;POINT (-95.363151 29.763374)')
self.assertTrue(xform_geom.equals_exact(cleaned_geom, tol))
- def test02_null(self):
+ def test_null(self):
"Testing GeometryField's handling of null (None) geometries."
# Form fields, by default, are required (`required=True`)
fld = forms.GeometryField()
@@ -46,7 +47,7 @@ def test02_null(self):
fld = forms.GeometryField(required=False)
self.assertIsNone(fld.clean(None))
- def test03_geom_type(self):
+ def test_geom_type(self):
"Testing GeometryField's handling of different geometry types."
# By default, all geometry types are allowed.
fld = forms.GeometryField()
@@ -60,7 +61,7 @@ def test03_geom_type(self):
# but rejected by `clean`
self.assertRaises(forms.ValidationError, pnt_fld.clean, 'LINESTRING(0 0, 1 1)')
- def test04_to_python(self):
+ def test_to_python(self):
"""
Testing to_python returns a correct GEOSGeometry object or
a ValidationError
@@ -74,13 +75,169 @@ def test04_to_python(self):
self.assertRaises(forms.ValidationError, fld.to_python, wkt)
-def suite():
- s = unittest.TestSuite()
- s.addTest(unittest.makeSuite(GeometryFieldTest))
- return s
+@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS,
+ "SpecializedFieldTest needs gdal support and a spatial database")
+class SpecializedFieldTest(SimpleTestCase):
+ def setUp(self):
+ self.geometries = {
+ 'point': GEOSGeometry("SRID=4326;POINT(9.052734375 42.451171875)"),
+ 'multipoint': GEOSGeometry("SRID=4326;MULTIPOINT("
+ "(13.18634033203125 14.504356384277344),"
+ "(13.207969665527 14.490966796875),"
+ "(13.177070617675 14.454917907714))"),
+ 'linestring': GEOSGeometry("SRID=4326;LINESTRING("
+ "-8.26171875 -0.52734375,"
+ "-7.734375 4.21875,"
+ "6.85546875 3.779296875,"
+ "5.44921875 -3.515625)"),
+ 'multilinestring': GEOSGeometry("SRID=4326;MULTILINESTRING("
+ "(-16.435546875 -2.98828125,"
+ "-17.2265625 2.98828125,"
+ "-0.703125 3.515625,"
+ "-1.494140625 -3.33984375),"
+ "(-8.0859375 -5.9765625,"
+ "8.525390625 -8.7890625,"
+ "12.392578125 -0.87890625,"
+ "10.01953125 7.646484375))"),
+ 'polygon': GEOSGeometry("SRID=4326;POLYGON("
+ "(-1.669921875 6.240234375,"
+ "-3.8671875 -0.615234375,"
+ "5.9765625 -3.955078125,"
+ "18.193359375 3.955078125,"
+ "9.84375 9.4921875,"
+ "-1.669921875 6.240234375))"),
+ 'multipolygon': GEOSGeometry("SRID=4326;MULTIPOLYGON("
+ "((-17.578125 13.095703125,"
+ "-17.2265625 10.8984375,"
+ "-13.974609375 10.1953125,"
+ "-13.359375 12.744140625,"
+ "-15.732421875 13.7109375,"
+ "-17.578125 13.095703125)),"
+ "((-8.525390625 5.537109375,"
+ "-8.876953125 2.548828125,"
+ "-5.888671875 1.93359375,"
+ "-5.09765625 4.21875,"
+ "-6.064453125 6.240234375,"
+ "-8.525390625 5.537109375)))"),
+ 'geometrycollection': GEOSGeometry("SRID=4326;GEOMETRYCOLLECTION("
+ "POINT(5.625 -0.263671875),"
+ "POINT(6.767578125 -3.603515625),"
+ "POINT(8.525390625 0.087890625),"
+ "POINT(8.0859375 -2.13134765625),"
+ "LINESTRING("
+ "6.273193359375 -1.175537109375,"
+ "5.77880859375 -1.812744140625,"
+ "7.27294921875 -2.230224609375,"
+ "7.657470703125 -1.25244140625))"),
+ }
-def run(verbosity=2):
- unittest.TextTestRunner(verbosity=verbosity).run(suite())
+ def assertMapWidget(self, form_instance):
+ """
+ Make sure the MapWidget js is passed in the form media and a MapWidget
+ is actually created
+ """
+ self.assertTrue(form_instance.is_valid())
+ rendered = form_instance.as_p()
+ self.assertIn('new MapWidget(options);', rendered)
+ self.assertIn('gis/js/OLMapWidget.js', str(form_instance.media))
+
+ def assertTextarea(self, geom, rendered):
+ """Makes sure the wkt and a textarea are in the content"""
+
+ self.assertIn('<textarea ', rendered)
+ self.assertIn('required', rendered)
+ self.assertIn(geom.wkt, rendered)
+
+ def test_pointfield(self):
+ class PointForm(forms.Form):
+ p = forms.PointField()
+
+ geom = self.geometries['point']
+ form = PointForm(data={'p': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(PointForm().is_valid())
+ invalid = PointForm(data={'p': 'some invalid geom'})
+ self.assertFalse(invalid.is_valid())
+ self.assertTrue('Invalid geometry value' in str(invalid.errors))
+
+ for invalid in [geom for key, geom in self.geometries.items() if key!='point']:
+ self.assertFalse(PointForm(data={'p': invalid.wkt}).is_valid())
+
+ def test_multipointfield(self):
+ class PointForm(forms.Form):
+ p = forms.MultiPointField()
+
+ geom = self.geometries['multipoint']
+ form = PointForm(data={'p': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(PointForm().is_valid())
+
+ for invalid in [geom for key, geom in self.geometries.items() if key!='multipoint']:
+ self.assertFalse(PointForm(data={'p': invalid.wkt}).is_valid())
+
+ def test_linestringfield(self):
+ class LineStringForm(forms.Form):
+ l = forms.LineStringField()
+
+ geom = self.geometries['linestring']
+ form = LineStringForm(data={'l': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(LineStringForm().is_valid())
+
+ for invalid in [geom for key, geom in self.geometries.items() if key!='linestring']:
+ self.assertFalse(LineStringForm(data={'p': invalid.wkt}).is_valid())
+
+ def test_multilinestringfield(self):
+ class LineStringForm(forms.Form):
+ l = forms.MultiLineStringField()
+
+ geom = self.geometries['multilinestring']
+ form = LineStringForm(data={'l': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(LineStringForm().is_valid())
+
+ for invalid in [geom for key, geom in self.geometries.items() if key!='multilinestring']:
+ self.assertFalse(LineStringForm(data={'p': invalid.wkt}).is_valid())
+
+ def test_polygonfield(self):
+ class PolygonForm(forms.Form):
+ p = forms.PolygonField()
+
+ geom = self.geometries['polygon']
+ form = PolygonForm(data={'p': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(PolygonForm().is_valid())
+
+ for invalid in [geom for key, geom in self.geometries.items() if key!='polygon']:
+ self.assertFalse(PolygonForm(data={'p': invalid.wkt}).is_valid())
+
+ def test_multipolygonfield(self):
+ class PolygonForm(forms.Form):
+ p = forms.MultiPolygonField()
+
+ geom = self.geometries['multipolygon']
+ form = PolygonForm(data={'p': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(PolygonForm().is_valid())
+
+ for invalid in [geom for key, geom in self.geometries.items() if key!='multipolygon']:
+ self.assertFalse(PolygonForm(data={'p': invalid.wkt}).is_valid())
+
+ def test_geometrycollectionfield(self):
+ class GeometryForm(forms.Form):
+ g = forms.GeometryCollectionField()
+
+ geom = self.geometries['geometrycollection']
+ form = GeometryForm(data={'g': geom})
+ self.assertTextarea(geom, form.as_p())
+ self.assertMapWidget(form)
+ self.assertFalse(GeometryForm().is_valid())
-if __name__=="__main__":
- run()
+ for invalid in [geom for key, geom in self.geometries.items() if key!='geometrycollection']:
+ self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid())
View
165 docs/ref/contrib/gis/forms-api.txt
@@ -0,0 +1,165 @@
+.. _ref-gis-forms-api:
+
+===================
+GeoDjango Forms API
+===================
+
+.. module:: django.contrib.gis.forms
+ :synopsis: GeoDjango forms API.
+
+.. versionadded:: 1.6
+
+GeoDjango provides some specialized form fields and widgets in order to visually
+display and edit geolocalized data on a map. By default, they use
+`OpenLayers`_-powered maps, with a base WMS layer provided by `Metacarta`_.
+
+.. _OpenLayers: http://openlayers.org/
+.. _Metacarta: http://metacarta.com/
+
+Field arguments
+===============
+In addition to the regular :ref:`form field arguments <core-field-arguments>`,
+GeoDjango form fields take the following optional arguments.
+
+``srid``
+~~~~~~~~
+
+.. attribute:: Field.srid
+
+ This is the SRID code that the field value should be transformed to. For
+ example, if the map widget SRID is different from the SRID more generally
+ used by your application or database, the field will automatically convert
+ input values into that SRID.
+
+``geom_type``
+~~~~~~~~~~~~~
+
+.. attribute:: Field.geom_type
+
+ You generally shouldn't have to set or change that attribute which should
+ be setup depending on the field class. It matches the OpenGIS standard
+ geometry name.
+
+Form field classes
+==================
+
+``GeometryField``
+~~~~~~~~~~~~~~~~~
+
+.. class:: GeometryField
+
+``PointField``
+~~~~~~~~~~~~~~
+
+.. class:: PointField
+
+``LineStringField``
+~~~~~~~~~~~~~~~~~~~
+
+.. class:: LineStringField
+
+``PolygonField``
+~~~~~~~~~~~~~~~~
+
+.. class:: PolygonField
+
+``MultiPointField``
+~~~~~~~~~~~~~~~~~~~
+
+.. class:: MultiPointField
+
+``MultiLineStringField``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: MultiLineStringField
+
+``MultiPolygonField``
+~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: MultiPolygonField
+
+``GeometryCollectionField``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: GeometryCollectionField
+
+Form widgets
+============
+
+.. module:: django.contrib.gis.widgets
+ :synopsis: GeoDjango widgets API.
+
+GeoDjango form widgets allow you to display and edit geographic data on a
+visual map.
+Note that none of the currently available widgets supports 3D geometries, hence
+geometry fields will fallback using a simple ``Textarea`` widget for such data.
+
+Widget attributes
+~~~~~~~~~~~~~~~~~
+
+GeoDjango widgets are template-based, so their attributes are mostly different
+from other Django widget attributes.
+
+
+.. attribute:: BaseGeometryWidget.geom_type
+
+ The OpenGIS geometry type, generally set by the form field.
+
+.. attribute:: BaseGeometryWidget.map_height
+.. attribute:: BaseGeometryWidget.map_width
+
+ Height and width of the widget map (default is 400x600).
+
+.. attribute:: BaseGeometryWidget.map_srid
+
+ SRID code used by the map (default is 4326).
+
+.. attribute:: BaseGeometryWidget.display_wkt
+
+ Boolean value specifying if a textarea input showing the WKT representation
+ of the current geometry is visible, mainly for debugging purposes (default
+ is ``False``).
+
+.. attribute:: BaseGeometryWidget.supports_3d
+
+ Indicates if the widget supports edition of 3D data (default is ``False``).
+
+.. attribute:: BaseGeometryWidget.template_name
+
+ The template used to render the map widget.
+
+You can pass widget attributes in the same manner that for any other Django
+widget. For example::
+
+ from django.contrib.gis import forms
+
+ class MyGeoForm(forms.Form):
+ point = forms.PointField(widget=
+ forms.OSMWidget(attrs={'map_width': 800, 'map_height': 500}))
+
+Widget classes
+~~~~~~~~~~~~~~
+
+``BaseGeometryWidget``
+
+.. class:: BaseGeometryWidget
+
+ This is an abstract base widget containing the logic needed by subclasses.
+ You cannot directly use this widget for a geometry field.
+ Note that the rendering of GeoDjango widgets is based on a template,
+ identified by the :attr:`template_name` class attribute.
+
+``OpenLayersWidget``
+
+.. class:: OpenLayersWidget
+
+ This is the default widget used by all GeoDjango form fields.
+ ``template_name`` is ``gis/openlayers.html``.
+
+``OSMWidget``
+
+.. class:: OSMWidget
+
+ This widget uses an OpenStreetMap base layer (Mapnik) to display geographic
+ objects on.
+ ``template_name`` is ``gis/openlayers-osm.html``.
View
1  docs/ref/contrib/gis/index.txt
@@ -18,6 +18,7 @@ of spatially enabled data.
install/index
model-api
db-api
+ forms-api
geoquerysets
measure
geos
View
2  docs/ref/forms/fields.txt
@@ -30,6 +30,8 @@ exception or returns the clean value::
...
ValidationError: [u'Enter a valid email address.']
+.. _core-field-arguments:
+
Core field arguments
--------------------
View
7 docs/releases/1.6.txt
@@ -114,6 +114,13 @@ Django 1.6 adds support for savepoints in SQLite, with some :ref:`limitations
A new :class:`django.db.models.BinaryField` model field allows to store raw
binary data in the database.
+GeoDjango form widgets
+~~~~~~~~~~~~~~~~~~~~~~
+
+GeoDjango now provides :ref:`form fields and widgets <ref-gis-forms-api>` for
+its geo-specialized fields. They are OpenLayers-based by default, but they can
+be customized to use any other JS framework.
+
Minor features
~~~~~~~~~~~~~~
Please sign in to comment.
Something went wrong with that request. Please try again.