* improves the way parcel-geometry relations are stored on the DB * instead of a numerical sequence we switched to UUID, so no sequence will run out at anytime (new model: ParcelIntersection) * instead of dropping all M2M relations between parcel and geometry on each calculation, we keep the ones that still exist, drop the ones that do not exist and add new ones (if new ones exist)
188 lines
6.2 KiB
Python
188 lines
6.2 KiB
Python
"""
|
|
Author: Michel Peltriaux
|
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
Created on: 15.11.21
|
|
|
|
"""
|
|
from django.contrib.gis.db.models import MultiPolygonField
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
from konova.models import BaseResource, UuidModel
|
|
from konova.utils.wfs.spatial import ParcelWFSFetcher
|
|
|
|
|
|
class Geometry(BaseResource):
|
|
"""
|
|
Geometry model
|
|
"""
|
|
from konova.settings import DEFAULT_SRID
|
|
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
|
|
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
self.check_for_conflicts()
|
|
|
|
def check_for_conflicts(self):
|
|
""" Checks for new geometry overlaps
|
|
|
|
Creates a new GeometryConflict entry for each overlap to another geometry, which has already been there before
|
|
|
|
Returns:
|
|
|
|
"""
|
|
# If no geometry is given or important data is missing, we can not perform any checks
|
|
if self.geom is None:
|
|
return None
|
|
|
|
self.recheck_existing_conflicts()
|
|
|
|
overlapping_geoms = Geometry.objects.filter(
|
|
geom__intersects=self.geom,
|
|
).exclude(
|
|
id=self.id
|
|
).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):
|
|
""" Rechecks GeometryConflict entries
|
|
|
|
If a conflict seems to be resolved due to no longer intersection between the two geometries, the entry
|
|
will be deleted.
|
|
|
|
Returns:
|
|
|
|
"""
|
|
all_conflicts_as_conflicting = self.conflicts_geometries.all()
|
|
still_conflicting_conflicts = all_conflicts_as_conflicting.filter(
|
|
affected_geometry__geom__intersects=self.geom
|
|
)
|
|
resolved_conflicts = all_conflicts_as_conflicting.exclude(id__in=still_conflicting_conflicts)
|
|
resolved_conflicts.delete()
|
|
|
|
all_conflicted_by_conflicts = self.conflicted_by_geometries.all()
|
|
still_conflicting_conflicts = all_conflicted_by_conflicts.filter(
|
|
conflicting_geometry__geom__intersects=self.geom
|
|
)
|
|
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
|
|
|
|
Returns:
|
|
objs (list): The list of objects
|
|
"""
|
|
objs = []
|
|
sets = [
|
|
self.intervention_set,
|
|
self.compensation_set,
|
|
self.ema_set,
|
|
self.ecoaccount_set,
|
|
]
|
|
for _set in sets:
|
|
set_objs = _set.filter(
|
|
deleted=None
|
|
)
|
|
objs += set_objs
|
|
return objs
|
|
|
|
def update_parcels(self):
|
|
""" Updates underlying parcel information
|
|
|
|
Returns:
|
|
|
|
"""
|
|
from konova.models import Parcel, District, ParcelIntersection
|
|
parcel_fetcher = ParcelWFSFetcher(
|
|
geometry_id=self.id,
|
|
)
|
|
typename = "ave:Flurstueck"
|
|
fetched_parcels = parcel_fetcher.get_features(
|
|
typename
|
|
)
|
|
_now = timezone.now()
|
|
underlying_parcels = []
|
|
for result in fetched_parcels:
|
|
fetched_parcel = result[typename]
|
|
# There could be parcels which include the word 'Flur',
|
|
# which needs to be deleted and just keep the numerical values
|
|
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
|
|
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "")
|
|
parcel_obj = Parcel.objects.get_or_create(
|
|
gmrkng=fetched_parcel["ave:gemarkung"],
|
|
flr=flr_val,
|
|
flrstck_nnr=fetched_parcel['ave:flstnrnen'],
|
|
flrstck_zhlr=fetched_parcel['ave:flstnrzae'],
|
|
)[0]
|
|
district = District.objects.get_or_create(
|
|
gmnd=fetched_parcel["ave:gemeinde"],
|
|
krs=fetched_parcel["ave:kreis"],
|
|
)[0]
|
|
parcel_obj.district = district
|
|
parcel_obj.updated_on = _now
|
|
parcel_obj.save()
|
|
underlying_parcels.append(parcel_obj)
|
|
|
|
# Update the linked parcels
|
|
self.parcels.set(underlying_parcels)
|
|
|
|
# Set the calculated_on intermediate field, so this related data will be found on lookups
|
|
intersections_without_ts = self.parcelintersection_set.filter(
|
|
parcel__in=self.parcels.all(),
|
|
calculated_on__isnull=True,
|
|
)
|
|
for entry in intersections_without_ts:
|
|
entry.calculated_on = _now
|
|
ParcelIntersection.objects.bulk_update(
|
|
intersections_without_ts,
|
|
["calculated_on"]
|
|
)
|
|
|
|
def get_underlying_parcels(self):
|
|
""" Getter for related parcels and their districts
|
|
|
|
Returns:
|
|
parcels (QuerySet): The related parcels as queryset
|
|
"""
|
|
|
|
parcels = self.parcels.filter(
|
|
parcelintersection__calculated_on__isnull=False,
|
|
).prefetch_related(
|
|
"district"
|
|
).order_by(
|
|
"gmrkng",
|
|
)
|
|
|
|
return parcels
|
|
|
|
|
|
class GeometryConflict(UuidModel):
|
|
"""
|
|
Geometry conflicts model
|
|
|
|
If a new/edited geometry overlays an existing geometry, there will be a new GeometryConflict on the db
|
|
"""
|
|
conflicting_geometry = models.ForeignKey(
|
|
Geometry,
|
|
on_delete=models.CASCADE,
|
|
help_text="The geometry which came second",
|
|
related_name="conflicts_geometries"
|
|
)
|
|
affected_geometry = models.ForeignKey(
|
|
Geometry,
|
|
on_delete=models.CASCADE,
|
|
help_text="The geometry which came first",
|
|
related_name="conflicted_by_geometries"
|
|
)
|
|
detected_on = models.DateTimeField(auto_now_add=True, null=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.conflicting_geometry.id} conflicts with {self.affected_geometry.id}"
|