Browse Source

Fixed #25004 -- Updated OpenLayers-based widget to OpenLayers 3

Thanks Tim Graham for the review.
Claude Paroz 9 years ago
parent
commit
2ebfda38e6

+ 11 - 8
django/contrib/gis/forms/widgets.py

@@ -80,13 +80,22 @@ class OpenLayersWidget(BaseGeometryWidget):
     template_name = 'gis/openlayers.html'
 
     class Media:
+        css = {
+            'all': (
+                'https://cdnjs.cloudflare.com/ajax/libs/ol3/3.20.1/ol.css',
+                'gis/css/ol3.css',
+            )
+        }
         js = (
-            'https://cdnjs.cloudflare.com/ajax/libs/openlayers/2.13.1/OpenLayers.js',
+            'https://cdnjs.cloudflare.com/ajax/libs/ol3/3.20.1/ol.js',
             'gis/js/OLMapWidget.js',
         )
 
+    def serialize(self, value):
+        return value.json if value else ''
+
 
-class OSMWidget(BaseGeometryWidget):
+class OSMWidget(OpenLayersWidget):
     """
     An OpenLayers/OpenStreetMap-based widget.
     """
@@ -95,12 +104,6 @@ class OSMWidget(BaseGeometryWidget):
     default_lat = 47
     map_srid = 3857
 
-    class Media:
-        js = (
-            'https://cdnjs.cloudflare.com/ajax/libs/openlayers/2.13.1/OpenLayers.js',
-            'gis/js/OLMapWidget.js',
-        )
-
     def __init__(self, attrs=None):
         super(OSMWidget, self).__init__()
         for key in ('default_lon', 'default_lat'):

+ 31 - 0
django/contrib/gis/static/gis/css/ol3.css

@@ -0,0 +1,31 @@
+.switch-type {
+    background-repeat: no-repeat;
+    cursor: pointer;
+    top: 0.5em;
+    width: 22px;
+    height: 20px;
+}
+
+.type-Point {
+    background-image: url("../img/draw_point_off.png");
+    right: 5px;
+}
+.type-Point.type-active {
+    background-image: url("../img/draw_point_on.png");
+}
+
+.type-LineString {
+    background-image: url("../img/draw_line_off.png");
+    right: 30px;
+}
+.type-LineString.type-active {
+    background-image: url("../img/draw_line_on.png");
+}
+
+.type-Polygon {
+    background-image: url("../img/draw_polygon_off.png");
+    right: 55px;
+}
+.type-Polygon.type-active {
+    background-image: url("../img/draw_polygon_on.png");
+}

BIN
django/contrib/gis/static/gis/img/draw_line_off.png


BIN
django/contrib/gis/static/gis/img/draw_line_on.png


BIN
django/contrib/gis/static/gis/img/draw_point_off.png


BIN
django/contrib/gis/static/gis/img/draw_point_on.png


BIN
django/contrib/gis/static/gis/img/draw_polygon_off.png


BIN
django/contrib/gis/static/gis/img/draw_polygon_on.png


+ 176 - 331
django/contrib/gis/static/gis/js/OLMapWidget.js

@@ -1,199 +1,59 @@
-/*global OpenLayers*/
-(function() {
-    'use strict';
-    /**
-     * 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 returned 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)
-                );
-            },
+/* global ol */
 
-            '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)
-                );
-            },
+var GeometryTypeControl = function(opt_options) {
+    'use strict';
+    // Map control to switch type when geometry type is unknown
+    var options = opt_options || {};
 
-            '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;
-            }
-        },
+    var element = document.createElement('div');
+    element.className = 'switch-type type-' + options.type + ' ol-control ol-unselectable';
+    if (options.active) {
+        element.className += " type-active";
+    }
 
