diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-02-23 13:23:32 +0100 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2023-02-23 13:32:44 +0100 |
commit | d86abf7eb1d5a7ea8c8c0ecfde24680d3a5d7094 (patch) | |
tree | 3bd0fe3b2228a6907f59ac899d3ffc143cb41eb4 | |
parent | 8416c63507d00f1aa6ec2048d403e4f99176585e (diff) | |
download | Ishtar-d86abf7eb1d5a7ea8c8c0ecfde24680d3a5d7094.tar.bz2 Ishtar-d86abf7eb1d5a7ea8c8c0ecfde24680d3a5d7094.zip |
Geo: create/edit form - new openlayers version - add default IGN tiles
-rw-r--r-- | CHANGES.md | 1 | ||||
-rw-r--r-- | ishtar_common/forms_common.py | 5 | ||||
-rw-r--r-- | ishtar_common/static/gis/js/OLMapWidget.js | 246 | ||||
-rw-r--r-- | ishtar_common/templates/gis/openlayers-osm.html | 59 | ||||
-rw-r--r-- | ishtar_common/widgets.py | 22 |
5 files changed, 312 insertions, 21 deletions
diff --git a/CHANGES.md b/CHANGES.md index 5202eb3fe..8b7127a78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Ishtar changelog - model: add history for document and containers - remove "Administration" entry - put Account management in "Directory" entry - Admin - Global variable: edit in table, add import/export in CSV/JSON +- Geo: create/edit form - new openlayers version - add default IGN tiles ### Bug fixes ### - find form: remove TAQ/TPQ check diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index 2018b8227..7ad89ed3e 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -30,7 +30,6 @@ from urllib.parse import urlparse, quote import zipfile from django import forms -from django.contrib.gis import forms as gis_forms from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType @@ -2780,7 +2779,7 @@ class GISForm(forms.ModelForm, CustomForm, ManageOldType): for keys in self.GEO_FIELDS: if any(key == self.geom_type for key in keys): map_srid = 4326 - widget = gis_forms.OSMWidget + widget = widgets.OSMWidget self.fields[keys[0]].widget = widget( attrs={"map_srid": map_srid, "cols": True, @@ -2812,7 +2811,7 @@ class GISForm(forms.ModelForm, CustomForm, ManageOldType): if keys[0] != "x": geom = getattr(instance, keys[0]) map_srid = geom.srid or 4326 - widget = gis_forms.OSMWidget + widget = widgets.OSMWidget if map_srid == 4326: widget = widgets.ReversedOSMWidget self.fields[keys[0]].widget = widget( diff --git a/ishtar_common/static/gis/js/OLMapWidget.js b/ishtar_common/static/gis/js/OLMapWidget.js new file mode 100644 index 000000000..7102b0bf8 --- /dev/null +++ b/ishtar_common/static/gis/js/OLMapWidget.js @@ -0,0 +1,246 @@ +/* global ol */ + +var GeometryTypeControl = function(opt_options) { + 'use strict'; + // Map control to switch type when geometry type is unknown + var options = opt_options || {}; + + var element = document.createElement('div'); + element.className = 'switch-type type-' + options.type + ' ol-control ol-unselectable'; + if (options.active) { + element.className += " type-active"; + } + + 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"; + } + }; + + element.addEventListener('click', switchType, false); + element.addEventListener('touchstart', switchType, false); + + ol.control.Control.call(this, { + element: element + }); +}; + +// custom Ishtar - inherits not available anymore on new ol +// ol.inherits(GeometryTypeControl, ol.control.Control); +var ol_ext_inherits = function(child, parent) { + child.prototype = Object.create(parent.prototype); + child.prototype.constructor = child; +}; +ol_ext_inherits(GeometryTypeControl, ol.control.Control); +// end custom Ishtar + + +// TODO: allow deleting individual features (#8972) +(function() { + 'use strict'; + var jsonFormat = new ol.format.GeoJSON(); + + function MapWidget(options) { + this.map = null; + this.interactions = {draw: null, modify: null}; + this.typeChoices = false; + this.ready = false; + + // Default options + this.options = { + default_lat: 0, + default_lon: 0, + default_zoom: 12, + is_collection: options.geom_name.indexOf('Multi') > -1 || options.geom_name.indexOf('Collection') > -1 + }; + + // Altering using user-provided options + for (var property in options) { + if (options.hasOwnProperty(property)) { + this.options[property] = options[property]; + } + } + 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 + } + } + }); + + 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, {maxZoom: this.options.default_zoom}); + } else { + this.map.getView().setCenter(this.defaultCenter()); + } + this.createInteractions(); + if (initial_value && !this.options.is_collection) { + this.disableDrawing(); + } + this.ready = true; + } + + MapWidget.prototype.createMap = function() { + // custom Ishtar + var layers = [this.options.base_layer]; + if (this.options.layers) layers = this.options.layers; + var options = { + target: this.options.map_id, + layers: layers, + view: new ol.View({ + zoom: this.options.default_zoom + }) + }; + if (this.options.controls) options["controls"] = this.options.controls; + // end custom Ishtar + var map = new ol.Map(options); + return map; + }; + + 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.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()); + } + return center; + }; + + 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"; + } + } + }; + + 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"; + } + } + } + }; + + MapWidget.prototype.clearFeatures = function() { + this.featureCollection.clear(); + // Empty textarea widget + document.getElementById(this.options.id).value = ''; + this.enableDrawing(); + }; + + 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(); + } + } + document.getElementById(this.options.id).value = jsonFormat.writeGeometry(geometry); + }; + + window.MapWidget = MapWidget; +})(); diff --git a/ishtar_common/templates/gis/openlayers-osm.html b/ishtar_common/templates/gis/openlayers-osm.html index a6797a580..0c4391a0f 100644 --- a/ishtar_common/templates/gis/openlayers-osm.html +++ b/ishtar_common/templates/gis/openlayers-osm.html @@ -1,25 +1,50 @@ {% extends "gis/openlayers.html" %} {% load l10n %} -{% block options %}{{ block.super }} -options['default_lon'] = {{ default_lon|unlocalize }}; -options['default_lat'] = {{ default_lat|unlocalize }}; -options['default_zoom'] = {{ default_zoom|unlocalize }}; -{% endblock %} - {% block base_layer %} -var base_layer = new ol.layer.Tile({source: new ol.source.OSM()}); + var base_layer;{% endblock %} + +{% block options %}{{ block.super }} + options['default_lon'] = {{ default_lon|unlocalize }}; + options['default_lat'] = {{ default_lat|unlocalize }}; + options['default_zoom'] = {{ default_zoom|unlocalize }}; + var map_layers_edit = [ + new ol.layer.Group({ + title: base_maps_msg, + visible: true, + layers: get_layers() + }) + ]; + options["layers"] = map_layers_edit; + delete options.base_layer; + var map_controls = ol.control.defaults().extend([ + new ol.control.FullScreen(), + new ol.control.ScaleLine() + ]); + if (location.protocol == 'https:'){ + map_controls.push( + new TrackPositionControl({map_id: map_id}) + ); + } + options["controls"] = map_controls; {% endblock %} {% block post_module %} -$(".ol-viewport").parent().width($(".form").width()); -$(document).ready(function() { - {{ module }}.map.updateSize(); - let features = {{ module }}.featureOverlay.getSource().getFeatures(); - let layer_extent = features[0].getGeometry().getExtent().slice(0); - features.forEach(function(feature){ ol.extent.extend(layer_extent, feature.getGeometry().getExtent())}); - {{ module }}.map.getView().fit(layer_extent, {{ module }}.map.getSize()); - let current_zoom = {{ module }}.map.getView().getZoom(); - if ({{ module }}.map.getView().getZoom() > 18) {{ module }}.map.getView().setZoom(18); -}); + var layer_switcher = new ol.control.LayerSwitcher({ + tipLabel: 'Légende', + groupSelectStyle: 'children' + }); + {{ module }}.map.addControl(layer_switcher); + $(".ol-viewport").parent().width($(".form").width()); + $(document).ready(function() { + {{ module }}.map.updateSize(); + let features = {{ module }}.featureOverlay.getSource().getFeatures(); + if (features.length){ + let layer_extent = features[0].getGeometry().getExtent().slice(0); + features.forEach(function(feature){ ol.extent.extend(layer_extent, feature.getGeometry().getExtent())}); + {{ module }}.map.getView().fit(layer_extent, {{ module }}.map.getSize()); + } + let current_zoom = {{ module }}.map.getView().getZoom(); + if ({{ module }}.map.getView().getZoom() > 18) {{ module }}.map.getView().setZoom(18); + }); {% endblock %} diff --git a/ishtar_common/widgets.py b/ishtar_common/widgets.py index 57dc1a730..2bcc598bf 100644 --- a/ishtar_common/widgets.py +++ b/ishtar_common/widgets.py @@ -24,6 +24,7 @@ import logging from django import forms from django.conf import settings from django.contrib.gis import forms as gis_forms +from django.contrib.gis.db import models as gis_models from django.core.exceptions import ValidationError from django.core.files import File from django.db.models import fields @@ -1333,7 +1334,26 @@ class RangeInput(NumberInput): input_type = "range" -class ReversedOSMWidget(gis_forms.OSMWidget): +class OSMWidget(gis_forms.OSMWidget): + """ + Replace Media with local ressource + """ + class Media: + extend = False + css = { + 'all': ( + "ol/ol.css", + 'gis/css/ol3.css', + ) + } + js = ( + "ol/ol.js", + "ol-layerswitcher/ol-layerswitcher.js", + "gis/js/OLMapWidget.js", + ) + + +class ReversedOSMWidget(OSMWidget): def get_context(self, name, value, attrs): if value: if not isinstance(value, str): # should be geo |