Skip to content

Commit

Permalink
Add command to identify and merge some paths
Browse files Browse the repository at this point in the history
  • Loading branch information
Chatewgne committed Jul 21, 2023
1 parent 7321a98 commit a2364cf
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ CHANGELOG
2.99.0+dev (XXXX-XX-XX)
-----------------------

**New features**

- Add ``merge_segmented_paths`` command to find and merge paths (#3607)


2.99.0 (2023-07-18)
-----------------------
Expand Down
82 changes: 82 additions & 0 deletions geotrek/core/management/commands/merge_segmented_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import copy
from time import sleep

from django.core.management.base import BaseCommand
from django.db import connection, transaction

from geotrek.core.models import Path


class Command(BaseCommand):
help = 'Find and merge Paths that are splitted in several segments\n'

def add_arguments(self, parser):
parser.add_argument('--dry', '-d', action='store_true', dest='dry', default=False,
help="Do not change the database, dry run. Show the number of potential merges")

def handle(self, *args, **options):
dry = options.get('dry')

# Get all neighbours for each path, identify paths that could be merged (2 neighbours only)
neighbours = dict()
mergeables = []
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute('''select id1, array_agg(id2) from
(select p1.id as id1, p2.id as id2
from core_path p1, core_path p2
where st_touches(p1.geom, p2.geom) and (p1.id != p2.id)
group by p1.id, p2.id
order by p1.id) a
group by id1;''')

for path_id, path_neighbours_ids in cursor.fetchall():
if len(path_neighbours_ids) == 2:
mergeables.append(path_id)
neighbours[path_id] = path_neighbours_ids

# Match couples of paths to merge together (first neighbour, if not matched with another path yet)
still_mergeables = copy.deepcopy(mergeables)
merges = 0
merges_couples = []
merges_in = []
merges_out = []
merges_flat = []
for i in mergeables:
if i in still_mergeables:
neighbour = neighbours.get(i, None)
if neighbour:
first_neighbour = neighbour[0]
if first_neighbour in still_mergeables:
merges += 1
merges_couples.append((i, first_neighbour))
merges_flat.append(i)
merges_flat.append(first_neighbour)
merges_in.append(i)
merges_out.append(first_neighbour)
still_mergeables.remove(i)
still_mergeables.remove(first_neighbour)

algo_ok = set(merges_in).isdisjoint(set(merges_out))

print(f"Found {merges} potential merges")

if algo_ok and not dry:
successes = 0
fails = 0
for (a, b) in merges_couples:
patha = Path.include_invisible.get(pk=int(a))
pathb = Path.include_invisible.get(pk=int(b))
success = patha.merge_path(pathb)
if success == 2:
print(f"3rd path in intersection of {a} and {b}")
fails += 1
elif success == 0:
print(f"No matching points to merge paths {a} and {b} found")
fails += 1
else:
print(f"Merged {b} into {a}")
successes += 1
sleep(0.5)

print(f"{successes} successful merges - {fails} failed merges")
58 changes: 58 additions & 0 deletions geotrek/core/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,61 @@ def test_split_reorder_fail(self):
output = StringIO()
call_command('reorder_topologies', stdout=output)
self.assertIn(f'Topologies with errors :\nTREK id: {topo.pk}\n', output.getvalue())


class MergePathsTest(TestCase):
@classmethod
def setUpTestData(cls):
geom_1 = LineString((0, 0), (1, 1))
cls.p1 = Path.objects.create(geom=geom_1)
geom_2 = LineString((1, 1), (2, 2))
cls.p2 = Path.objects.create(geom=geom_2)
geom_3 = LineString((2, 2), (3, 3))
cls.p3 = Path.objects.create(geom=geom_3)
geom_4 = LineString((2, 2), (3, 1))
cls.p4 = Path.objects.create(geom=geom_4)
geom_5 = LineString((3, 3), (4, 4))
cls.p5 = Path.objects.create(geom=geom_5)
geom_6 = LineString((4, 4), (5, 5))
cls.p6 = Path.objects.create(geom=geom_6)
geom_7 = LineString((5, 5), (6, 6))
cls.p7 = Path.objects.create(geom=geom_7)
geom_8 = LineString((6, 6), (7, 7))
cls.p8 = Path.objects.create(geom=geom_8)
geom_9 = LineString((7, 7), (8, 8))
cls.p9 = Path.objects.create(geom=geom_9)

def test_find_and_merge_paths(self):
# Before call
# p1 p2 p3 p5 p6 p7 p8 p9
# +-------+------+-------+------+-------+------+-------+------+
# |
# | p4
# |
# +
self.assertEqual(Path.objects.count(), 9)
output = StringIO()
call_command('merge_segmented_paths', stdout=output)
# After first call :
# p1 p2 p3 p6 p8 p9
# +-------+------+-------+--------------+-------------+------+
# |
# | p4
# |
# +
self.assertEqual(Path.objects.count(), 7)
with self.assertRaises(Path.DoesNotExist):
Path.objects.get(pk=self.p5.pk)
with self.assertRaises(Path.DoesNotExist):
Path.objects.get(pk=self.p7.pk)
call_command('merge_segmented_paths', stdout=output)
# After second call :
# p1 p2 p3 p8 p9
# +-------+------+-------+----------------------------+------+
# |
# | p4
# |
# +
with self.assertRaises(Path.DoesNotExist):
Path.objects.get(pk=self.p6.pk)
self.assertEqual(Path.objects.count(), 6)

0 comments on commit a2364cf

Please sign in to comment.