-        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;
-        },
+    var self = this;
+    var switchType = function(e) {
+        e.preventDefault();
+        if (options.widget.currentGeometryType !== self) {
+            options.widget.map.removeInteraction(options.widget.interactions.draw);
+            options.widget.interactions.draw = new ol.interaction.Draw({
+                features: options.widget.featureCollection,
+                type: options.type
+            });
+            options.widget.map.addInteraction(options.widget.interactions.draw);
+            var className = options.widget.currentGeometryType.element.className.replace(/ type-active/g, '');
+            options.widget.currentGeometryType.element.className = className;
+            options.widget.currentGeometryType = self;
+            element.className += " type-active";
+        }
+    };
 
-        /**
-         * Patched write: successfully writes WKT for geometries and
-         * geometrycollections.
-         */
-        write: function(features) {
-            var collection, 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('');
-        },
+    element.addEventListener('click', switchType, false);
+    element.addEventListener('touchstart', switchType, false);
 
-        CLASS_NAME: "OpenLayers.Format.DjangoWKT"
+    ol.control.Control.call(this, {
+        element: element
     });
+};
+ol.inherits(GeometryTypeControl, ol.control.Control);
+
+// TODO: allow deleting individual features (#8972)
+(function() {
+    'use strict';
+    var jsonFormat = new ol.format.GeoJSON();
 
     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 = OpenLayers.Geometry[options.geom_name];
-        }
+        this.interactions = {draw: null, modify: null};
+        this.typeChoices = false;
+        this.ready = false;
 
         // Default options
         this.options = {
-            color: 'ee9900',
             default_lat: 0,
             default_lon: 0,
-            default_zoom: 4,
-            is_collection: options.geom_name.indexOf('Multi') > -1 || options.geom_name.indexOf('Collection') > -1,
-            layerswitcher: false,
-            map_options: {},
-            map_srid: 4326,
-            modifiable: true,
-            mouse_position: false,
-            opacity: 0.4,
-            point_zoom: 12,
-            scale_text: false,
-            scrollable: true
+            default_zoom: 12,
+            is_collection: options.geom_name.indexOf('Multi') > -1 || options.geom_name.indexOf('Collection') > -1
         };
 
         // Altering using user-provided options
@@ -202,185 +62,170 @@
                 this.options[property] = options[property];
             }
         }
-
-        this.map = this.create_map();
-
-        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);
-        var 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())]);
+        if (!options.base_layer) {
+            this.options.base_layer = new ol.layer.Tile({source: new ol.source.OSM()});
+        }
+
+        this.map = this.createMap();
+        this.featureCollection = new ol.Collection();
+        this.featureOverlay = new ol.layer.Vector({
+            map: this.map,
+            source: new ol.source.Vector({
+                features: this.featureCollection,
+                useSpatialIndex: false // improve performance
+            }),
+            updateWhileAnimating: true, // optional, for instant visual feedback
+            updateWhileInteracting: true // optional, for instant visual feedback
+        });
+
+        // Populate and set handlers for the feature container
+        var self = this;
+        this.featureCollection.on('add', function(event) {
+            var feature = event.element;
+            feature.on('change', function() {
+                self.serializeFeatures();
+            });
+            if (self.ready) {
+                self.serializeFeatures();
+                if (!self.options.is_collection) {
+                    self.disableDrawing(); // Only allow one feature at a time
                 }
-            } 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);
             }
