diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 249617f771..2e221b7477 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/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 @@ class GeometryField(Field): 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 @@ class GeometryField(Field): # 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") diff --git a/django/contrib/gis/forms/__init__.py b/django/contrib/gis/forms/__init__.py index 82971da6be..93a2d3847b 100644 --- a/django/contrib/gis/forms/__init__.py +++ b/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 diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index d3feac83e7..6e2cbd59f5 100644 --- a/django/contrib/gis/forms/fields.py +++ b/django/contrib/gis/forms/fields.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ # 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 @@ class GeometryField(forms.Field): # 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 @@ class GeometryField(forms.Field): 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' diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py new file mode 100644 index 0000000000..d50c7c005a --- /dev/null +++ b/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}) diff --git a/django/contrib/gis/static/gis/js/OLMapWidget.js b/django/contrib/gis/static/gis/js/OLMapWidget.js new file mode 100644 index 0000000000..252196b369 --- /dev/null +++ b/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 + */ + +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; i0) { + 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 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{% 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 %} + + +
+
+ Delete all Features + {% if display_wkt %}

WKT debugging window:

{% endif %} + + +
diff --git a/django/contrib/gis/tests/test_geoforms.py b/django/contrib/gis/tests/test_geoforms.py index 24bb50c6bc..402d9b944b 100644 --- a/django/contrib/gis/tests/test_geoforms.py +++ b/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 @@ class GeometryFieldTest(unittest.TestCase): 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 @@ class GeometryFieldTest(unittest.TestCase): 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 @@ class GeometryFieldTest(unittest.TestCase): # 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 @@ class GeometryFieldTest(unittest.TestCase): 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)) -if __name__=="__main__": - run() + def assertTextarea(self, geom, rendered): + """Makes sure the wkt and a textarea are in the content""" + + self.assertIn('