#49 Calculation implementation

* implements update routine for Geometry model
* reorganizes fields of Parcel and District
* adds tests
* simplifies usage of ParcelWFSFetcher
This commit is contained in:
mpeltriaux 2022-01-04 15:59:53 +01:00
parent 62030c4dcc
commit 2494ecc493
5 changed files with 95 additions and 35 deletions

View File

@ -7,8 +7,10 @@ Created on: 15.11.21
""" """
from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models import MultiPolygonField
from django.db import models from django.db import models
from django.utils import timezone
from konova.models import BaseResource, UuidModel from konova.models import BaseResource, UuidModel
from konova.utils.wfs.spatial import ParcelWFSFetcher
class Geometry(BaseResource): class Geometry(BaseResource):
@ -21,7 +23,6 @@ class Geometry(BaseResource):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
self.check_for_conflicts() self.check_for_conflicts()
self.update_parcels()
def check_for_conflicts(self): def check_for_conflicts(self):
""" Checks for new geometry overlaps """ Checks for new geometry overlaps
@ -93,8 +94,38 @@ class Geometry(BaseResource):
return objs return objs
def update_parcels(self): def update_parcels(self):
# ToDo """ Updates underlying parcel information
pass
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): class GeometryConflict(UuidModel):

View File

@ -22,28 +22,30 @@ class Parcel(UuidModel):
To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped. 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") district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
flrstck_nnr = models.CharField( flrstck_nnr = models.CharField(
max_length=1000, max_length=1000,
help_text="Flurstücksnenner" help_text="Flurstücksnenner",
null=True,
blank=True,
) )
flrstck_zhlr = models.CharField( flrstck_zhlr = models.CharField(
max_length=1000, max_length=1000,
help_text="Flurstückszähler" help_text="Flurstückszähler",
null=True,
blank=True,
) )
flr = models.CharField( flr = models.CharField(
max_length=1000, max_length=1000,
help_text="Flur" help_text="Flur",
) null=True,
gmrkng = models.CharField( blank=True,
max_length=1000,
help_text="Gemarkung"
) )
updated_on = models.DateTimeField(auto_now_add=True) updated_on = models.DateTimeField(auto_now_add=True)
def __str__(self): 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): class District(UuidModel):
@ -54,18 +56,24 @@ class District(UuidModel):
District. District.
""" """
gmrkng = models.CharField(
max_length=1000,
help_text="Gemarkung",
null=True,
blank=True,
)
gmnd = models.CharField( gmnd = models.CharField(
max_length=1000, max_length=1000,
help_text="Gemeinde" help_text="Gemeinde",
) null=True,
vg = models.CharField( blank=True,
max_length=1000,
help_text="Verbandsgemeinde",
) )
krs = models.CharField( krs = models.CharField(
max_length=1000, max_length=1000,
help_text="Kreis" help_text="Kreis",
null=True,
blank=True,
) )
def __str__(self): def __str__(self):
return f"{self.krs} | {self.vg} | {self.gmnd}" return f"{self.gmrkng} | {self.gmnd} | {self.krs}"

View File

@ -8,7 +8,6 @@ Created on: 15.12.21
from django.contrib.gis.db.models.functions import Translate from django.contrib.gis.db.models.functions import Translate
from konova.models import Geometry, GeometryConflict 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.tests.test_views import BaseTestCase
from konova.utils.wfs.spatial import ParcelWFSFetcher from konova.utils.wfs.spatial import ParcelWFSFetcher
@ -25,6 +24,22 @@ class GeometryTestCase(BaseTestCase):
geom=geom, 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): def test_geometry_conflict(self):
""" Tests whether a geometry conflict will be present in case of identical/overlaying geometries and """ 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. if the conflict will be resolved if one geometry is edited.
@ -50,17 +65,13 @@ class GeometryTestCase(BaseTestCase):
def test_wfs_fetch(self): def test_wfs_fetch(self):
""" Tests the fetching functionality of ParcelWFSFetcher """ 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: Returns:
""" """
fetcher = ParcelWFSFetcher( fetcher = ParcelWFSFetcher(
base_url="https://www.geoportal.rlp.de/registry/wfs/519", geometry_id=self.geom_1.id,
version="2.0.0",
geometry=self.geom_1,
auth_user=PARCEL_WFS_USER,
auth_pw=PARCEL_WFS_PW
) )
features = fetcher.get_features( features = fetcher.get_features(
"ave:Flurstueck", "ave:Flurstueck",

View File

@ -214,7 +214,7 @@ class BaseTestCase(TestCase):
Returns: 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.srid = 4326
polygon = polygon.transform(3857, clone=True) polygon = polygon.transform(3857, clone=True)
return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form

View File

@ -10,11 +10,9 @@ from abc import abstractmethod
import requests import requests
import xmltodict import xmltodict
from django.contrib.gis.db.models.functions import AsGML, Transform from django.contrib.gis.db.models.functions import AsGML, Transform
from django.contrib.gis.geos import MultiPolygon
from requests.auth import HTTPDigestAuth from requests.auth import HTTPDigestAuth
from konova.models import Geometry from konova.settings import DEFAULT_SRID_RLP, PARCEL_WFS_USER, PARCEL_WFS_PW
from konova.settings import DEFAULT_SRID_RLP
class AbstractWFSFetcher: class AbstractWFSFetcher:
@ -38,6 +36,9 @@ class AbstractWFSFetcher:
self.auth_pw = auth_pw self.auth_pw = auth_pw
self.auth_user = auth_user 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: if self.auth_pw is not None and self.auth_user is not None:
self.auth_digest_obj = HTTPDigestAuth( self.auth_digest_obj = HTTPDigestAuth(
self.auth_user, self.auth_user,
@ -53,13 +54,20 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
""" Fetches features from a special parcel WFS """ Fetches features from a special parcel WFS
""" """
geometry = None geometry_id = None
geometry_property_name = None geometry_property_name = None
count = 100 count = 100
def __init__(self, geometry: MultiPolygon, geometry_property_name: str = "msGeometry", *args, **kwargs): def __init__(self, geometry_id: str, geometry_property_name: str = "msGeometry", *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(
self.geometry = geometry 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 self.geometry_property_name = geometry_property_name
def _create_spatial_filter(self, def _create_spatial_filter(self,
@ -74,10 +82,11 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
Returns: Returns:
spatial_filter (str): The spatial filter element spatial_filter (str): The spatial filter element
""" """
from konova.models import Geometry
if filter_srid is None: if filter_srid is None:
filter_srid = DEFAULT_SRID_RLP filter_srid = DEFAULT_SRID_RLP
geom_gml = Geometry.objects.filter( geom_gml = Geometry.objects.filter(
id=self.geometry.id id=self.geometry_id
).annotate( ).annotate(
transformed=Transform(srid=filter_srid, expression="geom") transformed=Transform(srid=filter_srid, expression="geom")
).annotate( ).annotate(
@ -124,6 +133,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
typenames (str): References to parameter 'typenames' in a WFS GetFeature request typenames (str): References to parameter 'typenames' in a WFS GetFeature request
spatial_operator (str): Defines the spatial operation for filtering 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 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: Returns:
features (list): A list of returned features features (list): A list of returned features