#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
							
								
									71f88f7218
								
							
						
					
					
						commit
						440a4b04d5
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user