diff --git a/django/contrib/admin/media/img/gis/move_vertex_off.png b/django/contrib/admin/media/img/gis/move_vertex_off.png
new file mode 100644
index 0000000000..296b2e29c9
Binary files /dev/null and b/django/contrib/admin/media/img/gis/move_vertex_off.png differ
diff --git a/django/contrib/admin/media/img/gis/move_vertex_on.png b/django/contrib/admin/media/img/gis/move_vertex_on.png
new file mode 100644
index 0000000000..21f4758d9c
Binary files /dev/null and b/django/contrib/admin/media/img/gis/move_vertex_on.png differ
diff --git a/django/contrib/gis/admin/__init__.py b/django/contrib/gis/admin/__init__.py
new file mode 100644
index 0000000000..1d80396165
--- /dev/null
+++ b/django/contrib/gis/admin/__init__.py
@@ -0,0 +1,9 @@
+from django.contrib.gis.admin.options import GeoModelAdmin
+from django.contrib.gis.admin.sites import GeoAdminSite, site
+from django.contrib.gis.admin.widgets import OpenLayersWidget
+
+try:
+ from django.contrib.gis.admin.options import OSMGeoAdmin
+ HAS_OSM = True
+except ImportError:
+ HAS_OSM = False
diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py
new file mode 100644
index 0000000000..71fb87bf09
--- /dev/null
+++ b/django/contrib/gis/admin/options.py
@@ -0,0 +1,128 @@
+from django.conf import settings
+from django.contrib.admin import ModelAdmin
+from django.contrib.gis.admin.widgets import OpenLayersWidget
+from django.contrib.gis.gdal import OGRGeomType
+from django.contrib.gis.db import models
+
+class GeoModelAdmin(ModelAdmin):
+ """
+ The administration options class for Geographic models. Map settings
+ may be overloaded from their defaults to create custom maps.
+ """
+ # The default map settings that may be overloaded -- still subject
+ # to API changes.
+ default_lon = 0
+ default_lat = 0
+ default_zoom = 4
+ display_wkt = False
+ display_srid = False
+ extra_js = []
+ num_zoom = 18
+ max_zoom = False
+ min_zoom = False
+ units = False
+ max_resolution = False
+ max_extent = False
+ modifiable = True
+ mouse_position = True
+ scale_text = True
+ layerswitcher = True
+ scrollable = True
+ admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
+ map_width = 600
+ map_height = 400
+ map_srid = 4326
+ map_template = 'gis/admin/openlayers.html'
+ openlayers_url = 'http://openlayers.org/api/2.6/OpenLayers.js'
+ wms_url = 'http://labs.metacarta.com/wms/vmap0'
+ wms_layer = 'basic'
+ wms_name = 'OpenLayers WMS'
+ debug = False
+ widget = OpenLayersWidget
+
+ def _media(self):
+ "Injects OpenLayers JavaScript into the admin."
+ media = super(GeoModelAdmin, self)._media()
+ media.add_js([self.openlayers_url])
+ media.add_js(self.extra_js)
+ return media
+ media = property(_media)
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ """
+ Overloaded from ModelAdmin so that an OpenLayersWidget is used
+ for viewing/editing GeometryFields.
+ """
+ if isinstance(db_field, models.GeometryField):
+ # Setting the widget with the newly defined widget.
+ kwargs['widget'] = self.get_map_widget(db_field)
+ return db_field.formfield(**kwargs)
+ else:
+ return super(GeoModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+
+ def get_map_widget(self, db_field):
+ """
+ Returns a subclass of the OpenLayersWidget (or whatever was specified
+ in the `widget` attribute) using the settings from the attributes set
+ in this class.
+ """
+ is_collection = db_field._geom in ('MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION')
+ if is_collection:
+ if db_field._geom == 'GEOMETRYCOLLECTION': collection_type = 'Any'
+ else: collection_type = OGRGeomType(db_field._geom.replace('MULTI', ''))
+ else:
+ collection_type = 'None'
+
+ class OLMap(self.widget):
+ template = self.map_template
+ geom_type = db_field._geom
+ params = {'admin_media_prefix' : self.admin_media_prefix,
+ 'default_lon' : self.default_lon,
+ 'default_lat' : self.default_lat,
+ 'default_zoom' : self.default_zoom,
+ 'display_wkt' : self.debug or self.display_wkt,
+ 'geom_type' : OGRGeomType(db_field._geom),
+ 'field_name' : db_field.name,
+ 'is_collection' : is_collection,
+ 'scrollable' : self.scrollable,
+ 'layerswitcher' : self.layerswitcher,
+ 'collection_type' : collection_type,
+ 'is_linestring' : db_field._geom in ('LINESTRING', 'MULTILINESTRING'),
+ 'is_polygon' : db_field._geom in ('POLYGON', 'MULTIPOLYGON'),
+ 'is_point' : db_field._geom in ('POINT', 'MULTIPOINT'),
+ 'num_zoom' : self.num_zoom,
+ 'max_zoom' : self.max_zoom,
+ 'min_zoom' : self.min_zoom,
+ 'units' : self.units, #likely shoud get from object
+ 'max_resolution' : self.max_resolution,
+ 'max_extent' : self.max_extent,
+ 'modifiable' : self.modifiable,
+ 'mouse_position' : self.mouse_position,
+ 'scale_text' : self.scale_text,
+ 'map_width' : self.map_width,
+ 'map_height' : self.map_height,
+ 'srid' : self.map_srid,
+ 'display_srid' : self.display_srid,
+ 'wms_url' : self.wms_url,
+ 'wms_layer' : self.wms_layer,
+ 'wms_name' : self.wms_name,
+ 'debug' : self.debug,
+ }
+ return OLMap
+
+# Using the Beta OSM in the admin requires the following:
+# (1) The Google Maps Mercator projection needs to be added
+# to your `spatial_ref_sys` table. You'll need at least GDAL 1.5:
+# >>> from django.contrib.gis.gdal import SpatialReference
+# >>> from django.contrib.gis.utils import add_postgis_srs
+# >>> add_postgis_srs(SpatialReference(900913)) # Adding the Google Projection
+from django.contrib.gis import gdal
+if gdal.HAS_GDAL:
+ class OSMGeoAdmin(GeoModelAdmin):
+ map_template = 'gis/admin/osm.html'
+ extra_js = ['http://openstreetmap.org/openlayers/OpenStreetMap.js']
+ num_zoom = 20
+ map_srid = 900913
+ max_extent = '-20037508,-20037508,20037508,20037508'
+ max_resolution = 156543.0339
+ units = 'm'
diff --git a/django/contrib/gis/admin/sites.py b/django/contrib/gis/admin/sites.py
new file mode 100644
index 0000000000..56821d9fb5
--- /dev/null
+++ b/django/contrib/gis/admin/sites.py
@@ -0,0 +1,37 @@
+from django.contrib.admin import sites
+from django.contrib.gis.admin.options import GeoModelAdmin
+from django.db.models.loading import get_apps
+
+class GeoAdminSite(sites.AdminSite):
+ """
+ The GeoAdminSite is overloaded from the AdminSite to provide facilities
+ for editing geographic fields (using the GeoModelAdmin for the options
+ class instead of ModelAdmin).
+ """
+ def register(self, model_or_iterable, admin_class=None, **options):
+ "Overloaded register method that uses GeoModelAdmin."
+ admin_class = admin_class or GeoModelAdmin
+ try:
+ return super(GeoAdminSite, self).register(model_or_iterable, admin_class, **options)
+ except sites.AlreadyRegistered:
+ # Unlike the default behavior in newforms-admin we won't
+ # raise this exception.
+ pass
+
+# `site` is an instance of GeoAdminSite
+site = GeoAdminSite()
+
+# Re-registering models that appear normally in AdminSite with the
+# GeoAdminSite (if the user has these installed).
+APPS = get_apps()
+
+# Registering the `auth` Group & User models.
+from django.contrib.auth import models, admin
+if models in APPS:
+ site.register(models.Group, admin.GroupAdmin)
+ site.register(models.User, admin.UserAdmin)
+
+# Registering the `sites` Site model.
+from django.contrib.sites import models, admin
+if models in APPS:
+ site.register(models.Site, admin.SiteAdmin)
diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py
new file mode 100644
index 0000000000..27abc8f59b
--- /dev/null
+++ b/django/contrib/gis/admin/widgets.py
@@ -0,0 +1,92 @@
+from django.contrib.gis.gdal import OGRException
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+from django.forms.widgets import Textarea
+from django.template.loader import render_to_string
+
+class OpenLayersWidget(Textarea):
+ """
+ Renders an OpenLayers map using the WKT of the geometry.
+ """
+ def render(self, name, value, attrs=None):
+ # Update the template parameters with any attributes passed in.
+ if attrs: self.params.update(attrs)
+
+ # Defaulting the WKT value to a blank string -- this
+ # will be tested in the JavaScript and the appropriate
+ # interfaace will be constructed.
+ self.params['wkt'] = ''
+
+ # If a string reaches here (via a validation error on another
+ # field) then just reconstruct the Geometry.
+ if isinstance(value, basestring):
+ try:
+ value = GEOSGeometry(value)
+ except (GEOSException, ValueError):
+ value = None
+
+ if value and value.geom_type.upper() != self.geom_type:
+ value = None
+
+ # Constructing the dictionary of the map options.
+ self.params['map_options'] = self.map_options()
+
+ # Constructing the JavaScript module name using the ID of
+ # the GeometryField (passed in via the `attrs` keyword).
+ self.params['module'] = 'geodjango_%s' % self.params['field_name']
+
+ if value:
+ # Transforming the geometry to the projection used on the
+ # OpenLayers map.
+ srid = self.params['srid']
+ if value.srid != srid:
+ try:
+ value.transform(srid)
+ wkt = value.wkt
+ except OGRException:
+ wkt = ''
+ else:
+ wkt = value.wkt
+
+ # Setting the parameter WKT with that of the transformed
+ # geometry.
+ self.params['wkt'] = wkt
+
+ return render_to_string(self.template, self.params)
+
+ def map_options(self):
+ "Builds the map options hash for the OpenLayers template."
+
+ # JavaScript construction utilities for the Bounds and Projection.
+ def ol_bounds(extent):
+ return 'new OpenLayers.Bounds(%s)' % str(extent)
+ def ol_projection(srid):
+ return 'new OpenLayers.Projection("EPSG:%s")' % srid
+
+ # An array of the parameter name, the name of their OpenLayers
+ # counterpart, and the type of variable they are.
+ map_types = [('srid', 'projection', 'srid'),
+ ('display_srid', 'displayProjection', 'srid'),
+ ('units', 'units', str),
+ ('max_resolution', 'maxResolution', float),
+ ('max_extent', 'maxExtent', 'bounds'),
+ ('num_zoom', 'numZoomLevels', int),
+ ('max_zoom', 'maxZoomLevels', int),
+ ('min_zoom', 'minZoomLevel', int),
+ ]
+
+ # Building the map options hash.
+ map_options = {}
+ for param_name, js_name, option_type in map_types:
+ if self.params.get(param_name, False):
+ if option_type == 'srid':
+ value = ol_projection(self.params[param_name])
+ elif option_type == 'bounds':
+ value = ol_bounds(self.params[param_name])
+ elif option_type in (float, int):
+ value = self.params[param_name]
+ elif option_type in (str,):
+ value = '"%s"' % self.params[param_name]
+ else:
+ raise TypeError
+ map_options[js_name] = value
+ return map_options
diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py
index 7acdc6d2f9..d32f1b118e 100644
--- a/django/contrib/gis/db/models/fields/__init__.py
+++ b/django/contrib/gis/db/models/fields/__init__.py
@@ -1,3 +1,4 @@
+from django.contrib.gis import forms
from django.db import connection
# Getting the SpatialBackend container and the geographic quoting method.
from django.contrib.gis.db.backend import SpatialBackend, gqn
@@ -111,7 +112,7 @@ class GeometryField(SpatialBackend.Field):
except SpatialBackend.GeometryException:
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
else:
- raise TypeError('Cannot use parameter of `%s` type as a geometry lookup parameter.' % type(value))
+ raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
# Assigning the SRID value.
geom.srid = self.get_srid(geom)
@@ -137,6 +138,14 @@ class GeometryField(SpatialBackend.Field):
# Setup for lazy-instantiated Geometry object.
setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
+ def formfield(self, **kwargs):
+ defaults = {'form_class' : forms.GeometryField,
+ 'geom_type' : self._geom,
+ 'null' : self.null,
+ }
+ defaults.update(kwargs)
+ return super(GeometryField, self).formfield(**defaults)
+
def get_db_prep_lookup(self, lookup_type, value):
"""
Returns the spatial WHERE clause and associated parameters for the
diff --git a/django/contrib/gis/forms/__init__.py b/django/contrib/gis/forms/__init__.py
new file mode 100644
index 0000000000..5441e6078d
--- /dev/null
+++ b/django/contrib/gis/forms/__init__.py
@@ -0,0 +1 @@
+from django.contrib.gis.forms.fields import GeometryField
diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py
new file mode 100644
index 0000000000..a65e76d5d4
--- /dev/null
+++ b/django/contrib/gis/forms/fields.py
@@ -0,0 +1,37 @@
+from django import forms
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+from django.utils.translation import ugettext_lazy as _
+
+class GeometryField(forms.Field):
+ # By default a Textarea widget is used.
+ widget = forms.Textarea
+
+ default_error_messages = {
+ 'no_geom' : _(u'No geometry value provided.'),
+ 'invalid_geom' : _(u'Invalid Geometry value.'),
+ 'invalid_geom_type' : _(u'Invalid Geometry type.'),
+ }
+ def __init__(self, **kwargs):
+ self.null = kwargs.pop('null')
+ self.geom_type = kwargs.pop('geom_type')
+ super(GeometryField, self).__init__(**kwargs)
+
+ def clean(self, value):
+ """
+ Validates that the input value can be converted to a Geometry
+ object (which is returned). A ValidationError is raised if
+ the value cannot be instantiated as a Geometry.
+ """
+ if not value:
+ if self.null:
+ # The geometry column allows NULL, return None.
+ return None
+ else:
+ raise forms.ValidationError(self.error_messages['no_geom'])
+ try:
+ geom = GEOSGeometry(value)
+ if geom.geom_type.upper() != self.geom_type:
+ raise forms.ValidationError(self.error_messages['invalid_geom_type'])
+ return geom
+ except GEOSException:
+ raise forms.ValidationError(self.error_messages['invalid_geom'])
diff --git a/django/contrib/gis/templates/gis/admin/openlayers.html b/django/contrib/gis/templates/gis/admin/openlayers.html
new file mode 100644
index 0000000000..acf82b284e
--- /dev/null
+++ b/django/contrib/gis/templates/gis/admin/openlayers.html
@@ -0,0 +1,37 @@
+{% block extrastyle %}
+
+
+{% endblock %}
+
+
+
+Delete all Features
+{% if display_wkt %} WKT debugging window:
{% endif %}
+
+
+
diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js
new file mode 100644
index 0000000000..719426127c
--- /dev/null
+++ b/django/contrib/gis/templates/gis/admin/openlayers.js
@@ -0,0 +1,157 @@
+{# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #}
+{% block vars %}var {{ module }} = {};
+{{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\d+;(.+)", "i"); {{ module }}.layers = {};
+{{ module }}.wkt_f = new OpenLayers.Format.WKT();
+{{ module }}.is_collection = {% if is_collection %}true{% else %}false{% endif %};
+{{ module }}.collection_type = '{{ collection_type }}';
+{{ module }}.is_linestring = {% if is_linestring %}true{% else %}false{% endif %};
+{{ module }}.is_polygon = {% if is_polygon %}true{% else %}false{% endif %};
+{{ module }}.is_point = {% if is_point %}true{% else %}false{% endif %};
+{% endblock %}
+{{ module }}.get_ewkt = function(feat){return 'SRID={{ srid }};' + {{ module }}.wkt_f.write(feat);}
+{{ module }}.read_wkt = function(wkt){
+ // OpenLayers cannot handle EWKT -- we make sure to strip it out.
+ // EWKT is only exposed to OL if there's a validation error in the admin.
+ var match = {{ module }}.re.exec(wkt);
+ if (match){wkt = match[1];}
+ return {{ module }}.wkt_f.read(wkt);
+}
+{{ module }}.write_wkt = function(feat){
+ if ({{ module }}.is_collection){ {{ module }}.num_geom = feat.geometry.components.length;}
+ else { {{ module }}.num_geom = 1;}
+ document.getElementById('{{ id }}').value = {{ module }}.get_ewkt(feat);
+}
+{{ module }}.add_wkt = function(event){
+ // This function will sync the contents of the `vector` layer with the
+ // WKT in the text field.
+ if ({{ module }}.is_collection){
+ var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}());
+ for (var i = 0; i < {{ module }}.layers.vector.features.length; i++){
+ feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]);
+ }
+ {{ module }}.write_wkt(feat);
+ } else {
+ // Make sure to remove any previously added features.
+ if ({{ module }}.layers.vector.features.length > 1){
+ old_feats = [{{ module }}.layers.vector.features[0]];
+ {{ module }}.layers.vector.removeFeatures(old_feats);
+ {{ module }}.layers.vector.destroyFeatures(old_feats);
+ }
+ {{ module }}.write_wkt(event.feature);
+ }
+}
+{{ module }}.modify_wkt = function(event){
+ if ({{ module }}.is_collection){
+ if ({{ module }}.is_point){
+ {{ module }}.add_wkt(event);
+ return;
+ } else {
+ // When modifying the selected components are added to the
+ // vector layer so we only increment to the `num_geom` value.
+ var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}());
+ for (var i = 0; i < {{ module }}.num_geom; i++){
+ feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]);
+ }
+ {{ module }}.write_wkt(feat);
+ }
+ } else {
+ {{ module }}.write_wkt(event.feature);
+ }
+}
+// Function to clear vector features and purge wkt from div
+{{ module }}.deleteFeatures = function(){
+ {{ module }}.layers.vector.removeFeatures({{ module }}.layers.vector.features);
+ {{ module }}.layers.vector.destroyFeatures();
+}
+{{ module }}.clearFeatures = function (){
+ {{ module }}.deleteFeatures();
+ document.getElementById('{{ id }}').value = '';
+ {{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }});
+}
+// Add Select control
+{{ module }}.addSelectControl = function(){
+ var select = new OpenLayers.Control.SelectFeature({{ module }}.layers.vector, {'toggle' : true, 'clickout' : true});
+ {{ module }}.map.addControl(select);
+ select.activate();
+}
+{{ module }}.enableDrawing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate();}
+{{ module }}.enableEditing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate();}
+// Create an array of controls based on geometry type
+{{ module }}.getControls = function(lyr){
+ {{ module }}.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'});
+ var nav = new OpenLayers.Control.Navigation();
+ var draw_ctl;
+ if ({{ module }}.is_linestring){
+ draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'});
+ } else if ({{ module }}.is_polygon){
+ draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'});
+ } else if ({{ module }}.is_point){
+ draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'});
+ }
+ {% if modifiable %}
+ var mod = new OpenLayers.Control.ModifyFeature(lyr, {'displayClass': 'olControlModifyFeature'});
+ {{ module }}.controls = [nav, draw_ctl, mod];
+ {% else %}
+ {{ module }}.controls = [nav, darw_ctl];
+ {% endif %}
+}
+{{ module }}.init = function(){
+ {% block map_options %}// The options hash, w/ zoom, resolution, and projection settings.
+ var options = {
+{% autoescape off %}{% for item in map_options.items %} '{{ item.0 }}' : {{ item.1 }}{% if not forloop.last %},{% endif %}
+{% endfor %}{% endautoescape %} };{% endblock %}
+ // The admin map for this geometry field.
+ {{ module }}.map = new OpenLayers.Map('{{ id }}_map', options);
+ // Base Layer
+ {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS( "{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'} );{% endblock %}
+ {{ module }}.map.addLayer({{ module }}.layers.base);
+ {% block extra_layers %}{% endblock %}
+ {% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %}
+ {{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}");
+ {{ module }}.map.addLayer({{ module }}.layers.vector);
+ // Read WKT from the text field.
+ var wkt = document.getElementById('{{ id }}').value;
+ if (wkt){
+ // After reading into geometry, immediately write back to
+ // WKT