Merge pull request '49_Parcel_calculation' (#56) from 49_Parcel_calculation into master
Reviewed-on: SGD-Nord/konova#56pull/58/head
commit
e618d454e2
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||||
|
Created on: 04.01.22
|
||||||
|
|
||||||
|
"""
|
||||||
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
|
from konova.models import Geometry, Parcel, District
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseKonovaCommand):
|
||||||
|
help = "Checks the database' sanity and removes unused entries"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
self.update_all_parcels()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._break_line()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def update_all_parcels(self):
|
||||||
|
num_parcels_before = Parcel.objects.count()
|
||||||
|
num_districts_before = District.objects.count()
|
||||||
|
self._write_warning("=== Update parcels and districts ===")
|
||||||
|
geometries = Geometry.objects.all().exclude(
|
||||||
|
geom=None
|
||||||
|
)
|
||||||
|
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
|
||||||
|
for geometry in geometries:
|
||||||
|
geometry.update_parcels()
|
||||||
|
|
||||||
|
num_parcels_after = Parcel.objects.count()
|
||||||
|
num_districts_after = District.objects.count()
|
||||||
|
if num_parcels_after != num_parcels_before:
|
||||||
|
self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.")
|
||||||
|
if num_districts_after != num_districts_before:
|
||||||
|
self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.")
|
||||||
|
|
||||||
|
self._write_success("Updating parcels done!")
|
||||||
|
self._break_line()
|
@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
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", blank=True)
|
||||||
|
district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
|
||||||
|
gmrkng = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
help_text="Gemarkung",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
flrstck_nnr = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
help_text="Flurstücksnenner",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
flrstck_zhlr = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
help_text="Flurstückszähler",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
flr = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
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_zhlr} | {self.flrstck_nnr}"
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
krs = models.CharField(
|
||||||
|
max_length=1000,
|
||||||
|
help_text="Kreis",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.gmnd} | {self.krs}"
|
@ -0,0 +1,29 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<div>
|
||||||
|
<h3>{% trans 'Spatial reference' %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="table-container w-100 scroll-300">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans 'Kreis' %}</th>
|
||||||
|
<th scope="col">{% trans 'Gemarkung' %}</th>
|
||||||
|
<th scope="col">{% trans 'Parcel' %}</th>
|
||||||
|
<th scope="col">{% trans 'Parcel counter' %}</th>
|
||||||
|
<th scope="col">{% trans 'Parcel number' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for parcel in parcels %}
|
||||||
|
<tr>
|
||||||
|
<td>{{parcel.district.krs|default_if_none:"-"}}</td>
|
||||||
|
<td>{{parcel.gmrkng|default_if_none:"-"}}</td>
|
||||||
|
<td>{{parcel.flr|default_if_none:"-"}}</td>
|
||||||
|
<td>{{parcel.flrstck_zhlr|default_if_none:"-"}}</td>
|
||||||
|
<td>{{parcel.flrstck_nnr|default_if_none:"-"}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -0,0 +1,175 @@
|
|||||||
|
"""
|
||||||
|
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 requests.auth import HTTPDigestAuth
|
||||||
|
|
||||||
|
from konova.settings import DEFAULT_SRID_RLP, PARCEL_WFS_USER, PARCEL_WFS_PW
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractWFSFetcher:
|
||||||
|
""" Base class for fetching WFS data
|
||||||
|
|
||||||
|
"""
|
||||||
|
# base_url represents not the capabilities url but the parameter-free base url
|
||||||
|
base_url = None
|
||||||
|
version = None
|
||||||
|
auth_user = None
|
||||||
|
auth_pw = None
|
||||||
|
auth_digest_obj = None
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, version: str = "1.1.0", auth_user: str = None, auth_pw: str = None, *args, **kwargs):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.version = version
|
||||||
|
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,
|
||||||
|
self.auth_pw
|
||||||
|
)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_features(self, feature_identifier: str, filter_str: str):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ParcelWFSFetcher(AbstractWFSFetcher):
|
||||||
|
""" Fetches features from a special parcel WFS
|
||||||
|
|
||||||
|
"""
|
||||||
|
geometry_id = None
|
||||||
|
geometry_property_name = None
|
||||||
|
count = 100
|
||||||
|
|
||||||
|
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,
|
||||||
|
geometry_operation: str,
|
||||||
|
filter_srid: str = None):
|
||||||
|
""" Creates a xml spatial filter according to the WFS filter specification
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities)
|
||||||
|
filter_srid (str): Used to transform the geometry into the spatial reference system identified by this srid
|
||||||
|
|
||||||
|
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
|
||||||
|
).annotate(
|
||||||
|
transformed=Transform(srid=filter_srid, expression="geom")
|
||||||
|
).annotate(
|
||||||
|
gml=AsGML('transformed')
|
||||||
|
).first().gml
|
||||||
|
spatial_filter = f"<Filter><{geometry_operation}><PropertyName>{self.geometry_property_name}</PropertyName>{geom_gml}</{geometry_operation}></Filter>"
|
||||||
|
return spatial_filter
|
||||||
|
|
||||||
|
def _create_post_data(self,
|
||||||
|
geometry_operation: str,
|
||||||
|
filter_srid: str = None,
|
||||||
|
typenames: str = None,
|
||||||
|
start_index: int = 0,
|
||||||
|
):
|
||||||
|
""" Creates a POST body content for fetching features
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities)
|
||||||
|
filter_srid (str): Used to transform the geometry into the spatial reference system identified by this srid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_filter (str): A proper xml WFS filter
|
||||||
|
"""
|
||||||
|
start_index = str(start_index)
|
||||||
|
spatial_filter = self._create_spatial_filter(
|
||||||
|
geometry_operation,
|
||||||
|
filter_srid
|
||||||
|
)
|
||||||
|
_filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>'
|
||||||
|
return _filter
|
||||||
|
|
||||||
|
def get_features(self,
|
||||||
|
typenames: str,
|
||||||
|
spatial_operator: str = "Intersects",
|
||||||
|
filter_srid: str = None,
|
||||||
|
start_index: int = 0,
|
||||||
|
):
|
||||||
|
""" Fetches features from the WFS using POST
|
||||||
|
|
||||||
|
POST is required since GET has a character limit around 4000. Having a larger filter would result in errors,
|
||||||
|
which do not occur in case of POST.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
features = []
|
||||||
|
while start_index is not None:
|
||||||
|
post_body = self._create_post_data(
|
||||||
|
spatial_operator,
|
||||||
|
filter_srid,
|
||||||
|
typenames,
|
||||||
|
start_index
|
||||||
|
)
|
||||||
|
response = requests.post(
|
||||||
|
url=self.base_url,
|
||||||
|
data=post_body,
|
||||||
|
auth=self.auth_digest_obj
|
||||||
|
)
|
||||||
|
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
content = xmltodict.parse(content)
|
||||||
|
collection = content.get(
|
||||||
|
"wfs:FeatureCollection",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
members = collection.get(
|
||||||
|
"wfs:member",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
if len(members) > 1:
|
||||||
|
features += members
|
||||||
|
else:
|
||||||
|
features += [members]
|
||||||
|
|
||||||
|
if collection.get("@next", None) is not None:
|
||||||
|
start_index += self.count
|
||||||
|
else:
|
||||||
|
start_index = None
|
||||||
|
|
||||||
|
return features
|
Binary file not shown.
Loading…
Reference in New Issue