Compare commits

..

2 Commits

Author SHA1 Message Date
440a4b04d5 #49 Parcels and Districts
* fixes bug in GeometryConflict conflict checking
* WIP: introduces new konova/utils/wfs/spatial holding SpatialWFSFetcher, which can be fed any geometry and it returns found features
* WIP: adds tests for wfs fetching
* updates requirements.txt
2021-12-17 17:30:12 +01:00
71f88f7218 #49 Parcels and Districts
* introduces new models: Parcel and District
2021-12-16 12:21:31 +01:00
6 changed files with 202 additions and 11 deletions

View File

@ -9,3 +9,4 @@ from .object import *
from .deadline import *
from .document import *
from .geometry import *
from .parcel import *

View File

@ -7,7 +7,6 @@ Created on: 15.11.21
"""
from django.contrib.gis.db.models import MultiPolygonField
from django.db import models
from django.db.models import Q
from konova.models import BaseResource, UuidModel
@ -22,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
@ -32,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()
@ -44,6 +44,9 @@ class Geometry(BaseResource):
).distinct()
for match in overlapping_geoms:
# 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):
@ -69,7 +72,6 @@ class Geometry(BaseResource):
resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts)
resolved_conflicts.delete()
def get_data_objects(self):
""" Getter for all objects which are related to this geometry
@ -90,6 +92,10 @@ class Geometry(BaseResource):
objs += set_objs
return objs
def update_parcels(self):
# ToDo
pass
class GeometryConflict(UuidModel):
"""

71
konova/models/parcel.py Normal file
View File

@ -0,0 +1,71 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.12.21
"""
from django.db import models
from konova.models import UuidModel
class Parcel(UuidModel):
""" The Parcel model holds administrative data on the covered properties.
Due to the unique but relevant naming of the administrative data, we have to use these namings as field
names in german. Any try to translate them to English result in strange or insufficient translations.
All fields have to be CharFields as well, since there are e.g. Flurstücksnummer holding e.g. '123____' which
can not be realized using numerical fields.
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)
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"
)
flrstck_zhlr = models.CharField(
max_length=1000,
help_text="Flurstückszähler"
)
flr = models.CharField(
max_length=1000,
help_text="Flur"
)
gmrkng = models.CharField(
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}"
class District(UuidModel):
""" The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde.
There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry.
Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one
District.
"""
gmnd = models.CharField(
max_length=1000,
help_text="Gemeinde"
)
vg = models.CharField(
max_length=1000,
help_text="Verbandsgemeinde",
)
krs = models.CharField(
max_length=1000,
help_text="Kreis"
)
def __str__(self):
return f"{self.krs} | {self.vg} | {self.gmnd}"

View File

@ -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!")

View File

@ -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"<Filter><{geometry_operation}><PropertyName>{self.geometry_property_name}</PropertyName>{geom_gml}</{geometry_operation}></Filter>"
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

View File

@ -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