+        });
+
+        var initial_value = document.getElementById(this.options.id).value;
+        if (initial_value) {
+            var features = jsonFormat.readFeatures('{"type": "Feature", "geometry": ' + initial_value + '}');
+            var extent = ol.extent.createEmpty();
+            features.forEach(function(feature) {
+                this.featureOverlay.getSource().addFeature(feature);
+                ol.extent.extend(extent, feature.getGeometry().getExtent());
+            }, this);
+            // Center/zoom the map
+            this.map.getView().fit(extent, this.map.getSize(), {maxZoom: this.options.default_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());
+            this.map.getView().setCenter(this.defaultCenter());
         }
-        if (!this.options.scrollable) {
-            this.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel();
-        }
-        if (wkt) {
-            if (this.options.modifiable) {
-                this.enableEditing();
-            }
-        } else {
-            this.enableDrawing();
+        this.createInteractions();
+        if (initial_value && !this.options.is_collection) {
+            this.disableDrawing();
         }
+        this.ready = true;
     }
 
-    MapWidget.prototype.create_map = function() {
-        var 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'});
-        }
-        map.addLayer(this.layers.base);
+    MapWidget.prototype.createMap = function() {
+        var map = new ol.Map({
+            target: this.options.map_id,
+            layers: [this.options.base_layer],
+            view: new ol.View({
+                zoom: this.options.default_zoom
+            })
+        });
         return map;
     };
 
-    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.createInteractions = function() {
+        // Initialize the modify interaction
+        this.interactions.modify = new ol.interaction.Modify({
+            features: this.featureCollection,
+            deleteCondition: function(event) {
+                return ol.events.condition.shiftKeyOnly(event) &&
+                    ol.events.condition.singleClick(event);
+            }
+        });
+
+        // Initialize the draw interaction
+        var geomType = this.options.geom_name;
+        if (geomType === "Unknown" || geomType === "GeometryCollection") {
+            // Default to Point, but create icons to switch type
+            geomType = "Point";
+            this.currentGeometryType = new GeometryTypeControl({widget: this, type: "Point", active: true});
+            this.map.addControl(this.currentGeometryType);
+            this.map.addControl(new GeometryTypeControl({widget: this, type: "LineString", active: false}));
+            this.map.addControl(new GeometryTypeControl({widget: this, type: "Polygon", active: false}));
+            this.typeChoices = true;
+        }
+        this.interactions.draw = new ol.interaction.Draw({
+            features: this.featureCollection,
+            type: geomType
+        });
+
+        this.map.addInteraction(this.interactions.draw);
+        this.map.addInteraction(this.interactions.modify);
     };
 
-    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;
+    MapWidget.prototype.defaultCenter = function() {
+        var center = [this.options.default_lon, this.options.default_lat];
+        if (this.options.map_srid) {
+            return ol.proj.transform(center, 'EPSG:4326', this.map.getView().getProjection());
         }
-        document.getElementById(this.options.id).value = this.get_ewkt(feat);
+        return center;
     };
 
-    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) {
-                var old_feats = [this.layers.vector.features[0]];
-                this.layers.vector.removeFeatures(old_feats);
-                this.layers.vector.destroyFeatures(old_feats);
+    MapWidget.prototype.enableDrawing = function() {
+        this.interactions.draw.setActive(true);
+        if (this.typeChoices) {
+            // Show geometry type icons
+            var divs = document.getElementsByClassName("switch-type");
+            for (var i = 0; i !== divs.length; i++) {
+                divs[i].style.visibility = "visible";
             }
-            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]);
+    MapWidget.prototype.disableDrawing = function() {
+        if (this.interactions.draw) {
+            this.interactions.draw.setActive(false);
+            if (this.typeChoices) {
+                // Hide geometry type icons
+                var divs = document.getElementsByClassName("switch-type");
+                for (var i = 0; i !== divs.length; i++) {
+                    divs[i].style.visibility = "hidden";
                 }
-                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();
+        this.featureCollection.clear();
+        // Empty textarea widget
         document.getElementById(this.options.id).value = '';
-        this.map.setCenter(this.defaultCenter(), this.options.default_zoom);
+        this.enableDrawing();
     };
 
-    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());
+    MapWidget.prototype.serializeFeatures = function() {
+        // Three use cases: GeometryCollection, multigeometries, and single geometry
+        var geometry = null;
+        var features = this.featureOverlay.getSource().getFeatures();
+        if (this.options.is_collection) {
+            if (this.options.geom_name === "GeometryCollection") {
+                var geometries = [];
+                for (var i = 0; i < features.length; i++) {
+                    geometries.push(features[i].getGeometry());
+                }
+                geometry = new ol.geom.GeometryCollection(geometries);
+            } else {
+                geometry = features[0].getGeometry().clone();
+                for (var j = 1; j < features.length; j++) {
+                    switch(geometry.getType()) {
+                        case "MultiPoint":
+                            geometry.appendPoint(features[j].getGeometry().getPoint(0));
+                            break;
+                        case "MultiLineString":
+                            geometry.appendLineString(features[j].getGeometry().getLineString(0));
+                            break;
+                        case "MultiPolygon":
+                            geometry.appendPolygon(features[j].getGeometry().getPolygon(0));
+                    }
+                }
+            }
+        } else {
+            if (features[0]) {
+                geometry = features[0].getGeometry();
+            }
         }
-        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();
+        document.getElementById(this.options.id).value = jsonFormat.writeGeometry(geometry);
     };
 
-    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.indexOf('LineString') >= 0 || this.options.geom_name === 'GeometryCollection' || this.options.geom_name === 'Unknown') {
-            this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'}));
-        }
-        if (this.options.geom_name.indexOf('Polygon') >= 0 || this.options.geom_name === 'GeometryCollection' || this.options.geom_name === 'Unknown') {
-            this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'}));
-        }
-        if (this.options.geom_name.indexOf('Point') >= 0 || this.options.geom_name === 'GeometryCollection' || 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;
 })();

