From 5e65156b542d6d6415cf7895f3b33295fc477564 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 20 Apr 2022 13:52:52 +0200 Subject: [PATCH] #138 WIP Validity * adds geometry validity checks for SimpleGeomForm is_valid() * shows validity problems on the form if a feature is invalid * optimizes merging of different features into one MultiPolygon * further enhances tests * adds as_feature_collection() method on Geometry model for converting geom MultiPolygon attribute into FeatureCollection json holding each polygon as an own feature -> makes each polygon selectable in new netgis map client --- .../tests/compensation/test_workflow.py | 3 +- konova/forms.py | 41 +++++++++++-------- konova/models/geometry.py | 32 +++++++++++++++ konova/tests/test_views.py | 3 +- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py index 386c59c9..bba12334 100644 --- a/compensation/tests/compensation/test_workflow.py +++ b/compensation/tests/compensation/test_workflow.py @@ -128,6 +128,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): new_identifier = self.create_dummy_string() new_comment = self.create_dummy_string() new_geometry = MultiPolygon(srid=4326) # Create an empty geometry + geojson = self.create_geojson(new_geometry) check_on_elements = { self.compensation.title: new_title, @@ -142,7 +143,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): "title": new_title, "intervention": self.intervention.id, # just keep the intervention as it is "comment": new_comment, - "geom": new_geometry.geojson, + "geom": geojson, } self.client_user.post(url, post_data) self.compensation.refresh_from_db() diff --git a/konova/forms.py b/konova/forms.py index fec2966a..a4e04fc9 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -17,8 +17,8 @@ from django.db.models.fields.files import FieldFile from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from user.models import User -from django.contrib.gis.forms import OSMWidget, MultiPolygonField -from django.contrib.gis.geos import MultiPolygon, GEOSGeometry, Polygon +from django.contrib.gis.forms import MultiPolygonField +from django.contrib.gis.geos import MultiPolygon, Polygon from django.db import transaction from django.http import HttpRequest, HttpResponseRedirect from django.shortcuts import render @@ -291,11 +291,13 @@ class SimpleGeomForm(BaseForm): # Initialize geometry try: geom = self.instance.geometry.geom - geom.transform(ct=DEFAULT_SRID_RLP) self.empty = geom.empty + if self.empty: raise AttributeError - geom = geom.geojson + + geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) + geom = json.dumps(geojson) except AttributeError: # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level geom = "" @@ -326,19 +328,26 @@ class SimpleGeomForm(BaseForm): self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) is_valid = False return is_valid - features.append(g) - if len(features) > 0: - form_geom = features[0] - for g in features[1:]: - form_geom = form_geom.union(g) - form_geom.transform(coord_trans=DEFAULT_SRID) - if form_geom.geom_type != "MultiPolygon": - form_geom = MultiPolygon(MultiPolygon.from_ewkt(form_geom.ewkt)) - # Write unioned Multipolygon into cleaned data - if self.cleaned_data is None: - self.cleaned_data = {} - self.cleaned_data["geom"] = form_geom.wkt + polygon = Polygon.from_ewkt(g.ewkt) + is_valid = polygon.valid + if not is_valid: + self.add_error("geom", polygon.valid_reason) + return is_valid + + features.append(polygon) + form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) + for feature in features: + form_geom = form_geom.union(feature) + + # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. + if form_geom.geom_type != "MultiPolygon": + form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP) + + # Write unioned Multipolygon into cleaned data + if self.cleaned_data is None: + self.cleaned_data = {} + self.cleaned_data["geom"] = form_geom.ewkt return is_valid diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 3460c3dd..ae734658 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -5,11 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 15.11.21 """ +import json + from django.contrib.gis.db.models import MultiPolygonField +from django.contrib.gis.geos import Polygon from django.db import models, transaction from django.utils import timezone from konova.models import BaseResource, UuidModel +from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.utils.wfs.spatial import ParcelWFSFetcher @@ -179,6 +183,34 @@ class Geometry(BaseResource): return parcels + def as_feature_collection(self, srid=DEFAULT_SRID_RLP): + """ Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features + + This method is used to convert a single MultiPolygon into multiple Polygons, which can be used as separated + features in the NETGIS map client. + + Args: + srid (int): The spatial reference system identifier to be transformed to + + Returns: + geojson (dict): The FeatureCollection json (as dict) + """ + geom = self.geom + geom.transform(ct=srid) + + polygons = [] + for coords in geom.coords: + p = Polygon(coords[0], srid=geom.srid) + polygons.append(p) + geojson = { + "type": "FeatureCollection", + "features": [ + json.loads(x.geojson) for x in polygons + ] + } + return geojson + + class GeometryConflict(UuidModel): """ diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index dd3c0e14..afc381df 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -432,11 +432,12 @@ class BaseTestCase(TestCase): return if geom1.srid != geom2.srid: + tolerance = 0.001 # Due to prior possible transformation of any of these geometries, we need to make sure there exists a # transformation from one coordinate system into the other, which is valid geom1_t = geom1.transform(geom2.srid, clone=True) geom2_t = geom2.transform(geom1.srid, clone=True) - self.assertTrue(geom1_t.equals(geom2) or geom2_t.equals(geom1)) + self.assertTrue(geom1_t.equals_exact(geom2, tolerance) or geom2_t.equals_exact(geom1, tolerance)) else: self.assertTrue(geom1.equals(geom2))