diff --git a/konova/models/geometry.py b/konova/models/geometry.py index db2f820..5954558 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -21,6 +21,7 @@ class Geometry(BaseResource): def save(self, *args, **kwargs): super().save(*args, **kwargs) self.check_for_conflicts() + self.update_parcels() def check_for_conflicts(self): """ Checks for new geometry overlaps @@ -31,7 +32,7 @@ class Geometry(BaseResource): """ # If no geometry is given or important data is missing, we can not perform any checks - if self.geom is None or (self.created is None and self.modified is None): + if self.geom is None: return None self.recheck_existing_conflicts() @@ -43,7 +44,10 @@ class Geometry(BaseResource): ).distinct() for match in overlapping_geoms: - GeometryConflict.objects.get_or_create(conflicting_geometry=self, affected_geometry=match) + # Make sure this conflict is not already known but in a swapped constellation + conflict_exists_swapped = GeometryConflict.objects.filter(conflicting_geometry=match, affected_geometry=self).exists() + if not conflict_exists_swapped: + GeometryConflict.objects.get_or_create(conflicting_geometry=self, affected_geometry=match) def recheck_existing_conflicts(self): """ Rechecks GeometryConflict entries @@ -88,6 +92,10 @@ class Geometry(BaseResource): objs += set_objs return objs + def update_parcels(self): + # ToDo + pass + class GeometryConflict(UuidModel): """ diff --git a/konova/models/parcel.py b/konova/models/parcel.py index 4165d2d..ad855a2 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -40,6 +40,7 @@ class Parcel(UuidModel): max_length=1000, help_text="Gemarkung" ) + updated_on = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.gmrkng} | {self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py index a9a840f..cf06e79 100644 --- a/konova/tests/test_geometries.py +++ b/konova/tests/test_geometries.py @@ -9,7 +9,7 @@ from django.contrib.gis.db.models.functions import Translate from konova.models import Geometry, GeometryConflict from konova.tests.test_views import BaseTestCase -from user.models import UserActionLogEntry +from konova.utils.wfs.spatial import SpatialWFSFetcher class GeometryTestCase(BaseTestCase): @@ -17,14 +17,11 @@ class GeometryTestCase(BaseTestCase): def setUpTestData(cls): super().setUpTestData() geom = cls.create_dummy_geometry() - action = UserActionLogEntry.get_created_action(cls.superuser) cls.geom_1 = Geometry.objects.create( geom=geom, - created=action, ) cls.geom_2 = Geometry.objects.create( geom=geom, - created=action, ) def test_geometry_conflict(self): @@ -34,8 +31,9 @@ class GeometryTestCase(BaseTestCase): Returns: """ - self.geom_1.check_for_conflicts() - conflict = GeometryConflict.objects.all().first() + conflict = GeometryConflict.objects.all() + self.assertEqual(1, conflict.count()) + conflict = conflict.first() self.assertEqual(conflict.conflicting_geometry, self.geom_2) self.assertEqual(conflict.affected_geometry, self.geom_1) @@ -47,3 +45,21 @@ class GeometryTestCase(BaseTestCase): self.geom_1.check_for_conflicts() num_conflict = GeometryConflict.objects.all().count() self.assertEqual(0, num_conflict) + + def test_wfs_fetch(self): + """ Tests the fetching functionality of SpatialWFSFetcher + + +++ Test relies on the availability of the RLP Gemarkung WFS +++ + + Returns: + + """ + fetcher = SpatialWFSFetcher( + base_url="http://geo5.service24.rlp.de/wfs/verwaltungsgrenzen_rp.fcgi", + version="1.1.0", + geometry=self.geom_1 + ) + features = fetcher.get_features( + "vermkv:fluren_rlp" + ) + self.assertNotEqual(0, len(features), msg="Spatial wfs get feature did not work!") diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py new file mode 100644 index 0000000..c763d53 --- /dev/null +++ b/konova/utils/wfs/spatial.py @@ -0,0 +1,92 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 17.12.21 + +""" +from abc import abstractmethod + +import requests +import xmltodict +from django.contrib.gis.db.models.functions import AsGML, Transform +from django.contrib.gis.geos import MultiPolygon +from owslib.wfs import WebFeatureService + +from konova.models import Geometry +from konova.settings import DEFAULT_SRID_RLP + + +class BaseWFSFetcher: + # base_url represents not the capabilities url but the parameter-free base url + base_url = "" + version = "" + wfs = None + + def __init__(self, base_url: str, version: str = "1.1.0", *args, **kwargs): + self.base_url = base_url + self.version = version + self.wfs = WebFeatureService( + url=base_url, + version=version, + ) + + @abstractmethod + def get_features(self, feature_identifier: str, filter: str): + raise NotImplementedError + + +class SpatialWFSFetcher(BaseWFSFetcher): + """ Fetches features from a parcel WFS + + """ + geometry = None + geometry_property_name = "" + + def __init__(self, geometry: MultiPolygon, geometry_property_name: str = "msGeometry", *args, **kwargs): + super().__init__(*args, **kwargs) + self.geometry = geometry + self.geometry_property_name = geometry_property_name + + def _create_geometry_filter(self, geometry_operation: str, filter_srid: str = None): + """ Creates an + + Args: + geometry_operation (): + filter_srid (): + + Returns: + + """ + if filter_srid is None: + filter_srid = DEFAULT_SRID_RLP + geom_gml = Geometry.objects.filter( + id=self.geometry.id + ).annotate( + transformed=Transform(srid=filter_srid, expression="geom") + ).annotate( + gml=AsGML('transformed') + ).first().gml + _filter = f"<{geometry_operation}>{self.geometry_property_name}{geom_gml}" + return _filter + + def get_features(self, typenames: str, geometry_operation: str = "Intersects", filter_srid: str = None, filter: str = None): + if filter is None: + filter = self._create_geometry_filter(geometry_operation, filter_srid) + response = requests.post( + url=f"{self.base_url}?request=GetFeature&service=WFS&version={self.version}&typenames={typenames}&count=10", + data={ + "filter": filter, + } + ) + content = response.content.decode("utf-8") + content = xmltodict.parse(content) + features = content.get( + "wfs:FeatureCollection", + {}, + ).get( + "gml:featureMember", + [], + ) + return features + diff --git a/requirements.txt b/requirements.txt index c29cb37..620316b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,10 +14,14 @@ django-tables2==2.3.4 et-xmlfile==1.1.0 idna==2.10 importlib-metadata==2.1.1 -itsdangerous<1.0.0 +itsdangerous==0.24 openpyxl==3.0.9 +OWSLib==0.25.0 psycopg2-binary==2.9.1 +pyproj==3.2.1 +python-dateutil==2.8.2 pytz==2020.4 +PyYAML==6.0 qrcode==7.3.1 requests==2.25.0 six==1.15.0 @@ -25,4 +29,5 @@ soupsieve==2.2.1 sqlparse==0.4.1 urllib3==1.26.2 webservices==0.7 +xmltodict==0.12.0 zipp==3.4.1