Fixed #5472 --Added OpenLayers-based widgets in contrib.gis
Largely inspired from django-floppyforms. Designed to not depend on OpenLayers at code level.
This commit is contained in:
parent
d4d1145632
commit
b16b72d415
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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})
|
|
@ -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;
|
||||
})();
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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('<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())
|
||||
|
||||
for invalid in [geom for key, geom in self.geometries.items() if key!='geometrycollection']:
|
||||
self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid())
|
||||
|
|
|
@ -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``.
|
|
@ -18,6 +18,7 @@ of spatially enabled data.
|
|||
install/index
|
||||
model-api
|
||||
db-api
|
||||
forms-api
|
||||
geoquerysets
|
||||
measure
|
||||
geos
|
||||
|
|
|
@ -30,6 +30,8 @@ exception or returns the clean value::
|
|||
...
|
||||
ValidationError: [u'Enter a valid email address.']
|
||||
|
||||
.. _core-field-arguments:
|
||||
|
||||
Core field arguments
|
||||
--------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
|
Loading…
Reference in New Issue