Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filter geometry #3125

Merged
merged 7 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG

**New features**

- Add filter valid geometries on topologies (#2515)[3.1]


2.83.0 (2022-05-01)
Expand Down
9 changes: 1 addition & 8 deletions geotrek/api/v2/functions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib.gis.db.models.functions import GeoFunc
from django.db.models import Func
from django.db.models.fields import FloatField, CharField
from django.db.models.fields import FloatField
from django.contrib.gis.db.models import GeometryField, PointField


Expand All @@ -11,13 +11,6 @@ def Buffer(geom, radius, num_seg):
return Func(geom, radius, num_seg, function='ST_Buffer', output_field=GeometryField())


def GeometryType(geom):
"""
GeometryType postgis function
"""
return Func(geom, function='GeometryType', output_field=CharField())


class Length3D(Func):
"""
ST_3DLENGTH postgis function
Expand Down
3 changes: 2 additions & 1 deletion geotrek/api/v2/views/sensitivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

from geotrek.api.v2 import serializers as api_serializers, \
viewsets as api_viewsets
from geotrek.api.v2.functions import Buffer, GeometryType, Area
from geotrek.api.v2.functions import Buffer, Area
from geotrek.common.functions import GeometryType
from geotrek.sensitivity import models as sensitivity_models
from ..filters import GeotrekQueryParamsFilter, GeotrekQueryParamsDimensionFilter, GeotrekInBBoxFilter, GeotrekSensitiveAreaFilter, NearbyContentFilter, UpdateOrCreateDateFilter

Expand Down
10 changes: 9 additions & 1 deletion geotrek/common/functions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from django.contrib.gis.db.models.functions import GeoFunc
from django.db.models import FloatField
from django.db.models import CharField, FloatField


class Length(GeoFunc):
""" ST_Length postgis function """
output_field = FloatField()


class GeometryType(GeoFunc):
"""
GeometryType postgis function
"""
output_field = CharField()
function = 'GeometryType'
24 changes: 21 additions & 3 deletions geotrek/core/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,40 @@
from geotrek.altimetry.filters import AltimetryAllGeometriesFilterSet
from geotrek.authent.filters import StructureRelatedFilterSet
from geotrek.common.filters import RightFilter
from geotrek.common.functions import GeometryType
from geotrek.maintenance import models as maintenance_models
from geotrek.maintenance.filters import InterventionFilterSet, ProjectFilterSet
from geotrek.zoning.filters import ZoningFilterSet


class ValidTopologyFilterSet(FilterSet):
is_valid = BooleanFilter(label=_("Valid topology"), method='filter_valid_topology')
# Do not forget to add geometry_types_allowed on models if you add this filterset
# geometry_types_allowed = ["LINESTRING"] for example
# Types possible with topologies are linestring and points only

if settings.TREKKING_TOPOLOGY_ENABLED:
is_valid_topology = BooleanFilter(label=_("Valid topology"), method='filter_valid_topology')
is_valid_geometry = BooleanFilter(label=_("Valid geometry"), method='filter_valid_geometry')

def filter_valid_topology(self, qs, name, value):
if value is not None:
qs = qs.annotate(distinct_same_order=Count('aggregations__order', distinct=True),
same_order=Count('aggregations__order'))
if value is True:
qs = qs.filter(geom__isvalid=True).exclude(geom__isnull=True).exclude(geom__isempty=True).filter(same_order=F('distinct_same_order'))
qs = qs.filter(same_order__gt=0, same_order=F('distinct_same_order'))
elif value is False:
qs = qs.filter(Q(same_order=0) | Q(distinct_same_order__lt=F('same_order')))
return qs

def filter_valid_geometry(self, qs, name, value):
if value is not None:
qs = qs.annotate(geometry_type=GeometryType('geom'))
if value is True:
qs = qs.filter(geom__isvalid=True).exclude(geom__isnull=True).exclude(geom__isempty=True).filter(
geometry_type__in=qs.model.geometry_types_allowed)
elif value is False:
qs = qs.filter(Q(geom__isnull=True) | Q(geom__isvalid=False) | Q(geom__isempty=True) | Q(distinct_same_order__lt=F('same_order')))
qs = qs.filter(Q(geom__isnull=True) | Q(geom__isvalid=False) | Q(geom__isempty=True) | ~Q(
geometry_type__in=qs.model.geometry_types_allowed))
return qs


Expand Down
3 changes: 3 additions & 0 deletions geotrek/core/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ msgstr ""
msgid "Valid topology"
msgstr ""

msgid "Valid geometry"
msgstr ""

msgid "length"
msgstr ""

Expand Down
3 changes: 3 additions & 0 deletions geotrek/core/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ msgstr ""
msgid "Valid topology"
msgstr ""

msgid "Valid geometry"
msgstr ""

msgid "length"
msgstr ""

Expand Down
3 changes: 3 additions & 0 deletions geotrek/core/locale/es/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ msgstr ""
msgid "Valid topology"
msgstr ""

msgid "Valid geometry"
msgstr ""

msgid "length"
msgstr ""

Expand Down
3 changes: 3 additions & 0 deletions geotrek/core/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ msgstr "La ligne a un snapping invalide"
msgid "Valid topology"
msgstr "Topologie valide"

msgid "Valid geometry"
msgstr "Géometrie valide"

msgid "length"
msgstr "Longueur"

Expand Down
3 changes: 3 additions & 0 deletions geotrek/core/locale/it/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ msgstr ""
msgid "Valid topology"
msgstr ""

msgid "Valid geometry"
msgstr ""

msgid "length"
msgstr ""

Expand Down
3 changes: 3 additions & 0 deletions geotrek/core/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ msgstr ""
msgid "Valid topology"
msgstr ""

msgid "Valid geometry"
msgstr ""

msgid "length"
msgstr ""

Expand Down
4 changes: 4 additions & 0 deletions geotrek/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ class Topology(ZoningPropertiesMixin, AddPropertyMixin, AltimetryMixin,
""" Fake srid attribute, that prevents transform() calls when using Django map widgets. """
srid = settings.API_SRID

geometry_types_allowed = ["LINESTRING", "POINT"]

class Meta:
verbose_name = _("Topology")
verbose_name_plural = _("Topologies")
Expand Down Expand Up @@ -946,6 +948,8 @@ class Trail(MapEntityMixin, Topology, StructureRelated):
comments = models.TextField(default="", blank=True, verbose_name=_("Comments"))
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Trail")
verbose_name_plural = _("Trails")
Expand Down
62 changes: 46 additions & 16 deletions geotrek/core/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,58 +49,88 @@ def test_trail_filters(self):
self.assertEqual(qs.count(), 1)


@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only')
class ValidTopologyFilterTest(TestCase):
@classmethod
def setUpTestData(cls):
if settings.TREKKING_TOPOLOGY_ENABLED:
cls.path = PathFactory()
cls.trek = TrekFactory.create(name="Crossed", paths=[(cls.path, 0, 1)])
else:
cls.trek = TrekFactory.create(geom=LineString((0, 0), (5, 5)))
cls.path = PathFactory()
cls.trek = TrekFactory.create(name="Crossed", paths=[(cls.path, 0, 1)])

@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only')
def test_trek_filters_not_valid(self):
trek = TrekFactory.create(name="Not crossed", paths=[(self.path, 0, 0.5)])
TrekFactory.create(paths=[])
qs = TrekFilterSet().qs
self.assertEqual(qs.count(), 3)

data = {'is_valid': True}
data = {'is_valid_topology': True}
qs = TrekFilterSet(data=data).qs
self.assertIn(self.trek, qs)
self.assertEqual(qs.count(), 2)

data = {'is_valid': False}
data = {'is_valid_topology': False}
qs = TrekFilterSet(data=data).qs
self.assertEqual(qs.count(), 1)

geom = LineString(Point(700100, 6600000), Point(700000, 6600100), srid=settings.SRID)
PathFactory.create(geom=geom)
self.trek.reload()
trek.reload()
data = {'is_valid': True}
data = {'is_valid_topology': True}
qs = TrekFilterSet(data=data).qs
self.assertNotIn(self.trek, qs)
self.assertIn(trek, qs)
self.assertEqual(qs.count(), 1)

data = {'is_valid': False}
data = {'is_valid_topology': False}
qs = TrekFilterSet(data=data).qs
self.assertIn(self.trek, qs)
self.assertNotIn(trek, qs)
self.assertEqual(qs.count(), 2)

@skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
def test_trek_filters_not_valid_nds(self):
TrekFactory.create(name="Empty", geom='SRID=2154;LINESTRING EMPTY')

@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only')
class ValidGeometryFilterTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.path = PathFactory()

def test_trek_filters_not_valid_geometry(self):
trek_linestring = TrekFactory.create(name="LineString", paths=[(self.path, 0, 1)])
trek_multilinestring = TrekFactory.create(name="Multilinestring", paths=[(self.path, 0, 0.4, 1),
(self.path, 0.6, 1, 2)])
trek_point = TrekFactory.create(name="Point", paths=[(self.path, 0, 0)])
trek_none = TrekFactory.create(name="None", paths=[])

qs = TrekFilterSet().qs
self.assertEqual(qs.count(), 4)

data = {'is_valid_geometry': True}
qs = TrekFilterSet(data=data).qs
self.assertIn(trek_linestring, qs)
self.assertEqual(qs.count(), 1)

data = {'is_valid_geometry': False}
qs = TrekFilterSet(data=data).qs
self.assertIn(trek_multilinestring, qs)
self.assertIn(trek_point, qs)
self.assertIn(trek_none, qs)
self.assertEqual(qs.count(), 3)


@skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
class ValidGeometryFilterNDSTest(TestCase):
def test_trek_filters_not_valid_geometry_nds(self):
trek_empty = TrekFactory.create(name="Empty", geom='SRID=2154;LINESTRING EMPTY')
trek_valid = TrekFactory.create(name="Valid", geom=LineString((0, 0), (5, 5)))
qs = TrekFilterSet().qs
self.assertEqual(qs.count(), 2)

data = {'is_valid': True}
data = {'is_valid_geometry': True}
qs = TrekFilterSet(data=data).qs
self.assertIn(self.trek, qs)
self.assertIn(trek_valid, qs)
self.assertEqual(qs.count(), 1)

data = {'is_valid': False}
data = {'is_valid_geometry': False}
qs = TrekFilterSet(data=data).qs
self.assertIn(trek_empty, qs)
self.assertEqual(qs.count(), 1)
2 changes: 2 additions & 0 deletions geotrek/infrastructure/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ class Infrastructure(MapEntityMixin, BaseInfrastructure):
related_name='infrastructures_set')
accessibility = models.TextField(verbose_name=_("Accessibility"), blank=True)

geometry_types_allowed = ["LINESTRING", "POINT"]

class Meta:
verbose_name = _("Infrastructure")
verbose_name_plural = _("Infrastructures")
Expand Down
10 changes: 10 additions & 0 deletions geotrek/land/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class PhysicalEdge(MapEntityMixin, Topology):
on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Physical edge")
verbose_name_plural = _("Physical edges")
Expand Down Expand Up @@ -109,6 +111,8 @@ class LandEdge(MapEntityMixin, Topology):
agreement = models.BooleanField(verbose_name=_("Agreement"), default=False)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Land edge")
verbose_name_plural = _("Land edges")
Expand Down Expand Up @@ -158,6 +162,8 @@ class CompetenceEdge(MapEntityMixin, Topology):
organization = models.ForeignKey(Organism, verbose_name=_("Organism"), on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Competence edge")
verbose_name_plural = _("Competence edges")
Expand Down Expand Up @@ -207,6 +213,8 @@ class WorkManagementEdge(MapEntityMixin, Topology):
organization = models.ForeignKey(Organism, verbose_name=_("Organism"), on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Work management edge")
verbose_name_plural = _("Work management edges")
Expand Down Expand Up @@ -256,6 +264,8 @@ class SignageManagementEdge(MapEntityMixin, Topology):
organization = models.ForeignKey(Organism, verbose_name=_("Organism"), on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Signage management edge")
verbose_name_plural = _("Signage management edges")
Expand Down
2 changes: 2 additions & 0 deletions geotrek/maintenance/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class Intervention(ZoningPropertiesMixin, AddPropertyMixin, MapEntityMixin, Alti

objects = InterventionManager()

geometry_types_allowed = ["LINESTRING", "POINT"]

class Meta:
verbose_name = _("Intervention")
verbose_name_plural = _("Interventions")
Expand Down
3 changes: 2 additions & 1 deletion geotrek/sensitivity/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
MapEntityDelete, MapEntityFormat, LastModifiedMixin)
from rest_framework import permissions as rest_permissions, viewsets

from geotrek.api.v2.functions import Buffer, GeometryType, Area
from geotrek.api.v2.functions import Buffer, Area
from geotrek.authent.decorators import same_structure_required
from geotrek.common.functions import GeometryType
from geotrek.common.mixins.views import CustomColumnsMixin
from geotrek.common.permissions import PublicOrReadPermMixin
from .filters import SensitiveAreaFilterSet
Expand Down
2 changes: 2 additions & 0 deletions geotrek/signage/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class Signage(MapEntityMixin, BaseInfrastructure):
type = models.ForeignKey(SignageType, related_name='signages', verbose_name=_("Type"), on_delete=models.CASCADE)
coordinates_verbose_name = _("Coordinates")

geometry_types_allowed = ["POINT"]

class Meta:
verbose_name = _("Signage")
verbose_name_plural = _("Signages")
Expand Down
4 changes: 4 additions & 0 deletions geotrek/trekking/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ class Trek(Topology, StructureRelated, PicturesMixin, PublishableMixin, MapEntit

capture_map_image_waitfor = '.poi_enum_loaded.services_loaded.info_desks_loaded.ref_points_loaded'

geometry_types_allowed = ["LINESTRING"]

class Meta:
verbose_name = _("Trek")
verbose_name_plural = _("Treks")
Expand Down Expand Up @@ -727,6 +729,8 @@ class POI(StructureRelated, PicturesMixin, PublishableMixin, MapEntityMixin, Top
type = models.ForeignKey('POIType', related_name='pois', verbose_name=_("Type"), on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

geometry_types_allowed = ["POINT"]

class Meta:
verbose_name = _("POI")
verbose_name_plural = _("POI")
Expand Down