+ 4 - 10
django/contrib/gis/templates/gis/openlayers-osm.html

@@ -1,17 +1,11 @@
 {% 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("OpenStreetMap (Mapnik)");
+{% endblock %}
+
+{% block base_layer %}
+var base_layer = new ol.layer.Tile({source: new ol.source.OSM()});
 {% endblock %}

+ 15 - 10
django/contrib/gis/templates/gis/openlayers.html

@@ -1,27 +1,32 @@
-{% load i18n l10n static %}
+{% load i18n l10n %}
 <style type="text/css">{% block map_css %}{% get_current_language_bidi as LANGUAGE_BIDI %}
     #{{ 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_raw %}#{{ id }} { display: none; }{% endif %}
-    .olControlEditingToolbar .olControlModifyFeatureItemActive {
-        background-image: url("{% static "admin/img/gis/move_vertex_on.svg" %}");
-        background-repeat: no-repeat;
-    }
-    .olControlEditingToolbar .olControlModifyFeatureItemInactive {
-        background-image: url("{% static "admin/img/gis/move_vertex_off.svg" %}");
-        background-repeat: no-repeat;
-    }{% endblock %}
+    {% endblock %}
 </style>
 
 <div id="{{ id }}_div_map">
     <div id="{{ id }}_map"></div>
-    <span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">{% trans "Delete all Features" %}</a></span>
+    {% if not disabled %}<span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">{% trans "Delete all Features" %}</a></span>{% endif %}
     {% if display_raw %}<p>{% trans "Debugging window (serialized value)" %}</p>{% endif %}
     <textarea id="{{ id }}" class="vSerializedField required" cols="150" rows="10" name="{{ name }}">{{ serialized }}</textarea>
     <script type="text/javascript">
         {% block map_options %}var map_options = {};{% endblock %}
+        {% block base_layer %}
+            var base_layer = new ol.layer.Tile({
+                source: new ol.source.XYZ({
+                    attributions: "NASA Worldview",
+                    maxZoom: 8,
+                    url: "https://map1{a-c}.vis.earthdata.nasa.gov/wmts-webmerc/" +
+                         "BlueMarble_ShadedRelief_Bathymetry/default/%7BTime%7D/" +
+                         "GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpg"
+                })
+            });
+        {% endblock %}
         {% block options %}var options = {
+            base_layer: base_layer,
             geom_name: '{{ geom_type }}',
             id: '{{ id }}',
             map_id: '{{ id }}_map',

+ 11 - 4
docs/ref/contrib/gis/forms-api.txt

@@ -164,18 +164,25 @@ Widget classes
         isn't suitable for production use since it offers no guaranteed uptime
         and runs on a slow server.
 
-    .. _tailored to your needs: http://docs.openlayers.org/library/deploying.html
+        Also, the widget nows uses OpenLayers 3 instead of OpenLayers 2.
+
+    .. _tailored to your needs: http://openlayers.org/en/latest/doc/tutorials/custom-builds.html
 
 ``OSMWidget``
 
 .. class:: OSMWidget
 
-    This widget uses an OpenStreetMap base layer (Mapnik) to display geographic
-    objects on.
-    ``template_name`` is ``gis/openlayers-osm.html``.
+    This widget uses an OpenStreetMap base layer to display geographic objects
+    on. ``template_name`` is ``gis/openlayers-osm.html``.
 
     The :class:`OpenLayersWidget` note about JavaScript file hosting above also
     applies here. See also this `FAQ answer`_ about ``https`` access to map
     tiles.
 
+    .. versionchanged:: 1.11
+
+        OpenLayers 2.x has been dropped in favor of OpenLayers 3. If you extend
+        the ``gis/openlayers-osm.html`` template, please review your custom
+        template.
+
     .. _FAQ answer: https://help.openstreetmap.org/questions/10920/how-to-embed-a-map-in-my-https-site

+ 7 - 1
docs/releases/1.11.txt

@@ -165,7 +165,8 @@ Minor features
 
 * The OpenLayers-based form widgets now use ``OpenLayers.js`` from
   ``https://cdnjs.cloudflare.com`` which is more suitable for production use
-  than the the old ``http://openlayers.org`` source.
+  than the the old ``http://openlayers.org`` source. They are also updated to
+  use OpenLayers 3.
 
 * PostGIS migrations can now change field dimensions.
 
@@ -469,6 +470,11 @@ Backwards incompatible changes in 1.11
 
 * The ``GEOSGeometry`` equality operator now also compares SRID.
 
+* The OpenLayers-based form widgets now use OpenLayers 3, and the
+  ``gis/openlayers.html`` and ``gis/openlayers-osm.html`` templates have been
+  updated. Check your project if you subclass these widgets or extend the
+  templates.
+
 Database backend API
 --------------------
 

+ 29 - 13
js_tests/gis/mapwidget.test.js

@@ -7,42 +7,58 @@ QUnit.module('gis.OLMapWidget');
 QUnit.test('MapWidget.featureAdded', function(assert) {
     var options = {id: 'id_point', map_id: 'id_point_map', geom_name: 'Point'};
     var widget = new MapWidget(options);
-    assert.equal(widget.layers.vector.features.length, 1);
+    assert.equal(widget.featureCollection.getLength(), 1);
+    widget.serializeFeatures();
     assert.equal(
-        widget.layers.vector.features[0].geometry.toString(),
-        'POINT(7.8177 47.397)',
-        'Point addded to vector layer'
+        document.getElementById('id_point').value,
+        '{"type":"Point","coordinates":[7.8177,47.397]}',
+        'Point added to vector layer'
     );
 });
 
 QUnit.test('MapWidget.map_srid', function(assert) {
     var options = {id: 'id_point', map_id: 'id_point_map', geom_name: 'Point'};
     var widget = new MapWidget(options);
-    assert.equal(widget.options.map_srid, 4326, 'SRID 4326');
+    assert.equal(widget.map.getView().getProjection().getCode(), 'EPSG:3857', 'SRID 3857');
 });
 
 QUnit.test('MapWidget.defaultCenter', function(assert) {
     var options = {id: 'id_point', map_id: 'id_point_map', geom_name: 'Point'};
     var widget = new MapWidget(options);
-    assert.equal(widget.defaultCenter().toString(), 'lon=0,lat=0', 'Default center at 0, 0');
+    assert.equal(widget.defaultCenter().toString(), '0,0', 'Default center at 0, 0');
     options.default_lat = 47.08;
     options.default_lon = 6.81;
     widget = new MapWidget(options);
     assert.equal(
         widget.defaultCenter().toString(),
-        'lon=6.81,lat=47.08',
+        '6.81,47.08',
         'Default center at 6.81, 47.08'
     );
+    assert.equal(widget.map.getView().getZoom(), 12);
 });
 
-QUnit.test('MapWidget.getControls', function(assert) {
+QUnit.test('MapWidget.interactions', function(assert) {
     var options = {id: 'id_point', map_id: 'id_point_map', geom_name: 'Point'};
     var widget = new MapWidget(options);
-    widget.getControls(widget.layers.vector);
-    assert.equal(widget.controls.length, 3);
-    assert.equal(widget.controls[0].displayClass, 'olControlNavigation', 'Navigation control');
-    assert.equal(widget.controls[1].displayClass, 'olControlDrawFeaturePoint', 'Draw control');
-    assert.equal(widget.controls[2].displayClass, 'olControlModifyFeature', 'Modify control');
+    assert.equal(Object.keys(widget.interactions).length, 2);
+    assert.equal(widget.interactions.draw.getActive(), false, "Draw is inactive with an existing point");
+    assert.equal(widget.interactions.modify.getActive(), true, "Modify is active with an existing point");
+});
+
+QUnit.test('MapWidget.clearFeatures', function(assert) {
+    var options = {id: 'id_point', map_id: 'id_point_map', geom_name: 'Point'};
+    var widget = new MapWidget(options);
+    var initial_value = document.getElementById('id_point').value;
+    widget.clearFeatures();
+    assert.equal(document.getElementById('id_point').value, "");
+    document.getElementById('id_point').value = initial_value;
+});
+
+QUnit.test('MapWidget.multipolygon', function(assert) {
+    var options = {id: 'id_multipolygon', map_id: 'id_multipolygon_map', geom_name: 'MultiPolygon'};
+    var widget = new MapWidget(options);
+    assert.ok(widget.options.is_collection);
+    assert.equal(widget.interactions.draw.getActive(), true, "Draw is active with no existing content");
 });
 
 QUnit.test('MapWidget.IsCollection', function(assert) {

+ 9 - 5
js_tests/tests.html

@@ -77,12 +77,16 @@
     <script src='../django/contrib/admin/static/admin/js/prepopulate.js' data-cover></script>
     <script src='../django/contrib/admin/static/admin/js/urlify.js' data-cover></script>
 
-    <div id="id_point_map">
-        <textarea id="id_point" name="point"
-                  class="vSerializedField required" style="display:none;"
-                  rows="10" cols="150">POINT (7.8177 47.397)</textarea>
+    <div id="id_point_map" style="display:none;">
+        <textarea id="id_point" name="point" class="vSerializedField required"
+                  style="display:none;" rows="10" cols="150"
+        >{&quot;type&quot;: &quot;Point&quot;, &quot;coordinates&quot;: [7.8177, 47.397]}</textarea>
     </div>
-    <script src='https://cdnjs.cloudflare.com/ajax/libs/openlayers/2.13.1/OpenLayers.js'></script>
+    <div id="id_multipolygon_map" style="display:none;">
+        <textarea id="id_multipolygon" name="multipolygon" class="vSerializedField required"
+                  style="display:none;" rows="10" cols="150"></textarea>
+    </div>
+    <script src='https://cdnjs.cloudflare.com/ajax/libs/ol3/3.20.0/ol.js'></script>
     <script src='../django/contrib/gis/static/gis/js/OLMapWidget.js' data-cover></script>
     <script src='./gis/mapwidget.test.js'></script>
 

+ 2 - 2
tests/gis_tests/test_geoforms.py

@@ -204,7 +204,7 @@ class SpecializedFieldTest(SimpleTestCase):
 
         self.assertIn('<textarea ', rendered)
         self.assertIn('required', rendered)
-        self.assertIn(geom.wkt, rendered)
+        self.assertIn(escape(geom.json), rendered)
 
     # map_srid in operlayers.html template must not be localized.
     @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True)
@@ -318,7 +318,7 @@ class OSMWidgetTest(SimpleTestCase):
         form = PointForm(data={'p': geom})
         rendered = form.as_p()
 
-        self.assertIn("OpenStreetMap (Mapnik)", rendered)
+        self.assertIn("ol.source.OSM()", rendered)
         self.assertIn("id: 'id_p',", rendered)
 
     def test_default_lat_lon(self):