#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
This commit is contained in:
parent
93d1cb9330
commit
46e237f0e2
@ -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):
|
||||
"""
|
||||
|
@ -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}"
|
||||
|
@ -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!")
|
||||
|
92
konova/utils/wfs/spatial.py
Normal file
92
konova/utils/wfs/spatial.py
Normal 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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user