Commit b16b72d4 authored by Claude Paroz's avatar Claude Paroz
Browse files

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

Largely inspired from django-floppyforms. Designed to not depend
on OpenLayers at code level.
parent d4d11456
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -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")
+4 −1
Original line number Diff line number Diff line
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
+33 −2
Original line number Diff line number Diff line
@@ -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'
+112 −0
Original line number Diff line number Diff line
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})
+371 −0
Original line number Diff line number Diff line
(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;
})();
Loading