From aa27b77885b7436ab90ccea3eeb4bbe9508e64b4 Mon Sep 17 00:00:00 2001 From: LePetitTim Date: Fri, 14 Oct 2022 16:53:53 +0200 Subject: [PATCH] Add reorder topologies command --- .../management/commands/reorder_topologies.py | 75 ++++++++++ .../templates/core/sql/post_10_utilities.sql | 14 +- .../templates/core/sql/pre_10_cleanup.sql | 1 + geotrek/core/tests/test_commands.py | 137 +++++++++++++++++- 4 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 geotrek/core/management/commands/reorder_topologies.py diff --git a/geotrek/core/management/commands/reorder_topologies.py b/geotrek/core/management/commands/reorder_topologies.py new file mode 100644 index 0000000000..a986bcd6d6 --- /dev/null +++ b/geotrek/core/management/commands/reorder_topologies.py @@ -0,0 +1,75 @@ +from django.core.management.base import BaseCommand +from django.db import connection + +from django.contrib.gis.geos import GEOSGeometry + +from geotrek.core.models import PathAggregation, Topology + + +class Command(BaseCommand): + help = """Reorder Pathaggregations of all topologies.""" + + def handle(self, *args, **options): + topologies = Topology.objects.all() + for topology in topologies: + cursor = connection.cursor() + # We get all sublines of the topology + cursor.execute(f""" + SELECT ST_ASTEXT(ST_SmartLineSubstring(t.geom, et.start_position, et.end_position)) + FROM core_topology e, core_pathaggregation et, core_path t + WHERE e.id = {topology.pk} AND et.topo_object_id = e.id AND et.path_id = t.id + AND GeometryType(ST_SmartLineSubstring(t.geom, et.start_position, et.end_position)) != 'POINT' + ORDER BY et."order", et.id + """) + geom_lines = cursor.fetchall() + # We use ft_Smart_MakeLine to get the order of the lines + cursor.execute(f"""SELECT * FROM ft_Smart_MakeLine(ARRAY{[geom[0] for geom in geom_lines]}::geometry[])""") + # We remove first value (algorithme use a 0 by default to go through the lines and will always be here) + # Then we need to remove first value and remove 1 to all of them because Path aggregation's orders begin at 0 + orders = [result - 1 for result in cursor.fetchall()[0][1][1:]] + # We get all the Points that we didn't get for smart make line + cursor = connection.cursor() + cursor.execute(f""" + SELECT ST_ASTEXT(ST_SmartLineSubstring(t.geom, et.start_position, et.end_position)) + FROM core_topology e, core_pathaggregation et, core_path t + WHERE e.id = {topology.pk} AND et.topo_object_id = e.id AND et.path_id = t.id + AND GeometryType(ST_SmartLineSubstring(t.geom, et.start_position, et.end_position)) = 'POINT' + ORDER BY et."order", et.id + """) + geom_points = cursor.fetchall() + new_orders = [] + order_point = 0 + number_points = 0 + id_order = 0 + for geom_point_wkt in geom_points: + geom_point = GEOSGeometry(geom_point_wkt[0], srid=2154) + while id_order < len(orders) - 1: + order_actual = orders[id_order] + order_next = orders[id_order + 1] + id_order += 1 + # We check if the point is on the actual line's end_point and next line's start_point to get its position + actual_point_end = GEOSGeometry(geom_lines[order_actual][0], srid=2154).boundary[1] # Get end point of the geometry + next_point_start = GEOSGeometry(geom_lines[order_next][0], srid=2154).boundary[0] # Get start point of the geometry + if geom_point == actual_point_end == next_point_start: + # If we find it position : + # we get all orders gnerated with 'smart make line' after last point (or start_point) to the actual point + new_orders.extend([order + number_points for order in orders[order_point:id_order]]) + # We add the point inside the list of orders + new_orders.append(id_order + number_points) + # We keep number of points added to add the value to lines orders and points orders in the next iteration + number_points += 1 + order_point = id_order + # We get next point + break + # We add all the lines orders after last points. If we have no point, we would get all lines in the order [0:) + new_orders.extend([order + number_points for order in orders[order_point:]]) + + pas = [] + # We update every order with new orders. + # The query is always in the same order between django and sql (order by "order" and id) + # Path aggregation will be created in this order too + for pa_order, pa_id in enumerate(PathAggregation.objects.filter(topo_object=topology).order_by('order', 'id').values_list('id', flat=True)): + pa = PathAggregation.objects.get(id=pa_id) + pa.order = new_orders[pa_order] + pas.append(pa) + PathAggregation.objects.bulk_update(pas, ['order']) diff --git a/geotrek/core/templates/core/sql/post_10_utilities.sql b/geotrek/core/templates/core/sql/post_10_utilities.sql index 802f544461..8035a15cd4 100644 --- a/geotrek/core/templates/core/sql/post_10_utilities.sql +++ b/geotrek/core/templates/core/sql/post_10_utilities.sql @@ -2,6 +2,12 @@ -- Interpolate along : the opposite of ST_LocateAlong ------------------------------------------------------------------------------- +CREATE TYPE {{ schema_geotrek }}.line_infos AS ( + new_geometry geometry, + new_order integer[] +); + + CREATE FUNCTION {{ schema_geotrek }}.ST_InterpolateAlong(line geometry, point geometry) RETURNS RECORD AS $$ DECLARE linear_offset float; @@ -65,7 +71,7 @@ $$ LANGUAGE plpgsql; -- A smart ST_MakeLine that will re-oder linestring before merging them ------------------------------------------------------------------------------- -CREATE FUNCTION {{ schema_geotrek }}.ft_Smart_MakeLine(lines geometry[]) RETURNS geometry AS $$ +CREATE FUNCTION {{ schema_geotrek }}.ft_Smart_MakeLine(lines geometry[]) RETURNS line_infos AS $$ DECLARE result geometry; t_line geometry; @@ -74,6 +80,7 @@ DECLARE i int; t_proceed boolean; t_found boolean; + final_result line_infos; BEGIN result := ST_GeomFromText('LINESTRING EMPTY'); nblines := array_length(lines, 1); @@ -125,9 +132,12 @@ BEGIN IF NOT t_found THEN result := ST_Union(lines); -- RAISE NOTICE 'Cannot connect Topology paths: %', ST_AsText(ST_Union(lines)); + current := ARRAY[]::integer[]; END IF; result := ST_SetSRID(result, ST_SRID(lines[1])); - RETURN result; + final_result.new_geometry = result; + final_result.new_order = current; + RETURN final_result; END; $$ LANGUAGE plpgsql; diff --git a/geotrek/core/templates/core/sql/pre_10_cleanup.sql b/geotrek/core/templates/core/sql/pre_10_cleanup.sql index b7ee393774..0601db62a6 100644 --- a/geotrek/core/templates/core/sql/pre_10_cleanup.sql +++ b/geotrek/core/templates/core/sql/pre_10_cleanup.sql @@ -1,5 +1,6 @@ -- 10 +DROP TYPE IF EXISTS line_infos CASCADE; DROP FUNCTION IF EXISTS ST_InterpolateAlong(geometry, geometry) CASCADE; DROP FUNCTION IF EXISTS ST_Smart_Line_Substring(geometry, float, float) CASCADE; DROP FUNCTION IF EXISTS ST_SmartLineSubstring(geometry, float, float) CASCADE; diff --git a/geotrek/core/tests/test_commands.py b/geotrek/core/tests/test_commands.py index da95d8ec2d..c3a8de892c 100644 --- a/geotrek/core/tests/test_commands.py +++ b/geotrek/core/tests/test_commands.py @@ -2,14 +2,15 @@ from unittest import mock, skipIf from django.conf import settings -from django.contrib.gis.geos import LineString +from django.contrib.gis.geos import LineString, Point, GEOSGeometry from django.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase, override_settings -from django.db import IntegrityError +from django.db import connection, IntegrityError from geotrek.authent.models import Structure -from geotrek.core.models import Path +from geotrek.core.models import Path, PathAggregation +from geotrek.core.tests.factories import PathFactory, TopologyFactory from geotrek.trekking.tests.factories import POIFactory import os @@ -229,3 +230,133 @@ def test_load_paths_within_spatial_extent_no_srid_geom(self): value = Path.objects.first() self.assertEqual(value.name, 'lulu') self.assertEqual(value.structure, self.structure) + + +@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only') +class ReorderTopologiesPathAggregationTest(TestCase): + def setUp(self): + """ + ⠳ ⠞ + ⠳ ⠞ + ⠳ ⠞ + ⠳ ⠞ + ⠿ + ⠞ ⠳ + ⠞ ⠳ + 1 ⠞ ⠳ 2 + ⠞ ⠳ + """ + self.path_1 = PathFactory.create(geom=LineString(Point(700000, 6600000), Point(700100, 6600100), + srid=settings.SRID)) + self.path_2 = PathFactory.create(geom=LineString(Point(700000, 6600100), Point(700100, 6600000), + srid=settings.SRID)) + self.path_1_a = Path.objects.get(geom=LineString(Point(700000, 6600000), Point(700050, 6600050), + srid=settings.SRID)) + self.path_1_b = Path.objects.get(geom=LineString(Point(700050, 6600050), Point(700100, 6600100), + srid=settings.SRID)) + self.path_2_a = Path.objects.get(geom=LineString(Point(700000, 6600100), Point(700050, 6600050), + srid=settings.SRID)) + self.path_2_b = Path.objects.get(geom=LineString(Point(700050, 6600050), Point(700100, 6600000), + srid=settings.SRID)) + + def get_geometries(self): + geometries = [] + for pathagg in PathAggregation.objects.all(): + cursor = connection.cursor() + cursor.execute(f"""SELECT * FROM ST_ASTEXT(ST_SmartLineSubstring('{pathagg.path.geom.wkt}'::geometry, + {pathagg.start_position}, + {pathagg.end_position} + )) + """) + geom = cursor.fetchall()[0][0] + geometries.append(GEOSGeometry(geom, srid=2154)) + return geometries + + def test_split_reorder_1(self): + """ + Part A + + ⠳ 🡥 + ⠳ 🡥 + ⠳ 🡥 + ⠳ 🡥 + 🡥 🡥 Topo 1 + 🡥 ⠳ ⠳ Paths (1 2) + 🡥 ⠳ + 1 🡥 ⠳ 2 + 🡥 ⠳ + + Part B + + ⠳ 🡥 + ⠳ 🡥 + ⠳ ⠳ 🡥 + ⠳ ⠳ 🡥 + ⠳ 🡥 🡥 Topo 1 + 🡥 ⠳ ⠳ Paths (1 2 3) + 🡥 ⠳ ⠳ + 1 🡥 ⠳ ⠳ 2 + 🡥 3 ⠳ ⠳ + """ + topo = TopologyFactory.create(paths=[(self.path_1_a, 0, 1), (self.path_1_b, 0, 1)]) + PathFactory.create(geom=LineString(Point(700000, 6600090), Point(700090, 6600000), srid=settings.SRID)) + call_command('reorder_topologies') + geometries = self.get_geometries() + self.assertEqual(geometries, [LineString((700000, 6600000), (700045, 6600045), srid=2154), + LineString((700045, 6600045), (700050, 6600050), srid=2154), + LineString((700050, 6600050), (700100, 6600100), srid=2154)]) + self.assertEqual(list(PathAggregation.objects.filter(topo_object=topo).values_list('order', flat=True)), + [0, 1, 2]) + + def test_split_reorder_2(self): + """ + Part A + + ⠳ 🡥 + ⠳ 🡥 + ⠳ 🡥 + ⠳ 🡥 + ⠳ 🡥 + 🡥 🡥 Topo 1 + 0 ⠳ 0 Topo 1 (point) + X ⠳ x Topo 1 (2 directions) + 0 ⠳ ⠳ Paths (1 2) + 🡥 ⠳ + 🡥 ⠳ + + Part B + + ⠳ 🡥 + ⠳ 🡥 + ⠳ 🡥 + ⠳ ⠳ 🡥 + ⠳ ⠳ 🡥 + ⠳ 🡥 🡥 Topo 1 + ⠳ 0 ⠳ 0 Topo 1 (point) + X ⠳ ⠳ x Topo 1 (2 directions) + 0 ⠳ ⠳ ⠳ Paths (1 2 3) + 🡥 ⠳ ⠳ + 🡥 ⠳ ⠳ + + """ + topo = TopologyFactory.create(paths=[(self.path_1_a, 0, 0.95), + (self.path_1_a, 0.95, 0.95), + (self.path_1_a, 0.95, 0.5), + (self.path_1_a, 0.5, 0.5), + (self.path_1_a, 0.5, 1), + (self.path_1_b, 0, 1)]) + PathFactory.create(geom=LineString(Point(700000, 6600090), Point(700090, 6600000), srid=settings.SRID)) + call_command('reorder_topologies') + geometries = self.get_geometries() + self.assertEqual(geometries, [LineString((700000, 6600000), (700045, 6600045), srid=2154), + LineString((700045, 6600045), (700047.5, 6600047.5), srid=2154), + Point(700047.5, 6600047.5, srid=2154), + LineString((700047.5, 6600047.5), (700045, 6600045), srid=2154), + LineString((700045, 6600045), (700025, 6600025), srid=2154), + Point(700025, 6600025, srid=2154), + LineString((700025, 6600025), (700045, 6600045), srid=2154), + LineString((700045, 6600045), (700050, 6600050), srid=2154), + LineString((700050, 6600050), (700100, 6600100), srid=2154)]) + + self.assertEqual(list(PathAggregation.objects.filter(topo_object=topo).values_list('order', flat=True)), + [0, 1, 2, 3, 4, 5, 6, 7, 8])