Compare commits

...

13 Commits

Author SHA1 Message Date
mpeltriaux b6b740a31c Merge pull request '585 invalid 25832 coordinates' (#586) from 585_Invalid_25832_coordinates into master
Reviewed-on: #586
2026-09-13 12:12:57 +02:00
mpeltriaux 7c200d9034 # Rename check method
* renames is_valid_coord_area to is_valid_coord_range for more clarity
2026-09-13 12:11:18 +02:00
mpeltriaux 750715105e # EPSG:25832 Coordinate validity
* adds epsg:25832 coordinate range check to GeoJsonValidator
* improves resolving of min/max coordinates from envelope on is_valid_coord_area()
2026-09-13 12:10:37 +02:00
mpeltriaux a994554650 # Coordinate validity check
* refactors coordinate validity check in generic base method
* introduces specific check for epsg:25832 (RLP)
* refactors check for epsg:4326 to use generic base method
2026-09-13 11:51:10 +02:00
mpeltriaux 4c7c667bdb # Error fetching
* adds proper fetching of GDAL Exceptions in case of further problems in the future
2026-09-13 11:36:28 +02:00
mpeltriaux 7e136f4022 Merge pull request '# HOTFIX' (#584) from hotfix_feature_checl into master
Reviewed-on: #584
2026-09-12 18:30:56 +02:00
mpeltriaux 1393491fb7 # HOTFIX
* fixes bug where specific case leads to no-feature detection
2026-09-12 18:30:13 +02:00
mpeltriaux a2a726fba2 Merge pull request '565 unify geometry quality checking' (#582) from 565_Unify_geometry_quality_checking into master
Reviewed-on: #582
2026-09-12 17:58:55 +02:00
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
mpeltriaux a067aafde1 # Refactoring geometry processing
* adds GeometryProcessor
* adds GeoJsonValidator
* refactors SimpleGeomForm is_valid() logic using these new classes
2026-09-12 16:34:34 +02:00
mpeltriaux 84b4acfb00 Merge pull request '580 uuid resolving 4xx error' (#581) from 580_UUID_resolving_4xx_error into master
Reviewed-on: #581
2026-09-12 15:40:29 +02:00
mpeltriaux 552811caf3 # Requirements update
* updates requirements.txt to latest stable versions
2026-09-12 15:39:35 +02:00
mpeltriaux 80ec64c002 # Client error code
* refactors internal server error (validationError) into assertionError (client error)
2026-09-12 15:29:47 +02:00
18 changed files with 610 additions and 447 deletions
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,46 +1,28 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[
[ [
[ 7.489245186249832,
[ 50.29519575559556
7.845568656921382, ],
50.79829702304368 [
], 7.5244137991026605,
[ 50.29519575559556
7.837371826171871, ],
50.80155187891526 [
], 7.5244137991026605,
[ 50.275124048323875
7.835698127746578, ],
50.805267562209806 [
], 7.489245186249832,
[ 50.275124048323875
7.841062545776364, ],
50.806623577403386 [
], 7.489245186249832,
[ 50.29519575559556
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
] ]
]
], ],
"properties": { "properties": {
"title": "TEST_compensation_CHANGED", "title": "TEST_compensation_CHANGED",
@@ -1,46 +1,28 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[
[ [
[ 7.489245186249832,
[ 50.29519575559556
7.845568656921382, ],
50.79829702304368 [
], 7.5244137991026605,
[ 50.29519575559556
7.837371826171871, ],
50.80155187891526 [
], 7.5244137991026605,
[ 50.275124048323875
7.835698127746578, ],
50.805267562209806 [
], 7.489245186249832,
[ 50.275124048323875
7.841062545776364, ],
50.806623577403386 [
], 7.489245186249832,
[ 50.29519575559556
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
] ]
]
], ],
"properties": { "properties": {
"title": "TEST_account_CHANGED", "title": "TEST_account_CHANGED",
+21 -39
View File
@@ -1,46 +1,28 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[
[ [
[ 7.489245186249832,
[ 50.29519575559556
7.845568656921382, ],
50.79829702304368 [
], 7.5244137991026605,
[ 50.29519575559556
7.837371826171871, ],
50.80155187891526 [
], 7.5244137991026605,
[ 50.275124048323875
7.835698127746578, ],
50.805267562209806 [
], 7.489245186249832,
[ 50.275124048323875
7.841062545776364, ],
50.806623577403386 [
], 7.489245186249832,
[ 50.29519575559556
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
] ]
]
], ],
"properties": { "properties": {
"title": "TEST_EMA_CHANGED", "title": "TEST_EMA_CHANGED",
@@ -1,46 +1,28 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[
[ [
[ 7.489245186249832,
[ 50.29519575559556
7.845568656921382, ],
50.79829702304368 [
], 7.5244137991026605,
[ 50.29519575559556
7.837371826171871, ],
50.80155187891526 [
], 7.5244137991026605,
[ 50.275124048323875
7.835698127746578, ],
50.805267562209806 [
], 7.489245186249832,
[ 50.275124048323875
7.841062545776364, ],
50.806623577403386 [
], 7.489245186249832,
[ 50.29519575559556
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
] ]
]
], ],
"properties": { "properties": {
"title": "Test_intervention_CHANGED", "title": "Test_intervention_CHANGED",
+10 -9
View File
@@ -9,10 +9,11 @@ Created on: 28.01.22
import json import json
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import MultiPolygon
from django.urls import reverse from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
from konova.models import Geometry from konova.utils.geometry.geometry_validator import GeometryProcessor
class APIV1UpdateTestCase(BaseAPIV1TestCase): class APIV1UpdateTestCase(BaseAPIV1TestCase):
@@ -64,8 +65,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.intervention.geometry.geom) self.assertTrue(put_geom.equals_exact(self.intervention.geometry.geom,0.00001))
self.assertEqual(put_props["title"], self.intervention.title) self.assertEqual(put_props["title"], self.intervention.title)
self.assertNotEqual(modified_on, self.intervention.modified) self.assertNotEqual(modified_on, self.intervention.modified)
self.assertEqual(put_props["responsible"]["registration_file_number"], self.intervention.responsible.registration_file_number) self.assertEqual(put_props["responsible"]["registration_file_number"], self.intervention.responsible.registration_file_number)
@@ -94,8 +95,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.compensation.geometry.geom) self.assertTrue(put_geom.equals_exact(self.compensation.geometry.geom, 0.00001))
self.assertEqual(put_props["title"], self.compensation.title) self.assertEqual(put_props["title"], self.compensation.title)
self.assertNotEqual(modified_on, self.compensation.modified) self.assertNotEqual(modified_on, self.compensation.modified)
self.assertEqual(put_props["is_cef"], self.compensation.is_cef) self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
@@ -124,8 +125,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.eco_account.geometry.geom) self.assertTrue(put_geom.equals_exact(self.eco_account.geometry.geom, 0.00001))
self.assertEqual(put_props["title"], self.eco_account.title) self.assertEqual(put_props["title"], self.eco_account.title)
self.assertNotEqual(modified_on, self.eco_account.modified) self.assertNotEqual(modified_on, self.eco_account.modified)
self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface)) self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface))
@@ -156,8 +157,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.ema.geometry.geom) self.assertTrue(put_geom.equals_exact(self.ema.geometry.geom,0.00001))
self.assertEqual(put_props["title"], self.ema.title) self.assertEqual(put_props["title"], self.ema.title)
self.assertNotEqual(modified_on, self.ema.modified) self.assertNotEqual(modified_on, self.ema.modified)
self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office) self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office)
+27 -18
View File
@@ -11,12 +11,14 @@ from abc import abstractmethod
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q from django.db.models import Q
from api.models import ExternalIdentifier from api.models import ExternalIdentifier
from konova.models import Geometry from konova.models import Geometry
from konova.sub_settings.lanis_settings import DEFAULT_SRID
from konova.utils.geometry.geometry_validator import GeoJsonValidator
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@@ -102,13 +104,16 @@ class AbstractModelAPISerializer:
Returns: Returns:
serialized_data (dict) serialized_data (dict)
""" """
entries = self.model.objects.filter( try:
**self.lookup entries = self.model.objects.filter(
).filter( **self.lookup
self.shared_lookup ).filter(
).order_by( self.shared_lookup
"id" ).order_by(
).distinct() "id"
).distinct()
except ValidationError:
raise AssertionError("ID expected to be of UUID4 for internal lookup.")
self.paginator = Paginator(entries, self.rpp) self.paginator = Paginator(entries, self.rpp)
requested_entries = self.paginator.page(self.page_number) requested_entries = self.paginator.page(self.page_number)
@@ -153,16 +158,20 @@ class AbstractModelAPISerializer:
Returns: Returns:
geometry (GEOSGeometry) geometry (GEOSGeometry)
""" """
if isinstance(geojson, dict): if isinstance(geojson, str):
geojson = json.dumps(geojson) geojson = json.loads(geojson)
geometry = geos.fromstr(geojson)
if not geometry.valid: geojson_validator = GeoJsonValidator(geojson, DEFAULT_SRID)
raise ValueError(f"Invalid geometry: {geometry.valid_reason}") geojson_validator.validate()
is_4326 = Geometry.is_valid_4326(geometry)
if not is_4326: if geojson_validator.errors:
raise ValueError("Geometry not in EPSG:4326 (WGS84). Unknown spatial reference system.") raise ValueError(geojson_validator.errors)
geometry = Geometry.cast_to_rlp_srid(geometry)
geometry = Geometry.cast_to_multipolygon(geometry) geometry = geojson_validator.validated_geometry
is_input_geometry_too_small = geometry.area == 0.0 and geojson_validator.num_ignored_geometries > 0
if is_input_geometry_too_small:
raise ValueError(f"Your area was too small to be processed! Must be > 1m²!")
return geometry return geometry
def _get_obj_from_db(self, id, user): def _get_obj_from_db(self, id, user):
+17 -120
View File
@@ -7,9 +7,7 @@ Created on: 15.08.22
""" """
import json import json
from django.contrib.gis import gdal from django.contrib.gis.geos import MultiPolygon
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.forms import JSONField from django.forms import JSONField
@@ -18,6 +16,7 @@ from konova.models import Geometry
from konova.settings import GEOM_MAX_VERTICES from konova.settings import GEOM_MAX_VERTICES
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.geometry.geometry_validator import GeoJsonValidator, GeometryProcessor
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -60,6 +59,11 @@ class SimpleGeomForm(BaseForm):
self.initialize_form_field("output", geom) self.initialize_form_field("output", geom)
def is_valid(self): def is_valid(self):
""" Custom is_valid method for SimpleGeomForm
Returns:
"""
super().is_valid() super().is_valid()
is_valid = True is_valid = True
@@ -75,118 +79,21 @@ class SimpleGeomForm(BaseForm):
geom = self.data.get("output", json.dumps({})) geom = self.data.get("output", json.dumps({}))
geom = json.loads(geom) geom = json.loads(geom)
# Initialize features list with empty MultiPolygon, so that an empty input will result in a geojson_validator = GeoJsonValidator(geom, DEFAULT_SRID_RLP)
# proper empty MultiPolygon object geojson_validator.validate()
features = []
features_json = geom.get("features", [])
accepted_ogr_types = [
"Polygon",
"Polygon25D",
"MultiPolygon",
"MultiPolygon25D",
]
# Check validity for each feature of the geometry
for feature in features_json:
feature_geom = feature.get("geometry", feature)
if feature_geom is None:
# Fallback for rare cases where a feature does not contain any geometry
continue
# Try to create a geometry object from the single feature if geojson_validator.errors:
feature_geom = json.dumps(feature_geom) self.add_error("output", geojson_validator.errors)
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP) return False
geometry_has_unwanted_dimensions = g.coord_dim > 2 # If cleaned_data is not proper empty dict, initialize one
if geometry_has_unwanted_dimensions:
g = self.__flatten_geom_to_2D(g)
geometry_type_is_accepted = g.geom_type not in accepted_ogr_types
if geometry_type_is_accepted:
self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid &= False
return is_valid
is_area_valid = self.__is_area_valid(g)
if not is_area_valid:
# Geometries with an invalid size will not be saved to the db
# We assume these are malicious snippets which are not supposed to be in the geometry in the first place
self._num_geometries_ignored += 1
continue
# Whatever this geometry object is -> try to create a Polygon from it
# The resulting polygon object automatically detects whether a valid polygon has been created or not
g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid
if not g.valid:
self.add_error("output", g.valid_reason)
return is_valid
# If the resulting polygon is just a single polygon, we add it to the list of properly casted features
if isinstance(g, Polygon):
features.append(g)
elif isinstance(g, MultiPolygon):
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
# If so, we extract all polygons from the MultiPolygon and extend the casted features list
features.extend(list(g))
# Unionize all polygon features into one new MultiPolygon
if features:
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
else:
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
form_geom = Geometry.cast_to_multipolygon(form_geom)
# Write unionized Multipolygon back into cleaned data
if self.cleaned_data is None: if self.cleaned_data is None:
self.cleaned_data = {} self.cleaned_data = {}
self.cleaned_data["output"] = form_geom.ewkt self.cleaned_data["output"] = geojson_validator.validated_geometry.ewkt
self._num_geometries_ignored = geojson_validator.num_ignored_geometries
return is_valid return is_valid
def __is_vertices_num_valid(self):
""" Checks whether the number of vertices in the geometry is not too high
Returns:
"""
geom = self.cleaned_data.get("output")
g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP)
num_vertices = g.num_coords
return num_vertices <= GEOM_MAX_VERTICES
def __is_area_valid(self, geom: gdal.OGRGeometry):
""" Checks whether the area is at least > 1m²
Returns:
"""
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
return is_area_valid
def __simplify_geometry(self, geom, max_vert: int):
""" Simplifies a geometry
Geometry will be simplified until a threshold of max vertices has been reached.
Args:
geom (MultiPolygon): The geometry
max_vert (int): Threshold of maximum vertices in geometry
Returns:
geom (MultiPolygon): The simplified geometry
"""
tolerance = 0.1
n = geom.num_coords
while(n > max_vert):
geom = geom.simplify(tolerance)
n = geom.num_coords
tolerance += 0.1
return geom
def save(self, action: UserActionLogEntry): def save(self, action: UserActionLogEntry):
""" Saves the form's geometry """ Saves the form's geometry
@@ -213,9 +120,9 @@ class SimpleGeomForm(BaseForm):
created=action, created=action,
) )
is_vertices_num_valid = self.__is_vertices_num_valid() is_vertices_num_valid = GeometryProcessor.is_vertices_num_valid(geometry.geom)
if not is_vertices_num_valid: if not is_vertices_num_valid:
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES) geometry.geom = GeometryProcessor.simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
geometry.save() geometry.save()
self._geometry_simplified = True self._geometry_simplified = True
@@ -240,16 +147,6 @@ class SimpleGeomForm(BaseForm):
""" """
return self._geometry_simplified return self._geometry_simplified
def __flatten_geom_to_2D(self, geom):
"""
Enforces a given OGRGeometry from higher dimensions into 2D
"""
wkt_w = WKTWriter(dim=2)
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
geom = gdal.OGRGeometry(g_wkt)
return geom
def _set_geojson_properties(self, geojson: dict, title: str = None): def _set_geojson_properties(self, geojson: dict, title: str = None):
""" Toggles the editable property of the geojson for proper handling in map client """ Toggles the editable property of the geojson for proper handling in map client
-53
View File
@@ -419,39 +419,6 @@ class Geometry(BaseResource):
return complexity_factor return complexity_factor
@staticmethod
def cast_to_multipolygon(input_geom):
""" If input_geom is not a MultiPolygon, cast to MultiPolygon
Args:
input_geom ():
Returns:
output_geom
"""
output_geom = input_geom
if not isinstance(input_geom, MultiPolygon):
try:
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
except TypeError as e:
raise AssertionError(f"Only (Multi)Polygon allowed! Could not convert {input_geom.geom_type} to MultiPolygon")
return output_geom
@staticmethod
def cast_to_rlp_srid(input_geom):
""" If input_geom is not of RLP SRID (25832), cast to RLP SRID
Args:
input_geom ():
Returns:
output_geom
"""
output_geom = input_geom
if output_geom.srid != DEFAULT_SRID_RLP:
output_geom.transform(DEFAULT_SRID_RLP)
return output_geom
def get_conflict_geometries(self): def get_conflict_geometries(self):
""" Getter for geometry ids which conflict with this geometry or are conflicted by this one """ Getter for geometry ids which conflict with this geometry or are conflicted by this one
@@ -462,26 +429,6 @@ class Geometry(BaseResource):
conflict_geoms = Geometry.objects.filter(id__in=conflict_geoms_id) conflict_geoms = Geometry.objects.filter(id__in=conflict_geoms_id)
return conflict_geoms return conflict_geoms
@staticmethod
def is_valid_4326(geometry):
""" Checks whether a given geometry's coordinates are in a valid range to be of EPSG:4326
Args:
geometry: The geometry
Returns:
ret_val (bool): Whether the geometry is valid EPSG:4326
"""
if not geometry.centroid.coords:
# No coordinates at all found, therefore technically proper 4326
return True
try:
lat,lon = geometry.centroid.coords
return (-90.0 <= lat <= 90.0) and (-180.0 <= lon <= 180.0)
except IndexError:
return False
class GeometryConflict(UuidModel): class GeometryConflict(UuidModel):
""" """
+5
View File
@@ -0,0 +1,5 @@
"""
Author: Michel Peltriaux
Created on: 12.09.26
"""
+353
View File
@@ -0,0 +1,353 @@
"""
Author: Michel Peltriaux
Created on: 12.09.26
"""
import json
from django.contrib.gis import gdal
from django.contrib.gis.gdal.srs import SpatialReference
from django.contrib.gis.geos import Polygon, MultiPolygon
from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _
from konova.settings import GEOM_MAX_VERTICES
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, DEFAULT_SRID
class GeometryProcessor:
""" GeometryProcessor wraps methods for checking or processing geometry objects
"""
@staticmethod
def simplify_geometry(geom: MultiPolygon, max_vert: int):
""" Simplifies a geometry
Geometry will be simplified until a threshold of max vertices has been reached.
Args:
geom (MultiPolygon): The geometry
max_vert (int): Threshold of maximum vertices in geometry
Returns:
geom (MultiPolygon): The simplified geometry
"""
tolerance = 0.1
n = geom.num_coords
while(n > max_vert):
geom = geom.simplify(tolerance)
n = geom.num_coords
tolerance += 0.1
return geom
@staticmethod
def is_vertices_num_valid(geometry: MultiPolygon):
""" Checks whether the number of vertices in the geometry is not too high
Returns:
"""
return geometry.num_coords <= GEOM_MAX_VERTICES
@staticmethod
def is_valid_4326(geometry: MultiPolygon) -> bool:
""" Checks whether a given geometry's coordinates are in a valid range to be of EPSG:4326
Args:
geometry: The geometry
Returns:
ret_val (bool): Whether the geometry is valid EPSG:4326
"""
assert geometry.srid == DEFAULT_SRID
return GeometryProcessor.is_valid_coord_range(
geometry,
-90.0,
90.0,
-180.0,
180.0,
)
@staticmethod
def is_valid_25832(geometry: MultiPolygon) -> bool:
""" Checks whether a given geometry's coordinates are in a valid range to be of EPSG:25832
Args:
geometry: The geometry
Returns:
ret_val (bool): Whether the geometry is valid EPSG:25832
"""
assert geometry.srid == DEFAULT_SRID_RLP
return GeometryProcessor.is_valid_coord_range(
geometry,
166_000,
834_000,
0,
9_329_000,
)
@staticmethod
def is_valid_coord_range(geometry: MultiPolygon,
min_x:float|int,
max_x:float|int,
min_y:float|int,
max_y: float|int) -> bool:
""" Checks whether a given geometry's coordinates are in a valid range defined by input parameters
Args:
geometry: The geometry
min_x (float|int): The min x value
max_x (float|int): The max x value
min_y (float|int): The min y value
max_y (float|int): The max y value
Returns:
ret_val (bool): Whether the geometry's coordinates stays inside the defined range
"""
if not geometry.centroid.coords:
# No coordinates at all found, therefore technically proper 4326
return True
try:
env_min_x = geometry.envelope.min_x
env_max_x = geometry.envelope.max_x
env_min_y = geometry.envelope.min_y
env_max_y = geometry.envelope.max_x
valid = (
min_x <= env_min_x and
env_max_x <= max_x and
min_y <= env_min_y and
env_max_y <= max_y
)
return valid
except IndexError:
return False
@staticmethod
def cast_to_multipolygon(input_geom: MultiPolygon) -> MultiPolygon:
""" If input_geom is not a MultiPolygon, cast to MultiPolygon
Args:
input_geom ():
Returns:
output_geom
"""
output_geom = input_geom
if not isinstance(input_geom, MultiPolygon):
try:
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
except TypeError as e:
raise AssertionError(f"Only (Multi)Polygon allowed! Could not convert {input_geom.geom_type} to MultiPolygon")
return output_geom
@staticmethod
def cast_to_rlp_srid(input_geom: gdal.OGRGeometry) -> gdal.OGRGeometry:
""" If input_geom is not of RLP SRID (25832), cast to RLP SRID
Args:
input_geom ():
Returns:
output_geom
"""
output_geom = input_geom
if output_geom.srid != DEFAULT_SRID_RLP:
output_geom.transform(DEFAULT_SRID_RLP)
return output_geom
@staticmethod
def flatten_geom_to_2D(geom: gdal.OGRGeometry) -> gdal.OGRGeometry:
"""
Enforces a given OGRGeometry from higher dimensions into 2D
"""
wkt_w = WKTWriter(dim=2)
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
geom = gdal.OGRGeometry(g_wkt)
return geom
@staticmethod
def is_area_valid(geom: gdal.OGRGeometry) -> bool:
""" Checks whether the area is at least > 1m²
Returns:
"""
assert geom.srid == DEFAULT_SRID_RLP
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
return is_area_valid
class GeoJsonValidator:
""" GeoJson Validator validates geojson (e.g. from API or form input)
"""
_errors: list
_input_geojson: dict
_validated_geometry: MultiPolygon
_num_ignored_geometries: int
_accepted_ogr_types = [
"Polygon",
"Polygon25D",
"MultiPolygon",
"MultiPolygon25D",
]
def __init__(self, input_geojson: dict, srs: int) -> None:
assert input_geojson is not None
self._input_geojson = input_geojson
self._num_ignored_geometries = 0
self._errors = list()
self._srs = SpatialReference(srs)
@property
def num_ignored_geometries(self):
""" Getter
Returns:
"""
return self._num_ignored_geometries
@property
def input_geojson(self):
""" Getter
Returns:
"""
return self._input_geojson
@property
def validated_geometry(self):
""" Getter
Returns:
"""
return self._validated_geometry
@property
def errors(self):
""" Getter
Returns:
"""
return self._errors
def __add_error(self, text: str):
""" Wraps pushing new error into error list
Args:
text (str): The error text
Returns:
"""
self._errors.append(text)
def validate(self):
""" Validates input geojson
Returns:
"""
features = self._input_geojson.get("features", None)
is_input_geojson_empty = len(self._input_geojson) == 0
no_features_in_input_found = features is None
if not is_input_geojson_empty and no_features_in_input_found:
# check if _input_geojson is a feature itself
if self.__is_geojson_feature():
features = [
{
"geometry": self._input_geojson
}
]
else:
self.__add_error("Input does not seem to be geojson")
return
try:
validated_features = self.__validate_single_features(features)
except AssertionError as e:
self.__add_error(str(e))
return
# Unionize all polygon features into one new MultiPolygon
if validated_features:
multipolygon_geometry = MultiPolygon(*validated_features, srid=self._srs).unary_union
else:
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
multipolygon_geometry = MultiPolygon(srid=self._srs)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
multipolygon_geometry = GeometryProcessor.cast_to_multipolygon(multipolygon_geometry)
self._validated_geometry = multipolygon_geometry
def __validate_single_features(self, features: list) -> list:
validated_features = []
# Check validity for each feature of the geometry
for feature in features:
feature_geom = feature.get("geometry", feature)
if feature_geom is None:
# Fallback for rare cases where a feature does not contain any geometry
continue
# Try to create a geometry object from the single feature
feature_geom = json.dumps(feature_geom)
g = gdal.OGRGeometry(feature_geom, srs=self._srs)
if g.empty:
continue
g = GeometryProcessor.cast_to_rlp_srid(g)
if not GeometryProcessor.is_valid_25832(g):
raise AssertionError(_("This feature does not hold valid EPSG:25832 coordinates:\n {}".format(g.geojson)))
geometry_has_unwanted_dimensions = g.coord_dim > 2
if geometry_has_unwanted_dimensions:
g = GeometryProcessor.flatten_geom_to_2D(g)
geometry_type_is_accepted = g.geom_type not in self._accepted_ogr_types
if geometry_type_is_accepted:
raise AssertionError(_("Only surfaces allowed. Points or lines must be buffered."))
is_area_valid = GeometryProcessor.is_area_valid(g)
if not is_area_valid:
# Geometries with an invalid size will not be saved to the db
# We assume these are malicious snippets which are not supposed to be in the geometry in the first place
self._num_ignored_geometries += 1
continue
# Whatever this geometry object is -> try to create a Polygon from it
# The resulting polygon object automatically detects whether a valid polygon has been created or not
g = Polygon.from_ewkt(g.ewkt)
if not g.valid:
raise AssertionError(g.valid_reason)
# If the resulting polygon is just a single polygon, we add it to the list of properly created features
if isinstance(g, Polygon):
validated_features.append(g)
elif isinstance(g, MultiPolygon):
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
# If so, we extract each single polygons from the MultiPolygon and add them to the created features list
validated_features.extend(list(g))
return validated_features
def __is_geojson_feature(self):
""" Checks whether _input_geojson is a proper geojson feature
Returns:
"""
has_type = self._input_geojson.get("type", None) is not None
has_coordinates = self._input_geojson.get("coordinates", None) is not None
has_properties = self._input_geojson.get("properties", None) is not None
return has_type and has_coordinates and has_properties
+5 -1
View File
@@ -9,6 +9,7 @@ import json
from json import JSONDecodeError from json import JSONDecodeError
import requests import requests
from django.contrib.gis import gdal
from konova.sub_settings import schneider_settings from konova.sub_settings import schneider_settings
from konova.sub_settings.lanis_settings import DEFAULT_SRID from konova.sub_settings.lanis_settings import DEFAULT_SRID
@@ -35,7 +36,10 @@ class ParcelFetcher:
if geom.area < buffer_threshold: if geom.area < buffer_threshold:
# Fallback for malicious geometries which are way too small and would disappear on negative buffering # Fallback for malicious geometries which are way too small and would disappear on negative buffering
geom = geometry.geom geom = geometry.geom
geom.transform(DEFAULT_SRID) try:
geom.transform(DEFAULT_SRID)
except gdal.GDALException as e:
raise ValueError(f"{str(e)}\nCould not transform geometry (id: {geometry.id}) to {DEFAULT_SRID}:\n {geom.geojson}")
self.geojson = geom.ewkt self.geojson = geom.ewkt
self.results = [] self.results = []
Binary file not shown.
+67 -48
View File
@@ -30,7 +30,7 @@
#: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23 #: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23
#: konova/filters/mixins/self_created.py:24 konova/filters/mixins/share.py:23 #: konova/filters/mixins/self_created.py:24 konova/filters/mixins/share.py:23
#: konova/filters/mixins/user_log.py:17 konova/filters/mixins/user_log.py:18 #: konova/filters/mixins/user_log.py:17 konova/filters/mixins/user_log.py:18
#: konova/forms/geometry_form.py:32 konova/forms/modals/document_form.py:26 #: konova/forms/geometry_form.py:31 konova/forms/modals/document_form.py:26
#: konova/forms/modals/document_form.py:36 #: konova/forms/modals/document_form.py:36
#: konova/forms/modals/document_form.py:50 #: konova/forms/modals/document_form.py:50
#: konova/forms/modals/document_form.py:62 #: konova/forms/modals/document_form.py:62
@@ -45,7 +45,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-14 17:23+0100\n" "POT-Creation-Date: 2026-09-13 12:06+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -129,6 +129,7 @@ msgstr ""
#: analysis/templates/analysis/reports/includes/old_data/amount.html:3 #: analysis/templates/analysis/reports/includes/old_data/amount.html:3
#: compensation/forms/modals/compensation_action.py:66 #: compensation/forms/modals/compensation_action.py:66
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34
#: compensation/templates/compensation/report/eco_account/report.html:50
#: intervention/templates/intervention/detail/includes/deductions.html:31 #: intervention/templates/intervention/detail/includes/deductions.html:31
msgid "Amount" msgid "Amount"
msgstr "Menge" msgstr "Menge"
@@ -334,6 +335,7 @@ msgstr "Typ"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24 #: analysis/templates/analysis/reports/includes/old_data/amount.html:24
#: compensation/tables/compensation.py:87 #: compensation/tables/compensation.py:87
#: compensation/templates/compensation/report/eco_account/report.html:47
#: intervention/forms/modals/deduction.py:58 #: intervention/forms/modals/deduction.py:58
#: intervention/forms/modals/deduction.py:65 intervention/tables.py:92 #: intervention/forms/modals/deduction.py:65 intervention/tables.py:92
#: intervention/templates/intervention/detail/view.html:19 #: intervention/templates/intervention/detail/view.html:19
@@ -448,7 +450,8 @@ msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
#: compensation/forms/compensation.py:114 #: compensation/forms/compensation.py:114
#: compensation/views/compensation/compensation.py:121 #: compensation/views/compensation/compensation.py:96
#: compensation/views/compensation/compensation.py:160
msgid "New compensation" msgid "New compensation"
msgstr "Neue Kompensation" msgstr "Neue Kompensation"
@@ -475,7 +478,8 @@ msgid "When did the parties agree on this?"
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
#: compensation/forms/eco_account.py:73 #: compensation/forms/eco_account.py:73
#: compensation/views/eco_account/eco_account.py:105 #: compensation/views/eco_account/eco_account.py:77
#: compensation/views/eco_account/eco_account.py:127
msgid "New Eco-Account" msgid "New Eco-Account"
msgstr "Neues Ökokonto" msgstr "Neues Ökokonto"
@@ -503,7 +507,8 @@ msgstr ""
#: compensation/forms/eco_account.py:257 #: compensation/forms/eco_account.py:257
msgid "Please contact the responsible conservation office to find a solution!" msgid "Please contact the responsible conservation office to find a solution!"
msgstr "Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!" msgstr ""
"Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!"
#: compensation/forms/mixins.py:37 #: compensation/forms/mixins.py:37
#: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/detail/eco_account/view.html:63
@@ -818,6 +823,7 @@ msgstr "Am {} von {} verzeichnet worden"
#: compensation/tables/eco_account.py:39 #: compensation/tables/eco_account.py:39
#: compensation/templates/compensation/detail/eco_account/view.html:36 #: compensation/templates/compensation/detail/eco_account/view.html:36
#: compensation/templates/compensation/report/eco_account/report.html:34
#: konova/templates/konova/widgets/progressbar.html:3 #: konova/templates/konova/widgets/progressbar.html:3
msgid "Available" msgid "Available"
msgstr "Verfügbar" msgstr "Verfügbar"
@@ -948,7 +954,7 @@ msgstr "Wiedervorlage"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:20 #: compensation/templates/compensation/detail/compensation/includes/controls.html:20
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:34 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:34
#: ema/templates/ema/detail/includes/controls.html:34 #: ema/templates/ema/detail/includes/controls.html:32
#: intervention/templates/intervention/detail/includes/controls.html:39 #: intervention/templates/intervention/detail/includes/controls.html:39
msgid "Edit" msgid "Edit"
msgstr "Bearbeiten" msgstr "Bearbeiten"
@@ -962,7 +968,7 @@ msgstr "Log anzeigen"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:27 #: compensation/templates/compensation/detail/compensation/includes/controls.html:27
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:41 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:41
#: ema/templates/ema/detail/includes/controls.html:41 #: ema/templates/ema/detail/includes/controls.html:43
#: intervention/templates/intervention/detail/includes/controls.html:46 #: intervention/templates/intervention/detail/includes/controls.html:46
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
@@ -1154,7 +1160,7 @@ msgstr "Verzeichnet am"
#: compensation/templates/compensation/detail/compensation/view.html:107 #: compensation/templates/compensation/detail/compensation/view.html:107
#: compensation/templates/compensation/detail/eco_account/view.html:85 #: compensation/templates/compensation/detail/eco_account/view.html:85
#: compensation/templates/compensation/report/compensation/report.html:54 #: compensation/templates/compensation/report/compensation/report.html:54
#: compensation/templates/compensation/report/eco_account/report.html:47 #: compensation/templates/compensation/report/eco_account/report.html:75
#: ema/templates/ema/detail/view.html:71 #: ema/templates/ema/detail/view.html:71
#: ema/templates/ema/report/report.html:34 #: ema/templates/ema/report/report.html:34
#: intervention/templates/intervention/detail/view.html:113 #: intervention/templates/intervention/detail/view.html:113
@@ -1188,7 +1194,7 @@ msgid "other users"
msgstr "weitere Nutzer" msgstr "weitere Nutzer"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:18 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:18
#: ema/templates/ema/detail/includes/controls.html:18 #: ema/templates/ema/detail/includes/controls.html:19
#: intervention/forms/modals/share.py:62 #: intervention/forms/modals/share.py:62
#: intervention/templates/intervention/detail/includes/controls.html:18 #: intervention/templates/intervention/detail/includes/controls.html:18
#: intervention/tests/unit/test_forms.py:150 #: intervention/tests/unit/test_forms.py:150
@@ -1258,11 +1264,11 @@ msgstr "Maßnahmenträger"
msgid "Report" msgid "Report"
msgstr "Bericht" msgstr "Bericht"
#: compensation/templates/compensation/report/eco_account/report.html:34 #: compensation/templates/compensation/report/eco_account/report.html:43
msgid "Deductions for" msgid "Deductions for"
msgstr "Abbuchungen für" msgstr "Abbuchungen für"
#: compensation/templates/compensation/report/eco_account/report.html:42 #: compensation/templates/compensation/report/eco_account/report.html:64
#: intervention/templates/intervention/report/report.html:53 #: intervention/templates/intervention/report/report.html:53
#: intervention/templates/intervention/report/report.html:66 #: intervention/templates/intervention/report/report.html:66
#: intervention/templates/intervention/report/report.html:76 #: intervention/templates/intervention/report/report.html:76
@@ -1292,40 +1298,44 @@ msgstr ""
msgid "Responsible data" msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen" msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation/compensation.py:52 #: compensation/views/compensation/compensation.py:53
msgid "Compensations - Overview" msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht" msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation/compensation.py:167 #: compensation/views/compensation/compensation.py:203
#: compensation/views/compensation/compensation.py:261
#: compensation/views/eco_account/eco_account.py:169
#: compensation/views/eco_account/eco_account.py:224 ema/views/ema.py:175
#: ema/views/ema.py:229 intervention/views/intervention.py:176
#: intervention/views/intervention.py:230
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/compensation.py:242
#: konova/utils/message_templates.py:40 #: konova/utils/message_templates.py:40
msgid "Compensation {} edited" msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:190
#: compensation/views/eco_account/eco_account.py:168 ema/views/ema.py:173
#: intervention/views/intervention.py:175
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:35 #: compensation/views/compensation/report.py:35
#: compensation/views/eco_account/report.py:35 ema/views/report.py:35 #: compensation/views/eco_account/report.py:35 ema/views/report.py:35
#: intervention/views/report.py:36 #: intervention/views/report.py:36
msgid "Report {}" msgid "Report {}"
msgstr "Bericht {}" msgstr "Bericht {}"
#: compensation/views/eco_account/eco_account.py:49 #: compensation/views/eco_account/eco_account.py:51
msgid "Eco-account - Overview" msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht" msgstr "Ökokonten - Übersicht"
#: compensation/views/eco_account/eco_account.py:82 #: compensation/views/eco_account/eco_account.py:108
msgid "Eco-Account {} added" msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt" msgstr "Ökokonto {} hinzugefügt"
#: compensation/views/eco_account/eco_account.py:145 #: compensation/views/eco_account/eco_account.py:205
msgid "Eco-Account {} edited" msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet" msgstr "Ökokonto {} bearbeitet"
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:107 #: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:79
#: ema/views/ema.py:129
msgid "New EMA" msgid "New EMA"
msgstr "Neue EMA hinzufügen" msgstr "Neue EMA hinzufügen"
@@ -1357,11 +1367,11 @@ msgstr "Ersatzzahlungsmaßnahme"
msgid "EMAs - Overview" msgid "EMAs - Overview"
msgstr "EMAs - Übersicht" msgstr "EMAs - Übersicht"
#: ema/views/ema.py:85 #: ema/views/ema.py:111
msgid "EMA {} added" msgid "EMA {} added"
msgstr "EMA {} hinzugefügt" msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:150 #: ema/views/ema.py:211
msgid "EMA {} edited" msgid "EMA {} edited"
msgstr "EMA {} bearbeitet" msgstr "EMA {} bearbeitet"
@@ -1425,7 +1435,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216 #: intervention/forms/intervention.py:216
#: intervention/tests/unit/test_forms.py:36 #: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:109 #: intervention/views/intervention.py:82 intervention/views/intervention.py:134
msgid "New intervention" msgid "New intervention"
msgstr "Neuer Eingriff" msgstr "Neuer Eingriff"
@@ -1661,15 +1671,15 @@ msgstr ""
msgid "Check performed" msgid "Check performed"
msgstr "Prüfung durchgeführt" msgstr "Prüfung durchgeführt"
#: intervention/views/intervention.py:53 #: intervention/views/intervention.py:56
msgid "Interventions - Overview" msgid "Interventions - Overview"
msgstr "Eingriffe - Übersicht" msgstr "Eingriffe - Übersicht"
#: intervention/views/intervention.py:86 #: intervention/views/intervention.py:114
msgid "Intervention {} added" msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt" msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:150 #: intervention/views/intervention.py:212
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
@@ -1797,16 +1807,11 @@ msgstr "Speichern"
msgid "Not editable" msgid "Not editable"
msgstr "Nicht editierbar" msgstr "Nicht editierbar"
#: konova/forms/geometry_form.py:31 konova/utils/quality.py:44 #: konova/forms/geometry_form.py:30 konova/utils/quality.py:44
#: konova/utils/quality.py:46 templates/form/collapsable/form.html:45 #: konova/utils/quality.py:46 templates/form/collapsable/form.html:45
msgid "Geometry" msgid "Geometry"
msgstr "Geometrie" msgstr "Geometrie"
#: konova/forms/geometry_form.py:105
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
#: konova/forms/modals/document_form.py:37 #: konova/forms/modals/document_form.py:37
msgid "When has this file been created? Important for photos." msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
@@ -1961,6 +1966,7 @@ msgstr "Raumreferenz"
#: konova/templates/konova/includes/parcels/parcels.html:28 #: konova/templates/konova/includes/parcels/parcels.html:28
msgid "No geometry entry found on database. Please contact an admin!" msgid "No geometry entry found on database. Please contact an admin!"
msgstr "" msgstr ""
"Keine Geometrie in Datenbank gefunden. Kontaktieren Sie einen Administrator!"
#: konova/templates/konova/includes/quickstart/compensations.html:20 #: konova/templates/konova/includes/quickstart/compensations.html:20
#: konova/templates/konova/includes/quickstart/ecoaccounts.html:20 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:20
@@ -2006,39 +2012,52 @@ msgstr "In Zwischenablage kopiert"
msgid "Search" msgid "Search"
msgstr "Suchen" msgstr "Suchen"
#: konova/utils/mailer.py:69 konova/utils/mailer.py:146 #: konova/utils/geometry/geometry_validator.py:311
msgid ""
"This feature does not hold valid EPSG:25832 coordinates:\n"
" {}"
msgstr ""
"Dieses Feature enthält keine validen EPSG:25832 Koordinaten:\n"
" {}"
#: konova/utils/geometry/geometry_validator.py:319
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
#: konova/utils/mailer.py:82 konova/utils/mailer.py:159
msgid "{} - Shared access removed" msgid "{} - Shared access removed"
msgstr "{} - Zugriff entzogen" msgstr "{} - Zugriff entzogen"
#: konova/utils/mailer.py:94 konova/utils/mailer.py:120 #: konova/utils/mailer.py:107 konova/utils/mailer.py:133
msgid "{} - Shared access given" msgid "{} - Shared access given"
msgstr "{} - Zugriff freigegeben" msgstr "{} - Zugriff freigegeben"
#: konova/utils/mailer.py:172 konova/utils/mailer.py:325 #: konova/utils/mailer.py:185 konova/utils/mailer.py:338
msgid "{} - Shared data unrecorded" msgid "{} - Shared data unrecorded"
msgstr "{} - Freigegebene Daten entzeichnet" msgstr "{} - Freigegebene Daten entzeichnet"
#: konova/utils/mailer.py:198 konova/utils/mailer.py:300 #: konova/utils/mailer.py:211 konova/utils/mailer.py:313
msgid "{} - Shared data recorded" msgid "{} - Shared data recorded"
msgstr "{} - Freigegebene Daten verzeichnet" msgstr "{} - Freigegebene Daten verzeichnet"
#: konova/utils/mailer.py:224 konova/utils/mailer.py:375 #: konova/utils/mailer.py:237 konova/utils/mailer.py:388
msgid "{} - Shared data checked" msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft" msgstr "{} - Freigegebene Daten geprüft"
#: konova/utils/mailer.py:249 konova/utils/mailer.py:401 #: konova/utils/mailer.py:262 konova/utils/mailer.py:414
msgid "{} - Deduction changed" msgid "{} - Deduction changed"
msgstr "{} - Abbuchung geändert" msgstr "{} - Abbuchung geändert"
#: konova/utils/mailer.py:275 konova/utils/mailer.py:350 #: konova/utils/mailer.py:288 konova/utils/mailer.py:363
msgid "{} - Shared data deleted" msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht" msgstr "{} - Freigegebene Daten gelöscht"
#: konova/utils/mailer.py:422 templates/email/api/verify_token.html:4 #: konova/utils/mailer.py:435 templates/email/api/verify_token.html:4
msgid "Request for new API token" msgid "Request for new API token"
msgstr "Anfrage für neuen API Token" msgstr "Anfrage für neuen API Token"
#: konova/utils/mailer.py:447 #: konova/utils/mailer.py:460
msgid "Resubmission - {}" msgid "Resubmission - {}"
msgstr "Wiedervorlage - {}" msgstr "Wiedervorlage - {}"
@@ -2289,11 +2308,11 @@ msgstr "Neuer Token generiert. Administratoren sind informiert."
msgid "missing" msgid "missing"
msgstr "fehlend" msgstr "fehlend"
#: konova/utils/tables.py:222 #: konova/utils/tables.py:224
msgid "Full access granted" msgid "Full access granted"
msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden"
#: konova/utils/tables.py:222 #: konova/utils/tables.py:224
msgid "Access not granted" msgid "Access not granted"
msgstr "Nicht freigegeben - Datensatz nur lesbar" msgstr "Nicht freigegeben - Datensatz nur lesbar"
@@ -2303,7 +2322,7 @@ msgstr ""
"Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein " "Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein "
"(>1950)." "(>1950)."
#: konova/views/home.py:75 templates/navbars/navbar.html:16 #: konova/views/home.py:76 templates/navbars/navbar.html:16
msgid "Home" msgid "Home"
msgstr "Home" msgstr "Home"
@@ -2323,7 +2342,7 @@ msgstr "{} verzeichnet"
msgid "Errors found:" msgid "Errors found:"
msgstr "Fehler gefunden:" msgstr "Fehler gefunden:"
#: konova/views/remove.py:35 #: konova/views/remove.py:34
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
+38 -38
View File
@@ -1,65 +1,65 @@
amqp==5.3.1 amqp==5.3.1
asgiref==3.11.1 asgiref==3.12.1
async-timeout==5.0.1 async-timeout==5.0.1
beautifulsoup4==4.14.3 beautifulsoup4==4.15.0
billiard==4.2.4 billiard==4.2.4
cached-property==2.0.1 cached-property==2.0.1
celery==5.6.3 celery==5.6.3
certifi==2026.4.22 certifi==2026.7.22
cffi==2.0.0 cffi==2.1.1
chardet==7.4.3 chardet==7.6.0
charset-normalizer==3.4.7 charset-normalizer==3.5.1
click==8.3.3 click==8.5.0
click-didyoumean==0.3.1 click-didyoumean==0.3.1
click-plugins==1.1.1.2 click-plugins==1.1.1.2
click-repl==0.3.0 click-repl==0.3.0
coverage==7.13.5 coverage==7.16.0
cryptography==48.0.0 cryptography==50.0.1
Deprecated==1.3.1 Deprecated==1.3.1
Django==6.0.5 Django==6.1.1
django-autocomplete-light==4.0.0 django-autocomplete-light==5.0.0
django-bootstrap-modal-forms==3.0.5 django-bootstrap-modal-forms==3.0.5
django-bootstrap4==26.1 django-bootstrap4==26.2
django-environ==0.13.0 django-environ==0.14.0
django-filter==25.2 django-filter==26.1
django-fontawesome-5==1.0.18 django-fontawesome-5==1.0.18
django-oauth-toolkit==3.2.0 django-oauth-toolkit==3.4.1
django-tables2==3.0.0 django-tables2==3.0.1
et_xmlfile==2.0.0 et_xmlfile==2.0.0
gunicorn==26.0.0 gunicorn==26.2.0
idna==3.13 idna==3.19
importlib_metadata==9.0.0 importlib_metadata==9.0.1
itsdangerous==2.2.0 itsdangerous==2.2.0
jwcrypto==1.5.7 jwcrypto==1.6.0
kombu==5.6.2 kombu==5.6.2
oauthlib==3.3.1 oauthlib==3.3.1
openpyxl==3.2.0b1 openpyxl==3.2.0b1
packaging==26.2 packaging==26.3
pika==1.4.0 pika==1.4.4
pillow==12.2.0 pillow==12.3.0
prompt_toolkit==3.0.52 prompt_toolkit==3.0.53
psycopg==3.3.4 psycopg==3.3.5
psycopg-binary==3.3.4 psycopg-binary==3.3.5
pycparser==3.0 pycparser==3.0
pyparsing==3.3.2 pyparsing==3.3.2
pypng==0.20220715.0 pypng==0.20220715.0
pyproj==3.7.2 pyproj==3.8.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytz==2026.2 pytz==2026.3
PyYAML==6.0.3 PyYAML==6.0.3
qrcode==8.2 qrcode==8.2
redis==7.4.0 redis==8.1.0
requests==2.33.1 requests==2.34.2
six==1.17.0 six==1.17.0
soupsieve==2.8.3 soupsieve==2.9.2
sqlparse==0.5.5 sqlparse==0.6.0
typing_extensions==4.15.0 typing_extensions==4.16.0
tzdata==2026.2 tzdata==2026.4
tzlocal==5.3.1 tzlocal==5.4.4
urllib3==2.7.0 urllib3==2.7.0
vine==5.1.0 vine==5.1.0
wcwidth==0.7.0 wcwidth==0.8.3
webservices==0.7 webservices==0.7
wrapt==2.1.2 wrapt==2.4.1
xmltodict==1.0.4 xmltodict==1.0.4
zipp==3.23.1 zipp==4.1.0