Files
konova/konova/forms/geometry_form.py
T
mpeltriaux 8ac8952729 # Refactors API geometry processing
* refactors API geometry processing to use GeoJsonValidator and GeometryProcessor just like SimpleGeomForm
* moves cast_to_multipolygon, cast_to_rlp_srid, flatten_geom_to_2D, is_valid_4326 and is_area_valid into GeometryProcessor
* adds changeable SRID to GeoJsonValidator constructor
* updates test data
* updates test cases
2026-09-12 17:58:00 +02:00

168 lines
5.6 KiB
Python

"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.22
"""
import json
from django.contrib.gis.geos import MultiPolygon
from django.utils.translation import gettext_lazy as _
from django.forms import JSONField
from konova.forms.base_form import BaseForm
from konova.models import Geometry
from konova.settings import GEOM_MAX_VERTICES
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.geometry.geometry_validator import GeoJsonValidator, GeometryProcessor
from user.models import UserActionLogEntry
class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
read_only: bool = True
_geometry_simplified: bool = False
output = JSONField(
label=_("Geometry"),
help_text=_(""),
label_suffix="",
required=False,
disabled=False,
)
_num_geometries_ignored: int = 0
empty = False
def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True)
super().__init__(*args, **kwargs)
# Initialize geometry
try:
geom = self.instance.geometry.geom
self.empty = geom.empty
if self.empty:
raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
geojson = self._set_geojson_properties(geojson, title=self.instance.identifier or None)
geom = json.dumps(geojson)
except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = json.dumps({})
self.empty = True
self.initialize_form_field("output", geom)
def is_valid(self):
""" Custom is_valid method for SimpleGeomForm
Returns:
"""
super().is_valid()
is_valid = True
# Make sure invalid geometry is properly rendered again to the user
# Therefore: write submitted data back into form field
# (does not matter whether we know if it is valid or invalid)
submitted_data = self.data["output"]
submitted_data = json.loads(submitted_data)
submitted_data = self._set_geojson_properties(submitted_data)
self.initialize_form_field("output", json.dumps(submitted_data))
# Get geojson from form for validity checking
geom = self.data.get("output", json.dumps({}))
geom = json.loads(geom)
geojson_validator = GeoJsonValidator(geom, DEFAULT_SRID_RLP)
geojson_validator.validate()
if geojson_validator.errors:
self.add_error("output", geojson_validator.errors)
return False
# If cleaned_data is not proper empty dict, initialize one
if self.cleaned_data is None:
self.cleaned_data = {}
self.cleaned_data["output"] = geojson_validator.validated_geometry.ewkt
self._num_geometries_ignored = geojson_validator.num_ignored_geometries
return is_valid
def save(self, action: UserActionLogEntry):
""" Saves the form's geometry
Creates a new geometry entry if none is set, yet
Args:
action ():
Returns:
"""
try:
if self.instance is None or self.instance.geometry is None:
raise LookupError
geometry = self.instance.geometry
geometry.geom = self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP))
geometry.modified = action
geometry.save()
except LookupError:
# No geometry or linked instance holding a geometry exist --> create a new one!
geometry = Geometry.objects.create(
geom=self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP)),
created=action,
)
is_vertices_num_valid = GeometryProcessor.is_vertices_num_valid(geometry.geom)
if not is_vertices_num_valid:
geometry.geom = GeometryProcessor.simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
geometry.save()
self._geometry_simplified = True
# Start parcel update and geometry conflict checking procedure in a background process
celery_update_parcels.delay(geometry.id)
celery_check_for_geometry_conflicts.delay(geometry.id)
return geometry
def get_num_geometries_ignored(self):
""" Returns the number of geometries which had to be ignored for various reasons
Returns:
"""
return self._num_geometries_ignored
def has_geometry_simplified(self):
""" Returns whether the geometry has been simplified or not.
Returns:
"""
return self._geometry_simplified
def _set_geojson_properties(self, geojson: dict, title: str = None):
""" Toggles the editable property of the geojson for proper handling in map client
Args:
geojson (dict): The GeoJson
title (str): An alternative title for the geometry
Returns:
geojson (dict): The altered GeoJson
"""
features = geojson.get("features", [])
for feature in features:
if not feature.get("properties", None):
feature["properties"] = {}
feature["properties"]["editable"] = not self.read_only
if title:
feature["properties"]["title"] = title
return geojson