From 2494ecc4930625184233c94ae289d5f8db27e29c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Jan 2022 15:59:53 +0100 Subject: [PATCH] #49 Calculation implementation * implements update routine for Geometry model * reorganizes fields of Parcel and District * adds tests * simplifies usage of ParcelWFSFetcher --- konova/models/geometry.py | 37 +++++++++++++++++++++++++++--- konova/models/parcel.py | 40 ++++++++++++++++++++------------- konova/tests/test_geometries.py | 25 +++++++++++++++------ konova/tests/test_views.py | 2 +- konova/utils/wfs/spatial.py | 26 ++++++++++++++------- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 5954558f..e9d8c61c 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -7,8 +7,10 @@ Created on: 15.11.21 """ from django.contrib.gis.db.models import MultiPolygonField from django.db import models +from django.utils import timezone from konova.models import BaseResource, UuidModel +from konova.utils.wfs.spatial import ParcelWFSFetcher class Geometry(BaseResource): @@ -21,7 +23,6 @@ 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 @@ -93,8 +94,38 @@ class Geometry(BaseResource): return objs def update_parcels(self): - # ToDo - pass + """ Updates underlying parcel information + + Returns: + + """ + from konova.models import Parcel, District + parcel_fetcher = ParcelWFSFetcher( + geometry_id=self.id, + ) + typename = "ave:Flurstueck" + fetched_parcels = parcel_fetcher.get_features( + typename + ) + underlying_parcels = [] + for result in fetched_parcels: + fetched_parcel = result[typename] + parcel_obj = Parcel.objects.get_or_create( + flr=fetched_parcel["ave:flur"], + flrstck_nnr=fetched_parcel['ave:flstnrnen'], + flrstck_zhlr=fetched_parcel['ave:flstnrzae'], + )[0] + district = District.objects.get_or_create( + gmrkng=fetched_parcel["ave:gemarkung"], + gmnd=fetched_parcel["ave:gemeinde"], + krs=fetched_parcel["ave:kreis"], + )[0] + parcel_obj.district = district + parcel_obj.updated_on = timezone.now() + parcel_obj.save() + underlying_parcels.append(parcel_obj) + + self.parcels.set(underlying_parcels) class GeometryConflict(UuidModel): diff --git a/konova/models/parcel.py b/konova/models/parcel.py index ad855a21..35f5978b 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -22,28 +22,30 @@ class Parcel(UuidModel): To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped. """ - geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", null=True, blank=True) + geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", blank=True) district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels") flrstck_nnr = models.CharField( max_length=1000, - help_text="Flurstücksnenner" + help_text="Flurstücksnenner", + null=True, + blank=True, ) flrstck_zhlr = models.CharField( max_length=1000, - help_text="Flurstückszähler" + help_text="Flurstückszähler", + null=True, + blank=True, ) flr = models.CharField( max_length=1000, - help_text="Flur" - ) - gmrkng = models.CharField( - max_length=1000, - help_text="Gemarkung" + help_text="Flur", + null=True, + blank=True, ) updated_on = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.gmrkng} | {self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" + return f"{self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" class District(UuidModel): @@ -54,18 +56,24 @@ class District(UuidModel): District. """ + gmrkng = models.CharField( + max_length=1000, + help_text="Gemarkung", + null=True, + blank=True, + ) gmnd = models.CharField( max_length=1000, - help_text="Gemeinde" - ) - vg = models.CharField( - max_length=1000, - help_text="Verbandsgemeinde", + help_text="Gemeinde", + null=True, + blank=True, ) krs = models.CharField( max_length=1000, - help_text="Kreis" + help_text="Kreis", + null=True, + blank=True, ) def __str__(self): - return f"{self.krs} | {self.vg} | {self.gmnd}" + return f"{self.gmrkng} | {self.gmnd} | {self.krs}" diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py index da5e82d0..69a4ef71 100644 --- a/konova/tests/test_geometries.py +++ b/konova/tests/test_geometries.py @@ -8,7 +8,6 @@ Created on: 15.12.21 from django.contrib.gis.db.models.functions import Translate from konova.models import Geometry, GeometryConflict -from konova.settings import PARCEL_WFS_USER, PARCEL_WFS_PW from konova.tests.test_views import BaseTestCase from konova.utils.wfs.spatial import ParcelWFSFetcher @@ -25,6 +24,22 @@ class GeometryTestCase(BaseTestCase): geom=geom, ) + def test_geometry_parcel_caluclation(self): + """ Tests whether newly created geometries already have parcels calculated during save + + Returns: + + """ + has_parcels = self.geom_1.parcels.all().exists() + self.assertFalse(has_parcels, msg=f"{self.geom_1.id} has parcels but should not!") + self.geom_1.update_parcels() + self.geom_1.refresh_from_db() + parcels = self.geom_1.parcels.all() + has_parcels = parcels.exists() + parcel_districts = parcels.values_list("district", flat=True) + self.assertTrue(has_parcels, msg=f"{self.geom_1.id} has no parcels but should!") + self.assertEqual(parcels.count(), len(parcel_districts), msg=f"Not every parcel has exactly one district!") + def test_geometry_conflict(self): """ Tests whether a geometry conflict will be present in case of identical/overlaying geometries and if the conflict will be resolved if one geometry is edited. @@ -50,17 +65,13 @@ class GeometryTestCase(BaseTestCase): def test_wfs_fetch(self): """ Tests the fetching functionality of ParcelWFSFetcher - +++ Test relies on the availability of the RLP Gemarkung WFS +++ + +++ Test relies on the availability of the RLP Flurstück WFS +++ Returns: """ fetcher = ParcelWFSFetcher( - base_url="https://www.geoportal.rlp.de/registry/wfs/519", - version="2.0.0", - geometry=self.geom_1, - auth_user=PARCEL_WFS_USER, - auth_pw=PARCEL_WFS_PW + geometry_id=self.geom_1.id, ) features = fetcher.get_features( "ave:Flurstueck", diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 72cbbea7..d31726ff 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -214,7 +214,7 @@ class BaseTestCase(TestCase): Returns: """ - polygon = Polygon.from_bbox((7.5971391556, 50.3600032354, 7.5993975756, 50.3612420894)) + polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874)) polygon.srid = 4326 polygon = polygon.transform(3857, clone=True) return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py index c53a6b7f..62f71f65 100644 --- a/konova/utils/wfs/spatial.py +++ b/konova/utils/wfs/spatial.py @@ -10,11 +10,9 @@ 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 requests.auth import HTTPDigestAuth -from konova.models import Geometry -from konova.settings import DEFAULT_SRID_RLP +from konova.settings import DEFAULT_SRID_RLP, PARCEL_WFS_USER, PARCEL_WFS_PW class AbstractWFSFetcher: @@ -38,6 +36,9 @@ class AbstractWFSFetcher: self.auth_pw = auth_pw self.auth_user = auth_user + self._create_auth_obj() + + def _create_auth_obj(self): if self.auth_pw is not None and self.auth_user is not None: self.auth_digest_obj = HTTPDigestAuth( self.auth_user, @@ -53,13 +54,20 @@ class ParcelWFSFetcher(AbstractWFSFetcher): """ Fetches features from a special parcel WFS """ - geometry = None + geometry_id = None geometry_property_name = None count = 100 - def __init__(self, geometry: MultiPolygon, geometry_property_name: str = "msGeometry", *args, **kwargs): - super().__init__(*args, **kwargs) - self.geometry = geometry + def __init__(self, geometry_id: str, geometry_property_name: str = "msGeometry", *args, **kwargs): + super().__init__( + version="2.0.0", + base_url="https://www.geoportal.rlp.de/registry/wfs/519", + auth_user=PARCEL_WFS_USER, + auth_pw=PARCEL_WFS_PW, + *args, + **kwargs + ) + self.geometry_id = geometry_id self.geometry_property_name = geometry_property_name def _create_spatial_filter(self, @@ -74,10 +82,11 @@ class ParcelWFSFetcher(AbstractWFSFetcher): Returns: spatial_filter (str): The spatial filter element """ + from konova.models import Geometry if filter_srid is None: filter_srid = DEFAULT_SRID_RLP geom_gml = Geometry.objects.filter( - id=self.geometry.id + id=self.geometry_id ).annotate( transformed=Transform(srid=filter_srid, expression="geom") ).annotate( @@ -124,6 +133,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher): typenames (str): References to parameter 'typenames' in a WFS GetFeature request spatial_operator (str): Defines the spatial operation for filtering filter_srid (str): Defines the spatial reference system, the geometry shall be transformed into for filtering + start_index (str): References to parameter 'startindex' in a Returns: features (list): A list of returned features