diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 507089e266..7540ad6b7e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: name: Python Unittests runs-on: ubuntu-latest container: makinacorpus/geodjango:${{ matrix.os }} - + permissions: packages: write # required to publish docker image @@ -60,7 +60,7 @@ jobs: services: postgres: - image: postgis/postgis:12-2.5 + image: pgrouting/pgrouting:12-3.0-3.0.0 env: POSTGRES_DB: ci_test POSTGRES_PASSWORD: ci_test @@ -261,7 +261,7 @@ jobs: services: postgres: - image: postgis/postgis:12-2.5 + image: pgrouting/pgrouting:12-3.0-3.0.0 env: POSTGRES_DB: ci_test POSTGRES_PASSWORD: ci_test @@ -348,7 +348,7 @@ jobs: services: postgres: - image: postgis/postgis:12-2.5 + image: pgrouting/pgrouting:12-3.0-3.0.0 env: POSTGRES_DB: ci_test POSTGRES_PASSWORD: ci_test diff --git a/.gitignore b/.gitignore index d6fad2ef77..8abac2cfd1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,13 @@ __pycache__/ geotrek/settings/custom.py geotrek/cypress/node_modules Makefile.perso.mk + +# Benchmarking +tools/benchmarking/time_measures/ +tools/benchmarking/cypress/videos/ +tools/benchmarking/cypress/screenshots/ +tools/benchmarking/node_modules/ + # buildout stuff .mr.developer.cfg .installed.cfg diff --git a/cypress/integration/nav_create_trek.js b/cypress/integration/nav_create_trek.js index 640c9a5092..86a529b9ba 100644 --- a/cypress/integration/nav_create_trek.js +++ b/cypress/integration/nav_create_trek.js @@ -25,14 +25,7 @@ describe('Create trek', () => { cy.visit('/trek/list'); cy.wait('@tiles'); cy.server(); - cy.route('/api/path/drf/paths/graph.json').as('graph'); cy.get("a.btn-success[href='/trek/add/']").contains('Add a new trek').click(); - cy.wait('@graph'); - cy.get("a.linetopology-control").click(); - cy.get("textarea[id='id_topology']").type('[{"pk": 2, "kind": "TREK", "offset": 0.0, "paths": [3], "positions": {"0": [0.674882030756843, 0.110030805790642]}}]', { - force: true, - parseSpecialCharSequences: false - }); cy.get("input[id='id_duration']").type('100'); cy.get("input[name='name_en']").type('Trek number 1'); cy.get("a[href='#name_fr']").click(); @@ -52,6 +45,10 @@ describe('Create trek', () => { cy.setTinyMceContent('id_description_teaser_en', 'Description teaser number 1'); cy.setTinyMceContent('id_ambiance_en', 'Ambiance number 1'); cy.setTinyMceContent('id_description_en', 'Description number 1'); + cy.get("a.linetopology-control").click(); + cy.clickOnPath(3, 67); + cy.clickOnPath(3, 11); + cy.get('[data-test^="route-step-"]'); cy.get('#save_changes').click(); cy.url().should('not.include', '/trek/add/'); cy.get('.content').should('contain', 'Trek number 1'); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7a3b3e8b20..18c2e51376 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -21,4 +21,55 @@ Cypress.Commands.add('loginByCSRF', (username, password) => { Cypress.Commands.add('mockTiles', (username, password) => { cy.intercept("https://*.tile.opentopomap.org/*/*/*.png", {fixture: "images/tile.png"}).as("tiles"); +}); + +Cypress.Commands.add('getCoordsOnMap', (pathPk, percentage) => { + cy.getPath(pathPk).then(path => { + let domPath = path.get(0); + + // Get the coordinates relative to the map element + let pathLength = domPath.getTotalLength(); + let lengthAtPercentage = percentage * pathLength / 100; + return domPath.getPointAtLength(lengthAtPercentage); + }) +}); + +Cypress.Commands.add('getCoordsOnPath', (pathPk, percentage) => { + cy.getPath(pathPk).then(path => { + cy.getCoordsOnMap(pathPk, percentage).then(coordsOnMap => { + // Convert the coords so they are relative to the path + cy.getMap().then(map => { + let domMap = map.get(0); + let domPath = path.get(0); + + // Get the coords of the map and the path relative to the root DOM element + let mapCoords = domMap.getBoundingClientRect(); + let pathCoords = domPath.getBoundingClientRect(); + let horizontalDelta = pathCoords.x - mapCoords.x; + let verticalDelta = pathCoords.y - mapCoords.y; + + // Return the coords relative to the path element + return { + x: coordsOnMap.x - horizontalDelta, + y: coordsOnMap.y - verticalDelta, + } + }); + }) + + }) +}) + +Cypress.Commands.add('getMap', () => cy.get('[id="id_topology-map"]')); +Cypress.Commands.add('getPath', pathPk => cy.get(`[data-test=pathLayer-${pathPk}]`)); + +Cypress.Commands.add('clickOnPath', (pathPk, percentage) => { + // Get the coordinates of the click and execute it + cy.getCoordsOnPath(pathPk, percentage).then(clickCoords => { + let startTime; + cy.getPath(pathPk) + .then((path) => {startTime = performance.now(); return path}) + .click(clickCoords.x, clickCoords.y, {force: true}) + // Return startTime so it is yielded by the command + .then(() => startTime); + }); }); \ No newline at end of file diff --git a/debian/postinst b/debian/postinst index df5221b18f..9b713ff140 100644 --- a/debian/postinst +++ b/debian/postinst @@ -65,6 +65,7 @@ if [ "$MANAGE_DB" = "true" ] && [ -z "$2" ]; then # postgis_raster is only useful on postgis 3, it fails on postgis 2 but this is harmless su postgres -c "psql -q -d $POSTGRES_DB -c 'CREATE EXTENSION postgis_raster;'" || true su postgres -c "psql -q -d $POSTGRES_DB -c 'CREATE EXTENSION pgcrypto;'" || true + su postgres -c "psql -q -d $POSTGRES_DB -c 'CREATE EXTENSION pgrouting;'" || true fi # Generate secret key diff --git a/docker-compose.yml b/docker-compose.yml index 1f1eea41e3..afed054745 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ x-images: services: postgres: - image: postgis/postgis:12-2.5 + image: pgrouting/pgrouting:12-3.0-3.0.0 env_file: - .env ports: diff --git a/docs/changelog.rst b/docs/changelog.rst index 87c8183fc6..1d882b78d8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,20 @@ CHANGELOG 2.111.0+dev (XXXX-XX-XX) ---------------------------- +**Warnings** + +- When adding a via-point to a linear object's topology, it is now required to drop the new marker onto a path before the updated route is displayed, as the preview is no longer available when dragging the marker. This is due to the route computation now being performed on the backend. For more information, see https://github.com/GeotrekCE/Geotrek-admin/issues/4286 +- After adding new paths, pre-existing topologies can follow routes that are no longer the shortest. When editing topology-based linear objects through the interface, their route will no longer be automatically recomputed to the shortest option, which was unwanted behavior. This means you might now encounter topologies that take a detour despite not using a via-point marker. Be careful when editing such a route, as moving or adding neighboring markers will remove the detour. Note: this does not address the topology ordering issue when adding new paths. For more information, see https://github.com/GeotrekCE/Geotrek-admin/issues/4286 + +**Improvements** + +- Optimize routing: computation is now done in the backend using the A* algorithm (#4070) +- Prevent from placing route markers outside of paths or on unreachable paths (#4070) + +**Bug fixes** + +- Fix editing of topology-based linear objects via the interface: objects are no longer automatically rerouted (#4070) + **Documentation** - Update theme color diff --git a/docs/install/import.rst b/docs/install/import.rst index 20ba9f909c..4c1e60728e 100644 --- a/docs/install/import.rst +++ b/docs/install/import.rst @@ -106,7 +106,7 @@ or `via QGis following this blog post ` - * **Good to know** : + * **Good to know** : * The default SRID code is 4326 * The default encoding is UTF-8 * Imported paths are unpublished by default @@ -139,6 +139,26 @@ or `via QGis following this blog post . -.. warning:: +.. warning:: Relaunching an import **with the same file** will create duplicates. @@ -534,7 +554,7 @@ You can also use some of Geotrek commands to import data from a vector file hand Possible data are e.g.: POI, infrastructures, signages, cities, districts, restricted areas, paths. -You must use these commands to import spatial data because of the dynamic segmentation, which will not be computed if you enter the data manually. +You must use these commands to import spatial data because of the dynamic segmentation, which will not be computed if you enter the data manually. Here are the Geotrek commands available to import data from file: @@ -553,7 +573,7 @@ To get help about a command: :: sudo geotrek help - + .. _import-dem-altimetry: @@ -671,7 +691,7 @@ Import POIs * **Geometric type** : Point * **Expected formats** (supported by GDAL) : Shapefile, Geojson, Geopackage * **Template** : :download:`poi.geojson <../files/import/poi.geojson>` - * **Good to know** : + * **Good to know** : * The SRID must be 4326 * The default encoding is UTF-8 * Imported POIs are unpublished by default @@ -801,7 +821,7 @@ Import Infrastructure * **Geometric type** : Point * **Expected formats** (supported by GDAL) : Shapefile, Geojson, Geopackage * **Template** : :download:`infrastructure.geojson <../files/import/infrastructure.geojson>` - * **Good to know** : + * **Good to know** : * The SRID must be 4326 * The default encoding is UTF-8 * Imported infrastructures are unpublished by default @@ -980,7 +1000,7 @@ Import Signage * **Geometric type** : Point * **Expected formats** (supported by GDAL) : Shapefile, Geojson, Geopackage * **Template** : :download:`signage.geojson <../files/import/signage.geojson>` - * **Good to know** : + * **Good to know** : * The default SRID code is 4326 * The default encoding is UTF-8 * Imported signage are unpublished by default @@ -1058,7 +1078,7 @@ Import Cities * **Geometric type** : Polygon * **Expected formats** (supported by GDAL) : Shapefile, Geojson, Geopackage * **Template** : :download:`cities.geojson <../files/import/cities.geojson>` - * **Good to know** : + * **Good to know** : * The default SRID code is 4326 * The default encoding is UTF-8 * Imported cities are unpublished by default @@ -1145,7 +1165,7 @@ Import Districts * **Geometric type** : Polygon * **Expected formats** (supported by GDAL) : Shapefile, Geojson, Geopackage * **Template** : :download:`districts.geojson <../files/import/districts.geojson>` - * **Good to know** : + * **Good to know** : * The default SRID code is 4326 * The default encoding is UTF-8 * Imported districts are unpublished by default @@ -1219,7 +1239,7 @@ Import Restricted areas * **Geometric type** : Polygon * **Expected formats** (supported by GDAL) : Shapefile, Geojson, Geopackage * **Template** : :download:`restrictedareas.geojson <../files/import/restrictedareas.geojson>` - * **Good to know** : + * **Good to know** : * The default SRID code is 4326 * The default encoding is UTF-8 * Imported restricted areas are unpublished by default @@ -1265,10 +1285,10 @@ Merge segmented paths A path network is most optimized when there is only one path between intersections. If the path database includes many fragmented paths, they could be merged to improve performances. -You can run ``sudo geotrek merge_segmented_paths``. +You can run ``sudo geotrek merge_segmented_paths``. .. danger:: - This command can take several hours to run. During the process, every topology on a path will be set on the path it is merged with, but it would still be more efficient (and safer) to run it before creating topologies. + This command can take several hours to run. During the process, every topology on a path will be set on the path it is merged with, but it would still be more efficient (and safer) to run it before creating topologies. Before : :: diff --git a/docs/install/installation.rst b/docs/install/installation.rst index 69b31f5fbc..7fdd719542 100644 --- a/docs/install/installation.rst +++ b/docs/install/installation.rst @@ -79,8 +79,8 @@ If you are not confident with the ``install.sh`` script, or if you are having tr 1. Add ``deb https://packages.geotrek.fr/ubuntu bionic main`` to APT sources list. 2. Add https://packages.geotrek.fr/geotrek.gpg.key to apt keyring. 3. Run ``apt-get update`` -4. If you want to use a local database, install PostGIS package (before installing Geotrek-admin, not at the same time). - If not, you must create database and enable PostGIS extension before. +4. If you want to use a local database, install the pgRouting package by running ``sudo apt install -y postgresql-pgrouting wget software-properties-common`` (before installing Geotrek-admin, not at the same time). + If not, you must create database and enable PostGIS and pgRouting extensions before. 5. Install the Geotrek-admin package (``sudo apt install geotrek-admin``). .. note :: diff --git a/docs/install/upgrade.rst b/docs/install/upgrade.rst index eb515a4ee9..51baa6e27b 100644 --- a/docs/install/upgrade.rst +++ b/docs/install/upgrade.rst @@ -89,6 +89,19 @@ Make sure to run the following command **BEFORE** upgrading: ``su postgres -c "psql -q -d $POSTGRES_DB -c 'CREATE EXTENSION pgcrypto;'"`` +From Geotrek-admin <= 2.110.0 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**WARNING!** + +Starting from version 2.111.0, Geotrek now requires pgRouting. Before upgrading: + +1. Install the pgRouting package by running ``sudo apt install -y postgresql-pgrouting wget software-properties-common`` + +2. Run the following command: ``su postgres -c "psql -q -d $POSTGRES_DB -c 'CREATE EXTENSION pgrouting;'"`` + + + Server migration ~~~~~~~~~~~~~~~~ @@ -180,7 +193,7 @@ Now, install newest version of PostgreSQL and PostGIS: :: - sudo apt install postgresql-14-postgis-3 + sudo apt install postgresql-14-pgrouting @@ -206,6 +219,7 @@ Recreate user and database: CREATE EXTENSION postgis; CREATE EXTENSION postgis_raster; CREATE EXTENSION pgcrypto; + CREATE EXTENSION pgrouting; \q .. warning:: diff --git a/geotrek/common/management/commands/check_versions.py b/geotrek/common/management/commands/check_versions.py index c4cbde480e..5da19bc9f2 100644 --- a/geotrek/common/management/commands/check_versions.py +++ b/geotrek/common/management/commands/check_versions.py @@ -8,7 +8,7 @@ class Command(BaseCommand): - help = "Check Geotrek-admin, Python, Django, PostgreSQL and PostGIS version used by your system." + help = "Check Geotrek-admin, Python, Django, PostgreSQL, PostGIS and pgRouting version used by your system." def add_arguments(self, parser): parser.add_argument('--geotrek', action='store_true', help="Show only Geotrek version.") @@ -16,6 +16,7 @@ def add_arguments(self, parser): parser.add_argument('--django', action='store_true', help="Show only Django version.") parser.add_argument('--postgresql', action='store_true', help="Show only PostgreSQL version.") parser.add_argument('--postgis', action='store_true', help="Show only PostGIS version.") + parser.add_argument('--pgrouting', action='store_true', help="Show only pgRouting version.") parser.add_argument('--full', action='store_true', help="Show full version infos.") def get_geotrek_version(self): @@ -49,6 +50,15 @@ def get_postgis_version(self, full=False): cursor.execute("SELECT PostGIS_version()") return cursor.fetchone()[0].split(' ')[0] + def get_pgrouting_version(self, full=False): + with connection.cursor() as cursor: + if full: + cursor.execute("SELECT pgr_full_version()") + return cursor.fetchone()[0] + else: + cursor.execute("SELECT pgr_version()") + return cursor.fetchone()[0].split(' ')[0] + def handle(self, *args, **options): full = options['full'] @@ -72,9 +82,14 @@ def handle(self, *args, **options): self.stdout.write(self.get_postgis_version(full)) return + if options['pgrouting']: + self.stdout.write(self.get_pgrouting_version(full)) + return + self.stdout.write(f"Geotrek version : {self.style.SUCCESS(self.get_geotrek_version())}") self.stdout.write(f"Python version : {self.style.SUCCESS(self.get_python_version(full))}") self.stdout.write(f"Django version : {self.style.SUCCESS(self.get_django_version())}") self.stdout.write(f"PostgreSQL version : {self.style.SUCCESS(self.get_postgresql_version(full))}") self.stdout.write(f"PostGIS version : {self.style.SUCCESS(self.get_postgis_version(full))}") + self.stdout.write(f"pgRouting version : {self.style.SUCCESS(self.get_pgrouting_version(full))}") return diff --git a/geotrek/common/templates/common/sql/pre_20_extensions.sql b/geotrek/common/templates/common/sql/pre_20_extensions.sql index 79b619bf14..bf611cb2a7 100644 --- a/geotrek/common/templates/common/sql/pre_20_extensions.sql +++ b/geotrek/common/templates/common/sql/pre_20_extensions.sql @@ -1,3 +1,6 @@ -- Used to ensure extension is enabled even in test database (migrations disabled) -- Otherwise UUIDs on objects can not be generated on insert (including path splits) -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; \ No newline at end of file +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "postgis"; +CREATE EXTENSION IF NOT EXISTS "postgis_raster"; +CREATE EXTENSION IF NOT EXISTS "pgrouting" CASCADE; \ No newline at end of file diff --git a/geotrek/common/tests/test_commands.py b/geotrek/common/tests/test_commands.py index fe7520141a..233b6b9d83 100644 --- a/geotrek/common/tests/test_commands.py +++ b/geotrek/common/tests/test_commands.py @@ -192,18 +192,25 @@ def test_postgis_version(self, mock_cursor): call_command('check_versions', '--postgis', '--no-color', stdout=self.output) self.assertEqual(self.output.getvalue().strip(), '3.1.0') + @patch('django.db.connection.cursor') + def test_pgrouting_version(self, mock_cursor): + mock_cursor.return_value.__enter__.return_value.fetchone.return_value = ['3.0.0'] + call_command('check_versions', '--pgrouting', '--no-color', stdout=self.output) + self.assertEqual(self.output.getvalue().strip(), '3.0.0') + @patch('geotrek.common.management.commands.check_versions.sys') @patch('django.get_version', return_value='3.2.2') @patch('django.db.connection.cursor') def test_full_version(self, mock_cursor, mock_get_version, mock_version_info): type(mock_version_info).version = PropertyMock(return_value="3.9.1") - mock_cursor.return_value.__enter__.return_value.fetchone.return_value = ['14', '3.0'] + mock_cursor.return_value.__enter__.return_value.fetchone.side_effect = [('14',), ('3.0',), ('3.0.0',)] call_command('check_versions', '--full', '--no-color', stdout=self.output) expected_result = ( f"Geotrek version : {__version__}\n" "Python version : 3.9.1\n" "Django version : 3.2.2\n" "PostgreSQL version : 14\n" - "PostGIS version : 14" + "PostGIS version : 3.0\n" + "pgRouting version : 3.0.0" ) self.assertEqual(self.output.getvalue().strip(), expected_result) diff --git a/geotrek/core/graph.py b/geotrek/core/graph.py deleted file mode 100644 index ea27af8e33..0000000000 --- a/geotrek/core/graph.py +++ /dev/null @@ -1,57 +0,0 @@ -import math -from collections import defaultdict - - -def path_modifier(path): - length = 0.0 if math.isnan(path.length) else path.length - return {"id": path.pk, "length": length} - - -def get_key_optimizer(): - next_id = iter(range(1, 1000000)).__next__ - mapping = defaultdict(next_id) - return lambda x: mapping[x] - - -def graph_edges_nodes_of_qs(qs): - """ - return a graph on the form: - nodes { - coord_point_a { - coord_point_b: edge_id - } - } - edges { - edge_id: { - nodes: [point_a, point_b, ...] - ** extra settings (length etc.) - } - } - - - coord_point are tuple of float - """ - - key_modifier = get_key_optimizer() - value_modifier = path_modifier - - edges = defaultdict(dict) - nodes = defaultdict(dict) - - for path in qs: - coords = path.geom.coords - start_point, end_point = coords[0], coords[-1] - k_start_point, k_end_point = key_modifier(start_point), key_modifier(end_point) - - v_path = value_modifier(path) - v_path['nodes_id'] = [k_start_point, k_end_point] - edge_id = v_path['id'] - - nodes[k_start_point][k_end_point] = edge_id - nodes[k_end_point][k_start_point] = edge_id - edges[edge_id] = v_path - - return { - 'edges': dict(edges), - 'nodes': dict(nodes), - } diff --git a/geotrek/core/locale/de/LC_MESSAGES/django.po b/geotrek/core/locale/de/LC_MESSAGES/django.po index e288c9a74e..aa6865ffe8 100644 --- a/geotrek/core/locale/de/LC_MESSAGES/django.po +++ b/geotrek/core/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-03 15:39+0000\n" +"POT-Creation-Date: 2024-05-28 14:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -311,6 +311,15 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Warning" +msgstr "" + +msgid "The marker was not dropped on a path." +msgstr "" + +msgid "No routing found for this marker. Please move or delete it." +msgstr "" + msgid "No certification" msgstr "" diff --git a/geotrek/core/locale/en/LC_MESSAGES/django.po b/geotrek/core/locale/en/LC_MESSAGES/django.po index e288c9a74e..aa6865ffe8 100644 --- a/geotrek/core/locale/en/LC_MESSAGES/django.po +++ b/geotrek/core/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-03 15:39+0000\n" +"POT-Creation-Date: 2024-05-28 14:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -311,6 +311,15 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Warning" +msgstr "" + +msgid "The marker was not dropped on a path." +msgstr "" + +msgid "No routing found for this marker. Please move or delete it." +msgstr "" + msgid "No certification" msgstr "" diff --git a/geotrek/core/locale/es/LC_MESSAGES/django.po b/geotrek/core/locale/es/LC_MESSAGES/django.po index e288c9a74e..aa6865ffe8 100644 --- a/geotrek/core/locale/es/LC_MESSAGES/django.po +++ b/geotrek/core/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-03 15:39+0000\n" +"POT-Creation-Date: 2024-05-28 14:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -311,6 +311,15 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Warning" +msgstr "" + +msgid "The marker was not dropped on a path." +msgstr "" + +msgid "No routing found for this marker. Please move or delete it." +msgstr "" + msgid "No certification" msgstr "" diff --git a/geotrek/core/locale/fr/LC_MESSAGES/django.po b/geotrek/core/locale/fr/LC_MESSAGES/django.po index 5dc65d6c19..f917d7d77c 100644 --- a/geotrek/core/locale/fr/LC_MESSAGES/django.po +++ b/geotrek/core/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-03 15:39+0000\n" +"POT-Creation-Date: 2024-05-28 14:23+0000\n" "PO-Revision-Date: 2020-09-23 07:10+0000\n" "Last-Translator: Emmanuelle Helly \n" "Language-Team: French \n" "Language-Team: LANGUAGE \n" @@ -311,6 +311,15 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Warning" +msgstr "" + +msgid "The marker was not dropped on a path." +msgstr "" + +msgid "No routing found for this marker. Please move or delete it." +msgstr "" + msgid "No certification" msgstr "" diff --git a/geotrek/core/locale/nl/LC_MESSAGES/django.po b/geotrek/core/locale/nl/LC_MESSAGES/django.po index e288c9a74e..aa6865ffe8 100644 --- a/geotrek/core/locale/nl/LC_MESSAGES/django.po +++ b/geotrek/core/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-03 15:39+0000\n" +"POT-Creation-Date: 2024-05-28 14:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -311,6 +311,15 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Warning" +msgstr "" + +msgid "The marker was not dropped on a path." +msgstr "" + +msgid "No routing found for this marker. Please move or delete it." +msgstr "" + msgid "No certification" msgstr "" diff --git a/geotrek/core/management/commands/generate_pgr_network_topology.py b/geotrek/core/management/commands/generate_pgr_network_topology.py new file mode 100644 index 0000000000..41d9da22f0 --- /dev/null +++ b/geotrek/core/management/commands/generate_pgr_network_topology.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +from geotrek.core.path_router import PathRouter + + +class Command(BaseCommand): + help = """ + Generates the paths graph (pgRouting network topology) by filling + columns source and target of the core_path table. + """ + + def handle(self, *args, **options): + PathRouter() # PathRouter's init method builds the network topology diff --git a/geotrek/core/migrations/0037_path_source_pgr_path_target_pgr.py b/geotrek/core/migrations/0037_path_source_pgr_path_target_pgr.py new file mode 100644 index 0000000000..dd1713183e --- /dev/null +++ b/geotrek/core/migrations/0037_path_source_pgr_path_target_pgr.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.13 on 2024-07-23 10:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0036_auto_20230503_0837'), + ] + + operations = [ + migrations.AddField( + model_name='path', + name='source_pgr', + field=models.IntegerField(blank=True, db_column='source', editable=False, help_text='Internal field used by pgRouting', null=True), + ), + migrations.AddField( + model_name='path', + name='target_pgr', + field=models.IntegerField(blank=True, db_column='target', editable=False, help_text='Internal field used by pgRouting', null=True), + ), + ] diff --git a/geotrek/core/models.py b/geotrek/core/models.py index c94ba7aa7c..4905421618 100644 --- a/geotrek/core/models.py +++ b/geotrek/core/models.py @@ -73,6 +73,17 @@ class Path(CheckBoxActionMixin, ZoningPropertiesMixin, AddPropertyMixin, Geotrek draft = models.BooleanField(default=False, verbose_name=_("Draft"), db_index=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + source_pgr = models.IntegerField(null=True, + blank=True, + help_text='Internal field used by pgRouting', + editable=False, + db_column='source') + target_pgr = models.IntegerField(null=True, + blank=True, + help_text='Internal field used by pgRouting', + editable=False, + db_column='target') + objects = PathManager() include_invisible = PathInvisibleManager() diff --git a/geotrek/core/path_router.py b/geotrek/core/path_router.py new file mode 100644 index 0000000000..2f68724a1e --- /dev/null +++ b/geotrek/core/path_router.py @@ -0,0 +1,340 @@ +import json + +from django.db import connection +from django.conf import settings +from django.contrib.gis.geos import GEOSGeometry, Point, LineString, MultiLineString, GeometryCollection + +from geotrek.common.utils import sqlfunction +from .models import Path + + +class PathRouter: + def __init__(self): + self.set_path_network_topology() + + def set_path_network_topology(self): + """ Builds or updates the paths graph (pgRouting network topology) """ + cursor = connection.cursor() + query = """ + SELECT + pgr_createTopology( + 'core_path', + %s::float, + 'geom', + 'id' + ) + """ + cursor.execute(query, [settings.PATH_SNAPPING_DISTANCE]) + return ('OK',) == cursor.fetchone() + + def get_route(self, steps): + """ + Returns the whole route's geojson and topology. Both of them is an array + with each element being a sub-route from one step to another. The geojson + geometry's srid corresponds to the frontend's. + """ + self.steps = steps + self.steps_topo = [ + { + 'edge_id': step.get('path_id'), + 'fraction': self.get_step_fraction(step) + } + for step in steps + ] + line_strings, serialized_topology = self.compute_all_steps_routes() + if line_strings == []: + return None + + multi_line_string = GeometryCollection(line_strings, srid=settings.SRID) + multi_line_string.transform(settings.API_SRID) + geojson = json.loads(multi_line_string.geojson) + + return {'geojson': geojson, 'serialized': serialized_topology} + + def get_step_fraction(self, step): + """ + For one step on a path, returns its position on the path. + """ + # Transform the point to the right SRID + point = Point(step.get('lng'), step.get('lat'), srid=settings.API_SRID) + point.transform(settings.SRID) + # Get the closest path + closest_path = Path.objects.get(pk=step.get('path_id')) + # Get which fraction of the Path this point is on + closest_path_geom = f"'{closest_path.geom}'" + point_geom = f"'{point.ewkt}'" + fraction_of_distance = sqlfunction('SELECT ST_LineLocatePoint', + closest_path_geom, point_geom)[0] + return fraction_of_distance + + def compute_all_steps_routes(self): + """ + Returns the whole route's geometries and topology. Both of them is an array + with each element being a sub-route from one step to another. + """ + all_steps_geometries = [] # Each elem is a linestring from one step to another + all_steps_topologies = [] # Each elem is the topology from one step to another + # Compute the shortest path for each pair of adjacent steps + for i in range(len(self.steps_topo) - 1): + from_step = self.steps_topo[i] + to_step = self.steps_topo[i + 1] + # Get the linestrings (segments of paths) between those two steps, + # then merge them into one + one_step_geometry, topology = self.get_two_steps_route(from_step, to_step) + all_steps_topologies.append(topology) + if one_step_geometry is None: + return [], [] + all_steps_geometries.append(one_step_geometry) + return all_steps_geometries, all_steps_topologies + + def get_two_steps_route(self, from_step, to_step): + """ + Returns the geometry (as a LineString) and the topology of a subroute. + Parameters: + from_step: {edge_id: int, fraction: float} + to_step: {edge_id: int, fraction: float} + """ + from_edge_id = from_step.get('edge_id') + to_edge_id = to_step.get('edge_id') + + if from_edge_id == to_edge_id: + from_fraction = from_step.get('fraction') + to_fraction = to_step.get('fraction') + # If both points are on same edge, split it from the 1st to the 2nd + path_substring = self.create_path_substring( + from_edge_id, + from_fraction, + to_fraction + ) + line_strings = [path_substring] + topology = { + 'positions': {'0': [from_fraction, to_fraction]}, + 'paths': [from_edge_id], + } + else: + # Compute the shortest path between the two points + line_strings, topology = self.compute_two_steps_route(from_step, to_step) + if line_strings == []: + return None, None + + step_geometry = self.merge_line_strings(line_strings) + return step_geometry, topology + + def compute_two_steps_route(self, from_step, to_step): + """ + Computes the geometry (as an array of LineStrings) and the topology of + a subroute by using pgRouting. + Parameters: + from_step: {edge_id: int, fraction: float} + to_step: {edge_id: int, fraction: float} + """ + start_edge = from_step.get('edge_id') + end_edge = to_step.get('edge_id') + fraction_start = self._fix_fraction(from_step.get('fraction')) + fraction_end = self._fix_fraction(to_step.get('fraction')) + + query = """ + DO $$ + DECLARE + max_edge_id integer; + max_vertex_id integer; + + BEGIN + + SELECT MAX(id) FROM core_path INTO max_edge_id; + SELECT MAX(vid) FROM ( + SELECT source AS vid FROM core_path + UNION + SELECT target AS vid FROM core_path + ) AS vids INTO max_vertex_id; + + DROP TABLE IF EXISTS temporary_edges_info; + CREATE TEMPORARY TABLE temporary_edges_info AS + -- This info will be added to the A* inner query edges_sql. + -- It represents the temporary edges created by adding the start + -- and end steps as nodes onto the graph (aka pgr network topology) + WITH graph_temporary_edges AS ( + SELECT + core_path.id AS path_id, + max_edge_id + index AS edge_id, + ST_SmartLineSubstring( + core_path.geom, + fraction_start, + fraction_end + ) as edge_geom, + CASE + WHEN (index % 2) != 0 + THEN core_path.source + ELSE max_vertex_id + (index > 2)::int + 1 + END AS source_id, + CASE + WHEN (index % 2) = 0 + THEN core_path.target + ELSE max_vertex_id + (index > 2)::int + 1 + END AS target_id, + CASE + WHEN (index % 2) != 0 + THEN ST_StartPoint(geom) + ELSE ST_LineInterpolatePoint(core_path.geom, fraction_start) + END AS source_geom, + CASE + WHEN (index % 2) = 0 + THEN ST_EndPoint(geom) + ELSE ST_LineInterpolatePoint(core_path.geom, fraction_end) + END AS target_geom + FROM ( + VALUES + (1, '{}'::int, 0, '{}'::float), + (2, '{}'::int, '{}'::float, 1), + (3, '{}'::int, 0, '{}'::float), + (4, '{}'::int, '{}'::float, 1) + ) AS tmp_edges_info (index, path_id, fraction_start, fraction_end) + JOIN core_path ON core_path.id = tmp_edges_info.path_id + ) + SELECT + path_id, + edge_id AS id, + source_id AS source, + target_id AS target, + ST_Length(edge_geom) AS cost, + ST_Length(edge_geom) AS reverse_cost, + ST_X(source_geom) AS x1, + ST_Y(source_geom) AS y1, + ST_X(target_geom) AS x2, + ST_Y(target_geom) AS y2, + edge_geom as geom + FROM graph_temporary_edges; + + DROP TABLE IF EXISTS route; + CREATE TEMPORARY TABLE route AS + WITH pgr AS ( + SELECT + pgr.path_seq, + pgr.node, + (LEAD(pgr.node) OVER (ORDER BY path_seq)) AS next_node, + COALESCE(core_path.source, temporary_edges_info.source) as source, + COALESCE(core_path.target, temporary_edges_info.target) as target, + COALESCE(core_path.geom, temporary_edges_info.geom) as edge_geom, + CASE + WHEN pgr.edge > max_edge_id + THEN temporary_edges_info.path_id + ELSE pgr.edge + END AS edge + FROM + pgr_aStar( + 'SELECT + id, + source, + target, + length AS cost, + length AS reverse_cost, + ST_X(ST_StartPoint(geom)) AS x1, + ST_Y(ST_StartPoint(geom)) AS y1, + ST_X(ST_EndPoint(geom)) AS x2, + ST_Y(ST_EndPoint(geom)) AS y2 + FROM core_path + WHERE draft = false AND visible = true + UNION ALL SELECT + id, source, target, cost, reverse_cost, x1, y1, x2, y2 + FROM temporary_edges_info + ORDER by id', + max_vertex_id + 1, max_vertex_id + 2 + ) AS pgr + LEFT JOIN core_path ON id = pgr.edge + LEFT JOIN temporary_edges_info ON temporary_edges_info.id = pgr.edge + WHERE edge != -1 + ) + SELECT + CASE + WHEN source = next_node THEN ST_Reverse(edge_geom) + ELSE edge_geom + END AS edge_geom, + edge, + CASE + WHEN node = max_vertex_id + 1 THEN '{}'::float + WHEN node = source THEN 0 + ELSE 1 -- node = target + END AS fraction_start, + CASE + WHEN next_node IS NULL THEN '{}'::float + WHEN next_node = source THEN 0 + ELSE 1 -- next_node = target + END AS fraction_end + FROM pgr; + + END $$; + + SELECT + edge_geom, + edge, + fraction_start, + fraction_end + FROM route + """.format( + start_edge, fraction_start, + start_edge, fraction_start, + end_edge, fraction_end, + end_edge, fraction_end, + fraction_start, fraction_end + ) + + with connection.cursor() as cursor: + cursor.execute(query) + + query_result = cursor.fetchall() + if query_result == []: + return [], None + + geometries, edge_ids, fraction_starts, fraction_ends = list(zip(*query_result)) + return ( + [ + # Convert each geometry to a LineString + MultiLineString(*[GEOSGeometry(geometry)]).merged + for geometry in geometries + ], + { + 'positions': dict([ + (str(i), [fraction_starts[i], fraction_ends[i]]) + for i in range(len(fraction_starts)) + ]), + 'paths': list(edge_ids), + } + ) + + def _fix_fraction(self, fraction): + """ This function is used to fix an issue with pgRouting where a point's + position on an edge being 0.0 or 1.0 create a routing topology problem. + See https://github.com/pgRouting/pgrouting/issues/760 + So we create a fake fraction near the vertices of the edge. + """ + if float(fraction) == 1.0: + return 0.99999 + elif float(fraction) == 0.0: + return 0.00001 + return fraction + + def create_path_substring(self, path_id, start_fraction, end_fraction): + """ + Returns a path's substring for which start_fraction can be bigger than + end_fraction. + """ + path = Path.objects.get(pk=path_id) + sql = """ + SELECT ST_AsText(ST_SmartLineSubstring('{}'::geometry, {}, {})) + """.format(path.geom, start_fraction, end_fraction) + + cursor = connection.cursor() + cursor.execute(sql) + result = cursor.fetchone()[0] + + # Convert the string into an array of arrays of floats + coords_str = result.split('(')[1].split(')')[0] + str_points_array = [elem.split(' ') for elem in coords_str.split(',')] + arr = [[float(nb) for nb in sub_array] for sub_array in str_points_array] + + line_substring = LineString(arr, srid=settings.SRID) + return line_substring + + def merge_line_strings(self, line_strings): + multi_line_string = MultiLineString(line_strings, srid=settings.SRID) + return multi_line_string.merged diff --git a/geotrek/core/static/core/dijkstra.js b/geotrek/core/static/core/dijkstra.js deleted file mode 100644 index e45905acb3..0000000000 --- a/geotrek/core/static/core/dijkstra.js +++ /dev/null @@ -1,251 +0,0 @@ -var Geotrek = Geotrek || {}; - -Geotrek.Dijkstra = (function() { - - // TODO: doc - function get_shortest_path_from_graph(graph, from_ids, to_ids, exclude_from_to) { - // coerce int to string - from_ids = $.map(from_ids, function(k) { return '' + k; }); - to_ids = $.map(to_ids, function(k) { return '' + k; }); - - var graph_nodes = graph.nodes; - var graph_edges = graph.edges; - - function getPairWeightNode(node_id) { - var l = []; - $.each(graph_nodes[node_id], function(node_dest_id, edge_id) { - // Warning - weight is in fact edge.length in our data - l.push({'node_id': node_dest_id, 'weight': graph_edges[edge_id].length}); - }); - return l; - } - - function is_source(node_id) { - return (from_ids.indexOf(node_id) != -1) - } - function is_destination(node_id) { - return (to_ids.indexOf(node_id) != -1) - } - - var djk = {}; - - // weight is smallest so far: take it whatever happens - from_ids.forEach(function(node_id) { - djk[node_id] = {'prev': null, 'node': node_id, 'weight': 0, 'visited': false}; - }); - - // return the ID of an unvisited node that has the less weight (less djk weight) - // TODO: performance -> shoud not contain visited node, should be sorted by weight - function djk_get_next_id () { - var nodes_id = Object.keys(djk); - var mini_weight = Number.MAX_VALUE; - var mini_id = null; - var node_djk = null; - var weight = null; - - for (var k = 0; k < nodes_id.length; k++) { - var node_id = nodes_id[k]; - node_djk = djk[node_id]; - weight = node_djk.weight; - - // if already visited - skip - if (node_djk.visited === true) - continue; - - // Weight can't get lower - take it - if (weight == 0) - return node_id; - - // Otherwise try to find the minimum - if (weight < mini_weight) { - mini_id = node_id; - mini_weight = weight; - } - } - return mini_id; - } - - - var djk_current_node, current_node_id; - - while (true) { - // Get the next node to visit - djk_current_node = djk_get_next_id(); - - // Last node exhausted - we didn't find a path - if (djk_current_node === null) - return null; - - // The node exist - var current_djk_node = djk[djk_current_node]; - // Mark as visited (won't be chosen) - current_djk_node.visited = true; - // we could del it out of djk - - current_node_id = current_djk_node.node; - - // Last point - if (is_destination(current_node_id)) - break; - - // refactor to get next - var pairs_weight_node = getPairWeightNode(current_node_id); - - - // if current_djk_node.weight > ... BREAK - for (var i = 0; i < pairs_weight_node.length; i++) { - var edge_weight = pairs_weight_node[i].weight; - var next_node_id = pairs_weight_node[i].node_id; - - var next_weight = current_djk_node.weight + edge_weight; - - var djk_next_node = djk[next_node_id]; - - // push new node or update it - if (djk_next_node) { - // update node ? - if (djk_next_node.visited === true) - continue; - - // If its weight is inferior, this node has a better previous edge already - // Do not update it - if (djk_next_node.weight < next_weight) - continue; - - djk_next_node.weight = next_weight; - djk_next_node.prev = current_djk_node; - - } else { - // push node - djk[next_node_id] = { - 'prev': current_djk_node - , 'node': next_node_id - , 'weight': next_weight - , 'visited': false - }; - - } - } - }; - - - var path = []; - // Extract path - // current_djk_node is the destination - var final_weight = current_djk_node.weight; - var tmp = current_djk_node; - while (!is_source(tmp.node)) { - path.push(tmp.node); - tmp = tmp.prev; - } - - - if (exclude_from_to) { - path.shift(); // remove last node - } else { - path.push(tmp.node); // push first node - } - - // miss first and last step - path.reverse(); - - var i, j, full_path = []; - for (i = 0; i < path.length - 1; i++) { - var node_1 = path[i], node_2 = path[i+1]; - var edge = graph_edges[graph_nodes[node_1][node_2]]; - - // start end and edge are just ids - full_path.push({ - 'start': node_1, - 'end': node_2, - 'edge': edge, - 'weight': edge.length - }); - } - - return { - 'path': full_path - , 'weight': final_weight - }; - - }; - - return { - 'get_shortest_path_from_graph': get_shortest_path_from_graph - }; -})(); - - - -// Computed_paths: -// -// Returns: -// Array of { -// path : Array of { start: Node_id, end: Node_id, edge: Edge, weight: Int (edge.length) } -// weight: Int -// } -// -Geotrek.shortestPath = (function() { - - function computePaths(graph, steps) { - /* - * Returns list of paths, and null if not found. - */ - var paths = []; - for (var j = 0; j < steps.length - 1; j++) { - var path = computeTwoStepsPath(graph, steps[j], steps[j + 1]); - if (!path) - return null; - - path.from_pop = steps[j]; - path.to_pop = steps[j+1]; - paths.push(path); - } - return paths; - } - - function computeTwoStepsPath(graph, from_pop, to_pop) { - - // alter graph - var from_pop_opt = from_pop.addToGraph(graph) - , to_pop_opt = to_pop.addToGraph(graph); - - var from_nodes = [ from_pop_opt.new_node_id ] - , to_nodes = [ to_pop_opt.new_node_id ]; - - // weighted_path: { - // path : Array of { start: Node_id, end: Node_id, edge: Edge, weight: Int (edge.length) } - // weight: Int - // } - - var weighted_path = Geotrek.Dijkstra.get_shortest_path_from_graph(graph, from_nodes, to_nodes); - - // restore graph - from_pop_opt.rmFromGraph(); - to_pop_opt.rmFromGraph(); - - if(!weighted_path) - return null; - - // Some path component may use an edge that does not belong to the graph - // (a transient edge that was created from a transient point - a marker). - // In this case, the path component gets a new `real_edge' attribute - // which is the edge that the virtual edge is part of. - var pops_opt = [ from_pop_opt, to_pop_opt ]; - $.each(weighted_path.path, function(i, path_component) { - var edge_id = path_component.edge.id; - // Those PointOnPolylines knows the virtual edge and the initial one - for (var i = 0; i < pops_opt.length; i++) { - var pop_opt = pops_opt[i], - edge = pop_opt.new_edges[edge_id]; - if (edge !== undefined) { - path_component.real_edge = pop_opt.initial_edge; - break; - } - } - }); - return weighted_path; - } - - return computePaths; -})(); diff --git a/geotrek/core/static/core/geotrek.forms.topology.js b/geotrek/core/static/core/geotrek.forms.topology.js index 2150efdc01..852d1b916b 100644 --- a/geotrek/core/static/core/geotrek.forms.topology.js +++ b/geotrek/core/static/core/geotrek.forms.topology.js @@ -124,25 +124,13 @@ MapEntity.GeometryField.TopologyField = MapEntity.GeometryField.extend({ return; } - // Path layer is ready, load graph ! + // Path layer is ready, display the route this._pathsLayer.fire('data:loading'); - var url = window.SETTINGS.urls.path_graph; - $.getJSON(url, this._onGraphLoaded.bind(this)) - .error(graphError.bind(this)); - - function graphError(jqXHR, textStatus, errorThrown) { - this._pathsLayer.fire('data:loaded'); - $(this._map._container).addClass('map-error'); - console.error("Could not load url '" + window.SETTINGS.urls.path_graph + "': " + textStatus); - console.error(errorThrown); - } - }, - - _onGraphLoaded: function (graph) { - // Load graph - this._lineControl.setGraph(graph); this.load(); - // Stop spinning ! + // For each path layer, set its data-test attribute + this._pathsLayer.eachLayer((layer) => { + layer._path.setAttribute('data-test', 'pathLayer-' + layer.properties.id) + }) this._pathsLayer.fire('data:loaded'); }, @@ -159,7 +147,7 @@ MapEntity.GeometryField.TopologyField = MapEntity.GeometryField.extend({ this.store.lock(); console.debug("Deserialize topology: " + JSON.stringify(topo)); if (this._lineControl && !topo.lat && !topo.lng) { - this._lineControl.handler.restoreTopology(topo); + this._lineControl.handler.restoreGeometry(topo) } if (this._pointControl && topo.lat && topo.lng) { this._pointControl.handler.restoreTopology(topo); diff --git a/geotrek/core/static/core/images/marker-bound-disabled.png b/geotrek/core/static/core/images/marker-bound-disabled.png new file mode 100644 index 0000000000..62645dad37 Binary files /dev/null and b/geotrek/core/static/core/images/marker-bound-disabled.png differ diff --git a/geotrek/core/static/core/images/marker-drag-disabled.png b/geotrek/core/static/core/images/marker-drag-disabled.png new file mode 100644 index 0000000000..94833e975d Binary files /dev/null and b/geotrek/core/static/core/images/marker-drag-disabled.png differ diff --git a/geotrek/core/static/core/images/marker-drag-highlighted.png b/geotrek/core/static/core/images/marker-drag-highlighted.png new file mode 100644 index 0000000000..6beaf7fc5a Binary files /dev/null and b/geotrek/core/static/core/images/marker-drag-highlighted.png differ diff --git a/geotrek/core/static/core/leaflet.textpath.js b/geotrek/core/static/core/leaflet.textpath.js deleted file mode 100644 index f0b783334f..0000000000 --- a/geotrek/core/static/core/leaflet.textpath.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Inspired by Tom Mac Wright article : - * http://mapbox.com/osmdev/2012/11/20/getting-serious-about-svg/ - */ - -var PolylineTextPath = { - - __updatePath: L.Polyline.prototype._updatePath, - __bringToFront: L.Polyline.prototype.bringToFront, - __onAdd: L.Polyline.prototype.onAdd, - __onRemove: L.Polyline.prototype.onRemove, - - onAdd: function (map) { - this.__onAdd.call(this, map); - this._textRedraw(); - }, - - onRemove: function (map) { - map = map || this._map; - if (map && this._textNode) - map._pathRoot.removeChild(this._textNode); - this.__onRemove.call(this, map); - }, - - bringToFront: function () { - this.__bringToFront.call(this); - this._textRedraw(); - }, - - _updatePath: function () { - this.__updatePath.call(this); - this._textRedraw(); - }, - - _textRedraw: function () { - var text = this._text, - options = this._textOptions; - if (text) { - this.setText(null).setText(text, options); - } - }, - - setText: function (text, options) { - this._text = text; - this._textOptions = options; - - var defaults = {repeat: false, fillColor: 'black', attributes: {}}; - options = L.Util.extend(defaults, options); - - /* If empty text, hide */ - if (!text) { - if (this._textNode) - this._map._pathRoot.removeChild(this._textNode); - return this; - } - - text = text.replace(/ /g, '\u00A0'); // Non breakable spaces - var id = 'pathdef-' + L.Util.stamp(this); - var svg = this._map._pathRoot; - this._path.setAttribute('id', id); - - if (options.repeat) { - /* Compute single pattern length */ - var pattern = L.Path.prototype._createElement('text'); - for (var attr in options.attributes) - pattern.setAttribute(attr, options.attributes[attr]); - pattern.appendChild(document.createTextNode(text)); - svg.appendChild(pattern); - var alength = pattern.getComputedTextLength(); - svg.removeChild(pattern); - - /* Create string as long as path */ - text = new Array(Math.ceil(this._path.getTotalLength() / alength)).join(text); - } - - /* Put it along the path using textPath */ - var textNode = L.Path.prototype._createElement('text'), - textPath = L.Path.prototype._createElement('textPath'); - - var dy = options.offset || this._path.getAttribute('stroke-width'); - - textPath.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", '#'+id); - textNode.setAttribute('dy', dy); - for (var attr in options.attributes) - textNode.setAttribute(attr, options.attributes[attr]); - textPath.appendChild(document.createTextNode(text)); - textNode.appendChild(textPath); - svg.appendChild(textNode); - this._textNode = textNode; - return this; - } -}; - -L.Polyline.include(PolylineTextPath); - -L.LayerGroup.include({ - setText: function(text, options) { - for (var layer in this._layers) { - if (typeof this._layers[layer].setText === 'function') { - this._layers[layer].setText(text, options); - } - } - return this; - } -}); diff --git a/geotrek/core/static/core/multipath.js b/geotrek/core/static/core/multipath.js index b6d9172055..0dda158861 100644 --- a/geotrek/core/static/core/multipath.js +++ b/geotrek/core/static/core/multipath.js @@ -1,3 +1,5 @@ +var Geotrek = Geotrek || {}; + L.Mixin.ActivableControl = { activable: function (activable) { /** @@ -173,14 +175,6 @@ L.Control.LineTopology = L.Control.extend({ this.handler = new L.Handler.MultiPath(map, guidesLayer, options); }, - setGraph: function (graph) { - /** - * Set the Dikjstra graph - */ - this.handler.setGraph(graph); - this.activable(true); - }, - onAdd: function (map) { this._container = L.DomUtil.create('div', 'leaflet-draw leaflet-control leaflet-bar leaflet-control-zoom'); var link = L.DomUtil.create('a', 'leaflet-control-zoom-out linetopology-control', this._container); @@ -191,7 +185,7 @@ L.Control.LineTopology = L.Control.extend({ .addListener(link, 'click', L.DomEvent.preventDefault) .addListener(link, 'click', this.toggle, this); - // Control is not activable until paths and graph are loaded + // Control is not activable until paths are loaded this.activable(false); return this._container; @@ -240,9 +234,27 @@ L.Handler.MultiPath = L.Handler.extend({ this.map = map; this._container = map._container; this._guidesLayer = guidesLayer; + this._routeLayer = null + this._routeTopology = [] + this._currentStepsNb = 0 + this._previousStepsNb = 0 this.options = options; - this.graph = null; + this.spinner = new Spinner() + + // Toast displayed when a marker was not dropped on a path: + this._unsnappedMarkerToast = new bootstrap.Toast( + document.getElementById("routing-unsnapped-marker-error-toast"), + {delay: 5000} + ) + // Toast displayed when there is no route due to a marker on an unreachable path: + this._isolatedMarkerToast = new bootstrap.Toast( + document.getElementById("routing-isolated-marker-error-toast"), + {delay: 5000} + ) + + // Is the currently displayed route valid? i.e are all its markers linkable? + this._routeIsValid = null // markers this.markersFactory = this.getMarkers(); @@ -254,6 +266,26 @@ L.Handler.MultiPath = L.Handler.extend({ return guidesLayer.getLayer(id); }; + this.stepIndexToLayer = function(idx, layerArray) { + if (!layerArray) + return null + for (var i = 0; i < layerArray.length; i++) { + var layer = layerArray[i] + if (layer.step_idx == idx) + return layer + } + return null; + }; + + this.layersOrderedByIdx = function() { + if (!this._routeLayer) + return [] + var layers = this._routeLayer.getLayers() + var sortedLayers = layers.toSorted((first, second) => { + return first.step_idx - second.step_idx + }) + return sortedLayers + } /* * Draggable via steps @@ -268,7 +300,6 @@ L.Handler.MultiPath = L.Handler.extend({ }, this); // Draggable marker initialisation and step creation - var draggable_marker = null; var self = this; (function() { function dragstart(e) { @@ -291,35 +322,8 @@ L.Handler.MultiPath = L.Handler.extend({ init(); })(); - this.on('computed_paths', this.onComputedPaths, this); - }, - - setGraph: function (graph) { - this.graph = graph; - }, - - setState: function(state, autocompute) { - autocompute = autocompute === undefined ? true : autocompute; - var self = this; - - // Ensure we got a fresh start - this.disable(); - this.reset(); - this.enable(); - console.debug('setState('+JSON.stringify({start:{pk:state.start_layer.properties.id, - latlng:state.start_ll.toString()}, - end: {pk:state.end_layer.properties.id, - latlng:state.end_ll.toString()}})+')'); - - this._onClick({latlng: state.start_ll, layer:state.start_layer}); - this._onClick({latlng: state.end_ll, layer:state.end_layer}); - - state.via_markers && $.each(state.via_markers, function(idx, via_marker) { - console.debug('Add via marker (' + JSON.stringify({pk: via_marker.layer.properties.id, - latlng: via_marker.marker.getLatLng().toString()}) + ')'); - self.addViaStep(via_marker.marker, idx + 1); - self.forceMarkerToLayer(via_marker.marker, via_marker.layer); - }); + this.on('fetched_route', this.onFetchedRoute, this); + this.on('invalid_route', this.onInvalidRoute, this); }, // Reset the whole state @@ -335,10 +339,12 @@ L.Handler.MultiPath = L.Handler.extend({ // reset state this.steps = []; - this.computed_paths = []; - this.all_edges = []; - this.marker_source = this.marker_dest = null; + this._routeTopology = [] + this._routeLayer = null + this._currentStepsNb = 0 + this._previousStepsNb = 0 + this.fire('computed_topology', null); }, // Activate/Deactivate existing steps and markers - mostly about (un)bindings listeners @@ -357,6 +363,7 @@ L.Handler.MultiPath = L.Handler.extend({ addHooks: function () { L.DomUtil.addClass(this._container, 'cursor-topo-start'); + this._guidesLayer.on('click', this._onClick, this); this.stepsToggleActivate(true); @@ -377,41 +384,36 @@ L.Handler.MultiPath = L.Handler.extend({ // On click on a layer with the graph _onClick: function(e) { - if (this.steps.length >= 2) return; - var self = this; - var layer = e.layer , latlng = e.latlng - , len = this.steps.length; - var next_step_idx = this.steps.length; + var pop = this.addStartOrEndStep(layer, latlng) + pop.events.fire('placed'); + }, + + addStartOrEndStep: function(layer, latlng) { + if (this.steps.length >= 2) return; - // 1. Click - you are adding a new marker + var self = this; + var next_step_idx = this.steps.length; var marker; + if (next_step_idx == 0) { L.DomUtil.removeClass(this._container, 'cursor-topo-start'); L.DomUtil.addClass(this._container, 'cursor-topo-end'); marker = this.markersFactory.source(latlng); - marker.on('unsnap', function () { - this.showPathGeom(null); - }, this); this.marker_source = marker; } else { L.DomUtil.removeClass(this._container, 'cursor-topo-start'); L.DomUtil.removeClass(this._container, 'cursor-topo-end'); marker = this.markersFactory.dest(latlng) - marker.on('unsnap', function () { - this.showPathGeom(null); - }, this); this.marker_dest = marker; } var pop = self.createStep(marker, next_step_idx); - pop.toggleActivate(); - - // If this was clicked, the marker should be close enough, snap it. self.forceMarkerToLayer(marker, layer); + return pop }, forceMarkerToLayer: function(marker, layer) { @@ -425,8 +427,56 @@ L.Handler.MultiPath = L.Handler.extend({ var pop = new Geotrek.PointOnPolyline(marker); this.steps.splice(idx, 0, pop); // Insert pop at position idx - pop.events.on('valid', function() { - self.computePaths(); + pop.events.on('placed', () => { + + if (!pop.isValid()) { // If the pop was not dropped on a path + // Display an alert message + this._unsnappedMarkerToast.show() + + if (pop.previousPosition) { + // If the pop was on a path before, set it to its previous position + pop.marker.setLatLng(pop.previousPosition.ll) + self.forceMarkerToLayer(pop.marker, pop.previousPosition.polyline); + if (!this._routeIsValid) { + // If the route is not valid, the marker must stay highlighted + L.DomUtil.removeClass(pop.marker._icon, 'marker-snapped'); + } + } else { + // If not, then it is a new pop: remove it + self.removeViaStep(pop) + } + return + } + if (this.steps.length > 1) + this.enableLoadingMode() + + pop.previousPosition = {ll: pop.ll, polyline: pop.polyline} + + var currentStepIdx = self.getStepIdx(pop) + + // Create the array of new step indexes after the route is updated + var newStepsIndexes = [] + if (currentStepIdx > 0) + newStepsIndexes.push(currentStepIdx - 1) + newStepsIndexes.push(currentStepIdx) + if (currentStepIdx < self.steps.length - 1) + newStepsIndexes.push(currentStepIdx + 1) + + // Create the array of step indexes before the route is updated + var oldStepsIndexes + if (this._currentStepsNb == this.steps.length) // If a marker is being moved + oldStepsIndexes = [...newStepsIndexes] + else { // If a marker is being added + if (this._currentStepsNb == 1) // If it's the destination + oldStepsIndexes = [] + else + oldStepsIndexes = newStepsIndexes.slice(0, -1) + } + + this._previousStepsNb = this._currentStepsNb + this._currentStepsNb = this.steps.length + + self.fetchRoute(oldStepsIndexes, newStepsIndexes, pop) }); return pop; @@ -434,8 +484,6 @@ L.Handler.MultiPath = L.Handler.extend({ // add an in between step addViaStep: function(marker, step_idx) { - var self = this; - // A via step idx must be inserted between first and last... if (! (step_idx >= 1 && step_idx <= this.steps.length - 1)) { throw "StepIndexError"; @@ -443,35 +491,36 @@ L.Handler.MultiPath = L.Handler.extend({ var pop = this.createStep(marker, step_idx); - // remove marker on click - function removeViaStep() { - self.steps.splice(self.getStepIdx(pop), 1); - self.map.removeLayer(marker); - self.computePaths(); - } + var removeOnClick = () => this.removeViaStepFromRoute(pop) - function removeOnClick() { marker.on('click', removeViaStep); } - pop.marker.activate_cbs.push(removeOnClick); - pop.marker.deactivate_cbs.push(function() { marker.off('click', removeViaStep); }); + pop.marker.activate_cbs.push(() => marker.on('click', removeOnClick)); + pop.marker.deactivate_cbs.push(() => marker.off('click', removeOnClick)); - // marker is already activated, trigger manually removeOnClick - removeOnClick(); + // marker is already activated, enable removeOnClick manually + marker.on('click', removeOnClick) pop.toggleActivate(); + return pop }, - canCompute: function() { - if (!this.graph) - return false; - - if (this.steps.length < 2) - return false; - - for (var i = 0; i < this.steps.length; i++) { - if (! this.steps[i].isValid()) - return false; - } + // Remove an existing step by clicking on it + removeViaStepFromRoute: function(pop) { + this.enableLoadingMode() + var step_idx = this.getStepIdx(pop) + this.removeViaStep(pop) + this._previousStepsNb = this._currentStepsNb + this._currentStepsNb = this.steps.length + this.fetchRoute( + [step_idx - 1, step_idx, step_idx + 1], + [step_idx - 1, step_idx], + pop + ); + }, - return true; + // Remove a step from the steps list + removeViaStep: function(pop) { + var step_idx = this.getStepIdx(pop) + this.steps.splice(step_idx, 1) + this.map.removeLayer(pop.marker) }, getStepIdx: function(step) { @@ -486,179 +535,203 @@ L.Handler.MultiPath = L.Handler.extend({ return -1; }, - computePaths: function() { - if (this.canCompute()) { - var computed_paths = Geotrek.shortestPath(this.graph, this.steps); - this._onComputedPaths(computed_paths); + getCookie: function(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } } + return cookieValue; }, - - // Extract the complete edges list from the first to the last one - _eachInnerComputedPathsEdges: function(computed_paths, f) { - if (computed_paths) { - computed_paths.forEach(function(cpath) { - cpath.path.forEach(function(path_component) { - f(path_component.edge); - }); - }); + fetchRoute: function(oldStepsIndexes, newStepsIndexes, pop) { + /* + oldStepsIndexes: indexes of the steps for which to update the route + newStepsIndexes: indexes of these steps after the route is updated + pop (PointOnPolyline): step that is being added/modified/deleted + */ + + var stepsToRoute = [] + newStepsIndexes.forEach(idx => { + stepsToRoute.push(this.steps[idx]) + }) + + function canFetchRoute() { + if (stepsToRoute.length < 2) + return false; + for (var i = 0; i < stepsToRoute.length; i++) { + if (!stepsToRoute[i].isValid()) + return false; + } + return true; } - }, - - // Extract the complete edges list from the first to the last one - _extractAllEdges: function(computed_paths) { - if (! computed_paths) - return []; - - var edges = $.map(computed_paths, function(cpath) { - var dups = $.map(cpath.path, function(path_component) { - return path_component.real_edge || path_component.edge; - }); + if (!canFetchRoute()) { + this.disableLoadingMode() + return + } - // Remove adjacent duplicates - var dedup = []; - for (var i=0; i { + var sentStep = { + path_id: step.polyline.properties.id, + lat: step.ll.lat, + lng: step.ll.lng, } - return [dedup]; - }); - - return edges; + sentSteps.push(sentStep) + }) + + fetch(window.SETTINGS.urls['route_geometry'], { + method: 'POST', + headers: { + "X-CSRFToken": this.getCookie('csrftoken'), + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: JSON.stringify({ + steps: sentSteps, + }) + }) + .then(response => { + if (response.status == 200) + return response.json() + return Promise.reject(response) + }) + .then( + data => { // Status code 200: + if (data) { + var route = { + 'geojson': data.geojson, + 'serialized': data.serialized, + 'oldStepsIndexes': oldStepsIndexes, + 'newStepsIndexes': newStepsIndexes, + } + this.fire('fetched_route', route); + } + }, + // If the promise was rejected: + response => { + console.log("fetchRoute:", response) + this.fire('invalid_route', pop) + } + ) + .catch(e => { + console.log("fetchRoute", e) + this.disableLoadingMode() + }) }, - _onComputedPaths: function(new_computed_paths) { - var self = this; - var old_computed_paths = this.computed_paths; - this.computed_paths = new_computed_paths; - - // compute and store all edges of the new paths (usefull for further computation) - this.all_edges = this._extractAllEdges(new_computed_paths); - - this.fire('computed_paths', { - 'computed_paths': new_computed_paths, - 'new_edges': this.all_edges, - 'old': old_computed_paths, - 'marker_source': this.marker_source, - 'marker_dest': this.marker_dest - }); + enableLoadingMode: function () { + // Prevent from modifying steps while the route is fetched + this.spinner.spin(this._container); + this.disableMarkers() }, - restoreTopology: function (topo) { + disableLoadingMode: function () { + this.spinner.stop() + // If the route is invalid, don't reenable the markers: some have + // been disabled to guide the user through correcting the route. + if (this._routeIsValid) + this.enableMarkers() + }, - /* - * Topo is a list of sub-topologies. - * - * X--+--+---O-------+----O--+---+--X - * - * Each sub-topoogy is a way between markers. The first marker - * of the first sub-topology is the beginning, the last of the last is the end. - * All others are intermediary points (via markers) - */ + restoreGeometry: function (serializedTopology) { var self = this; - - // Only first and last positions - if (topo.length == 1 && topo[0].paths.length == 1) { - // There is only one path, both positions values are relevant - // and each one represents a marker - var topo = topo[0] - , paths = topo.paths - , positions = topo.positions; - - var first_pos = positions[0][0]; - var last_pos = positions[0][1]; - - var start_layer = this.idToLayer(paths[0]); - var end_layer = this.idToLayer(paths[paths.length - 1]); - - var start_ll = L.GeometryUtil.interpolateOnLine(this.map, start_layer, first_pos).latLng; - var end_ll = L.GeometryUtil.interpolateOnLine(this.map, end_layer, last_pos).latLng; - - var state = { - start_ll: start_ll, - end_ll: end_ll, - start_layer: start_layer, - end_layer: end_layer - }; - this.setState(state); - } - else { - var start_layer_ll = {} - , end_layer_ll = {} - , via_markers = []; - - var pos2latlng = function (pos, layer) { - var used_pos = pos; - if (pos instanceof Array) { - used_pos = pos[1]; // Default is second position (think of last path of topology) - if (pos[0] == 0.0 && pos[1] != 1.0) - used_pos = pos[1]; - if (pos[0] == 1.0 && pos[1] != 0.0) - used_pos = pos[1]; - if (pos[0] != 1.0 && pos[1] == 0.0) - used_pos = pos[0]; - if (pos[0] != 0.0 && pos[1] == 1.0) - used_pos = pos[0]; - console.log("Chose " + used_pos + " for " + pos); - } - var interpolated = L.GeometryUtil.interpolateOnLine(self.map, layer, used_pos); - if (!interpolated) { - throw ('Could not interpolate ' + used_pos + ' on layer ' + layer.properties.id); - } - return interpolated.latLng; - }; - - for (var i=0; i { + var groupLayer = this.buildGeometryFromTopology(topology); + groupLayer.step_idx = idx + restoredRouteLayer.addLayer(groupLayer) + }) + this._routeLayer = restoredRouteLayer + this.showPathGeom(restoredRouteLayer); + + this.setupNewRouteDragging(restoredRouteLayer) + + // Add the start marker + var topology = serializedTopology[0] + var pathLayer = this.idToLayer(topology.paths[0]) + var latlng = pos2latlng(topology.positions[0][0], pathLayer) + var popStart = this.addStartOrEndStep(pathLayer, latlng) + popStart.previousPosition = {ll: popStart.ll, polyline: popStart.polyline} + + // Add the end marker + topology = serializedTopology[serializedTopology.length - 1] + var lastPosIdx = topology.paths.length - 1 + pathLayer = this.idToLayer(topology.paths[lastPosIdx]) + latlng = pos2latlng(topology.positions[lastPosIdx][1], pathLayer) + var popEnd = this.addStartOrEndStep(pathLayer, latlng) + popEnd.previousPosition = {ll: popEnd.ll, polyline: popEnd.polyline} + + // Add the via markers: for each topology, use its first position, + // except for the first topology (it would be the start marker) + serializedTopology.forEach((topo, idx) => { + if (idx == 0) + return + pathLayer = this.idToLayer(topo.paths[0]) + latlng = pos2latlng(topo.positions[0][0], pathLayer) + var viaMarker = { + layer: pathLayer, + marker: self.markersFactory.drag(latlng, null, true) + } + var pop = self.addViaStep(viaMarker.marker, idx); + self.forceMarkerToLayer(viaMarker.marker, viaMarker.layer); + pop.previousPosition = {ll: pop.ll, polyline: pop.polyline} + }) + + // Set the state + serializedTopology.forEach(topo => { + this._routeTopology.push({ + positions: topo.positions, + paths: topo.paths, + }) + }) + this._currentStepsNb = this.steps.length + this._previousStepsNb = this.steps.length + this._routeIsValid = true + }, - // Restore state as if a user clicks. - this.setState(state); + buildGeometryFromTopology: function (topology) { + var latlngs = []; + for (var i = 0; i < topology.paths.length; i++) { + var path = topology.paths[i], + positions = topology.positions[i], + polyline = this.idToLayer(path); + if (positions) { + latlngs.push(L.GeometryUtil.extract(polyline._map, polyline, positions[0], positions[1])); + } + else { + console.warn('Topology problem: ' + i + ' not in ' + JSON.stringify(topology.positions)); + } } + return L.multiPolyline(latlngs); }, showPathGeom: function (layer) { @@ -680,16 +753,22 @@ L.Handler.MultiPath = L.Handler.extend({ if (new_path_layer) { self.map.addLayer(new_path_layer); new_path_layer.setStyle({'color': 'yellow', 'weight': 5, 'opacity': 0.8}); + let stepIdx = 0; new_path_layer.eachLayer(function (l) { if (typeof l.setText == 'function') { l.setText('> ', {repeat: true, attributes: {'fill': '#FF5E00'}}); + l.eachLayer((layer) => { + layer._path.setAttribute('data-test', 'route-step-' + stepIdx); + }) } + stepIdx++; }); } } } })(); - this.markPath.updateGeom(layer); + + this.markPath.updateGeom(layer); }, getMarkers: function() { @@ -755,12 +834,120 @@ L.Handler.MultiPath = L.Handler.extend({ return markersFactory; }, - onComputedPaths: function(data) { - var self = this; - var topology = Geotrek.TopologyHelper.buildTopologyFromComputedPath(this.idToLayer, data); + disableMarkers: function() { + // Disable all markers on the map + this.steps.forEach(step => { + step.marker.deactivate(); + }) + // Prevent from creating new markers + this.map.off('mousemove', this.drawOnMouseMove); + this.map.removeLayer(this.draggable_marker); + }, + + enableMarkers: function() { + // Enable all markers on the map + this.steps.forEach(step => { + step.marker.activate(); + }) + // Allow creating new markers again + this.map.on('mousemove', this.drawOnMouseMove); + }, + + updateRouteLayers: function(geometries, oldStepsIndexes, newStepsIndexes) { + var nbOldSteps = oldStepsIndexes.length + var nbNewSteps = newStepsIndexes.length + var previousRouteLayer = this._routeLayer.getLayers() + + // If a NEW via marker was unreachable, this invalid part of the path + // was not displayed. So at this point, there is one more step, but + // there isn't one more layer. + var isNewMarkerBeingCorrected = this._routeIsValid == false && this._previousStepsNb > previousRouteLayer.length + 1 + + // Remove out of date layers + oldStepsIndexes.slice(0, -1).forEach((index, i) => { + if (isNewMarkerBeingCorrected && i == nbOldSteps - 2) + return + var oldLayer = this.stepIndexToLayer(index, previousRouteLayer) + if (oldLayer) + this._routeLayer.removeLayer(oldLayer) + }) + + // Update the remaining layers' indexes + this._routeLayer.eachLayer(function(subRouteLayer) { + if (subRouteLayer.step_idx >= oldStepsIndexes.at(-1)) { + // Adding a step: increment the next layers' indexes + // Removing a step: decrement the next layers' indexes + // Moving a step: no changes except... + subRouteLayer.step_idx += (nbNewSteps - nbOldSteps) + } else if (subRouteLayer.step_idx >= oldStepsIndexes.at(-1) - (isNewMarkerBeingCorrected && nbOldSteps == nbNewSteps)) { + // ... if a new, unreachable marker is being moved and is now reachable, the + // nb of layers increases by 1 and the next layers' indexes must be incremented + subRouteLayer.step_idx += 1 + } + }) + + // Add the new layers + newStepsIndexes.slice(0, -1).forEach((index, i) => { + var newLayer = L.geoJson(geometries[i]) + newLayer.step_idx = index + newLayer.setStyle({'color': 'yellow', 'weight': 5, 'opacity': 0.8}); + newLayer.eachLayer(function (l) { + if (typeof l.setText == 'function') { + l.setText('> ', {repeat: true, attributes: {'fill': '#FF5E00'}}); + } + }); + this._routeLayer.addLayer(newLayer) + }) + + this._routeLayer.eachLayer(function (l) { + if (typeof l.setText == 'function') { + l.eachLayer((layer) => { + layer._path.setAttribute('data-test', 'route-step-' + l.step_idx); + }) + } + }); + }, + + onFetchedRoute: function(data) { + // Reset all the markers to 'snapped' appearance + this.steps.forEach(step => { + L.DomUtil.removeClass(step.marker._icon, 'marker-highlighted'); + L.DomUtil.removeClass(step.marker._icon, 'marker-disabled'); + L.DomUtil.addClass(step.marker._icon, 'marker-snapped'); + step.marker.activate(); + }) + oldStepsIndexes = data.oldStepsIndexes + newStepsIndexes = data.newStepsIndexes + + if (!this._routeLayer) { + this._routeLayer = L.featureGroup() + this.map.addLayer(this._routeLayer); + this.showPathGeom(this._routeLayer) + } + var previousRouteLayer = this._routeLayer.getLayers() + this.updateRouteLayers(data.geojson.geometries, oldStepsIndexes, newStepsIndexes) + + var isNewMarkerBeingCorrected = this._routeIsValid == false && this._previousStepsNb > previousRouteLayer.length + 1 + + // Store the new topology + var nbSubToposToRemove = 0 + if (isNewMarkerBeingCorrected) { + nbSubToposToRemove = 1 + } else if (oldStepsIndexes.length > 0) { + // If it's not the first time displaying a layer + nbSubToposToRemove = oldStepsIndexes.length - 1 + } + var spliceArgs = [newStepsIndexes[0], nbSubToposToRemove].concat(data.serialized) + this._routeTopology.splice.apply(this._routeTopology, spliceArgs) + this.fire('computed_topology', {topology: this._routeTopology}); - this.showPathGeom(topology.layer); - this.fire('computed_topology', {topology:topology.serialized}); + this._routeIsValid = true + this.setupNewRouteDragging(this._routeLayer) + this.disableLoadingMode() + }, + + setupNewRouteDragging: function(routeLayer) { + var self = this; // ## ONCE ## if (this.drawOnMouseMove) { @@ -779,7 +966,6 @@ L.Handler.MultiPath = L.Handler.extend({ dragTimer = date; - for (var i = 0; i < self.steps.length; i++) { // Compare point rather than ll var marker_ll = self.steps[i].marker.getLatLng(); @@ -798,7 +984,7 @@ L.Handler.MultiPath = L.Handler.extend({ , closest_point = null , matching_group_layer = null; - topology.layer && topology.layer.eachLayer(function(group_layer) { + routeLayer && routeLayer.eachLayer(function(group_layer) { group_layer.eachLayer(function(layer) { var p = layer.closestLayerPoint(layerPoint); if (p && p.distance < min_dist && p.distance < MIN_DIST) { @@ -821,26 +1007,52 @@ L.Handler.MultiPath = L.Handler.extend({ }; this.map.on('mousemove', this.drawOnMouseMove); - } + }, -}); + onInvalidRoute: function(pop) { + this._routeIsValid = false + this.fire('computed_topology', {topology: null}); + // Display an alert message + this._isolatedMarkerToast.show() -Geotrek.getNextId = (function() { - var next_id = 90000000; - return function() { - return next_id++; - }; -})(); + if (this.steps.length <= 2) { + // If there are only two steps, both should be movable: enable them + this.enableMarkers() + } else { + // Highlight the invalid marker and enable it + L.DomUtil.removeClass(pop.marker._icon, 'marker-snapped'); + L.DomUtil.addClass(pop.marker._icon, 'marker-highlighted'); + pop.marker.activate() + + // Set the other markers to grey and disable them + this.steps.forEach(step => { + if (step._leaflet_id != pop._leaflet_id) { + L.DomUtil.removeClass(step.marker._icon, 'marker-snapped'); + L.DomUtil.addClass(step.marker._icon, 'marker-disabled'); + step.marker.deactivate(); + } + }) + // Prevent from creating new via-steps + this.map.off('mousemove', this.drawOnMouseMove); + this.map.removeLayer(this.draggable_marker); + } + this.disableLoadingMode() + } + +}); // pol: point on polyline Geotrek.PointOnPolyline = function (marker) { this.marker = marker; - // if valid + + // If valid: this.ll = null; this.polyline = null; - this.path_length = null; - this.percent_distance = null; + + // To reset the pop to its previous valid position when not dropped on a path: + this.previousPosition = null; + this._activated = false; this.events = L.Util.extend({}, L.Mixin.Events); @@ -853,14 +1065,15 @@ Geotrek.PointOnPolyline = function (marker) { 'snap': function onSnap(e) { this.ll = e.latlng; this.polyline = e.layer; - this.path_length = L.GeometryUtil.length(this.polyline); - this.percent_distance = L.GeometryUtil.locateOnLine(this.polyline._map, this.polyline, this.ll); this.events.fire('valid'); }, 'unsnap': function onUnsnap(e) { this.ll = null; this.polyline = null; this.events.fire('invalid'); + }, + 'dragend': function onDragEnd(e) { + this.events.fire('placed'); } }; }; @@ -885,66 +1098,9 @@ Geotrek.PointOnPolyline.prototype.toggleActivate = function(activate) { marker[method]('move', markerEvents.move, this); marker[method]('snap', markerEvents.snap, this); marker[method]('unsnap', markerEvents.unsnap, this); + marker[method]('dragend', markerEvents.dragend, this); }; Geotrek.PointOnPolyline.prototype.isValid = function(graph) { return (this.ll && this.polyline); }; - -// Alter the graph: adding two edges and one node (the polyline gets break in two parts by the point) -// The polyline MUST be an edge of the graph. -Geotrek.PointOnPolyline.prototype.addToGraph = function(graph) { - if (! this.isValid()) - return null; - - var self = this; - - var edge = graph.edges[this.polyline.properties.id] - , first_node_id = edge.nodes_id[0] - , last_node_id = edge.nodes_id[1]; - - // To which nodes dist start_point/end_point corresponds ? - // The edge.nodes_id are ordered, it corresponds to polylines: coords[0] and coords[coords.length - 1] - var dist_start_point = this.percent_distance * this.path_length - , dist_end_point = (1 - this.percent_distance) * this.path_length - ; - - var new_node_id = Geotrek.getNextId(); - - var edge1 = {'id': Geotrek.getNextId(), 'length': dist_start_point, 'nodes_id': [first_node_id, new_node_id] }; - var edge2 = {'id': Geotrek.getNextId(), 'length': dist_end_point, 'nodes_id': [new_node_id, last_node_id]}; - - var first_node = {}, last_node = {}, new_node = {}; - first_node[new_node_id] = new_node[first_node_id] = edge1.id; - last_node[new_node_id] = new_node[last_node_id] = edge2.id; - - // - var new_edges = {}; - new_edges[edge1.id] = graph.edges[edge1.id] = edge1; - new_edges[edge2.id] = graph.edges[edge2.id] = edge2; - - graph.nodes[new_node_id] = new_node; - $.extend(graph.nodes[first_node_id], first_node); - $.extend(graph.nodes[last_node_id], last_node); - // - - function rmFromGraph() { - delete graph.edges[edge1.id]; - delete graph.edges[edge2.id]; - - delete graph.nodes[new_node_id]; - delete graph.nodes[first_node_id][new_node_id]; - delete graph.nodes[last_node_id][new_node_id]; - } - - return { - self: self, - new_node_id: new_node_id, - new_edges: new_edges, - dist_start_point: dist_start_point, - dist_end_point: dist_end_point, - initial_edge: edge, - rmFromGraph: rmFromGraph - }; -}; - diff --git a/geotrek/core/static/core/style.css b/geotrek/core/static/core/style.css index b84268a224..4c129f89df 100644 --- a/geotrek/core/static/core/style.css +++ b/geotrek/core/static/core/style.css @@ -56,6 +56,22 @@ background-image: url('images/marker-target.png'); } +.marker-source.marker-disabled { + background-image: url('images/marker-bound-disabled.png'); +} + +.marker-target.marker-disabled { + background-image: url('images/marker-bound-disabled.png'); +} + +.marker-drag.marker-disabled { + background-image: url('images/marker-drag-disabled.png'); +} + +.marker-drag.marker-highlighted { + background-image: url('images/marker-drag-highlighted.png'); +} + .marker-source.marker-snapped { background-image: url('images/marker-source-snap.png'); } @@ -94,3 +110,30 @@ span.aggregation { background-image: none; background-color: yellow; } + +.toast-stack { + position: absolute; + right: 1.5rem; + bottom: 1.5rem; +} + +.toast-error { + background-color: #f8d7da; + border-color: #f5c2c7; + display: none; +} + +.toast-error-header { + justify-content: space-between; + color: #842029; + background-color: #f8d7da; +} + +.toast-error-button { + color: #842029; +} + +.toast-error-body { + background-color: #f8d7da; + color: #842029; +} diff --git a/geotrek/core/static/core/topology_helper.js b/geotrek/core/static/core/topology_helper.js deleted file mode 100644 index b1b23e01b7..0000000000 --- a/geotrek/core/static/core/topology_helper.js +++ /dev/null @@ -1,311 +0,0 @@ -var Geotrek = Geotrek || {}; - -Geotrek.TopologyHelper = (function() { - - /** - * This static function takes a list of Dijkstra results, and returns - * a serialized topology, as expected by form widget, as well as a - * multiline geometry for highlight the result. - */ - function buildSubTopology(paths, polylines, ll_start, ll_end, offset) { - var polyline_start = polylines[0] - , polyline_end = polylines[polylines.length - 1] - , single_path = paths.length == 1 - , cleanup = true - , positions = {}; - - if (!polyline_start || !polyline_end) { - console.error("Could not compute distances without polylines."); - return null; // TODO: clean-up before give-up ? - } - - var pk_start = L.GeometryUtil.locateOnLine(polyline_start._map, polyline_start, ll_start), - pk_end = L.GeometryUtil.locateOnLine(polyline_end._map, polyline_end, ll_end); - console.debug('Start on layer ' + polyline_start.properties.id + ' ' + pk_start + ' ' + ll_start.toString()); - console.debug('End on layer ' + polyline_end.properties.id + ' ' + pk_end + ' ' + ll_end.toString()); - - if (single_path) { - var path_pk = paths[0], - lls = polyline_start.getLatLngs(); - - single_path_loop = lls[0].equals(lls[lls.length-1]); - if (single_path_loop) - cleanup = false; - - if (single_path_loop && Math.abs(pk_end - pk_start) > 0.5) { - /* - * A - * //=|---+ - * +// | It is shorter to go through - * \\ | extremeties than the whole loop - * \\=|---+ - * B - */ - if (pk_end - pk_start > 0.5) { - paths = [path_pk, path_pk]; - positions[0] = [pk_start, 0.0]; - positions[1] = [1.0, pk_end]; - } - else if (pk_end - pk_start < -0.5) { - paths = [path_pk, path_pk]; - positions[0] = [pk_end, 0.0]; - positions[1] = [1.0, pk_start]; - } - } - else { - /* A B - * +----|=====|----> - * - * B A - * +----|=====|----> - */ - paths = $.unique(paths); - positions[0] = [pk_start, pk_end]; - } - } - else if (paths.length == 3 && polyline_start == polyline_end) { - var start_lls = polylines[0].getLatLngs() - , mid_lls = polylines[1].getLatLngs(); - cleanup = false; - if (pk_start < pk_end) { - positions[0] = [pk_start, 0.0]; - if (start_lls[0].equals(mid_lls[0])) - positions[1] = [0.0, 1.0]; - else - positions[1] = [1.0, 0.0]; - positions[2] = [1.0, pk_end]; - } - else { - positions[0] = [pk_start, 1.0]; - if (start_lls[0].equals(mid_lls[0])) - positions[1] = [1.0, 0.0]; - else - positions[1] = [0.0, 1.0]; - positions[2] = [0.0, pk_end]; - } - } - else { - /* - * Add first portion of line - */ - var start_lls = polyline_start.getLatLngs(), - first_end = start_lls[start_lls.length-1], - start_on_loop = start_lls[0].equals(first_end); - - if (L.GeometryUtil.startsAtExtremity(polyline_start, polylines[1])) { - var next_lls = polylines[1].getLatLngs(), - next_end = next_lls[next_lls.length-1], - share_end = first_end.equals(next_end), - two_paths_loop = first_end.equals(next_lls[0]); - if ((start_on_loop && pk_start > 0.5) || - (share_end && (pk_start + pk_end) >= 1) || - (two_paths_loop && (pk_start - pk_end) > 0)) { - /* - * A - * /--|===+ B - * +/ \\+==|--- - * \ / - * \-----+ - * - * A B - * +----|------><-------|---- - * - * +----|=====|><=======|---- - * - */ - positions[0] = [pk_start, 1.0]; - } - else { - /* - * A B - * <----|------++-------|---- - * - * <----|=====|++=======|---- - * - */ - positions[0] = [pk_start, 0.0]; - } - } else { - /* - * A B - * +----|------>+-------|---- - * - * +----|=====|>+=======|---- - * - */ - positions[0] = [pk_start, 1.0]; - } - - /* - * Add all intermediary lines - */ - for (var i=1; i 0.5) || - (share_end && (pk_start + pk_end) >= 1) || - (two_paths_loop && (pk_start - pk_end) <= 0)) { - /* - * B - * A //==|-+ - * ---|==+// | - * \ | - * \-----+ - * - * A B - * -----|------><-------|----+ - * - * -----|======>|+======>----> - */ - positions[polylines.length - 1] = [1.0, pk_end]; - } - else { - /* - * A B - * -----|------++-------|----> - * - * -----|======+|=======>----> - */ - positions[polylines.length - 1] = [0.0, pk_end]; - } - } else { - /* - * A B - * -----|------+<-------|----+ - * - * -----|=====|+<=======|----+ - */ - positions[polylines.length - 1] = [1.0, pk_end]; - } - } - - // Clean-up : - // We basically remove all points where position is [x,x] - // This can happen at extremity points... - - if (cleanup) { - var cleanpaths = [], - cleanpositions = {}; - for (var i=0; i < paths.length; i++) { - var path = paths[i]; - if (i in positions) { - if (positions[i][0] != positions[i][1] && cleanpaths.indexOf(path) == -1) { - cleanpaths.push(path); - cleanpositions[i] = positions[i]; - } - } - else { - cleanpaths.push(path); - } - } - paths = cleanpaths; - positions = cleanpositions; - } - - // Safety warning. - if (paths.length === 0) - console.error('Empty topology. Expect problems. (' + JSON.stringify({positions:positions, paths:paths}) + ')'); - - return { - offset: offset, // Float for offset - positions: positions, // Positions on paths - paths: paths // List of pks - }; - } - - /** - * @param topology {Object} with ``offset``, ``positions`` and ``paths`` as returned by buildSubTopology() - * @param idToLayer {function} callback to obtain layer from id - * @returns L.multiPolyline - */ - function buildGeometryFromTopology(topology, idToLayer) { - var latlngs = []; - for (var i=0; i < topology.paths.length; i++) { - var path = topology.paths[i], - positions = topology.positions[i], - polyline = idToLayer(path); - if (positions) { - latlngs.push(L.GeometryUtil.extract(polyline._map, polyline, positions[0], positions[1])); - } - else { - console.warn('Topology problem: ' + i + ' not in ' + JSON.stringify(topology.positions)); - } - } - return L.multiPolyline(latlngs); - } - - /** - * @param idToLayer : callback to obtain a layer object from a pk/id. - * @param data : computed_path - */ - function buildTopologyFromComputedPath(idToLayer, data) { - if (!data.computed_paths) { - return { - layer: null, - serialized: null - } - } - - var computed_paths = data['computed_paths'] - , edges = data['new_edges'] - , offset = 0.0 // TODO: input for offset - , data = [] - , layer = L.featureGroup(); - - console.debug('----'); - console.debug('Topology has ' + computed_paths.length + ' sub-topologies.'); - - for (var i = 0; i < computed_paths.length; i++ ) { - var cpath = computed_paths[i], - paths = $.map(edges[i], function(edge) { return edge.id; }), - polylines = $.map(edges[i], function(edge) { return idToLayer(edge.id); }); - - var topo = buildSubTopology(paths, - polylines, - cpath.from_pop.ll, - cpath.to_pop.ll, - offset); - if (topo === null) break; - - data.push(topo); - console.debug('subtopo[' + i + '] : ' + JSON.stringify(topo)); - - // Geometry for each sub-topology - var group_layer = buildGeometryFromTopology(topo, idToLayer); - group_layer.from_pop = cpath.from_pop; - group_layer.to_pop = cpath.to_pop; - group_layer.step_idx = i; - layer.addLayer(group_layer); - } - console.debug('----'); - - return { - layer: layer, - serialized: data - }; - } - - return { - buildTopologyFromComputedPath: buildTopologyFromComputedPath - }; -})(); diff --git a/geotrek/core/static/core/leaflet.lineextremities.js b/geotrek/core/static/vendor/leaflet.lineextremities.v0.1.1.js similarity index 90% rename from geotrek/core/static/core/leaflet.lineextremities.js rename to geotrek/core/static/vendor/leaflet.lineextremities.v0.1.1.js index dc68ef8d76..3e0d887f55 100644 --- a/geotrek/core/static/core/leaflet.lineextremities.js +++ b/geotrek/core/static/vendor/leaflet.lineextremities.v0.1.1.js @@ -43,6 +43,14 @@ var PolylineExtremities = { // http://stackoverflow.com/a/10477334 'path': 'M 22.5, 22.5 m -20, 0 a 20,20 0 1,0 40,0 a 20,20 0 1,0 -40,0' }, + arrowM: { + 'viewBox': '0 0 10 10', + 'refX': '1', + 'refY': '5', + 'markerUnits': 'strokeWidth', + 'orient': 'auto', + 'path': 'M 0 0 L 10 5 L 0 10 z' + }, }, onAdd: function (map) { @@ -71,7 +79,7 @@ var PolylineExtremities = { /* If not in SVG mode or Polyline not added to map yet return */ /* showExtremities will be called by onAdd, using value stored in this._pattern */ if (!L.Browser.svg || typeof this._map === 'undefined') { - return this; + return this; } /* If empty pattern, hide */ @@ -87,12 +95,12 @@ var PolylineExtremities = { var defsNode; if (L.DomUtil.hasClass(svg, 'defs')) { defsNode = svg.getElementById('defs'); - - } else{ + } else { L.DomUtil.addClass(svg, 'defs'); defsNode = L.Path.prototype._createElement('defs'); defsNode.setAttribute('id', 'defs'); - svg.appendChild(defsNode); + var svgFirstChild = svg.childNodes[0]; + svg.insertBefore(defsNode, svgFirstChild); } // Add the marker to the line @@ -117,7 +125,7 @@ var PolylineExtremities = { } // Copy the path apparence to the marker - var styleProperties = ['stroke', 'stroke-opacity']; + var styleProperties = ['class', 'stroke', 'stroke-opacity']; for (var i=0; i diff --git a/geotrek/core/templates/core/error_toast.html b/geotrek/core/templates/core/error_toast.html new file mode 100644 index 0000000000..1b2538c108 --- /dev/null +++ b/geotrek/core/templates/core/error_toast.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/geotrek/core/templates/core/sql/post_40_paths.sql b/geotrek/core/templates/core/sql/post_40_paths.sql index 59818f18b1..643a79ab34 100644 --- a/geotrek/core/templates/core/sql/post_40_paths.sql +++ b/geotrek/core/templates/core/sql/post_40_paths.sql @@ -158,3 +158,20 @@ $$ LANGUAGE plpgsql; CREATE TRIGGER core_path_latest_updated_d_tgr AFTER DELETE ON core_path FOR EACH ROW EXECUTE PROCEDURE path_latest_updated_d(); + + +---------------------------------------------------------------------------- +-- Set pgRouting-related values to null after the geometry has been modified +---------------------------------------------------------------------------- + +CREATE FUNCTION {{ schema_geotrek }}.set_pgrouting_values_to_null() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE +BEGIN + UPDATE core_path SET source = NULL, target = NULL WHERE id = NEW.id; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER core_path_pgrouting_values_null_tgr +AFTER UPDATE OF geom ON core_path +FOR EACH ROW EXECUTE PROCEDURE set_pgrouting_values_to_null(); diff --git a/geotrek/core/templates/core/sql/pre_10_cleanup.sql b/geotrek/core/templates/core/sql/pre_10_cleanup.sql index 0601db62a6..e3956328ec 100644 --- a/geotrek/core/templates/core/sql/pre_10_cleanup.sql +++ b/geotrek/core/templates/core/sql/pre_10_cleanup.sql @@ -48,6 +48,7 @@ DROP FUNCTION IF EXISTS paths_related_objects_d() CASCADE; DROP FUNCTION IF EXISTS troncon_latest_updated_d() CASCADE; DROP FUNCTION IF EXISTS path_latest_updated_d() CASCADE; +DROP FUNCTION IF EXISTS set_pgrouting_values_to_null() CASCADE; -- 50 diff --git a/geotrek/core/templates/core/toast_wrong_topology_route.html b/geotrek/core/templates/core/toast_wrong_topology_route.html new file mode 100644 index 0000000000..85b102e653 --- /dev/null +++ b/geotrek/core/templates/core/toast_wrong_topology_route.html @@ -0,0 +1,6 @@ +{% load i18n %} + +
+ {% include "core/error_toast.html" with id="routing-unsnapped-marker-error-toast" title=_("Warning") message=_("The marker was not dropped on a path.") %} + {% include "core/error_toast.html" with id="routing-isolated-marker-error-toast" title=_("Warning") message=_("No routing found for this marker. Please move or delete it.") %} +
\ No newline at end of file diff --git a/geotrek/core/templates/core/topology_widget_fragment.html b/geotrek/core/templates/core/topology_widget_fragment.html index ef76eb7a52..a1f23c2a2b 100644 --- a/geotrek/core/templates/core/topology_widget_fragment.html +++ b/geotrek/core/templates/core/topology_widget_fragment.html @@ -1,5 +1,8 @@ {% extends "leaflet/widget.html" %} - +{% block map %} + {{ block.super }} + {% include "core/toast_wrong_topology_route.html" %} +{% endblock map %} {% block vars %} {{ block.super }} diff --git a/geotrek/core/tests/test_commands.py b/geotrek/core/tests/test_commands.py index 3c2c31a25b..4d68df3392 100644 --- a/geotrek/core/tests/test_commands.py +++ b/geotrek/core/tests/test_commands.py @@ -733,3 +733,20 @@ def test_find_and_merge_paths(self): f"--- RAN 6 MERGES - FROM 16 TO 10 PATHS ---\n") self.assertEqual(Path.objects.count(), 10) self.assertIn(output_str, output.getvalue()) + + +@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only') +class GeneratePgrNetworkTopologyTest(TestCase): + + def test_generate_newtork_topology(self): + geom_1 = LineString(Point(700000, 6600000), Point(700100, 6600100), srid=settings.SRID) + geom_2 = LineString(Point(700000, 6600100), Point(700100, 6600000), srid=settings.SRID) + path_1 = PathFactory.create(geom=geom_1) + path_2 = PathFactory.create(geom=geom_2) + call_command('generate_pgr_network_topology') + path_1.refresh_from_db() + path_2.refresh_from_db() + self.assertIsNotNone(path_1.source_pgr) + self.assertIsNotNone(path_1.target_pgr) + self.assertIsNotNone(path_2.source_pgr) + self.assertIsNotNone(path_2.target_pgr) diff --git a/geotrek/core/tests/test_graph.py b/geotrek/core/tests/test_graph.py deleted file mode 100644 index d63c7b8be3..0000000000 --- a/geotrek/core/tests/test_graph.py +++ /dev/null @@ -1,98 +0,0 @@ -from unittest import skipIf - -from django.conf import settings -from django.contrib.gis.geos import LineString -from django.test import TestCase -from django.urls import reverse -from mapentity.tests.factories import UserFactory - -from geotrek.core.graph import graph_edges_nodes_of_qs -from geotrek.core.models import Path -from geotrek.core.tests.factories import PathFactory - - -@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only') -class SimpleGraph(TestCase): - @classmethod - def setUpTestData(cls): - cls.user = UserFactory() - cls.url = reverse('core:path-drf-graph') - - def setUp(self): - self.client.force_login(user=self.user) - - def test_python_graph_from_path(self): - p_1_1 = (1., 1.) - p_2_2 = (2., 2.) - p_3_3 = (3., 3.) - p_4_4 = (4., 4.) - p_5_5 = (5., 5.) - - def gen_random_point(): - """Return unique (non-conflicting) point""" - return ((0., x + 1.) for x in range(10, 100)) - - r_point = gen_random_point().__next__ - - e_1_2 = PathFactory(geom=LineString(p_1_1, r_point(), p_2_2)) - e_2_3 = PathFactory(geom=LineString(p_2_2, r_point(), p_3_3)) - - # Non connex - e_4_5 = PathFactory(geom=LineString(p_4_4, r_point(), p_5_5)) - - graph = { - 'nodes': { - 1: {2: e_1_2.pk}, - 2: {1: e_1_2.pk, 3: e_2_3.pk}, - 3: {2: e_2_3.pk}, - 4: {5: e_4_5.pk}, - 5: {4: e_4_5.pk} - }, - 'edges': { - e_1_2.pk: {'nodes_id': [1, 2], 'length': e_1_2.length, 'id': e_1_2.pk}, - e_2_3.pk: {'nodes_id': [2, 3], 'length': e_2_3.length, 'id': e_2_3.pk}, - e_4_5.pk: {'nodes_id': [4, 5], 'length': e_4_5.length, 'id': e_4_5.pk} - } - } - - computed_graph = graph_edges_nodes_of_qs(Path.objects.order_by('id')) - self.assertDictEqual(computed_graph, graph) - - def test_json_graph_empty(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - graph = response.json() - self.assertDictEqual({'edges': {}, 'nodes': {}}, graph) - - def test_json_graph_simple(self): - path = PathFactory(geom=LineString((0, 0), (1, 1))) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - graph = response.json() - - length = graph['edges'][str(path.pk)].pop('length') - self.assertDictEqual({'edges': {str(path.pk): {'id': path.pk, 'nodes_id': [1, 2]}}, - 'nodes': {'1': {'2': path.pk}, '2': {'1': path.pk}}}, graph) - self.assertAlmostEqual(length, 1.4142135623731) - - def test_json_graph_simple_cached(self): - path = PathFactory(geom=LineString((0, 0), (1, 1))) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - graph = response.json() - - length = graph['edges'][str(path.pk)].pop('length') - self.assertDictEqual({'edges': {str(path.pk): {'id': path.pk, 'nodes_id': [1, 2]}}, - 'nodes': {'1': {'2': path.pk}, '2': {'1': path.pk}}}, graph) - self.assertAlmostEqual(length, 1.4142135623731) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - - def test_json_graph_headers(self): - """ - - Last modified depends on - """ - PathFactory(geom=LineString((0, 0), (1, 1))) - response = self.client.get(self.url) - self.assertNotEqual(response['Cache-Control'], None) diff --git a/geotrek/core/tests/test_views.py b/geotrek/core/tests/test_views.py index 5580910b01..3a3d886041 100644 --- a/geotrek/core/tests/test_views.py +++ b/geotrek/core/tests/test_views.py @@ -1,5 +1,6 @@ import re from unittest import mock, skipIf +from collections import ChainMap from bs4 import BeautifulSoup from django.conf import settings @@ -39,7 +40,7 @@ class MultiplePathViewsTest(AuthentFixturesTest, TestCase): @classmethod def setUpTestData(cls): - cls.user = PathManagerFactory.create(password='booh') + cls.user = PathManagerFactory.create() def setUp(self): self.login() @@ -97,6 +98,10 @@ def test_delete_multiple_path(self): self.assertEqual(Path.objects.filter(pk__in=[path_1.pk, path_2.pk]).count(), 0) +def get_route_exception_mock(arg1, arg2): + raise Exception('This is an error message') + + @skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only') class PathViewsTest(CommonTest): model = Path @@ -153,6 +158,9 @@ def _post_add_form(self): p.delete() super()._post_add_form() + def get_route_geometry(self, body): + return self.client.post(reverse('core:path-drf-route-geometry'), body, content_type='application/json') + def test_draft_permission_detail(self): path = PathFactory(name="DRAFT_PATH", draft=True) user = UserFactory(password='booh') @@ -650,6 +658,1091 @@ def test_path_layer_cache(self): self.client.get(obj.get_layer_url()) +@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only') +class PathRouteViewTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory() + + """ + ─ : path + > : path direction + X : route step + + step1 path1 + X────────>────────┐ + │ │ + │ │ + │ │ + path3 ^ ^ path4 + │ │ + │ │ + X────────>────────┘ + step2 path2 + """ + cls.path_geometries = { + '1': LineString([[1.3974995, 43.5689304], [1.4138075, 43.5688646]], srid=settings.API_SRID), + '2': LineString([[1.3964173, 43.538244], [1.4125435, 43.5381258]], srid=settings.API_SRID), + '3': LineString([[1.3964173, 43.538244], [1.3974995, 43.5689304]], srid=settings.API_SRID), + '4': LineString([[1.4125435, 43.5381258], [1.4138075, 43.5688646]], srid=settings.API_SRID), + } + for geom in cls.path_geometries.values(): + geom.transform(settings.SRID) + + cls.steps_coordinates = { + '1': {"lat": 43.5689304, "lng": 1.3974995}, + '2': {"lat": 43.538244, "lng": 1.3964173} + } + + def setUp(self): + self.client.force_login(self.user) + + def get_expected_data(self, case, path_pks): + """ + Get expected response data depending on the routing case: going straight + though path3 or taking a detour through path4. + A `path_pks` dictionary mapping the paths nb to their pks is needed in + order to generate the topology. + + ─ : path + ═ : expected route + > : path direction + X : route step + + 'through_path3' case: + + step1 path1 + X────────>────────┐ + ║ │ + ║ │ + ║ │ + path3 ^ ^ path4 + ║ │ + ║ │ + X────────>────────┘ + step2 path2 + + + 'through_path4' case: + + step1 path1 + X════════>════════╗ + │ ║ + │ ║ + │ ║ + path3 ^ ^ path4 + │ ║ + │ ║ + X════════>════════╝ + step2 path2 + """ + if case == 'through_path3': + return { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.397499663080186, 43.56893039935414], + [1.3974995, 43.56893039999989], + [1.3964173, 43.5382439999999], + [1.396417461262331, 43.53824399882988] + ] + } + ] + }, + 'serialized': [ + { + 'positions': { + '0': [1e-05, 0.0], + '1': [1.0, 0.0], + '2': [0.0, 1e-05] + }, + 'paths': [path_pks['1'], path_pks['3'], path_pks['2']] + } + ] + } + elif case == 'through_path4': + return { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.397499663080186, 43.56893039935414], + [1.4138075, 43.56886459999987], + [1.4125435, 43.538125799999904], + [1.396417461262331, 43.53824399882988] + ] + } + ] + }, + 'serialized': [ + { + 'positions': { + '0': [1e-05, 1.0], + '1': [1.0, 0.0], + '2': [1.0, 1e-05] + }, + 'paths': [path_pks['1'], path_pks['4'], path_pks['2']] + } + ] + } + else: + return None + + def get_route_geometry(self, body): + return self.client.post(reverse('core:path-drf-route-geometry'), body, content_type='application/json') + + def check_route_geometry_response(self, actual_response, expected_response): + def check_value(actual_value, expected_value): + if isinstance(expected_value, list): + assertListAlmostEqual(actual_value, expected_value) + elif isinstance(expected_value, dict): + assertDictAlmostEqual(actual_value, expected_value) + elif isinstance(expected_value, float): + self.assertAlmostEqual(actual_value, expected_value, 6) + else: + self.assertEqual(actual_value, expected_value) + + def assertDictAlmostEqual(actual_dict, expected_dict): + self.assertEqual(actual_dict.keys(), expected_dict.keys()) + expected_items = expected_dict.items() + for key, expected_value in (expected_items): + actual_value = actual_dict[key] + check_value(actual_value, expected_value) + + def assertListAlmostEqual(actual_list, expected_list): + self.assertEqual(len(actual_list), len(expected_list)) + for i, expected_value in enumerate(expected_list): + actual_value = actual_list[i] + check_value(actual_value, expected_value) + + check_value(actual_response, expected_response) + + def test_route_geometry_fail_no_steps_array(self): + response = self.get_route_geometry({}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Request parameters should contain a 'steps' array") + + def test_route_geometry_fail_empty_steps_array(self): + response = self.get_route_geometry({"steps": []}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "There must be at least 2 steps") + + def test_route_geometry_fail_one_step(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lat": 48.866667, "lng": 2.333333}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "There must be at least 2 steps") + + def test_route_geometry_fail_no_lat(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lng": 2.333333}, {"path_id": 1, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Each step should contain a valid latitude and longitude") + + def test_route_geometry_fail_no_lng(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lat": 48.866667}, {"path_id": 1, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Each step should contain a valid latitude and longitude") + + def test_route_geometry_fail_no_path_id(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"lat": 40.5267991, "lng": 0.5305685}, {"lat": 40.5266465, "lng": 0.5765381}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Each step should contain a valid path id") + + def test_route_geometry_fail_incorrect_lat(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lat": 1000, "lng": 2.333333}, {"path_id": 0, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lat": "abc", "lng": 2.333333}, {"path_id": 0, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Each step should contain a valid latitude and longitude") + + def test_route_geometry_fail_incorrect_lng(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lat": 48.866667, "lng": 1000}, {"path_id": 0, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + response = self.get_route_geometry({"steps": [{"path_id": path.pk, "lat": 48.866667, "lng": "abc"}, {"path_id": 0, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Each step should contain a valid latitude and longitude") + + def test_route_geometry_fail_incorrect_path_id(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + PathFactory(geom=path_geom) + response = self.get_route_geometry({"steps": [{"path_id": 'abc', "lat": 48.866667, "lng": 1.333333}, {"path_id": 0, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + response = self.get_route_geometry({"steps": [{"path_id": -999, "lat": 48.866667, "lng": 1.333333}, {"path_id": 0, "lat": 47.866667, "lng": 1.333333}]}) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "Each step should contain a valid path id") + + @mock.patch('geotrek.core.path_router.PathRouter.get_route', get_route_exception_mock) + def test_route_geometry_fail_error_500(self): + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + response = self.get_route_geometry({ + "steps": [ + {"path_id": path.pk, "lat": 40.5267991, "lng": 0.5305685}, + {"path_id": path.pk, "lat": 40.5266465, "lng": 0.5765381} + ] + }) + self.assertEqual(response.status_code, 500, response.json()) + self.assertEqual(response.data.get('error'), "This is an error message") + + def test_route_geometry_not_fail_no_via_point_one_path(self): + """ + Simple route: 2 markers on one path + + ─ : path + > : path direction + X : route step + + ──X─────────>───────X── + start end + + """ + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + + response = self.get_route_geometry({ + "steps": [ + {"path_id": path.pk, "lat": 43.456434372150945, "lng": 1.4050149210509666}, + {"path_id": path.pk, "lat": 43.45443525706161, "lng": 1.568413282119847} + ] + }) + self.assertEqual(response.status_code, 200) + expected_data = { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.405015712586838, 43.45647101723782], + [1.568414248971206, 43.45447481620145] + ] + } + ] + }, + 'serialized': [ + { + 'positions': { + '0': [0.15786509111560937, 0.8263090975648387] + }, + 'paths': [path.pk] + } + ] + } + self.check_route_geometry_response(response.data, expected_data) + + def test_route_geometry_not_fail_no_via_point_several_paths(self): + """ + Simple route: 2 markers on 2 paths + + ─ : path + > : path direction + X : route step + + X end + │ + │ + │ + ^ path2 + │ + │ + start │ + X─────>──────┘ + path1 + + """ + pathGeom1 = LineString([ + [1.3904572, 43.5271443], + [1.4451303, 43.5270311], + ], srid=settings.API_SRID) + pathGeom1.transform(settings.SRID) + path1 = PathFactory(geom=pathGeom1) + + pathGeom2 = LineString([ + [1.4451303, 43.5270311], + [1.4447021, 43.5803909], + ], srid=settings.API_SRID) + pathGeom2.transform(settings.SRID) + path2 = PathFactory(geom=pathGeom2) + + response = self.get_route_geometry({ + "steps": [ + {"path_id": path1.pk, "lat": 43.5271443, "lng": 1.3904572}, + {"path_id": path2.pk, "lat": 43.5803909, "lng": 1.4447021} + ] + }) + self.assertEqual(response.status_code, 200) + expected_data = { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.390457746732034, 43.52714429900562], + [1.4451303, 43.5270310999999], + [1.444702104285981, 43.58039036639194] + ] + } + ] + }, + 'serialized': [ + { + 'positions': { + '0': [1e-05, 1.0], '1': [0, 0.99999] + }, + 'paths': [path1.pk, path2.pk] + } + ] + } + self.check_route_geometry_response(response.data, expected_data) + + def test_route_geometry_not_fail_with_via_point_one_path(self): + """ + 3 markers on one path + + ─ : path + > : path direction + X : route step + + + ───X─────>────X──────────X── + start via-pt1 end + + """ + path_geom = LineString([ + [1.3664246, 43.4569065], + [1.6108704, 43.4539158], + ], srid=settings.API_SRID) + path_geom.transform(settings.SRID) + path = PathFactory(geom=path_geom) + + response = self.get_route_geometry({ + "steps": [ + {"path_id": path.pk, "lat": 43.45672005573014, "lng": 1.3816640340701447}, + {"path_id": path.pk, "lat": 43.45549487037786, "lng": 1.4818060951734013}, + {"path_id": path.pk, "lat": 43.4543343323152, "lng": 1.5766622578279499} + ] + }) + + self.assertEqual(response.status_code, 200) + expected_data = { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.381664375566539, 43.4567361685322], + [1.481807670658969, 43.45556356375513] + ] + }, + { + 'type': 'LineString', + 'coordinates': [ + [1.481807670658969, 43.45556356375513], + [1.576663073400372, 43.45436750716386] + ] + } + ] + }, + 'serialized': [ + { + 'positions': { + '0': [0.06234123320580364, 0.47200610033599394] + }, + 'paths': [path.pk] + }, + { + 'positions': { + '0': [0.47200610033599394, 0.8600553166716347] + }, + 'paths': [path.pk] + } + ] + } + self.check_route_geometry_response(response.data, expected_data) + + def test_route_geometry_not_fail_with_via_points_several_paths(self): + """ + 4 markers on 3 paths + + ─ : path + > : path direction + X : route step + + │ + X end + │ + │ + │ + ^ path3 + │ │ + X start X via-pt2 + │ │ + │ │ + V path1 X via-pt1 + │ │ + └────────>────────┘ + path2 + """ + pathGeom1 = LineString([ + [1.4447021, 43.5803909], + [1.4451303, 43.5270311] + ], srid=settings.API_SRID) + pathGeom1.transform(settings.SRID) + path1 = PathFactory(geom=pathGeom1) + + pathGeom2 = LineString([ + [1.4451303, 43.5270311], + [1.5305685, 43.5267991] + ], srid=settings.API_SRID) + pathGeom2.transform(settings.SRID) + path2 = PathFactory(geom=pathGeom2) + + pathGeom3 = LineString([ + [1.5305685, 43.5267991], + [1.5277863, 43.6251412] + ], srid=settings.API_SRID) + pathGeom3.transform(settings.SRID) + path3 = PathFactory(geom=pathGeom3) + + response = self.get_route_geometry({ + "steps": [ + {"path_id": path1.pk, "lat": 43.57192876776824, "lng": 1.4447700319492318}, + {"path_id": path3.pk, "lat": 43.546062327348785, "lng": 1.5300238809766273}, + {"path_id": path3.pk, "lat": 43.57342803491799, "lng": 1.5292498854902847}, + {"path_id": path3.pk, "lat": 43.60030465103801, "lng": 1.5284893807630917}, + ] + }) + self.assertEqual(response.status_code, 200) + expected_data = { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.444770058683145, 43.57192876788164], + [1.4451303, 43.5270310999999], + [1.5305685, 43.526799099999884], + [1.530024258596995, 43.546062332995334] + ] + }, + { + 'type': 'LineString', + 'coordinates': [ + [1.530024258596995, 43.54606233299537], + [1.52925048361151, 43.5734280438619] + ] + }, + { + 'type': 'LineString', + 'coordinates': [ + [1.52925048361151, 43.5734280438619], + [1.528489833875641, 43.60030465781372] + ] + } + ] + }, + 'serialized': [ + { + 'positions': { + '0': [0.1585837876873254, 1.0], + '1': [0.0, 1.0], + '2': [0.0, 0.19588517457745494] + }, + 'paths': [path1.pk, path2.pk, path3.pk] + }, + { + 'positions': { + '0': [0.19588517457745494, 0.47415881891337064] + }, + 'paths': [path3.pk] + }, + { + 'positions': { + '0': [0.47415881891337064, 0.7474538771223748] + }, + 'paths': [path3.pk] + }, + ] + } + self.check_route_geometry_response(response.data, expected_data) + + def test_route_geometry_steps_on_different_paths(self): + """ + The route geometry and topology depends on which paths the steps are created on. + + ─ : path + > : path direction + X : route step + + start path1 + X───────>──── + │ + │ + │ + path3 ^ + │ + │ + X──────>───── + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path3 = PathFactory(geom=self.path_geometries['3']) + + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + response1 = self.get_route_geometry(steps) + self.assertEqual(response1.status_code, 200) + expected_data = self.get_expected_data('through_path3', { + '1': path1.pk, + '2': path2.pk, + '3': path3.pk, + }) + self.check_route_geometry_response(response1.data, expected_data) + + steps = { + "steps": [ + dict(ChainMap({"path_id": path3.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path3.pk}, self.steps_coordinates['2'])) + ] + } + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = { + 'geojson': { + 'type': 'GeometryCollection', + 'geometries': [ + { + 'type': 'LineString', + 'coordinates': [ + [1.3974995, 43.56893039999986], + [1.3964173, 43.538243999999885] + ] + } + ] + }, + 'serialized': [{'positions': {'0': [1.0, 0.0]}, 'paths': [path3.pk]}] + } + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_with_draft_path_fail_then_succeed(self): + """ + Routing fails because path4 is a draft, then succeeds when it is no longer a draft + + ─ : path + > : path direction + X : route step + + start path1 + X────────>────────┐ + │ + │ + │ + ^ path4 (draft then not draft) + │ + │ + X────────>────────┘ + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path4 = PathFactory(geom=self.path_geometries['4'], draft=True) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + self.assertEqual(response1.status_code, 400) + self.assertEqual(response1.data.get('error'), "No path between the given points") + + path4.draft = False + path4.save() + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_with_draft_path_succeed_then_succeed_with_detour(self): + """ + Go through path3 when it is not a draft, then take a detour via path4 after path3 has become a draft + + ─ : path + > : path direction + X : route step + + start path1 + X───────>───┐ + │ │ + │ │ + │ │ + path3 (not draft ^ ^ path4 + then draft) │ │ + │ │ + X──────>────┘ + end path2 + + """ + + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path3 = PathFactory(geom=self.path_geometries['3']) + path4 = PathFactory(geom=self.path_geometries['4']) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + # This response data is already tested in test_route_geometry_steps_on_different_paths, + # so we only make sure that the route goes through path3: + self.assertEqual(response1.status_code, 200) + self.assertIn(path3.pk, response1.data.get('serialized')[0].get('paths')) + + path3.draft = True + path3.save() + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_with_invisible_path_fail_then_succeed(self): + """ + Routing fails because path4 is invisible, then succeeds when it is no longer invisible + + ─ : path + > : path direction + X : route step + + start path1 + X────────>────────┐ + │ + │ + │ + ^ path4 (invisible then visible) + │ + │ + X────────>────────┘ + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path4 = PathFactory(geom=self.path_geometries['4'], visible=False) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + self.assertEqual(response1.status_code, 400) + self.assertEqual(response1.data.get('error'), "No path between the given points") + + path4.visible = True + path4.save() + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_with_invisible_path_succeed_then_succeed_with_detour(self): + """ + Go through path3 when it is visible, then take a detour via path4 after path3 has become invisible + + ─ : path + > : path direction + X : route step + + start path1 + X───────>───┐ + │ │ + │ │ + │ │ + path3 (visible ^ ^ path4 + then invisible) │ │ + │ │ + X──────>────┘ + end path2 + + """ + + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path3 = PathFactory(geom=self.path_geometries['3']) + path4 = PathFactory(geom=self.path_geometries['4']) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + # This response data is already tested in test_route_geometry_steps_on_different_paths, + # so we only make sure that the route goes through path3: + self.assertEqual(response1.status_code, 200) + self.assertIn(path3.pk, response1.data.get('serialized')[0].get('paths')) + + path3.visible = False + path3.save() + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_fail_then_add_path_and_succeed(self): + """ + Routing fails because paths do not touch, then succeeds after path4 has been added + + ─ : path + > : path direction + X : route step + + start path1 + X────────>────────┐ + │ + │ + │ + ^ path4 (added after 1st routing) + │ + │ + X────────>────────┘ + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + self.assertEqual(response1.status_code, 400) + self.assertEqual(response1.data.get('error'), "No path between the given points") + + path4 = PathFactory(geom=self.path_geometries['4']) + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_succeed_with_detour_then_add_path_and_succeed(self): + """ + Route once going through path4, then add path3: the route should now go through path3 + + ─ : path + > : path direction + X : route step + + start path1 + X───────>───┐ + │ │ + │ │ + │ │ + path3 ^ ^ path4 + (added later) │ │ + │ │ + X──────>────┘ + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path4 = PathFactory(geom=self.path_geometries['4']) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + self.assertEqual(response1.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response1.data, expected_data) + + path3 = PathFactory(geom=self.path_geometries['3']) + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path3', { + '1': path1.pk, + '2': path2.pk, + '3': path3.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_succeed_then_delete_path_and_fail(self): + """ + Route once from path2 to path1 going through path3, then delete path3: routing now fails + + ─ : path + > : path direction + X : route step + + start path1 + X────────>──────── + │ + │ + │ + ^ path3 + │ + │ + X────────>──────── + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path3 = PathFactory(geom=self.path_geometries['3']) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + # This response data is already tested in test_route_geometry_steps_on_different_paths, + # so we only make sure that it succeeds: + self.assertEqual(response1.status_code, 200) + + path3.delete() + response = self.get_route_geometry(steps) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data.get('error'), "No path between the given points") + + def test_route_geometry_succeed_then_delete_path_and_succeed_with_detour(self): + """ + Route once through path3, then delete it: the route now takes a detour via path4 + + ─ : path + > : path direction + X : route step + + start path1 + X───────>───┐ + │ │ + │ │ + │ │ + path3 (deleted ^ ^ path4 + after 1st routing) │ │ + │ │ + X──────>────┘ + end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path3 = PathFactory(geom=self.path_geometries['3']) + path4 = PathFactory(geom=self.path_geometries['4']) + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + # This response data is already tested in test_route_geometry_steps_on_different_paths, + # so we only make sure that the route goes through path3: + self.assertEqual(response1.status_code, 200) + self.assertIn(path3.pk, response1.data.get('serialized')[0].get('paths')) + + path3.delete() + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_fail_then_edit_and_succeed(self): + """ + Route once from path1 to path2 (no possible route), then edit path4 + so it links path1 with path2: there is now a route going through path4 + + ─ : path + > : path direction + X : route step + + start path1 start path1 + X───────>─── X───────>───┐ + / │ + / │ + / path4 -> ^ path4 + / │ + X──────>───── X──────>────┘ + end path2 end path2 + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + + pathGeom4 = LineString([ + [1.4507103, 43.5547065], + [1.4611816, 43.5567592] + ], srid=settings.API_SRID) + pathGeom4.transform(settings.SRID) + path4 = PathFactory(geom=pathGeom4) + + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + + response1 = self.get_route_geometry(steps) + self.assertEqual(response1.status_code, 400) + self.assertEqual(response1.data.get('error'), "No path between the given points") + + path4.geom = self.path_geometries['4'] + path4.save() + + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 200) + expected_data = self.get_expected_data('through_path4', { + '1': path1.pk, + '2': path2.pk, + '4': path4.pk, + }) + self.check_route_geometry_response(response2.data, expected_data) + + def test_route_geometry_fail_after_editing_path(self): + """ + Route once from path1 to path2 (going through path3), then edit path3 + so it doesn't link path1 with path2 anymore: there is no possible route + + ─ : path + > : path direction + X : route step + + start path1 start path1 + X───────>─── X───────>──── + │ / + │ / + path3 ^ -> / path3 + │ / + X─────>───── X──────>───── + end path2 end path2 + + """ + path1 = PathFactory(geom=self.path_geometries['1']) + path2 = PathFactory(geom=self.path_geometries['2']) + path3 = PathFactory(geom=self.path_geometries['3']) + + steps = { + "steps": [ + dict(ChainMap({"path_id": path1.pk}, self.steps_coordinates['1'])), + dict(ChainMap({"path_id": path2.pk}, self.steps_coordinates['2'])) + ] + } + response1 = self.get_route_geometry(steps) + # This response data is already tested in test_route_geometry_steps_on_different_paths, + # so we only make sure that it succeeds: + self.assertEqual(response1.status_code, 200) + + newPathGeom3 = LineString([ + [1.4507103, 43.5547065], + [1.4611816, 43.5567592] + ], srid=settings.API_SRID) + newPathGeom3.transform(settings.SRID) + path3.geom = newPathGeom3 + path3.save() + + response2 = self.get_route_geometry(steps) + self.assertEqual(response2.status_code, 400) + self.assertEqual(response2.data.get('error'), "No path between the given points") + + @skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only') class PathKmlGPXTest(TestCase): @classmethod diff --git a/geotrek/core/views.py b/geotrek/core/views.py index 5154f27c85..fe0017a561 100644 --- a/geotrek/core/views.py +++ b/geotrek/core/views.py @@ -5,7 +5,6 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.gis.db.models.functions import Transform -from django.core.cache import caches from django.db.models import Sum, Prefetch from django.http import HttpResponseRedirect from django.http.response import HttpResponse @@ -13,15 +12,14 @@ from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ -from django.views.decorators.cache import cache_control -from django.views.decorators.http import last_modified as cache_last_modified from django.views.generic import TemplateView from django.views.generic.detail import BaseDetailView from mapentity.serializers import GPXSerializer from mapentity.views import (MapEntityList, MapEntityDetail, MapEntityDocument, MapEntityCreate, MapEntityUpdate, MapEntityDelete, MapEntityFormat, LastModifiedMixin) +from rest_framework import permissions from rest_framework.decorators import action -from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer +from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from geotrek.authent.decorators import same_structure_required @@ -30,7 +28,7 @@ from geotrek.common.mixins.forms import FormsetMixin from geotrek.common.permissions import PublicOrReadPermMixin from geotrek.common.viewsets import GeotrekMapentityViewSet -from . import graph as graph_lib +from .path_router import PathRouter from .filters import PathFilterSet, TrailFilterSet from .forms import PathForm, TrailForm, CertificationTrailFormSet from .models import AltimetryMixin, Path, Trail, Topology, CertificationTrail @@ -234,6 +232,11 @@ class PathViewSet(GeotrekMapentityViewSet): filterset_class = PathFilterSet mapentity_list_class = PathList + def get_permissions(self): + if self.action == 'route_geometry': + return [permissions.IsAuthenticated()] + return super().get_permissions() + def view_cache_key(self): """Used by the ``view_cache_response_content`` decorator.""" language = self.request.LANGUAGE_CODE @@ -277,29 +280,6 @@ def get_filter_count_infos(self, qs): data = super().get_filter_count_infos(qs) return f"{data} ({round(qs.aggregate(sumPath=Sum(Length('geom') / 1000)).get('sumPath') or 0, 1)} km)" - @method_decorator(cache_control(max_age=0, must_revalidate=True)) - @method_decorator(cache_last_modified(lambda x: Path.no_draft_latest_updated())) - @action(methods=['GET'], detail=False, url_path='graph.json', renderer_classes=[JSONRenderer, BrowsableAPIRenderer]) - def graph(self, request, *args, **kwargs): - """ Return a graph of the path. """ - cache = caches['fat'] - key = 'path_graph_json' - - result = cache.get(key) - latest = Path.no_draft_latest_updated() - - if result and latest: - cache_latest, graph = result - # Not empty and still valid - if cache_latest and cache_latest >= latest: - return Response(graph) - - # cache does not exist or is not up-to-date, rebuild the graph and cache it - graph = graph_lib.graph_edges_nodes_of_qs(Path.objects.exclude(draft=True)) - - cache.set(key, (latest, graph)) - return Response(graph) - @method_decorator(permission_required('core.change_path')) @action(methods=['POST'], detail=False, renderer_classes=[JSONRenderer]) def merge_path(self, request, *args, **kwargs): @@ -335,6 +315,39 @@ def merge_path(self, request, *args, **kwargs): return Response(response) + @action(methods=['POST'], detail=False, url_path="route-geometry", + renderer_classes=[JSONRenderer]) + def route_geometry(self, request, *args, **kwargs): + try: + params = request.data + steps = params.get('steps') + if steps is None: + raise Exception("Request parameters should contain a 'steps' array") + if len(steps) < 2: + raise Exception("There must be at least 2 steps") + for step in steps: + lat = step.get('lat') + lng = step.get('lng') + if not isinstance(lat, (int, float)) or not isinstance(lng, (int, float)) or lat < 0 or 90 < lat or lng < -180 or 180 < lng: + raise Exception("Each step should contain a valid latitude and longitude") + path_id = step.get('path_id') + if not isinstance(path_id, int) or Path.objects.filter(pk=path_id).first() is None: + raise Exception("Each step should contain a valid path id") + except Exception as exc: + return Response({'error': '%s' % exc, }, 400) + + try: + path_router = PathRouter() + response = path_router.get_route(steps) + if response is not None: + status = 200 + else: + response = {'error': 'No path between the given points'} + status = 400 + except Exception as exc: + response, status = {'error': '%s' % exc, }, 500 + return Response(response, status) + class CertificationTrailMixin(FormsetMixin): context_name = 'certificationtrail_formset' diff --git a/geotrek/jstests/index.html b/geotrek/jstests/index.html index 2433106210..ca17bf140b 100644 --- a/geotrek/jstests/index.html +++ b/geotrek/jstests/index.html @@ -18,9 +18,6 @@ - - - diff --git a/geotrek/sensitivity/tests/factories.py b/geotrek/sensitivity/tests/factories.py index 542f83b540..5289dc5059 100644 --- a/geotrek/sensitivity/tests/factories.py +++ b/geotrek/sensitivity/tests/factories.py @@ -18,7 +18,7 @@ class RuleFactory(factory.django.DjangoModelFactory): class Meta: model = models.Rule - django_get_or_create = ('code', 'name', 'url', 'pictogram') + django_get_or_create = ('code', ) class SportPracticeFactory(factory.django.DjangoModelFactory): diff --git a/geotrek/settings/base.py b/geotrek/settings/base.py index 0c4603d66a..e83b63d08e 100644 --- a/geotrek/settings/base.py +++ b/geotrek/settings/base.py @@ -478,15 +478,13 @@ def api_bbox(bbox, buffer): 'SPATIAL_EXTENT': api_bbox(SPATIAL_EXTENT, VIEWPORT_MARGIN), 'NO_GLOBALS': False, 'PLUGINS': { - 'geotrek': {'js': ['core/leaflet.lineextremities.js', - 'core/leaflet.textpath.js', + 'geotrek': {'js': ['vendor/leaflet.lineextremities.v0.1.1.js', + 'vendor/leaflet.textpath.v1.1.0.js', 'common/points_reference.js', 'trekking/parking_location.js']}, 'topofields': {'js': ['core/geotrek.forms.snap.js', 'core/geotrek.forms.topology.js', - 'core/dijkstra.js', - 'core/multipath.js', - 'core/topology_helper.js']} + 'core/multipath.js']} } } diff --git a/tools/benchmarking/README.md b/tools/benchmarking/README.md new file mode 100644 index 0000000000..87dbb3ac3c --- /dev/null +++ b/tools/benchmarking/README.md @@ -0,0 +1,168 @@ +# Benchmarking of the route calculation systems +This benchmarking system allows to measure execution times to compare performances of several versions of the routing system. + +It uses two different scripts. The main one, `benchmark.sh`, enables to measure execution times on the frontend-side as well as on the backend-side, for each individual action taken during the route plotting. The second one, `get_backend_measures.sh`, only allows to measure backend-side times, but is much quicker due to not interacting with the frontend and allows for more freedom in which requests to measure. + + +## `benchmark.sh` + +### How it works +This script launches a Cypress spec file (a scenario) which plots a route using one version of the routing. + +Time measurements are taken during the plotting, and this scenario is run a number of times, resulting in several values for each measure which will then be averaged. + +This process of repeatedly running a scenario and getting the average measurements is executed twice: first emptying the pgRouting network topology before each run, and then keeping it. This way, one can obtain average execution times when plotting a route, with and without initial network topology. + +To compare several versions of the routing, this process has to be done with each version. + +### How to use it +To compare two versions of the route calculation, follow these steps: +1. Switch to the first version of the routing system +2. If needed, add a custom topology and scenario (see below: 'How to create a new scenario') and ensure you are using the corresponding database +3. If needed, modify the measurements in the Python or Cypress code (see below: 'How to make custom measurements') +4. Launch the geotrek admin server +5. Go to the the `tools/benchmarking/` directory +6. Launch the script using this command: + ``` + ./benchmark.sh path_to_scenario + ``` + `path_to_scenario`: path to the Cypress spec file containing the scenario for which to take the measurements + +7. When script execution is completed, you can find the output in `time_measure/time_averages.txt`: + ``` + Branch: backend_routing_benchmark + Scenario: cypress/e2e/mediumDB100ViaPts.cy.js + pgr network topology: false + + Number of runs: 15 + Python: [2357.8577518463135, 291.6573842366536, 272.33864466349286] + JavaScript: [2418.733333333349, 481.58000000001243, 323.77999999996973, 372.513333333274, 1605.0466666667373] + + Branch: backend_routing_benchmark + Scenario: cypress/e2e/mediumDB100ViaPts.cy.js + pgr network topology: true + Number of runs: 15 + Python: [426.1796474456787, 419.2177454630534, 288.23556900024414] + JavaScript: [496.8599999999627, 480.46666666660457, 358.0933333334513, 415.24666666686534, 431.48666666677843] + ``` + `Branch`: the checked out branch when the script was run + + `Scenario`: path to the Cypress spec file containing the scenario that was run + + `pgr network topology`: whether or not the pgRouting network topology was kept before running the scenario + + `Number of runs`: how many times the scenario was run before averaging the time measurements + + `Python`: average time measurements for Python code + + `JavaScript`: average time measurements for JavaScript code + +8. **Save this output**, as the file will be overwritten next time the script is run +9. Switch to the second version of the routing system and repeat steps 3 to 8 + +### How to create a new scenario +Each scenario corresponds to a Cypress spec file you can find in `cypress/e2e/`. All the scenarios you can find there use the `generateRouteTracingTimes` custom command, which takes the name of a topology (defined as a fixture) and plots it, measuring the time between click and route display for each new marker. + +#### If you simply want to use a custom topology +1. Duplicate one of the spec files mentionned above +2. Set the right `username` and `password` values for the login process in the `before` function +3. Add the topology you want to use in `fixtures/topologies.json` (only the positions and paths are needed) +4. Set the name of this topology as argument of the `generateRouteTracingTimes` command +5. You should also set a fitting description as first argument of the `it` function +6. Pass the path to this new spec file as argument when launching the script + +#### If you want to use a completely different scenario +1. Create a new spec file in `cypress/e2e/` +2. Setup the login process in the `before` function in the same way as in the existing spec files +3. Create your custom scenario in a `it` function, as any other Cypress test +4. For taking measurements, see next section: 'How to make custom measurements' +5. Pass the path to this new spec file as argument when launching the script + +### How to make custom measurements +During a scenario run, measurements are recorded in the `time_measures/` directory. This allows to access them later in order to compute average times. + +#### Adding a Cypress measurement +Since browsers do not allow to write on an arbitrary location on the disk for security reasons, the JavaScript measurements file must be filled in from Cypress tests. + +To append a measurement to the JavaScript time measures file which will later be used to compute average times, use: +``` +cy.write('time_measures/time_measures_js.txt', measurement + ' ', {flag: 'a+'}) +``` +* Note the added space following the measurement: all time measures for a scenario run are written on the same line, allowing to then make the average of each column +* Make sure to use the `a+` flag so as not to overwrite the file + +#### Adding a Python measurement +The file in which to write Python time measures is `time_measures/time_measures_py.txt`. +* As for Cypress, do not forget to add a space after each measurement +* Specify `newline=''` so all measurements for a scenario run are on the same line +* Make sure to open the file for appending so as not to overwrite the file + +#### Removing an existing measurement +All measurements for the typical scenario (described above) can be found in custom Cypress commands in `cypress/support/commands.js`. + +You can simply remove the `cy.write` call as well as any storing or computing of dates and times corresponding to this call. + +### Additional adjustments +* The number of runs for each cycle (i.e. how many times a scenario is run before averaging the measurements) can be set in `benchmark.sh` by modify the value of `NB_MEASURES` +* The timeout for Cypress commands, requests, etc, and many other settings can be set in `cypress.config.js` ([click here to know more](https://docs.cypress.io/guides/references/configuration)) + +## `get_backend_measures.sh` + +### How it works +This script works in the same manner as `benchmark.sh`, this time launching curl calls (instead of a Cypress spec file) repeatedly to allow the backend to record a number of time measurements for a request, and then computing the averages of these measures. +Again, this process is executed twice, first emptying the pgRouting network topology before each curl call, and then keeping it. +This allows to obtain average execution times for the route calculation with specific parameters, with and without initial network topology. + +### How to use it +To compare two versions of the route calculation, follow these steps: +1. Switch to the first version of the routing system +2. If needed, add the route steps (see below: 'How to use a custom set of steps') and ensure you are using the corresponding database +3. If needed, modify the measurements in the Python code in the same manner as described above in the 'How to make custom measurements' section for the `benchmark.sh` script +4. Launch the geotrek admin server +5. Go to the the `tools/benchmarking/` directory +6. Launch the script using this command: + ``` + ./get_backend_measures.sh database session_id + ``` + `database`: (`"medium"` or `"big"`) which set of steps to use, corresponding to a medium or big database (see below: 'How to use a custom set of steps') + + `session_id`: id of a valid session on the server +7. When script execution is completed, you can find the output in `time_measure/time_averages.txt`: + ``` + Branch: backend_routing_benchmark + Database: big + pgr network topology: false + Number of runs: 15 + Python: [485806.6602389018] + JavaScript: No data + + Branch: backend_routing_benchmark + Database: big + pgr network topology: true + Number of runs: 15 + Python: [21456.734053293865] + JavaScript: No data + ``` + `Branch`: the checked out branch when the script was run + + `Database`: which set of steps was used, corresponding to a medium or big database + + `pgr network topology`: whether or not the pgRouting network topology was kept before sending the request + + `Number of runs`: how many times the request was sent before averaging the time measurements + + `Python`: average time measurements for Python code + + `JavaScript`: not measured by this script +8. **Save this output**, as the file will be overwritten next time the script is run +9. Switch to the second version of the routing system and repeat steps 3 to 8 + +### How to use a custom set of steps +A set of steps is stringified JSON corresponding to the body that will be sent with the request to the `route_geometry` view. It contains the list of all the steps the route should go through. A step is defined by its latitude and longitude. + +Here is an example for a route containing the start marker, a via-point and the end marker: + +`'{"steps":[{"lat":48.7886,"lng":-0.7288},{"lat":48.7761,"lng":0.6948},{"lat":48.7438,"lng":0.4986}]}'` + +To use a custom set of steps, you can set it as a variable in the script. To make it easier, the script includes two variables: `STEPS_MEDIUM_DB` corresponds to steps for a medium database, while `STEPS_BIG_DB` corresponds to steps for a big database. Which of these sets will be used is determined by the first command line argument `database` when launching the script (see above). + diff --git a/tools/benchmarking/benchmark.sh b/tools/benchmarking/benchmark.sh new file mode 100755 index 0000000000..7aa5c96ba1 --- /dev/null +++ b/tools/benchmarking/benchmark.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +NB_MEASURES=1 +MEASURES_DIR=./time_measures +PATH_TO_SCENARIO=$1 + +launch_scenario() { +# $1 (string): cypress spec path +# $2 (boolean): if true, keep the backend cache before each spec run + + # Reset the time measure files + if [ -d "$MEASURES_DIR" ]; then + rm -f "$MEASURES_DIR"/time_measures_* + else + mkdir "$MEASURES_DIR" + fi + + for i in $(seq 1 $NB_MEASURES) + do + if ! $2; then + # Empty the pgRouting network topology + docker compose run --rm web ./manage.py dbshell -- -c "update core_path set source=null, target=null;" + fi + # Launch the cypress test to take the time measures + npx cypress run --spec $1 --browser edge + done + + # Compute and display the average times + echo "Branch:" $(git rev-parse --abbrev-ref HEAD) >> "$MEASURES_DIR"/time_averages.txt + echo "Scenario:" $1 >> "$MEASURES_DIR"/time_averages.txt + echo "pgr network topology:" $2 >> "$MEASURES_DIR"/time_averages.txt + echo "Number of runs:" $NB_MEASURES >> "$MEASURES_DIR"/time_averages.txt + python3 ./time_averages.py >> "$MEASURES_DIR"/time_averages.txt + echo "" >> "$MEASURES_DIR"/time_averages.txt +} + +# Reset the time averages files +if [ -d "$MEASURES_DIR" ]; then + rm -f "$MEASURES_DIR"/time_averages.txt + else + mkdir "$MEASURES_DIR" + fi +launch_scenario "$PATH_TO_SCENARIO" false +launch_scenario "$PATH_TO_SCENARIO" true diff --git a/tools/benchmarking/cypress.config.js b/tools/benchmarking/cypress.config.js new file mode 100644 index 0000000000..ea4e0b1c9f --- /dev/null +++ b/tools/benchmarking/cypress.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + e2e: { + baseUrl: 'http://geotrek.local:8000', + }, + video: false, + defaultCommandTimeout: 1800000, // 30min + requestTimeout: 1800000, + responseTimeout: 1800000, + projectId: "ktpy7v" +}); \ No newline at end of file diff --git a/tools/benchmarking/cypress/e2e/bigDB2ViaPts.cy.js b/tools/benchmarking/cypress/e2e/bigDB2ViaPts.cy.js new file mode 100644 index 0000000000..39e857dda7 --- /dev/null +++ b/tools/benchmarking/cypress/e2e/bigDB2ViaPts.cy.js @@ -0,0 +1,15 @@ +describe('Benchmark scenarios', function() { + before(function() { + const username = 'geotrek'; + const password = 'geotrek'; + cy.loginByCSRF(username, password) + .then((resp) => { + expect(resp.status).to.eq(200); + }); + cy.mockTiles(); + }); + + it('Big database, no via-point', function() { + cy.generateRouteTracingTimes('bigDB2ViaPoints') + }) +}) \ No newline at end of file diff --git a/tools/benchmarking/cypress/e2e/bigDBNoViaPts.cy.js b/tools/benchmarking/cypress/e2e/bigDBNoViaPts.cy.js new file mode 100644 index 0000000000..95dcaa8d21 --- /dev/null +++ b/tools/benchmarking/cypress/e2e/bigDBNoViaPts.cy.js @@ -0,0 +1,15 @@ +describe('Benchmark scenarios', function() { + before(function() { + const username = 'geotrek'; + const password = 'geotrek'; + cy.loginByCSRF(username, password) + .then((resp) => { + expect(resp.status).to.eq(200); + }); + cy.mockTiles(); + }); + + it('Big database, no via-point', function() { + cy.generateRouteTracingTimes('bigDBNoViaPoints') + }) +}) \ No newline at end of file diff --git a/tools/benchmarking/cypress/e2e/mediumDB100ViaPts.cy.js b/tools/benchmarking/cypress/e2e/mediumDB100ViaPts.cy.js new file mode 100644 index 0000000000..54d7113d0a --- /dev/null +++ b/tools/benchmarking/cypress/e2e/mediumDB100ViaPts.cy.js @@ -0,0 +1,15 @@ +describe('Benchmark scenarios', function() { + before(function() { + const username = 'geotrek'; + const password = 'geotrek'; + cy.loginByCSRF(username, password) + .then((resp) => { + expect(resp.status).to.eq(200); + }); + cy.mockTiles(); + }); + + it('Medium database, 100 via-points', function() { + cy.generateRouteTracingTimes('mediumDB100ViaPoints') + }) +}) diff --git a/tools/benchmarking/cypress/fixtures/images/tile.png b/tools/benchmarking/cypress/fixtures/images/tile.png new file mode 100644 index 0000000000..a967301839 Binary files /dev/null and b/tools/benchmarking/cypress/fixtures/images/tile.png differ diff --git a/tools/benchmarking/cypress/fixtures/topologies.json b/tools/benchmarking/cypress/fixtures/topologies.json new file mode 100644 index 0000000000..17c7cd7adb --- /dev/null +++ b/tools/benchmarking/cypress/fixtures/topologies.json @@ -0,0 +1,11 @@ +{ + "mediumDB2ViaPoints": [{"positions":{"0":[0.6471905171358331,1],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[1,0],"17":[0,1],"18":[1,0],"19":[0,1],"20":[1,0],"21":[0,1],"22":[0,1],"23":[1,0],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[1,0],"35":[1,0],"36":[1,0],"37":[1,0],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[1,0],"63":[1,0],"64":[1,0],"65":[1,0],"66":[1,0],"67":[0,1],"68":[1,0],"69":[0,1],"70":[0,1],"71":[0,1],"72":[0,1],"73":[0,1],"74":[1,0],"75":[1,0],"76":[1,0],"77":[1,0],"78":[1,0],"79":[1,0],"80":[1,0],"81":[1,0],"82":[0,1],"83":[0,1],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[1,0],"89":[1,0],"90":[0,1],"91":[0,1],"92":[0,1],"93":[1,0],"94":[0,1],"95":[1,0],"96":[0,1],"97":[0,1],"98":[0,1],"99":[0,1],"100":[0,1],"101":[0,1],"102":[0,1],"103":[0,1],"104":[0,1],"105":[0,1],"106":[0,1],"107":[1,0],"108":[0,1],"109":[0,1],"110":[1,0],"111":[1,0],"112":[0,0.30468077224062]},"paths":[4111,46,47,51,50,56,59,67,68,61,7060,4130,4133,4136,4138,7056,7053,7055,134,4149,113,123,219,224,4183,308,351,393,407,427,428,431,4235,470,473,481,485,4240,542,551,672,700,782,834,858,891,912,926,4320,1023,1032,1044,8546,8545,1054,6625,6628,7158,6627,6624,6623,6622,1383,1390,6696,6693,6692,6694,6689,6690,4478,4489,4490,6706,1523,4495,4497,1526,1533,8157,8156,4499,4502,8154,5742,5746,5745,4503,4506,4507,1572,1588,4510,4518,1600,1602,4522,4523,1610,8578,1617,8560,4536,4540,8563,8606,8564,8569,8567,8568,8570,8572,8571]},{"positions":{"0":[0.30468077224062,0],"1":[0,1],"2":[0,1],"3":[1,0],"4":[1,0],"5":[0,1],"6":[1,0],"7":[1,0],"8":[1,0],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[1,0],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[1,0],"34":[0,1],"35":[0,1],"36":[0,1],"37":[1,0],"38":[1,0],"39":[1,0],"40":[0,1],"41":[1,0],"42":[1,0],"43":[1,0],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[1,0],"63":[1,0],"64":[1,0],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[0,1],"71":[0,1],"72":[0,1],"73":[1,0],"74":[1,0],"75":[1,0],"76":[0,1],"77":[0,1],"78":[0,1],"79":[1,0],"80":[1,0],"81":[0,1],"82":[0,1],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0.34649307078333413]},"paths":[8571,8572,8570,8568,8567,8569,8564,8606,8563,8562,8601,8164,8162,4564,1720,4567,4569,8028,7459,7455,7456,7453,7476,4582,7450,7475,7470,7472,7474,4610,4614,4616,8061,1993,4633,2039,4645,2075,2104,2105,2148,2176,4684,2247,4695,2264,2281,6573,4726,4728,2325,2326,8080,8085,8083,4734,2475,4787,2504,4798,2517,2555,2559,2566,2569,2575,2579,2589,2591,2592,6470,2604,4830,4836,2658,2667,2668,4855,2686,8379,6468,6523,6469,8374,8372,8377,5858]},{"positions":{"0":[0.34649307078333413,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0.3892830845240561]},"paths":[5858,8377,8372,8374,6469,6523,6464,4881,5878,2836,4917,2896,2916,4930]}], + + "mediumDB100ViaPoints": [{"positions":{"0":[0.5031754073974036,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[1,0],"17":[1,0],"18":[0,0.24471499207315336]},"paths":[7061,7062,52,51,50,56,59,67,68,61,7060,4130,4133,4136,4138,7056,89,86,4139]},{"positions":{"0":[0.24471499207315336,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[0,1],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0.44479721889890944]},"paths":[4139,86,4140,7055,134,4149,113,112,108,109]},{"positions":{"0":[0.44479721889890944,0],"1":[1,0.4875551579573869]},"paths":[109,4177]},{"positions":{"0":[0.4875551579573869,0],"1":[0,1],"2":[0,1],"3":[1,0],"4":[0,1],"5":[1,0],"6":[0,1],"7":[1,0.5352984013594716]},"paths":[4177,4213,314,4204,4193,271,283,4183]},{"positions":{"0":[0.5352984013594716,1],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0.31328428663360375]},"paths":[4183,308,350,378,383,4219,368,358,346,4214,349,352,359,334,7047]},{"positions":{"0":[0.31328428663360375,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[0,1],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[0,1],"12":[0,1],"13":[0,0.4522935624963013]},"paths":[7047,333,338,319,4200,278,262,259,4191,4184,4163,4160,6012,110]},{"positions":{"0":[0.4522935624963013,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[1,0.24699394305635367]},"paths":[110,111,128,148,162,174,176,180,167,7068,7070,7071,168,169,4178]},{"positions":{"0":[0.24699394305635367,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0.4021199759257587]},"paths":[4178,235,249,4190,276,284,342,364,6015,7021]},{"positions":{"0":[0.4021199759257587,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[1,0],"8":[0,0.28460100079859846]},"paths":[7021,380,391,388,389,5605,7081,4210,4211]},{"positions":{"0":[0.28460100079859846,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[0,0.3652855403225355]},"paths":[4211,5607,411,5608,5612,5611]},{"positions":{"0":[0.3652855403225355,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,0.5825950868759976]},"paths":[5611,4232,7037,6937,472,5617,5618,5623,531,5622,5631,537,6932,6079,6080,5632,643,651,4273,637]},{"positions":{"0":[0.5825950868759976,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,0.3888628959600622]},"paths":[637,4273,651,654,5532,5533,4282,5530,713,698]},{"positions":{"0":[0.3888628959600622,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0.5942745404666544]},"paths":[698,697,691,692,670,4269,4270]},{"positions":{"0":[0.5942745404666544,1],"1":[0,1],"2":[1,0],"3":[0,1],"4":[0,1],"5":[1,0.45416613947361134]},"paths":[4270,4269,670,696,4286,737]},{"positions":{"0":[0.45416613947361134,1],"1":[0,0.1310751554269812]},"paths":[737,4300]},{"positions":{"0":[0.1310751554269812,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0.38100561957071744]},"paths":[4300,737,4291,6785,6903]},{"positions":{"0":[0.38100561957071744,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[0,0.35228326082347083]},"paths":[6903,6784,6795,920,6911,6864,6862]},{"positions":{"0":[0.35228326082347083,1],"1":[0,0.42217145021828073]},"paths":[6862,6863]},{"positions":{"0":[0.42217145021828073,1],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[1,0.1995692788521957]},"paths":[6863,925,6895,6900,980,6893,6892,6899,5522]},{"positions":{"0":[0.1995692788521957,0],"1":[0,1],"2":[1,0],"3":[0,1],"4":[1,0],"5":[0,1],"6":[1,0],"7":[1,0],"8":[1,0.7722624007207219]},"paths":[5522,5523,6776,6777,6773,6775,6770,6768,6767]},{"positions":{"0":[0.7722624007207219,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[1,0],"7":[1,0],"8":[0,1],"9":[0,1],"10":[0,1],"11":[1,0],"12":[0,1],"13":[0,0.15185372184956517]},"paths":[6767,4373,1232,6765,4387,1235,6711,6720,6719,6721,1351,4428,1430,1439]},{"positions":{"0":[0.15185372184956517,1],"1":[0,1],"2":[0,1],"3":[1,0],"4":[0,1],"5":[1,0],"6":[1,0],"7":[1,0],"8":[0,1],"9":[1,0],"10":[0,1],"11":[0,1],"12":[1,0.4312316193502718]},"paths":[1439,1444,1447,1449,4441,4442,5822,5808,5823,1463,4452,4454,6715]},{"positions":{"0":[0.4312316193502718,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0.606198147292418]},"paths":[6715,6702,1475,4463,4464,4465,6705,6742,6743,6741,6738]},{"positions":{"0":[0.606198147292418,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[1,0],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[1,0.19911076247172205]},"paths":[6738,6741,6743,6742,6705,4465,4464,1480,1485,1486,1489,4472,1491,4481,1497,1499,1501]},{"positions":{"0":[0.19911076247172205,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[1,0.7153936310023241]},"paths":[1501,1500,4483,1506,4485,1509,1510,1512,1513,6678,6683,4496]},{"positions":{"0":[0.7153936310023241,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0.3160020099122761]},"paths":[4496,6680,1538,1543,1550,1552,8093]},{"positions":{"0":[0.3160020099122761,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,0.4858394096142629]},"paths":[8093,6598,1553,4505,1566,1567]},{"positions":{"0":[0.4858394096142629,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[0,1],"7":[1,0.7160244986719165]},"paths":[1567,1580,1583,1590,6590,1597,1598,6587]},{"positions":{"0":[0.7160244986719165,0],"1":[1,0],"2":[0,1],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,0.32600497523706046]},"paths":[6587,4518,1600,1602,4522,1605,4512,8118,8087,8088]},{"positions":{"0":[0.32600497523706046,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,0.18914560293679963]},"paths":[8088,8087,8118,4512,1606,1607,1610,8578,4530,1715,4569,8028,7459,7455,7456]},{"positions":{"0":[0.18914560293679963,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,0.567195399169492]},"paths":[7456,7453,7476,4582,7451,4583,7466]},{"positions":{"0":[0.567195399169492,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,0.4005939301043599]},"paths":[7466,7464,7465,4590,7461,1797,6290,6288,6286,6284,6282,6319,6318]},{"positions":{"0":[0.4005939301043599,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,0.27670703643816824]},"paths":[6318,6339,6338,6340,2073,2120,4665]},{"positions":{"0":[0.27670703643816824,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0.7187005907142168]},"paths":[4665,2120,2121,2168,2179,2195,2232,4689]},{"positions":{"0":[0.7187005907142168,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,0.8684469349630791]},"paths":[4689,2266,2283,2294,2321,4712]},{"positions":{"0":[0.8684469349630791,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[1,0.638282370155452]},"paths":[4712,2321,2322,2330,4720]},{"positions":{"0":[0.638282370155452,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0.8250482120593825]},"paths":[4720,2330,2329,2332,2371,2391,2403,4765,2482,2486,4790]},{"positions":{"0":[0.8250482120593825,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[1,0.7631061313579816]},"paths":[4790,2486,2482,2483,2487,6380,6377,6376]},{"positions":{"0":[0.7631061313579816,0],"1":[0,1],"2":[1,0.5125545807514924]},"paths":[6376,6378,6345]},{"positions":{"0":[0.5125545807514924,1],"1":[0,1],"2":[0,1],"3":[1,0],"4":[0,0.5656200177284457]},"paths":[6345,6347,2583,2626,2640]},{"positions":{"0":[0.5656200177284457,0],"1":[1,0],"2":[1,0.21698729647313633]},"paths":[2640,2639,2650]},{"positions":{"0":[0.21698729647313633,1],"1":[1,0.43013762018514223]},"paths":[2650,4841]},{"positions":{"0":[0.43013762018514223,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,0.551392499730203]},"paths":[4841,2682,2679,2676,4849]},{"positions":{"0":[0.551392499730203,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[1,0],"8":[1,0],"9":[1,0.5428916906562021]},"paths":[4849,4825,2554,2528,6351,6374,4806,6370,6369,6367]},{"positions":{"0":[0.5428916906562021,0],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[0,0.4917589109227467]},"paths":[6367,6368,6364,6362,6361,6359,5396,5377,5395,5375,6078,7163,7162,7164,7165]},{"positions":{"0":[0.4917589109227467,1],"1":[1,0],"2":[0,1],"3":[1,0],"4":[1,0],"5":[1,0],"6":[0,1],"7":[0,1],"8":[1,0],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[1,0],"14":[1,0.6137047106684287]},"paths":[7165,7169,7168,4873,2739,2743,2744,7171,2807,4891,7174,7175,7176,6071,7787]},{"positions":{"0":[0.6137047106684287,1],"1":[1,0],"2":[1,0],"3":[0,0.565323557654408]},"paths":[7787,6070,6068,6069]},{"positions":{"0":[0.565323557654408,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[1,0],"15":[1,0],"16":[0,1],"17":[1,0],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[1,0],"24":[1,0],"25":[1,0.81180087373492]},"paths":[6069,6067,6065,4889,3043,6064,3042,3047,5371,7788,5820,6074,5821,5819,3131,5249,5247,3139,3162,3164,3176,4972,3196,4984,6392,4995]},{"positions":{"0":[0.81180087373492,0],"1":[1,0.21267137955070065]},"paths":[4995,5006]},{"positions":{"0":[0.21267137955070065,0],"1":[1,0],"2":[0,1],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,0.23459160037643034]},"paths":[5006,5781,5779,3429,3499,3494,3482,5042]},{"positions":{"0":[0.23459160037643034,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[1,0],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[1,0],"13":[1,0],"14":[0,1],"15":[1,0],"16":[1,0],"17":[1,0],"18":[0,1],"19":[0,0.45011543527948183]},"paths":[5042,3482,3493,3534,5784,3533,3572,6396,3596,5790,3599,3601,3607,6388,6389,6385,3611,3608,3582,3581]},{"positions":{"0":[0.45011543527948183,1],"1":[0,1],"2":[1,0],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0.3023550510947976]},"paths":[3581,5094,3669,5101,3693,5102,3704,3714,3715,3739]},{"positions":{"0":[0.3023550510947976,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[0,1],"10":[0,0.24576303063452387]},"paths":[3739,3715,3714,3704,5102,3693,3680,3678,3677,3649,3623]},{"positions":{"0":[0.24576303063452387,0.49465486062820674]},"paths":[3623]},{"positions":{"0":[0.49465486062820674,1],"1":[0,1],"2":[1,0],"3":[0,1],"4":[1,0.7675902430476823]},"paths":[3623,3622,5088,3606,3593]},{"positions":{"0":[0.7675902430476823,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[0,1]},"paths":[3593,3584,7872,3585,3591,3626]},{"positions":{"0":[0,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,0.2839183644744899]},"paths":[3630,3631,5079,3539,3516,3488,3459,3456,3446,3445,3443,3441]},{"positions":{"0":[0.2839183644744899,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0]},"paths":[3441,3435,5050,3434,3389,7842]},{"positions":{"1":[1,0],"2":[0,1],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,0.3162853462198628]},"paths":[3364,3358,7181,7226,7179,3298,5355,7760,7761,7763,7764,7766]},{"positions":{"0":[0.3162853462198628,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[1,0],"8":[1,0.2417211098500671]},"paths":[7766,7764,6060,6056,5013,3306,5021,5108,5353]},{"positions":{"0":[0.2417211098500671,1],"1":[0,1],"2":[1,0],"3":[0,1],"4":[0,1],"5":[1,0],"6":[0,1],"7":[1,0],"8":[1,0],"9":[0,1],"10":[1,0],"11":[1,0],"12":[1,0],"13":[0,1],"14":[1,0],"15":[0,1],"16":[1,0],"17":[1,0],"18":[1,0],"19":[0,1],"20":[0,0.3364347971752222]},"paths":[5353,3725,6028,6027,6029,6032,6030,3563,3541,3511,3478,3475,3463,3451,6036,6038,6044,5030,6047,6045,6051]},{"positions":{"0":[0.3364347971752222,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[1,0],"6":[1,0],"7":[0,1],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,1],"13":[1,0],"14":[1,0],"15":[1,0],"16":[0,1],"17":[1,0.6431218555685602]},"paths":[6051,6162,6161,6165,6163,3269,6168,6166,5018,3359,3362,3375,3415,3421,5053,6199,6198,6204]},{"positions":{"0":[0.6431218555685602,1],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,0.7200872258323404]},"paths":[6204,6198,6199,5053,3421,3415,3375,3362,3359,5018,6167,6169,6171,6172,3253,3243,6174]},{"positions":{"0":[0.7200872258323404,1],"1":[1,0],"2":[1,0],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[0,1],"9":[1,0],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[1,0],"18":[0,1],"19":[1,0],"20":[1,0],"21":[1,0],"22":[0,1],"23":[1,0],"24":[1,0],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[1,0],"35":[1,0],"36":[1,0],"37":[0,0.18611990206701337]},"paths":[6174,2993,4931,2930,2883,2884,4920,2908,2909,2923,4928,2925,2905,2899,2889,2873,2860,2857,2853,6107,6105,2823,2809,2808,2795,2793,2785,2780,2772,4872,4867,2703,2693,2687,2649,2634,2606,4777]},{"positions":{"0":[0.18611990206701337,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[1,0],"13":[0,1],"14":[0,1],"15":[0,1],"16":[1,0],"17":[1,0],"18":[0,1],"19":[0,1],"20":[0,1],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[1,0],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[0,1],"48":[1,0],"49":[1,0],"50":[0,1],"51":[0,1],"52":[1,0],"53":[0,1],"54":[0,0.4190648819931497]},"paths":[4777,2455,2443,4727,2318,4657,5476,2061,5385,5388,5389,5299,2059,2062,2040,5298,2036,2025,2016,5393,5303,5302,2004,2003,4642,4641,4638,4626,7567,7478,7481,7538,7541,7542,1981,7543,7546,7547,1914,1899,1886,4598,1812,4577,4576,1796,1800,1805,1833,1842,1852,1871,1872,1870,8528]},{"positions":{"0":[0.4190648819931497,0],"1":[1,0],"2":[0,1],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[1,0],"8":[0,1],"9":[0,1],"10":[0,1],"11":[1,0],"12":[1,0.2770948074878494]},"paths":[8528,1870,1872,1871,1852,1842,1833,1805,1800,1796,4576,1754,1732]},{"positions":{"0":[0.2770948074878494,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0],"9":[1,0.9864220017338664]},"paths":[1732,1753,1726,1706,1681,4529,5908,1604,1596,1562]},{"positions":{"0":[0.9864220017338664,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[0,1],"7":[0,1],"8":[0,0.7065490569866308]},"paths":[1562,1560,5437,5925,5906,5441,5438,5435,5394]},{"positions":{"0":[0.7065490569866308,1],"1":[1,0.08181339091072232]},"paths":[5394,5629]},{"positions":{"0":[0.08181339091072232,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,0.7880874419365179]},"paths":[5629,5615,4432,4431,1429,5619,7672,7673,1391,1369,1360,7698,1307,1295,1257,4378,1197,7390,4370,4359,7395,1128,7392,1113,4335]},{"positions":{"0":[0.7880874419365179,1],"1":[0,1],"2":[0,1],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0.5442114568060363]},"paths":[4335,986,5981,5986,5984,5985,923,896,878]},{"positions":{"0":[0.5442114568060363,0],"1":[0,1],"2":[1,0],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[1,0],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[1,0],"13":[1,0.47584196329473405]},"paths":[878,4310,7102,7101,7103,7104,7107,5407,5408,576,570,512,468,4223]},{"positions":{"0":[0.47584196329473405,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0.32121536141784346]},"paths":[4223,355,354,313,312,4206,326]},{"positions":{"0":[0.32121536141784346,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0.2570779612604212]},"paths":[326,5565,451,460,494]},{"positions":{"0":[0.2570779612604212,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[1,0.21821548733554835]},"paths":[494,548,574,584,614,615,8459,8460,711,715]},{"positions":{"0":[0.21821548733554835,0],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[0,1],"12":[1,0.6773520739500051]},"paths":[715,729,728,760,761,6019,4296,786,7696,7676,831,839,886]},{"positions":{"0":[0.6773520739500051,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0.4698207428166738]},"paths":[886,4323,990,4332,1011,1014]},{"positions":{"0":[0.4698207428166738,0],"1":[0,1],"2":[1,0.3887447047228646]},"paths":[1014,4331,983]},{"positions":{"0":[0.3887447047228646,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[1,0],"6":[0,1],"7":[0,1],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0.5700955017386089]},"paths":[983,997,5967,5965,5968,7603,7713,1007,7598,1018,7600,1027,1029,1050]},{"positions":{"0":[0.5700955017386089,1],"1":[1,0],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[0,1],"7":[1,0.6020949383992512]},"paths":[1050,7594,7593,7614,1053,7985,7984,5961]},{"positions":{"0":[0.6020949383992512,0],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[1,0],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,0.5396788418343059]},"paths":[5961,5962,5958,5956,5954,7701,1297,7639,1319,5242,4421,5456,5457,5454,5455,5451,5450,5448,5445,5444,5443,5442,1555,5924,5928,5437,1559,4462,5930,5929,5934,5932,5940]},{"positions":{"0":[0.5396788418343059,1],"1":[0,1],"2":[0,1],"3":[0,0.5847859950528849]},"paths":[5940,8058,8059,8055]},{"positions":{"0":[0.5847859950528849,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0.49352263646191624]},"paths":[8055,8056,5944,5941,5932,5934,5929,5920]},{"positions":{"0":[0.49352263646191624,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[0,0.5410765115443165]},"paths":[5920,5922,1710,1719,8593,4571,1685]},{"positions":{"0":[0.5410765115443165,1],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,0.17376153271457007]},"paths":[1685,8597,8595,8594,8596,8600,8583]},{"positions":{"0":[0.17376153271457007,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,0.34384729838135564]},"paths":[8583,8600,8599,8606,8564]},{"positions":{"0":[0.34384729838135564,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[0,1],"12":[1,0],"13":[0,1],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[0,1],"21":[0,1],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[1,0],"27":[1,0.5696004136569888]},"paths":[8564,8606,8563,4540,4536,8560,1617,8578,1610,4523,4522,1602,1600,4518,4510,1589,1579,1578,1581,1584,1583,1590,6590,1597,5795,8101,8103,8099]},{"positions":{"0":[0.5696004136569888,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[0,1],"10":[0,0.5765823546254818]},"paths":[8099,5793,5794,1769,4578,1815,1823,1831,8106,8107,1867]},{"positions":{"0":[0.5765823546254818,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,0.5384103558168581]},"paths":[1867,1883,1893,1916,1921,1935,1939,1953,1954,1959,1974,1977,1995]},{"positions":{"0":[0.5384103558168581,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[0,0.2329030182096193]},"paths":[1995,2078,2107,2138,2160,2167,2239,2243,2252]},{"positions":{"0":[0.2329030182096193,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[1,0],"7":[1,0],"8":[0,0.5837743928011342]},"paths":[2252,2253,2250,2245,2244,2254,2284,2287,2295]},{"positions":{"0":[0.5837743928011342,1],"1":[1,0],"2":[1,0],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0.07741208803970652]},"paths":[2295,2343,4738,2397,2405,2412,2411,2431,5661,5667,2436,2449,2465,2466,2481,5761]},{"positions":{"0":[0.07741208803970652,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[0,0.20588923558132896]},"paths":[5761,5759,2484,2535,2527,2524,4805,2522,4807,2537,2550,2551,6534,6532]},{"positions":{"0":[0.20588923558132896,0],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[0,1],"7":[0,1],"8":[0,0.11058348053352077]},"paths":[6532,6534,2552,4813,2613,2618,4835,4833,6495]},{"positions":{"0":[0.11058348053352077,0],"1":[0,1],"2":[1,0],"3":[0,1],"4":[0,0.2419906761926603]},"paths":[6495,6496,6492,6493,6491]},{"positions":{"0":[0.2419906761926603,1],"1":[0,1],"2":[0,1],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[1,0],"21":[1,0],"22":[1,0],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[1,0.7463365539585286]},"paths":[6491,2598,2548,6482,2545,6478,6477,6479,2568,2573,6476,2574,2575,2579,2589,2591,2592,6470,2604,4830,4836,2658,2667,2668,4855,2686,6467,4863]},{"positions":{"0":[0.7463365539585286,0],"1":[0,1],"2":[0,1],"3":[0,0.38327174602767766]},"paths":[4863,4880,4887,6522]},{"positions":{"0":[0.38327174602767766,1],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,0.330614814276331]},"paths":[6522,2851,4906,4909,2870,4914,4917,4918,5874,5860]},{"positions":{"0":[0.330614814276331,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[1,0.47750026772949444]},"paths":[5860,4973,3339,5025,3344,3314]},{"positions":{"0":[0.47750026772949444,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[1,0],"14":[1,0],"15":[1,0],"16":[1,0],"17":[0,0.3555968663823624]},"paths":[3314,5036,5037,5039,5040,8317,6437,6438,8320,5251,8305,5259,3594,3632,5256,5833,6443,6442]},{"positions":{"0":[0.3555968663823624,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,0.5798835427821117]},"paths":[6442,6440,5832,5829,5093,5070,5253,5024]}], + + "bigDBNoViaPoints": [{"positions":{"0":[0.5260740330902653,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[1,0],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[1,0],"23":[0,1],"24":[1,0],"25":[1,0],"26":[1,0],"27":[1,0],"28":[1,0],"29":[0,1],"30":[0,1],"31":[0,1],"32":[1,0],"33":[1,0],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[0,1],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[0,1],"59":[0,1],"60":[1,0],"61":[1,0],"62":[0,1],"63":[1,0],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[1,0],"83":[1,0],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[0,1],"89":[0,1],"90":[1,0],"91":[0,1],"92":[0,1],"93":[0,1],"94":[1,0],"95":[0,1],"96":[1,0],"97":[1,0],"98":[1,0],"99":[1,0],"100":[1,0],"101":[1,0],"102":[1,0],"103":[1,0],"104":[1,0],"105":[1,0],"106":[1,0],"107":[1,0]},"paths":[113613,84860,144128,144175,144169,104480,143713,143807,143806,181566,58267,143808,143803,143604,143595,143593,143591,143594,143590,143596,61059,109129,18394,32721,47114,47113,89717,5080,60949,1985,57564,72045,15486,115486,5079,47110,98938,101157,71927,32713,47097,73954,32634,60948,143376,143377,143370,143371,143368,29678,44060,99225,15296,130433,181713,143358,176461,123798,143357,115547,176449,176450,176451,143353,143346,143354,143340,143344,143343,143339,143333,143330,143335,89710,123019,123014,123011,123017,123013,123024,89707,123023,123012,123016,120422,89633,60867,55846,4994,18161,142745,4945,89552,60792,18219,32626,97227,97471,12428,32625,55844,89630,4993,60864,47018,104064,32621,118806]}], + + "bigDB2ViaPoints": [{"positions":{"0":[0.5427159063239744,1],"1":[0,1],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[1,0],"25":[1,0],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[1,0],"35":[1,0],"36":[1,0],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[1,0],"62":[0,1],"63":[0,1],"64":[1,0],"65":[0,1],"66":[0,1],"67":[0,1],"68":[1,0],"69":[1,0],"70":[0,1],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[0,1],"83":[0,1],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[0,1],"89":[0,1],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[1,0],"95":[1,0],"96":[1,0],"97":[1,0],"98":[0,1],"99":[0,1],"100":[1,0],"101":[1,0],"102":[1,0],"103":[1,0],"104":[1,0],"105":[0,1],"106":[0,1],"107":[0,1],"108":[1,0],"109":[1,0],"110":[0,1],"111":[0,1],"112":[0,1],"113":[0,1],"114":[0,1],"115":[0,1],"116":[0,1],"117":[1,0],"118":[0,1],"119":[1,0],"120":[1,0],"121":[1,0],"122":[0,1],"123":[0,1],"124":[0,1],"125":[0,1],"126":[0,1],"127":[0,1],"128":[0,1],"129":[0,1],"130":[0,1],"131":[0,1],"132":[0,1],"133":[1,0],"134":[1,0],"135":[1,0],"136":[0,1],"137":[1,0],"138":[1,0],"139":[0,1],"140":[1,0],"141":[0,1],"142":[0,1],"143":[1,0],"144":[1,0],"145":[1,0],"146":[0,1],"147":[0,1],"148":[0,1],"149":[0,1],"150":[0,1],"151":[1,0],"152":[0,1],"153":[1,0],"154":[1,0],"155":[0,1],"156":[0,1],"157":[0,1],"158":[0,1],"159":[0,1],"160":[0,1],"161":[0,1],"162":[0,1],"163":[1,0],"164":[1,0],"165":[1,0],"166":[1,0],"167":[1,0],"168":[1,0],"169":[1,0],"170":[1,0],"171":[1,0],"172":[1,0],"173":[1,0],"174":[1,0],"175":[1,0],"176":[1,0],"177":[1,0],"178":[1,0],"179":[1,0],"180":[1,0],"181":[1,0],"182":[1,0],"183":[1,0],"184":[1,0],"185":[0,1],"186":[0,1],"187":[0,1],"188":[1,0],"189":[1,0],"190":[1,0],"191":[1,0],"192":[1,0],"193":[1,0],"194":[1,0],"195":[0,1],"196":[0,1],"197":[0,1],"198":[0,1],"199":[0,1],"200":[0,1],"201":[0,1],"202":[1,0],"203":[0,1],"204":[1,0],"205":[1,0],"206":[1,0],"207":[1,0],"208":[1,0],"209":[1,0],"210":[0,1],"211":[1,0],"212":[0,1],"213":[1,0],"214":[1,0],"215":[1,0],"216":[1,0],"217":[0,1],"218":[0,1],"219":[0,1],"220":[0,1],"221":[0,1],"222":[0,1],"223":[0,1],"224":[0,1],"225":[0,1],"226":[0,1],"227":[0,1],"228":[0,1],"229":[0,1],"230":[0,1],"231":[0,1],"232":[0,1],"233":[1,0],"234":[1,0],"235":[1,0],"236":[1,0],"237":[1,0],"238":[1,0],"239":[1,0],"240":[1,0],"241":[0,1],"242":[0,1],"243":[0,1],"244":[1,0],"245":[1,0],"246":[0,1],"247":[0,1],"248":[1,0],"249":[0,1],"250":[1,0],"251":[1,0],"252":[0,1],"253":[1,0],"254":[0,1],"255":[1,0],"256":[1,0],"257":[1,0],"258":[0,1],"259":[0,1],"260":[1,0],"261":[1,0],"262":[1,0],"263":[1,0],"264":[1,0],"265":[1,0],"266":[0,1],"267":[1,0],"268":[1,0],"269":[1,0],"270":[1,0],"271":[1,0],"272":[1,0],"273":[1,0],"274":[1,0],"275":[1,0],"276":[1,0],"277":[1,0],"278":[1,0],"279":[1,0],"280":[0,1],"281":[0,1],"282":[0,1],"283":[0,1],"284":[1,0],"285":[1,0],"286":[1,0],"287":[0,1],"288":[0,1],"289":[0,1],"290":[0,1],"291":[0,1],"292":[1,0],"293":[1,0],"294":[0,1],"295":[0,1],"296":[0,1],"297":[0,1],"298":[1,0],"299":[1,0],"300":[0,0.03926679085597927]},"paths":[51777,65650,162079,162077,162073,161862,161863,161860,161859,125386,161598,161597,161590,116117,125397,9561,54205,161367,158926,169688,158924,158922,158923,158921,158858,169687,169686,158868,169685,158867,158870,183997,120597,158794,158728,169684,169683,169682,158727,169673,158725,64989,160660,160437,158647,169681,158644,189632,64950,158543,158544,158542,108161,78899,158429,24678,186125,8880,8879,22120,78859,8857,158218,78835,8858,93597,54337,158078,158076,158075,157946,157947,93498,8759,50815,157752,129306,129298,129299,132211,132212,132213,78618,132445,8644,129190,122819,129198,129197,156817,186854,192115,191675,129205,187863,36285,36229,8526,156488,156487,125201,188920,156281,156287,58047,156291,156292,188919,191125,125187,125189,125196,125197,155986,125155,155985,125154,117743,8250,107505,50325,35957,107506,92987,64183,35894,64182,83543,35893,68800,50152,50160,35806,78119,107377,50172,107378,35849,50104,64039,64040,63961,107236,63960,21213,50047,28779,35711,120671,154938,63957,71265,86785,86786,58020,92644,70054,35488,35409,63581,27546,7630,63500,92260,35257,20721,20720,20719,77478,193169,189649,152111,152110,152112,176037,151441,179411,175487,55303,175486,175485,175489,151417,63178,84790,84791,63177,191817,190961,150839,150840,176042,176041,150833,181308,62992,49117,77027,77026,7065,106188,62893,149714,85094,149710,149715,177680,149709,177679,177678,149213,149219,6795,149236,149201,62734,34377,48852,119212,148877,148571,34271,105797,90998,91302,186985,190178,98487,187884,189169,190491,186529,193141,148564,62520,145265,34151,19701,6478,91184,27593,31607,76365,91085,111896,83162,90997,105369,71943,115584,44256,84839,33775,47954,113629,27630,19242,29740,115637,33550,104922,98296,15239,29835,44224,44040,90455,75732,115461,57558,57224,29631,100933,72021,85940,86162,43932,85921,115610,72062,47859,33441,47541,90348,73484,18940,18939,33332,33331,75532,2358,75533,115733,44080,101051,15465,115530,29611,44049,104467,75351]},{"positions":{"0":[0.03926679085597927,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[0,1],"15":[0,1],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[1,0],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[1,0],"39":[1,0],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[0,1],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[1,0],"63":[1,0],"64":[1,0],"65":[0,1],"66":[0,1],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[1,0],"80":[1,0],"81":[1,0],"82":[1,0],"83":[1,0],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[0,1],"89":[0,1],"90":[0,1],"91":[0,1],"92":[1,0],"93":[0,1],"94":[0,1],"95":[1,0],"96":[1,0],"97":[1,0],"98":[1,0],"99":[0,1],"100":[0,1],"101":[0,1],"102":[0,1],"103":[1,0],"104":[1,0],"105":[1,0],"106":[0,1],"107":[0,1],"108":[1,0],"109":[1,0],"110":[1,0],"111":[1,0],"112":[1,0],"113":[1,0],"114":[1,0],"115":[1,0],"116":[1,0],"117":[1,0],"118":[1,0],"119":[0,1],"120":[0,1],"121":[0,1],"122":[0,1],"123":[1,0],"124":[1,0],"125":[1,0],"126":[0,1],"127":[1,0],"128":[1,0],"129":[1,0],"130":[1,0],"131":[1,0],"132":[1,0],"133":[1,0],"134":[1,0],"135":[1,0],"136":[1,0],"137":[1,0],"138":[1,0],"139":[1,0],"140":[1,0],"141":[1,0],"142":[1,0],"143":[1,0],"144":[1,0],"145":[0,1],"146":[0,1],"147":[0,1],"148":[0,1],"149":[1,0],"150":[0,1],"151":[0,1],"152":[0,1],"153":[0,1],"154":[1,0],"155":[1,0],"156":[1,0],"157":[0,1],"158":[0,1],"159":[0,1],"160":[1,0],"161":[0,1],"162":[0,1],"163":[0,1],"164":[0,1],"165":[0,1],"166":[0,1],"167":[0,1],"168":[0,1],"169":[1,0],"170":[1,0],"171":[0,1],"172":[0,1],"173":[1,0],"174":[1,0],"175":[1,0],"176":[1,0],"177":[1,0],"178":[0,1],"179":[1,0],"180":[1,0],"181":[0,1],"182":[1,0],"183":[0,1],"184":[0,1],"185":[1,0],"186":[1,0],"187":[1,0],"188":[1,0],"189":[1,0],"190":[1,0],"191":[0,1],"192":[0,1],"193":[1,0],"194":[1,0],"195":[1,0],"196":[1,0],"197":[1,0],"198":[1,0],"199":[1,0],"200":[1,0],"201":[1,0],"202":[0,1],"203":[0,1],"204":[0,1],"205":[0,1],"206":[0,1],"207":[0,1],"208":[0,1],"209":[0,1],"210":[1,0],"211":[1,0],"212":[1,0],"213":[1,0],"214":[0,1],"215":[0,1],"216":[0,1],"217":[0,1],"218":[0,1],"219":[0,1],"220":[0,1],"221":[0,1],"222":[1,0],"223":[1,0],"224":[1,0],"225":[1,0],"226":[1,0],"227":[1,0],"228":[1,0],"229":[1,0],"230":[1,0],"231":[1,0],"232":[0,1],"233":[0,1],"234":[1,0],"235":[0,1],"236":[0,1],"237":[0,1],"238":[1,0],"239":[0,1],"240":[0,1],"241":[1,0],"242":[1,0],"243":[0,1],"244":[0,1],"245":[0,1],"246":[0,1],"247":[0,1],"248":[0,1],"249":[0,1],"250":[0,1],"251":[1,0],"252":[1,0],"253":[1,0],"254":[1,0],"255":[1,0],"256":[1,0],"257":[1,0],"258":[1,0],"259":[0,1],"260":[0,1],"261":[0,1],"262":[1,0],"263":[1,0],"264":[1,0],"265":[1,0],"266":[0,1],"267":[0,1],"268":[0,1],"269":[0,1],"270":[1,0],"271":[0,1],"272":[0,1],"273":[0,1],"274":[1,0],"275":[1,0],"276":[1,0],"277":[0,1],"278":[0,1],"279":[0,1],"280":[1,0],"281":[1,0],"282":[1,0],"283":[1,0],"284":[1,0],"285":[1,0],"286":[0,1],"287":[0,1],"288":[0,1],"289":[1,0],"290":[1,0],"291":[1,0],"292":[1,0],"293":[0,1],"294":[1,0],"295":[1,0],"296":[1,0],"297":[1,0],"298":[1,0],"299":[1,0],"300":[1,0],"301":[0,1],"302":[0,1],"303":[1,0],"304":[1,0],"305":[1,0],"306":[1,0],"307":[1,0],"308":[1,0],"309":[1,0],"310":[1,0],"311":[1,0],"312":[0,1],"313":[0,1],"314":[0,1],"315":[0,1],"316":[0,1],"317":[0,1],"318":[1,0],"319":[0,1],"320":[0,1],"321":[0,1],"322":[1,0],"323":[1,0],"324":[1,0.7523946946604594]},"paths":[75351,104465,18613,33031,32905,104348,5261,47319,61150,104346,75255,104345,32931,5257,69205,177725,143770,143769,143771,143772,195417,195380,195379,195240,195241,195358,195360,195194,195230,195232,195233,74972,143062,143065,143064,143067,143063,143308,143061,143060,143052,143053,143058,126783,47013,89616,143044,177890,143035,143043,43734,43733,143036,178158,178162,143031,142724,142725,142714,43463,174386,179056,174385,174380,174377,174376,142705,103984,60781,103985,89540,18144,97422,4935,142681,18104,142677,142676,142401,142402,142400,142394,142395,4835,46861,46860,46850,32484,103907,4839,74796,4840,89440,103906,4834,4836,4833,46849,46859,89448,142382,142381,142380,142137,29510,74720,118895,83851,194141,194053,193905,193996,193410,119837,30064,30065,69730,74637,4594,46606,89082,46605,60488,120515,32201,141634,141633,179885,141632,141492,141490,141488,141491,89083,141493,141489,103603,141487,141484,141449,132965,141470,141393,141368,141387,141374,141385,84219,141384,141386,141369,136056,180978,136173,136695,140841,140707,141229,141232,140332,17577,141239,141228,120093,21,141069,141071,131134,131128,131120,131659,131648,131669,71143,131654,131682,46321,114797,70716,74203,31905,46271,60145,17454,4246,17511,17510,60207,98353,74253,46316,31960,46318,88828,74251,88768,118980,140921,140917,194892,74200,140918,184701,140905,178075,140910,140901,140913,140912,140914,140892,140902,121398,121397,121395,121396,43774,121394,121392,121393,121391,121390,121380,121383,121377,120933,140632,176951,140631,176950,140630,140623,17390,140606,4081,122694,122697,122693,122699,140372,140370,139908,139909,182450,139902,185484,139898,185483,185482,182452,16,103102,139894,139638,139637,183498,139633,139632,59869,130255,130257,130261,130253,130259,129395,59871,31566,130254,130260,45943,130256,130252,129344,102989,45868,31503,17081,88376,45870,31500,88301,102859,31432,31420,31430,84744,73743,59659,3744,45801,16999,17001,88299,59668,3693,88223,102784,88225,59595,102783,59592,59590,100495,128895,88160,88088,59463,56399,16831,59462,85029,102587,88034,26694,45436,59313,73407,73419,16700,87890,45366,59259,3390,31072,102436]},{"positions":{"0":[0.7523946946604594,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[1,0],"13":[0,1],"14":[1,0],"15":[1,0],"16":[0,1],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[0,1],"25":[1,0],"26":[1,0],"27":[1,0],"28":[1,0],"29":[1,0],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[1,0],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[0,1],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[0,1],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[1,0],"78":[1,0],"79":[1,0],"80":[0,1],"81":[0,1],"82":[0,1],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0],"87":[0,1],"88":[1,0],"89":[1,0],"90":[0,1],"91":[0,1],"92":[0,1],"93":[1,0],"94":[1,0],"95":[1,0],"96":[1,0],"97":[1,0],"98":[0,1],"99":[1,0],"100":[0,1],"101":[0,1],"102":[0,1],"103":[0,1],"104":[0,1],"105":[0,1],"106":[0,1],"107":[0,1],"108":[1,0],"109":[0,1],"110":[0,1],"111":[1,0],"112":[1,0],"113":[1,0],"114":[1,0],"115":[1,0],"116":[1,0],"117":[0,1],"118":[0,1],"119":[0,1],"120":[0,1],"121":[0,1],"122":[1,0],"123":[1,0],"124":[1,0],"125":[0,1],"126":[1,0],"127":[1,0],"128":[1,0],"129":[1,0],"130":[0,1],"131":[0,1],"132":[1,0],"133":[1,0],"134":[1,0],"135":[0,1],"136":[0,1],"137":[0,1],"138":[0,1],"139":[0,1],"140":[0,1],"141":[0,1],"142":[0,1],"143":[0,1],"144":[0,1],"145":[0,1],"146":[1,0],"147":[1,0],"148":[1,0],"149":[1,0],"150":[1,0],"151":[0,1],"152":[0,0.32536945954120766]},"paths":[102436,45312,45311,102394,137594,137592,54966,178553,137404,137405,137392,176806,181163,137162,137164,136957,55368,136960,183694,136956,136734,136731,136733,136732,183716,136730,136457,136456,136455,136453,59003,136451,176860,176866,183734,136229,136230,136228,136231,136227,87548,102095,27684,58923,58919,3044,27681,87545,112800,73075,42199,42198,102096,58883,44999,41428,41426,73070,102092,99372,114987,56335,135749,102014,135554,135552,135548,135095,135096,72871,58738,16055,2798,101878,174178,134590,182114,134337,185689,134336,101798,134333,134334,134150,133937,133938,133940,181990,72658,44564,101671,101636,133625,133626,44535,178419,15818,133521,133518,133522,133519,133437,133438,133436,174174,44465,133386,182126,114995,99754,52423,43666,53,191363,171521,171520,171519,52402,52398,52427,125504,52426,52425,15002,82831,125502,96643,81723,24972,67660,171375,189207,132650,11664,67645,111084,67643,195995,195991,195949,196128,196131,196133,195951,195948,195950,196132,196130,196016,196011,196012,196009,120348]}], + + "bigDB25ViaPoints": [{"positions":{"0":[0.9388467333900758,0],"1":[1,0],"2":[0,1],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0],"9":[1,0],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[1,0],"39":[1,0],"40":[1,0],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[0,1],"54":[0,1],"55":[0,1],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[0,1],"63":[0,1],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[0,1],"69":[1,0],"70":[0,1],"71":[0,1],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[1,0],"78":[0,1],"79":[1,0],"80":[1,0],"81":[1,0.5643139810000969]},"paths":[9704,111768,109006,109005,51776,9703,79710,109004,162073,161862,161863,161860,161859,125386,161598,161597,161590,116117,125397,9561,54205,161367,158926,169688,158924,158922,158923,158921,158858,169687,169686,158868,169685,158867,158870,183997,120597,158794,158728,169684,169683,169682,158727,169673,158725,158726,64991,158717,158633,158723,158722,158721,158711,158712,174424,158710,158698,158697,54210,65895,183452,158689,158688,158638,93704,72289,86747,82669,64964,158611,8960,158625,158200,185037,158613,83430,26011,158507,158509,93701,115264,158596]},{"positions":{"0":[0.5643139810000969,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[0,1],"11":[0,1],"12":[1,0],"13":[0,1],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[1,0],"21":[1,0],"22":[1,0],"23":[0,1],"24":[0,1],"25":[0,1],"26":[1,0],"27":[1,0],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[1,0],"36":[0,1],"37":[1,0],"38":[1,0],"39":[1,0],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[0,1],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[1,0],"59":[1,0],"60":[1,0],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[0,1],"71":[0,1],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[1,0],"79":[1,0],"80":[1,0],"81":[1,0],"82":[1,0],"83":[0,1],"84":[0,1],"85":[0,1],"86":[1,0],"87":[1,0],"88":[0,1],"89":[0,1],"90":[1,0],"91":[1,0],"92":[0,1],"93":[0,1],"94":[0,1],"95":[0,1],"96":[0,1],"97":[1,0],"98":[1,0],"99":[1,0],"100":[0,1],"101":[1,0],"102":[1,0],"103":[1,0],"104":[1,0],"105":[1,0],"106":[1,0],"107":[1,0],"108":[1,0],"109":[1,0],"110":[1,0],"111":[1,0],"112":[1,0],"113":[0,1],"114":[0,1],"115":[0,1],"116":[0,1],"117":[0,1],"118":[0,1],"119":[0,1],"120":[1,0],"121":[1,0],"122":[1,0],"123":[1,0],"124":[1,0],"125":[1,0],"126":[1,0.4035046826426078]},"paths":[158596,158597,158598,158589,158590,158587,158593,158584,158586,158577,158578,124747,124744,124725,124724,124729,124732,124719,124892,124878,132092,132093,124883,124891,124887,124836,124840,22151,124843,124844,124827,124831,132084,132086,124851,93589,132090,124806,124807,124804,189813,124809,191998,188516,190222,186722,188728,188078,187160,191114,192889,192689,186246,187159,188902,124756,158180,93587,158176,158177,174477,22073,158181,158188,158182,158171,158172,158170,158234,158273,158272,158269,8871,158270,22106,158267,158268,158271,158266,195106,195495,8870,99586,158260,158261,158254,70408,28593,26008,178598,178596,182880,178594,178588,182901,178601,158352,158354,158357,158362,22175,69106,108173,64934,36589,8950,36608,22192,51002,56533,51004,108201,69108,35331,78928,8946,22187,78927,64955,78925,116303,184292,160408,160403,180019,160401,160619]},{"positions":{"0":[0.4035046826426078,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[1,0],"27":[0,1],"28":[0,1],"29":[0,1],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[1,0],"35":[0,1],"36":[0,1],"37":[1,0],"38":[1,0],"39":[0,1],"40":[0,1],"41":[1,0],"42":[1,0],"43":[1,0],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[1,0],"63":[1,0],"64":[1,0],"65":[1,0],"66":[0,1],"67":[0,1],"68":[0,1],"69":[1,0],"70":[0,1],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[0,1],"76":[1,0],"77":[0,1],"78":[0,1],"79":[0,1],"80":[1,0],"81":[1,0],"82":[0,1],"83":[0,1],"84":[0,1],"85":[0,1],"86":[1,0],"87":[0,1],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[1,0],"95":[1,0],"96":[1,0],"97":[1,0],"98":[1,0],"99":[0,1],"100":[1,0],"101":[1,0],"102":[1,0],"103":[1,0],"104":[0,1],"105":[0,1],"106":[1,0],"107":[0,1],"108":[0,1],"109":[0,1],"110":[1,0],"111":[1,0],"112":[0,1],"113":[0,1],"114":[1,0],"115":[1,0],"116":[1,0],"117":[1,0],"118":[1,0],"119":[0,1],"120":[0,1],"121":[0,1],"122":[0,1],"123":[1,0],"124":[1,0],"125":[1,0],"126":[1,0],"127":[1,0],"128":[1,0],"129":[1,0],"130":[1,0],"131":[1,0],"132":[1,0],"133":[1,0],"134":[1,0],"135":[1,0],"136":[1,0],"137":[1,0],"138":[0,1],"139":[0,1],"140":[0,1],"141":[0,1],"142":[0,1],"143":[0,1],"144":[0,1],"145":[0,1],"146":[0,1],"147":[0,1],"148":[0,1],"149":[1,0],"150":[1,0],"151":[1,0],"152":[1,0],"153":[1,0],"154":[1,0],"155":[0,1],"156":[0,1],"157":[0,0.608759719229085]},"paths":[160619,160401,180019,160403,160408,184292,116303,78925,64955,78927,22187,8946,78928,35331,69108,108201,51004,56533,51002,22192,36608,8950,36589,64934,108173,69106,22175,158362,158357,158354,158352,178601,182901,178588,178594,182880,178596,178598,26008,28593,70408,158254,158261,158260,99586,8870,195495,195106,158266,158271,158268,158267,22106,158270,8871,158269,158272,158273,158234,158170,158172,158171,158182,158188,158181,22073,174477,158177,78818,194268,195516,194261,50834,21989,36463,120539,157895,157896,189810,187158,78755,108035,108037,50835,64795,108038,107983,93478,191113,124796,157727,192434,189359,173650,182732,157729,124720,107990,36419,36418,127074,157443,127070,181246,127068,190290,191602,187695,188133,127046,188963,50718,127101,127109,127096,127108,127090,127094,78617,127104,127105,127093,119016,118940,120552,132544,119017,119018,119224,64770,120680,120681,119225,132545,132499,120448,119227,120682,36476,64766,107996,132547,132783,132500,132546,132599,21957,8755,21962,78721,157748,64769,157749,116375,129298,28538,157720,187864]},{"positions":{"0":[0.608759719229085,1],"1":[1,0],"2":[1,0],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[0,1],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[1,0],"37":[1,0],"38":[1,0],"39":[1,0],"40":[1,0],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[0,1],"47":[1,0],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[1,0],"71":[1,0],"72":[1,0],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[1,0],"82":[1,0],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0],"87":[1,0],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[0,1],"93":[0,1],"94":[0,1],"95":[0,1],"96":[0,1],"97":[0,1],"98":[1,0]},"paths":[187864,186997,78669,78670,50770,36386,128957,185428,129216,107908,78621,107907,129221,93397,129211,129213,128987,78567,64607,115784,115783,117962,64540,117961,119302,120678,113703,36232,50545,78448,107742,78450,64480,68796,97854,12802,83429,50547,36170,70628,14717,50549,64483,64482,93225,107660,78379,78382,14276,113706,93149,78385,107663,107587,107591,36009,64329,64331,50328,67824,118095,117820,118566,118386,118343,118342,118341,117714,119011,117611,117613,186707,155215,189800,189798,178870,191524,155217,132039,132041,132040,124504,124509,124519,124495,124503,124499,124497,124524,35577,13896,92584,63841,187143,186658,190647,189338,191100,49843]},{"positions":{"0":[0,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[1,0],"24":[1,0],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[1,0],"45":[0,1],"46":[1,0],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[1,0],"54":[1,0],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[1,0],"67":[1,0],"68":[0,1],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[1,0],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[1,0],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0],"87":[1,0],"88":[0,1],"89":[1,0.14849646596420266]},"paths":[49843,188056,188887,189334,124542,181979,154026,188485,92367,192874,124530,124537,124491,84926,116379,92185,49524,152757,152158,152148,152156,152157,152160,151518,151522,183174,151043,151054,181962,181963,151063,151066,150564,150563,150562,150284,150286,181946,150179,34661,150287,150013,150008,150002,149967,54785,83518,62827,149994,6899,149993,149978,149981,79874,175497,41461,175538,175535,149962,175501,68648,97691,96784,177880,25487,175496,176131,56926,176125,176128,176134,180433,149471,149458,149477,149518,149517,149525,56267,34406,76694,119363,119362,126910,126911,126944,178911,126941,126942,126936]},{"positions":{"0":[0.14849646596420266,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[0,1],"5":[0,1],"6":[0,1],"7":[1,0],"8":[1,0],"9":[0,1],"10":[0,1],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[1,0],"19":[0,1],"20":[1,0],"21":[0,1],"22":[0,1],"23":[1,0],"24":[1,0],"25":[1,0],"26":[1,0],"27":[1,0],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[1,0],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[1,0],"41":[0,1],"42":[0,1],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[0,1],"54":[1,0],"55":[0,1],"56":[0,1],"57":[1,0],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[1,0],"78":[1,0],"79":[1,0],"80":[1,0],"81":[0,1],"82":[0,1],"83":[0,1],"84":[1,0],"85":[1,0],"86":[1,0],"87":[0,1],"88":[0,1],"89":[0,1],"90":[0,1],"91":[1,0],"92":[1,0],"93":[1,0],"94":[1,0],"95":[0,1],"96":[0,1],"97":[1,0],"98":[1,0],"99":[1,0],"100":[1,0],"101":[1,0],"102":[0,1],"103":[0,1],"104":[1,0],"105":[1,0],"106":[0,1],"107":[1,0],"108":[0,1],"109":[0,1],"110":[1,0],"111":[1,0],"112":[1,0],"113":[1,0],"114":[0,1],"115":[1,0],"116":[1,0],"117":[1,0],"118":[1,0],"119":[0,1],"120":[1,0],"121":[1,0],"122":[0,1],"123":[0,1],"124":[0,1],"125":[0,1],"126":[0,1],"127":[0,1],"128":[1,0],"129":[1,0.8355589908043348]},"paths":[126936,192039,190719,188127,191593,190723,120661,121698,121697,122154,126951,126950,185258,121696,13494,122158,121695,114965,176137,181915,71467,14234,91018,91019,121665,119873,121669,121664,70868,43679,121648,121646,115176,121647,147182,147181,147177,181879,147175,147171,147174,176354,147173,147170,184787,147161,147163,123707,123701,123704,43230,123706,123702,123700,90913,19422,105385,147149,90810,118913,62052,62041,6056,90811,48220,62049,48219,76089,76090,119838,146793,181892,181891,19341,105268,62046,32714,48215,76088,105273,32699,105283,146422,126548,187561,187662,126572,192931,188555,40873,188556,38597,112851,55426,79878,126626,126564,115446,146127,146126,189844,188945,190700,186285,186282,126568,145792,61695,99246,56169,58072,24345,43410,115684,101174,29659,75546,61472,90261,18842,90259,18843,129056,104726,47559,125377,144691,1439,1127,1064]},{"positions":{"0":[0.8355589908043348,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[1,0],"9":[1,0],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[1,0],"15":[0,1],"16":[0,1],"17":[1,0],"18":[0,1],"19":[0,1],"20":[0,1],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[0,1],"26":[0,1],"27":[0,1],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[1,0],"37":[0,1],"38":[0,1],"39":[1,0],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[0,1],"58":[1,0],"59":[1,0],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[1,0],"66":[1,0],"67":[1,0],"68":[0,1],"69":[0,1],"70":[0,1],"71":[0,1],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[0,1],"78":[0,1],"79":[1,0],"80":[0,1],"81":[0,1],"82":[0,1],"83":[0,1],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[0,1],"95":[0,1],"96":[0,1],"97":[1,0],"98":[1,0],"99":[1,0],"100":[1,0.8738722177816997]},"paths":[1064,1127,144690,144680,1065,5380,61169,113607,123154,123157,61170,43192,84854,61056,104251,69799,143572,18298,143197,176440,176444,143361,176441,176447,143359,75061,143364,176455,176461,123798,143357,115547,57567,143084,47001,125763,18163,4437,103998,74896,69791,55843,44058,129963,72178,44251,44203,26570,89376,129960,72139,72012,86012,100993,86120,46778,74741,129736,103791,74664,60501,17836,69783,99243,89183,60434,129943,101203,29694,44005,23616,72322,4539,89098,27159,28185,86487,98841,14790,121408,67100,42326,13617,67097,67099,121905,71560,141178,141179,141043,180308,140824,140825,140826,140828,140827,140553,140548,88652,140549,140544]},{"positions":{"0":[0.8738722177816997,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[1,0],"17":[0,1],"18":[0,1],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[1,0],"49":[1,0],"50":[0,1],"51":[0,1],"52":[0,1],"53":[0,1],"54":[0,1],"55":[1,0],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[1,0],"78":[1,0],"79":[1,0],"80":[0,1],"81":[0,1],"82":[0,1],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0],"87":[0,1],"88":[0,1],"89":[0,1],"90":[0,1],"91":[0,1],"92":[1,0],"93":[0,1],"94":[0,1],"95":[0,1],"96":[0,1],"97":[0,1],"98":[0,1],"99":[1,0],"100":[1,0],"101":[0,1],"102":[0,1],"103":[0,1],"104":[0,1],"105":[0,1],"106":[0,1],"107":[0,1],"108":[0,1],"109":[0,1],"110":[1,0],"111":[1,0],"112":[1,0],"113":[1,0],"114":[1,0],"115":[1,0],"116":[1,0],"117":[1,0],"118":[1,0],"119":[1,0],"120":[1,0],"121":[1,0],"122":[1,0],"123":[1,0],"124":[1,0],"125":[0,1],"126":[0,1],"127":[0,1],"128":[0,1],"129":[0,1],"130":[1,0],"131":[0,1],"132":[0,1],"133":[0,1],"134":[0,1],"135":[0,1],"136":[0,1],"137":[0,1],"138":[0,1],"139":[0,1],"140":[1,0],"141":[0,1],"142":[0,1],"143":[1,0],"144":[1,0],"145":[1,0],"146":[1,0],"147":[1,0],"148":[1,0],"149":[1,0],"150":[1,0],"151":[1,0],"152":[0,1],"153":[0,1],"154":[0,1],"155":[0,1],"156":[0,1],"157":[0,1],"158":[0,1],"159":[0,1],"160":[0,1],"161":[0,1],"162":[0,1],"163":[1,0],"164":[1,0],"165":[1,0],"166":[1,0],"167":[1,0],"168":[1,0],"169":[1,0],"170":[1,0],"171":[1,0],"172":[1,0],"173":[0,1],"174":[1,0],"175":[1,0],"176":[0,1],"177":[0,1],"178":[0,1],"179":[1,0],"180":[1,0],"181":[1,0],"182":[1,0],"183":[1,0],"184":[1,0],"185":[1,0],"186":[1,0],"187":[1,0],"188":[1,0],"189":[1,0],"190":[0,1],"191":[0,1],"192":[0,1],"193":[0,1],"194":[0,0.22716803625213633]},"paths":[140544,140549,88652,140548,140819,140816,178728,140814,141029,179404,141031,183512,180370,141024,180369,180371,141011,141018,141137,17539,55478,98537,126617,183509,187668,190271,103472,42401,126616,32109,89005,46385,116496,116510,180703,141955,141956,181519,181520,142234,142235,142237,142477,184335,116692,177824,142467,142465,142473,184340,142022,142275,142740,142735,4943,18219,89631,18292,32718,18291,5062,18290,47193,5151,18373,32803,5150,61043,75263,47332,47308,57465,115518,72067,101268,32941,5273,43922,47431,90008,12854,83600,33041,90152,43167,104604,90154,116803,116802,116601,116801,116598,145370,70101,100936,72161,123059,123060,29761,145708,145709,145713,191875,187137,132265,188966,188811,192610,132032,132031,48206,33782,6142,90796,146725,146723,146722,146724,147056,147059,147058,76174,42394,67815,147060,86062,101258,184723,147839,147821,147840,147814,147812,147820,147818,147813,147811,145707,76460,42336,19706,26803,105800,19805,91302,90998,105797,34271,148571,148877,119212,48852,34377,62734,149207,149197,149196,149200,149206,149199,111908,91516,91600,149703,177684,149690,149706,174428,149701,149699,26718,149708,174430,42014,150164,150163,150150,150142,150147,150176,20284,177616,177615,174466,49114,132869,91777,91776,62990,34744,62988,62889,62985,77144,91869]},{"positions":{"0":[0.22716803625213633,0],"1":[0,1],"2":[0,1],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[1,0],"22":[1,0],"23":[1,0],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[0,1],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[0,1],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[1,0],"63":[0,1],"64":[0,1],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[0,1],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[1,0],"79":[1,0],"80":[0,1],"81":[0,1],"82":[1,0],"83":[1,0.9999876491856386]},"paths":[91869,7145,77145,150661,55211,177614,179193,69408,150831,150835,150838,174398,177612,150834,179190,179189,68762,42346,179191,150837,190961,191817,63177,84791,84790,63178,151417,175489,175485,175486,55303,175487,179411,151441,176037,152112,152110,152111,189649,193169,77478,20719,63497,35250,49590,7629,14086,67821,84800,49755,84767,28714,7787,49818,7866,154425,181234,187394,154460,106401,190043,190476,188299,70429,154694,182749,14983,154692,28498,12658,49971,154703,154697,154702,154699,21129,154698,154915,154913,183703,154914,154912,154911,50035]},{"positions":{"0":[0.9999876491856386,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[0,1],"8":[1,0],"9":[0,1],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[0,1],"20":[0,1],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[0,1],"35":[0,1],"36":[1,0],"37":[1,0],"38":[1,0],"39":[0,1],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[0,1],"46":[1,0],"47":[0,1],"48":[0,1],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[0,1],"60":[0,1],"61":[0,1],"62":[1,0],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[1,0],"69":[0,1],"70":[0,1],"71":[0,0.5014890766809454]},"paths":[50035,130735,130736,130737,21204,56160,56159,71322,14118,13354,69403,70431,26807,122861,154666,154669,154665,154667,107223,27529,77983,21205,154903,154902,21202,195571,71278,21199,14139,58060,84821,122454,122453,100578,82757,58059,114726,84829,155162,8055,118933,155159,21079,83204,64028,21270,107356,8099,35836,154123,120446,155365,155366,155364,192856,190937,155363,155575,190938,190613,187128,43813,27089,189185,189189,189186,189187,194423,186477,155723,107480,155724]},{"positions":{"0":[0.5014890766809454,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[1,0],"8":[0,1],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[0,1],"24":[1,0],"25":[0,1],"26":[1,0],"27":[0,1],"28":[0,1],"29":[1,0],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[1,0],"35":[1,0],"36":[1,0],"37":[0,1],"38":[1,0],"39":[1,0],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[0,1],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[0,1],"51":[0,1],"52":[1,0],"53":[1,0],"54":[1,0],"55":[0,1],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[0,1],"63":[0,1],"64":[0,0.000006380972213229773]},"paths":[155724,155919,8290,107551,107553,155915,180266,180265,107544,93033,64294,78274,155907,155697,21493,50366,120001,43697,120017,56750,120016,85612,120015,156046,120030,156214,156209,156211,119901,100142,14814,56754,56755,120022,119896,85457,120021,124170,124164,124169,100143,29242,43800,14818,120031,43703,43799,14817,43803,29081,190891,157038,157013,122626,157014,157033,157024,177454,157023,157022,173589,157021,157008,157331,50740]},{"positions":{"0":[0.000006380972213229773,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[1,0],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[0,1],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0.9999783226263098]},"paths":[50740,157331,157008,157333,157332,157330,157329,157326,93421,36364,157324,157322,157001,93364,64628,64627,8614,50686,28162,50633,71878,85609,114983,70893,29079,43704,114984,124961,124960,114982,124959,14821,14820,124142,124144,124143,123913,156657,156654,156595,156649,156650,156652,156653,156651,122248,122229,122228,122249,122240,119879,122238,156989,156991,36304,8606,50683,156986]},{"positions":{"0":[0.9999783226263098,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[1,0],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[1,0],"61":[1,0],"62":[1,0],"63":[1,0],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[0,1],"78":[0,1],"79":[1,0],"80":[0,1],"81":[0,1],"82":[0,1],"83":[0,1],"84":[1,0],"85":[0,1],"86":[0,1],"87":[1,0],"88":[1,0],"89":[0,1],"90":[1,0.000009421034735630876]},"paths":[156986,156984,156980,156981,156982,156972,156968,156955,156969,116811,156967,189954,157281,157273,129424,129432,129431,172333,51210,122274,36841,94051,29296,159316,159318,172334,159314,122972,157271,157270,50783,79163,159381,159311,159312,159310,159371,22393,36837,159302,22384,65151,79073,24280,11122,67067,222,188674,186428,193079,191717,190595,197233,196659,196656,196657,197196,195893,195815,195805,193694,196652,196653,196972,130901,130900,195826,195766,195680,196975,196974,196973,195900,195864,195863,195775,41250,70343,42512,67044,110459,24264,119234,119317,167143,190506,190897,190192,188716,11103,167164]},{"positions":{"0":[0.000009421034735630876,1],"1":[1,0],"2":[0,1],"3":[0,1],"4":[1,0],"5":[1,0],"6":[0,1],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[0,1],"24":[1,0],"25":[0,1],"26":[0,1],"27":[1,0],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[1,0],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[1,0],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[1,0],"51":[1,0],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[0,1],"71":[0,1],"72":[0,1],"73":[1,0],"74":[1,0],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[1,0],"80":[0,1],"81":[0,1],"82":[0,1],"83":[0,1],"84":[0,1],"85":[1,0],"86":[0,1],"87":[0,1],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[0,1],"95":[0,1],"96":[1,0],"97":[1,0],"98":[1,0.5743719396287279]},"paths":[167164,11103,188716,190192,190897,190506,167143,119317,119234,167144,95911,26042,95909,39454,195776,11095,110453,53097,67036,81010,95903,81012,110452,81011,95895,119534,39443,53087,24256,167124,128318,167126,167127,128507,119476,119480,119479,119478,119477,119481,119483,8389,36109,64417,55163,124963,98221,112624,21598,70885,119469,119468,119466,119579,119580,119470,119460,119467,119453,180026,156012,156014,155816,155829,155831,183389,155856,155859,155855,155832,155846,155847,155826,155825,155870,155871,124099,124098,155688,183388,155685,155687,179719,98597,21366,92870,50193,78138,92872,64070,35798,28585,21309,50120,21312,50121,21314,8081,35796]},{"positions":{"0":[0.5743719396287279,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[0,1],"15":[0,1],"16":[0,1],"17":[1,0],"18":[0,1],"19":[0,1],"20":[0,1],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[1,0],"27":[1,0],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[0,1],"63":[1,0.255309477682445]},"paths":[35796,8081,29076,119981,119980,119982,119978,29075,29405,119974,107262,107259,8034,107261,55560,21305,64066,35792,78012,26999,63990,63915,63916,35593,154551,154536,21091,107105,43583,49917,35592,7884,7779,77820,49856,49857,154276,154277,21003,20841,7652,86234,27003,106802,13603,42648,55645,55574,55644,49433,122220,166885,180034,180033,166886,166861,80939,66952,127474,180035,127477,192067,53005,24187]},{"positions":{"0":[0.255309477682445,0],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[1,0],"27":[0,1],"28":[0,1],"29":[1,0],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[1,0],"35":[1,0],"36":[1,0],"37":[1,0],"38":[0,1],"39":[1,0],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[0,1],"50":[1,0],"51":[1,0],"52":[1,0],"53":[0,1],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[1,0],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[0,1],"71":[0,1],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[1,0],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[1,0],"82":[0,1],"83":[0,1],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[1,0],"89":[1,0],"90":[1,0],"91":[0,1],"92":[1,0],"93":[0,1],"94":[0,1],"95":[0,1],"96":[1,0],"97":[0,1],"98":[1,0],"99":[1,0],"100":[0,1],"101":[0,1],"102":[0,1],"103":[1,0],"104":[1,0],"105":[1,0],"106":[0,1],"107":[0,1],"108":[0,1],"109":[0,1],"110":[0,1],"111":[0,1],"112":[0,1],"113":[0,1],"114":[0,1],"115":[1,0],"116":[0,1],"117":[0,1],"118":[0,1],"119":[0,1],"120":[0,1],"121":[0,1],"122":[0,1],"123":[1,0],"124":[1,0],"125":[1,0],"126":[1,0.0008024828183509789]},"paths":[24187,52999,196600,197297,127448,127492,127493,192499,188415,188155,191196,197282,196596,196594,196595,197258,187484,196597,196593,196598,178203,77067,150675,112750,13248,150689,150664,178217,14785,150656,29191,150648,120146,150650,150646,178208,150683,178220,150633,150635,150630,29068,56855,150421,29227,85603,120148,29228,43783,56854,43784,56742,114967,114968,43780,43781,29065,43682,118918,6718,105833,62573,19821,70395,34302,83237,62451,34305,48711,105729,6520,48610,91224,91223,105732,91226,48612,34201,19754,62458,105735,1814,1827,1824,76515,6525,1829,6535,105739,99110,76514,13453,148437,62469,19765,34099,148101,148106,148113,148439,148440,6531,76382,48621,6534,91204,19771,48721,48724,19854,19863,91358,76623,34323,62595,91357,62590,48722,19870,112459,85180,84227,105854,62598,48732,19874,34330]},{"positions":{"0":[0.0008024828183509789,0],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[1,0],"11":[0,1],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[0,1],"17":[1,0],"18":[1,0],"19":[1,0],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[1,0],"45":[1,0],"46":[1,0],"47":[0,1],"48":[1,0],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[0,1],"58":[1,0],"59":[1,0],"60":[0,1],"61":[1,0],"62":[1,0],"63":[1,0],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[0,1],"71":[0,1],"72":[0,1],"73":[0,1],"74":[0,1],"75":[1,0],"76":[1,0],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[0,1],"83":[1,0],"84":[1,0],"85":[1,0],"86":[0,1],"87":[1,0],"88":[1,0],"89":[1,0],"90":[1,0],"91":[0,1],"92":[1,0],"93":[0,1],"94":[1,0],"95":[0,1],"96":[0,1],"97":[0,1],"98":[0,1],"99":[0,1],"100":[1,0],"101":[0,1],"102":[0,1],"103":[1,0],"104":[0,1],"105":[0,1],"106":[0,1],"107":[0,1],"108":[0,1],"109":[1,0],"110":[0,1],"111":[1,0],"112":[1,0],"113":[1,0],"114":[1,0],"115":[0,1],"116":[0,1],"117":[1,0],"118":[1,0],"119":[1,0],"120":[1,0],"121":[1,0],"122":[1,0],"123":[1,0],"124":[1,0],"125":[1,0],"126":[1,0],"127":[1,0],"128":[1,0],"129":[1,0],"130":[1,0],"131":[0,1],"132":[0,1],"133":[0,1],"134":[0,1],"135":[1,0],"136":[1,0],"137":[1,0],"138":[1,0],"139":[1,0],"140":[1,0.999991687134034]},"paths":[34330,48734,76627,91089,62605,91363,148787,148790,176973,148793,148791,9877,9876,9878,148784,149108,62696,76733,100041,113148,106059,62781,76794,20054,62775,76795,91570,6938,20164,106148,49002,55701,6924,20157,76904,49001,106152,91641,76905,20162,120430,34606,106140,62945,20240,63040,77085,34798,70785,106359,34917,7190,69675,151157,151674,151672,151690,12642,175458,83394,125648,125645,125659,125657,49337,35006,77318,63227,35008,27120,7301,7309,77321,179490,35023,151696,151695,7304,106556,20523,7313,77323,92022,7306,92039,20527,20525,7319,35019,29484,20529,7320,7317,20526,63240,113141,113142,13722,98772,29516,28134,114022,63363,92125,63361,63364,20620,7422,77427,92127,7430,35124,35123,123968,49552,77535,74373,63472,7513,49557,119292,63469,63466,77536,39630,153313,85288,39625,183325,153267,27203,39656,153812,77692,20874,153817,153814,39605,153818,63552,49643]},{"positions":{"0":[0.999991687134034,1],"1":[0,1],"2":[1,0],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[0,1],"13":[0,1],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[0,1],"25":[1,0],"26":[1,0],"27":[0,1],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[1,0],"33":[1,0],"34":[1,0],"35":[1,0],"36":[1,0],"37":[1,0],"38":[1,0],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[1,0],"46":[1,0],"47":[1,0],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[1,0],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[1,0],"65":[0,1],"66":[0,1],"67":[0,1],"68":[0,1],"69":[0,1],"70":[1,0],"71":[1,0],"72":[1,0],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[1,0],"80":[1,0],"81":[1,0],"82":[1,0.896478208741943]},"paths":[49643,63552,153330,7689,49734,106918,35387,77695,20876,39619,153826,153834,7690,20878,153829,153825,153833,153823,182849,153838,153344,153346,153345,153348,153350,186018,153349,106761,85419,27113,71855,77542,28128,180185,190622,152831,152850,152336,179109,152335,152849,152854,180274,117138,180184,152857,174620,174621,152858,152867,152868,177601,152866,152865,181554,152872,63384,152345,152342,152344,152368,152356,152365,152367,152357,152385,98261,98488,187934,189625,177597,177599,49484,35139,194457,194459,194461,194462,195437,35046,49378,49380,106590]},{"positions":{"0":[0.896478208741943,1],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[1,0],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[1,0],"36":[1,0],"37":[0,1],"38":[1,0],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,0.5826695443075014]},"paths":[106590,20557,195217,195218,195219,195204,195205,195207,195055,195078,194303,194304,194311,194309,194305,194306,195170,195171,195173,195418,195419,195392,195393,194375,194437,48376,48372,62206,90982,76204,33748,90972,120660,33759,62205,6292,62204,76253,48286,48282,113060,47877,90888,48284]},{"positions":{"0":[0.5826695443075014,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[0,1],"6":[1,0],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[1,0],"32":[0,1],"33":[0,1],"34":[1,0],"35":[0,1],"36":[1,0],"37":[1,0],"38":[1,0],"39":[1,0],"40":[0,1],"41":[0,1],"42":[0,1],"43":[1,0],"44":[1,0],"45":[1,0],"46":[0,1],"47":[0,1],"48":[0,1],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[1,0],"61":[1,0],"62":[0,1],"63":[0,1],"64":[1,0],"65":[0,1],"66":[0,1],"67":[0,0.000003201586111658853]},"paths":[48284,105244,33760,76059,105243,90780,115775,115803,115818,115816,115817,19227,61906,61908,90676,6021,97529,121613,121610,119871,33537,5893,90567,19126,104902,90446,104901,90447,75716,145684,145686,61547,5676,33318,18927,18930,33316,75612,75611,61545,90330,47639,61451,126803,126804,188115,188560,192469,190888,186462,190742,188285,188462,132130,191569,192929,144086,144085,33024,47417,33023,104448,5361,89993,5358,47412,89989,61237]},{"positions":{"0":[0.000003201586111658853,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[1,0],"6":[1,0],"7":[0,1],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[1,0],"14":[1,0],"15":[1,0],"16":[1,0],"17":[1,0],"18":[1,0],"19":[0,1],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[1,0],"27":[1,0],"28":[1,0],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[1,0],"37":[1,0],"38":[0,1],"39":[0,1],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[1,0],"45":[0,1],"46":[0,1],"47":[1,0],"48":[1,0],"49":[1,0],"50":[1,0],"51":[1,0],"52":[1,0],"53":[0,1],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[0,1],"60":[0,1],"61":[0,1],"62":[1,0],"63":[1,0],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[0,1],"72":[1,0],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[0,1],"83":[0,1],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[0,1],"89":[0,1],"90":[0,1],"91":[0,1],"92":[1,0],"93":[1,0],"94":[1,0],"95":[0,1],"96":[1,0],"97":[0,1],"98":[0,1],"99":[0,1],"100":[0,1],"101":[0,1],"102":[0,1],"103":[0,1],"104":[0,1],"105":[0,1],"106":[0,1],"107":[0,1],"108":[0,1],"109":[0,1],"110":[0,1],"111":[1,0],"112":[1,0],"113":[1,0],"114":[1,0],"115":[1,0],"116":[1,0],"117":[1,0],"118":[1,0],"119":[1,0],"120":[1,0],"121":[0,1],"122":[1,0],"123":[1,0],"124":[1,0],"125":[1,0],"126":[1,0],"127":[1,0],"128":[1,0],"129":[1,0],"130":[1,0],"131":[1,0],"132":[1,0],"133":[0,1],"134":[1,0],"135":[1,0],"136":[1,0],"137":[1,0],"138":[1,0],"139":[1,0],"140":[1,0],"141":[1,0],"142":[1,0],"143":[1,0],"144":[1,0],"145":[1,0],"146":[1,0],"147":[1,0],"148":[1,0],"149":[1,0],"150":[1,0],"151":[1,0],"152":[1,0],"153":[1,0],"154":[1,0],"155":[1,0],"156":[1,0],"157":[1,0],"158":[0,1],"159":[1,0],"160":[0,1],"161":[0,1],"162":[0,1],"163":[0,1],"164":[0,1],"165":[0,1],"166":[1,0],"167":[0,1],"168":[0,1],"169":[0,1],"170":[0,1],"171":[0,1],"172":[1,0.5699660160907498]},"paths":[61237,47414,75237,32906,47291,143724,130043,97423,61018,192571,143543,143540,143545,143298,143295,143290,143293,75037,143300,143027,143034,143045,177895,178164,143032,143033,18152,142714,43463,85182,142713,142726,142715,142710,18074,142716,142708,103988,103918,46867,195120,195414,4850,74810,89458,25719,4771,89363,89273,121478,121462,121461,141506,112909,98533,89094,141251,141517,141497,174189,141499,17750,4521,60420,89086,46548,74401,141431,141428,141433,141432,88930,4386,119198,118892,120413,74274,88855,103403,60220,60221,60222,120414,31983,4332,17531,4263,88785,4262,103338,4264,4261,60164,140966,125676,140714,125765,187117,187990,189252,38773,80331,80330,194193,194805,193932,193902,194069,193889,193919,194122,194068,193993,194067,194049,194101,130821,194212,194091,194063,194059,195542,194756,194735,194758,194740,194731,194743,194739,195567,194737,194736,194744,195496,194752,194723,194721,194722,194720,194954,194968,194950,194054,193927,193867,197153,196236,196221,196220,196223,196222,176539,176537,138781,138783,138567,138570,45632,55828,28240,3553,138345,138346,59366,138093,31172,29691,184614,129909,15326,137936,137935,102481]},{"positions":{"0":[0.7442603581467412,1],"1":[1,0],"2":[1,0],"3":[1,0],"4":[1,0],"5":[1,0],"6":[0,1],"7":[1,0],"8":[1,0],"9":[1,0],"10":[1,0],"11":[1,0],"12":[1,0],"13":[0,1],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[1,0],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[1,0],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[1,0],"63":[1,0],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[0,1],"77":[1,0],"78":[0,1],"79":[0,1],"80":[0,1],"81":[1,0],"82":[1,0],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0],"87":[1,0],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[1,0],"95":[1,0],"96":[1,0],"97":[1,0],"98":[1,0],"99":[1,0],"100":[0,1],"101":[1,0],"102":[0,1],"103":[0,1],"104":[0,1],"105":[0,1],"106":[0,1],"107":[0,1],"108":[0,1],"109":[0,1],"110":[0,1],"111":[1,0],"112":[1,0],"113":[1,0],"114":[0,1],"115":[0,1],"116":[0,1],"117":[0,1],"118":[0,1],"119":[1,0],"120":[0,1],"121":[0,1],"122":[0,1],"123":[0,1],"124":[1,0],"125":[0,1],"126":[1,0],"127":[1,0],"128":[1,0],"129":[1,0],"130":[1,0],"131":[1,0],"132":[1,0],"133":[0,1],"134":[1,0],"135":[1,0],"136":[1,0],"137":[1,0],"138":[1,0],"139":[1,0],"140":[1,0],"141":[1,0],"142":[1,0],"143":[0,1],"144":[0,1],"145":[1,0],"146":[0,1],"147":[0,1],"148":[1,0],"149":[1,0],"150":[0,1],"151":[0,1],"152":[0,1],"153":[1,0],"154":[0,1],"155":[0,1],"156":[0,1],"157":[0,1],"158":[0,1],"159":[0,1],"160":[0,1],"161":[0,1],"162":[0,1],"163":[0,1],"164":[0,1],"165":[1,0],"166":[1,0],"167":[1,0],"168":[1,0],"169":[1,0],"170":[1,0],"171":[0,1],"172":[1,0],"173":[0,1],"174":[0,1],"175":[0,1],"176":[0,1],"177":[0,1],"178":[1,0],"179":[1,0],"180":[1,0],"181":[0,1],"182":[0,1],"183":[0,1],"184":[0,1],"185":[0,1],"186":[1,0],"187":[1,0],"188":[1,0],"189":[0,1],"190":[1,0],"191":[0,1],"192":[0,1],"193":[1,0],"194":[1,0],"195":[1,0],"196":[1,0],"197":[1,0],"198":[0,1],"199":[0,1],"200":[0,1],"201":[0,1],"202":[0,1],"203":[0,1],"204":[0,1],"205":[0,1],"206":[0,1],"207":[1,0],"208":[1,0],"209":[1,0],"210":[1,0],"211":[1,0],"212":[0,1],"213":[0,1],"214":[0,1],"215":[0,1],"216":[0,1],"217":[0,1],"218":[1,0],"219":[1,0],"220":[1,0],"221":[1,0],"222":[1,0],"223":[0,1],"224":[1,0.029880556936832794]},"paths":[102481,137935,137936,15326,129909,184614,29691,31172,138093,59366,138346,138345,3553,28240,55828,45632,138570,138567,138783,138781,176537,176539,196222,196223,196220,196221,196236,197153,193867,193927,194054,194950,194968,194954,194720,194722,194721,194723,194752,195496,194744,194736,194737,195567,194739,194743,194731,194740,194758,194735,194756,195542,194059,194063,194091,194212,130821,194101,194049,194067,193993,194068,194122,193919,193889,194069,193902,193932,194805,194193,80330,80331,38773,189252,187990,187117,125765,140714,125676,140966,60164,4261,4264,103338,4262,88785,4263,17531,4332,31983,120414,60222,60221,60220,103403,88855,74274,120413,118892,119198,4386,88930,141432,141433,141428,141431,74401,46548,89086,60420,4521,17750,141499,174189,141497,141517,141251,89094,98533,112909,141506,121461,121462,121478,121477,121482,121484,195523,195154,121483,115168,56843,115167,121488,121491,121490,115169,71628,121487,56844,4844,126743,185205,142421,142427,142417,142415,142408,89452,142414,142678,142677,18143,142673,74871,74869,4932,4931,32548,4929,4930,74868,46925,18141,103980,46923,46920,104058,18207,47003,104062,18208,4983,5051,47080,89680,47079,18267,104129,5049,5047,32684,18347,32773,85782,29469,32770,44187,85882,27043,99954,89868,75221,61126,47271,18480,89864,75218,84435,18475,104439,18583,18582,75416,75418,75419,104547,104548,33207,33204,33203,61411,68405,61527,18900,145278,5650,75597,18807,18899,18898,5648,5649,47709,47815]},{"positions":{"0":[0.029880556936832794,1],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[0,1],"8":[1,0],"9":[0,1],"10":[1,0],"11":[0,1],"12":[0,1],"13":[1,0],"14":[1,0],"15":[0,1],"16":[0,1],"17":[1,0],"18":[1,0],"19":[1,0],"20":[0,1],"21":[0,1],"22":[0,1],"23":[1,0],"24":[1,0],"25":[1,0],"26":[0,1],"27":[0,1],"28":[0,1],"29":[1,0],"30":[1,0],"31":[1,0],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[0,1],"38":[0,1],"39":[0,1],"40":[1,0],"41":[1,0],"42":[1,0],"43":[1,0],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[1,0],"52":[1,0],"53":[1,0],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[1,0],"59":[1,0],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[1,0],"67":[1,0],"68":[0,1],"69":[0,1],"70":[1,0],"71":[0,1],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[1,0],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[0,1],"83":[1,0],"84":[0,1],"85":[0,1],"86":[0,1],"87":[0,1],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[1,0],"95":[1,0],"96":[1,0],"97":[1,0],"98":[1,0],"99":[1,0],"100":[1,0],"101":[1,0],"102":[1,0],"103":[1,0],"104":[1,0],"105":[1,0],"106":[1,0],"107":[1,0],"108":[1,0],"109":[1,0],"110":[1,0],"111":[1,0],"112":[1,0],"113":[0,1],"114":[0,1],"115":[0,1],"116":[0,1],"117":[1,0],"118":[1,0],"119":[1,0],"120":[1,0],"121":[0,1],"122":[0,1],"123":[0,1],"124":[0,1],"125":[0,1],"126":[0,1],"127":[1,0],"128":[1,0],"129":[1,0],"130":[1,0],"131":[1,0],"132":[0,1],"133":[1,0],"134":[1,0],"135":[1,0],"136":[1,0],"137":[1,0],"138":[1,0],"139":[1,0],"140":[1,0],"141":[1,0],"142":[1,0],"143":[1,0],"144":[1,0],"145":[1,0],"146":[1,0],"147":[1,0],"148":[1,0],"149":[1,0],"150":[1,0],"151":[1,0],"152":[1,0],"153":[1,0],"154":[0,1],"155":[0,1],"156":[0,1],"157":[1,0],"158":[0,1],"159":[0,1],"160":[0,1],"161":[1,0],"162":[0,1],"163":[0,1],"164":[0,1],"165":[1,0],"166":[1,0],"167":[1,0],"168":[1,0],"169":[1,0],"170":[1,0],"171":[1,0],"172":[1,0],"173":[1,0],"174":[1,0],"175":[1,0],"176":[1,0],"177":[1,0],"178":[1,0],"179":[1,0],"180":[1,0],"181":[1,0],"182":[1,0],"183":[1,0],"184":[1,0],"185":[1,0],"186":[0,1],"187":[0,1],"188":[0,1],"189":[0,1],"190":[0,1],"191":[1,0],"192":[1,0],"193":[1,0.6767218281895299]},"paths":[47815,47709,5649,5648,47710,104776,47704,85503,72350,90208,104661,47504,5436,90085,75405,90086,5449,61314,89967,47393,61220,18463,75208,32764,61000,60999,47139,32763,5072,89760,5036,32668,75014,47069,32663,32665,32664,32670,32669,47065,60908,74948,32605,84701,89600,32606,104043,89530,4923,46851,57853,99078,60629,103837,89348,98519,55466,32378,32402,103761,103760,17811,60486,42996,89151,17652,103515,103440,14589,4368,74311,60262,73715,103442,103441,46371,74314,32020,4319,88812,103384,4318,120415,101008,46273,46272,103259,88696,60081,103169,60012,103170,74077,74018,193986,193884,46068,121337,122065,56920,122054,29383,121340,121336,121335,122064,55481,4028,73937,120111,17209,59889,59881,99532,70702,42033,83138,59426,31627,88480,45999,3958,31248,3960,103044,88446,17149,3903,68417,68416,83137,23610,45812,102800,3697,138841,26689,138839,138838,138843,138698,138697,59545,102732,45663,55269,45665,73628,73629,112802,73630,42989,88181,16849,102683,31264,45524,73512,73450,102509,87914,59272,174272,191151,188107,68556,126011,174741,126006,137425,137427,125987,125984,125985,137212,3284,102326,16505,45225,137221,137213,137219,137005,137004,176782,176781,54984,122578,183745,137003,122570,59102,45144,136843]},{"positions":{"0":[0.6767218281895299,1],"1":[0,1],"2":[0,1],"3":[1,0],"4":[1,0],"5":[1,0],"6":[1,0],"7":[1,0],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[0,1],"23":[0,1],"24":[0,1],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[1,0],"30":[1,0],"31":[1,0],"32":[0,1],"33":[1,0],"34":[1,0],"35":[1,0],"36":[0,1],"37":[1,0],"38":[1,0],"39":[1,0],"40":[0,1],"41":[0,1],"42":[0,1],"43":[0,1],"44":[0,1],"45":[0,1],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[0,1],"52":[0,1],"53":[0,1],"54":[0,1],"55":[0,1],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[1,0],"62":[0,1],"63":[0,1],"64":[0,1],"65":[0,1],"66":[0,1],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[1,0],"79":[1,0],"80":[1,0],"81":[1,0],"82":[0,1],"83":[0,1],"84":[0,1],"85":[1,0],"86":[0,1],"87":[0,1],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[0,1],"95":[0,1],"96":[0,1],"97":[0,1],"98":[0,1],"99":[0,1],"100":[0,1],"101":[0,1],"102":[0,1],"103":[0,1],"104":[0,1],"105":[0,1],"106":[0,1],"107":[0,1],"108":[0,1],"109":[0,1],"110":[0,1],"111":[0,1],"112":[1,0],"113":[1,0],"114":[1,0],"115":[1,0],"116":[0,1],"117":[1,0],"118":[1,0],"119":[1,0],"120":[1,0],"121":[1,0],"122":[1,0],"123":[1,0],"124":[1,0],"125":[0,1],"126":[0,1],"127":[0,1],"128":[0,1],"129":[1,0],"130":[1,0],"131":[1,0],"132":[1,0],"133":[1,0],"134":[1,0],"135":[1,0],"136":[1,0],"137":[0,1],"138":[1,0],"139":[1,0],"140":[1,0],"141":[1,0],"142":[1,0],"143":[1,0],"144":[0,1],"145":[0,1],"146":[1,0],"147":[1,0],"148":[0,1],"149":[0,1],"150":[0,1],"151":[1,0],"152":[1,0],"153":[1,0],"154":[1,0],"155":[1,0],"156":[1,0],"157":[1,0],"158":[0,1],"159":[0,1],"160":[1,0],"161":[0,1],"162":[0,1],"163":[0,1],"164":[0,1],"165":[0,1],"166":[0,1],"167":[0,1],"168":[0,1],"169":[0,1],"170":[0,1],"171":[0,1],"172":[1,0],"173":[0,1],"174":[0,1],"175":[0,1],"176":[0,1],"177":[0,1],"178":[0,1],"179":[0,1],"180":[1,0],"181":[0,1],"182":[1,0],"183":[0,1],"184":[0,1],"185":[1,0],"186":[1,0],"187":[1,0],"188":[0,1],"189":[0,1],"190":[0,1],"191":[0,1],"192":[1,0],"193":[0,1],"194":[1,0],"195":[1,0],"196":[1,0],"197":[0,1],"198":[0,1],"199":[0,1],"200":[0,1],"201":[0,1],"202":[0,1],"203":[0,1],"204":[0,1],"205":[0,1],"206":[0,1],"207":[0,1],"208":[0,1],"209":[0,1],"210":[0,1],"211":[0,1],"212":[0,1],"213":[0,1],"214":[0,1],"215":[0,1],"216":[0,1],"217":[0,1],"218":[0,1],"219":[1,0],"220":[1,0],"221":[0,1],"222":[0,1],"223":[0,1],"224":[0,1],"225":[0,1],"226":[0,1],"227":[0,1],"228":[0,1],"229":[0,1],"230":[0,1],"231":[0,1],"232":[0,1],"233":[1,0],"234":[1,0],"235":[1,0],"236":[0,1],"237":[0,1],"238":[0,1],"239":[0,1],"240":[1,0],"241":[1,0],"242":[1,0],"243":[1,0],"244":[0,1],"245":[0,1],"246":[0,1],"247":[0,1],"248":[0,1],"249":[0,1],"250":[1,0],"251":[1,0],"252":[1,0],"253":[1,0],"254":[1,0],"255":[1,0],"256":[1,0],"257":[1,0],"258":[1,0],"259":[1,0],"260":[1,0],"261":[0,1],"262":[0,1],"263":[0,1],"264":[1,0],"265":[0,1],"266":[0,1],"267":[1,0],"268":[1,0],"269":[1,0],"270":[1,0],"271":[0,1],"272":[0,1],"273":[1,0],"274":[1,0],"275":[1,0],"276":[1,0],"277":[0,1],"278":[1,0],"279":[1,0],"280":[1,0],"281":[1,0],"282":[1,0],"283":[0,1],"284":[0,1],"285":[1,0],"286":[1,0],"287":[1,0],"288":[1,0],"289":[1,0],"290":[0,1],"291":[0,1],"292":[0,1],"293":[0,1],"294":[0,1],"295":[0,1],"296":[1,0.7978630812906882]},"paths":[136843,45144,59102,122570,137003,183745,122578,54984,176781,176782,137004,137005,137219,137213,137221,45225,16505,102326,3284,137212,125985,125984,125987,137427,137425,126006,174741,126011,68556,188107,191151,174272,59272,87914,102509,73450,73512,45524,31264,102683,16849,88181,42989,73630,112802,73629,73628,45665,55269,45663,102732,59545,138697,138698,138843,138838,138839,26689,138841,3697,102800,45812,23610,83137,68416,68417,3903,17149,88446,103044,3960,31248,3958,45999,88480,31627,59426,45595,73953,120113,122147,121326,121323,121322,121358,121361,120409,125845,46216,194118,193978,125437,55296,125413,125412,140667,140651,140650,140894,178075,140910,140901,140913,140912,185033,140818,125411,194019,193874,194077,17453,193914,193970,193921,195506,195631,195630,195490,195611,193876,194902,193880,194134,193913,194169,194086,29271,194167,42110,193651,194106,194046,194036,194201,193864,194898,194897,194119,194199,193930,194207,193920,196267,141619,196266,196275,196273,196272,196270,196271,197248,197273,142091,197265,197290,197204,196293,46758,196298,194170,4820,46907,89511,197239,194131,193934,194901,194026,194900,194217,194870,193968,196320,196327,196328,196330,196329,196331,196332,197107,116488,184318,116645,142872,142871,196326,196325,196338,196337,197049,196292,196134,143237,143238,143500,143501,143498,143497,177288,143689,143693,143694,143691,177284,177285,177286,143692,144005,143997,144003,144002,144018,177110,144001,177121,144010,144013,144473,9968,196406,196407,144521,177135,144514,43747,121557,122056,178334,177129,122011,178344,144427,144453,9983,175060,26637,25941,177156,175063,144371,144361,144369,69307,144437,178335,175361,144344,175050,175051,9981,197090,197091,196402,196397,197077,144816,144815,144838,90189,144837,112497,177144,183063,144824,5523,177125,144811,145150,174996,191013,145144,174684,122580,195620,194984,193650,174681,174683,177247,173749,173748,145143,145139,177245,145140,144885,145022,174980,145137,145134,145132,145523,145490,145522,145521,122567,112857]},{"positions":{"0":[0.7978630812906882,1],"1":[1,0],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[1,0],"7":[1,0],"8":[0,1],"9":[0,1],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[0,1],"20":[0,1],"21":[0,1],"22":[1,0],"23":[1,0],"24":[1,0],"25":[0,1],"26":[0,1],"27":[0,1],"28":[0,1],"29":[0,1],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[0,1],"37":[1,0],"38":[0,1],"39":[0,1],"40":[0,1],"41":[1,0],"42":[1,0],"43":[0,1],"44":[0,1],"45":[0,1],"46":[1,0],"47":[0,1],"48":[0,1],"49":[0,1],"50":[0,1],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[1,0],"57":[1,0],"58":[1,0],"59":[1,0],"60":[0,1],"61":[0,1],"62":[0,1],"63":[1,0],"64":[0,1],"65":[0,1],"66":[0,1],"67":[0,1],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[1,0],"74":[1,0],"75":[1,0],"76":[1,0],"77":[1,0],"78":[0,1],"79":[0,1],"80":[0,1],"81":[1,0],"82":[1,0],"83":[1,0],"84":[0,1],"85":[0,1],"86":[1,0],"87":[1,0],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[0,0.3317129131130851]},"paths":[112857,122567,99450,27471,18978,86727,114460,99130,193872,194078,193653,194082,193637,193951,193873,194075,194161,193912,194121,194198,194216,195482,196434,196433,189673,197223,197256,197268,196432,196431,105057,146167,146170,146168,197217,197278,196428,197161,194832,196427,197211,195774,195830,195832,195814,195782,130899,195743,196567,196563,193692,195795,196565,165712,195816,189223,195791,196560,195796,195845,195888,195892,196555,165661,197114,195804,196554,195767,195763,66508,165657,165632,165631,165629,38867,193247,165624,165623,38850,165603,165611,165594,175724,175679,165612,165620,165609,23730,29942,29940,29935,165621,112154]},{"positions":{"0":[0.3317129131130851,0],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[1,0],"8":[1,0],"9":[0,1],"10":[0,1],"11":[0,1],"12":[1,0],"13":[1,0],"14":[1,0],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[0,1],"24":[0,1],"25":[0,1],"26":[1,0],"27":[1,0],"28":[1,0],"29":[0,1],"30":[1,0],"31":[1,0],"32":[1,0],"33":[1,0],"34":[0,1],"35":[0,1],"36":[1,0],"37":[1,0],"38":[1,0],"39":[0,1],"40":[0,1],"41":[1,0],"42":[0,1],"43":[0,1],"44":[1,0],"45":[1,0],"46":[1,0],"47":[1,0],"48":[1,0],"49":[0,1],"50":[0,1],"51":[1,0],"52":[1,0],"53":[1,0],"54":[1,0],"55":[1,0],"56":[0,1],"57":[1,0],"58":[1,0],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[1,0],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[1,0],"73":[0,1],"74":[0,1],"75":[1,0],"76":[1,0],"77":[0,1],"78":[1,0],"79":[1,0],"80":[1,0],"81":[1,0],"82":[1,0],"83":[1,0],"84":[1,0],"85":[1,0],"86":[1,0],"87":[1,0],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[0,1],"93":[0,1],"94":[1,0],"95":[1,0],"96":[1,0],"97":[1,0],"98":[1,0],"99":[1,0],"100":[1,0],"101":[1,0],"102":[1,0],"103":[1,0],"104":[0,1],"105":[1,0],"106":[1,0],"107":[1,0],"108":[1,0],"109":[1,0],"110":[0,1],"111":[0,1],"112":[0,1],"113":[0,1],"114":[0,1],"115":[0,1],"116":[0,1],"117":[0,1],"118":[0,1],"119":[0,1],"120":[0,1],"121":[0,1],"122":[0,1],"123":[0,1],"124":[0,1],"125":[0,1],"126":[0,1],"127":[0,1],"128":[0,1],"129":[1,0],"130":[0,1],"131":[0,1],"132":[0,1],"133":[0,1],"134":[1,0],"135":[0,1],"136":[0,1],"137":[0,1],"138":[0,1],"139":[0,1],"140":[0,1],"141":[1,0],"142":[1,0],"143":[1,0],"144":[1,0],"145":[1,0],"146":[1,0],"147":[1,0],"148":[1,0],"149":[1,0],"150":[1,0],"151":[1,0],"152":[1,0],"153":[0,1],"154":[0,1],"155":[0,1],"156":[0,1],"157":[0,1],"158":[1,0],"159":[1,0],"160":[1,0],"161":[1,0],"162":[0,1],"163":[0,1],"164":[0,1],"165":[1,0],"166":[1,0],"167":[1,0],"168":[0,1],"169":[1,0],"170":[0,1],"171":[0,1],"172":[1,0],"173":[1,0],"174":[0,1],"175":[0,1],"176":[1,0],"177":[1,0],"178":[1,0],"179":[1,0],"180":[1,0],"181":[1,0],"182":[1,0],"183":[0,1],"184":[0,1],"185":[0,0.15102146045191484]},"paths":[112154,165621,29935,29940,29942,23730,165609,165620,165612,175679,175724,165594,165611,165603,38850,165623,165624,193247,38867,165627,109828,10496,26699,26698,66476,61483,61485,61482,96001,18760,61375,104620,104622,5403,10011,104492,5402,5399,5404,5405,75372,47466,113829,121538,121536,122042,121537,119906,89820,5171,121502,121503,121505,121499,89822,32833,32727,143433,1499,143432,185831,42538,143187,1047,1053,42532,1052,1048,1059,32643,18230,5015,142837,104022,46966,98159,184738,47046,46967,18181,60811,32580,193923,46896,103954,4900,27337,74845,60749,89501,103878,46819,32373,4734,120115,13933,89220,74597,74596,4634,32295,89203,14052,32296,4636,103740,103652,4569,46587,84073,103651,103520,103558,60377,32146,74353,74354,32059,60238,46352,103421,46351,74296,103355,60186,74230,59857,17488,4291,4290,74229,74189,88747,46254,27871,4146,17384,4144,88667,46123,88598,46126,118979,46127,31748,4067,74058,4068,59998,88600,46128,29275,17264,13456,17265,88536,3948,17190,88484,31563,73893,130256,130252,129344,102989,45868,127229,127242,88375,188978,182435,102860,28489,138824,190305,138822,16895,45647,3577,27400,3574,68924,3627,14397,70304,31312]},{"positions":{"0":[0.15102146045191484,0],"1":[1,0],"2":[1,0],"3":[0,1],"4":[0,1],"5":[0,1],"6":[0,1],"7":[1,0],"8":[1,0],"9":[1,0],"10":[0,1],"11":[0,1],"12":[0,1],"13":[0,1],"14":[0,1],"15":[0,1],"16":[0,1],"17":[0,1],"18":[0,1],"19":[1,0],"20":[1,0],"21":[1,0],"22":[1,0],"23":[1,0],"24":[1,0],"25":[1,0],"26":[1,0],"27":[1,0],"28":[1,0],"29":[1,0],"30":[0,1],"31":[0,1],"32":[0,1],"33":[0,1],"34":[0,1],"35":[0,1],"36":[1,0],"37":[1,0],"38":[1,0],"39":[1,0],"40":[1,0],"41":[1,0],"42":[0,1],"43":[0,1],"44":[1,0],"45":[1,0],"46":[0,1],"47":[0,1],"48":[0,1],"49":[0,1],"50":[1,0],"51":[0,1],"52":[0,1],"53":[0,1],"54":[1,0],"55":[1,0],"56":[0,1],"57":[0,1],"58":[0,1],"59":[0,1],"60":[0,1],"61":[0,1],"62":[0,1],"63":[0,1],"64":[0,1],"65":[1,0],"66":[1,0],"67":[1,0],"68":[1,0],"69":[1,0],"70":[1,0],"71":[1,0],"72":[0,1],"73":[0,1],"74":[0,1],"75":[0,1],"76":[0,1],"77":[0,1],"78":[0,1],"79":[0,1],"80":[0,1],"81":[0,1],"82":[0,1],"83":[0,1],"84":[1,0],"85":[1,0],"86":[1,0],"87":[1,0],"88":[1,0],"89":[1,0],"90":[1,0],"91":[1,0],"92":[1,0],"93":[1,0],"94":[1,0],"95":[0,1],"96":[1,0],"97":[1,0],"98":[1,0],"99":[1,0],"100":[1,0],"101":[1,0],"102":[0,1],"103":[0,1],"104":[0,1],"105":[1,0],"106":[1,0],"107":[1,0],"108":[1,0],"109":[0,1],"110":[1,0],"111":[1,0],"112":[0,1],"113":[0,1],"114":[0,1],"115":[1,0],"116":[1,0],"117":[1,0],"118":[1,0],"119":[1,0],"120":[0,1],"121":[1,0],"122":[0,1],"123":[0,1],"124":[0,1],"125":[0,1],"126":[0,1],"127":[0,1],"128":[0,1],"129":[0,1],"130":[1,0],"131":[0,1],"132":[0,1],"133":[1,0],"134":[1,0],"135":[1,0],"136":[1,0],"137":[1,0],"138":[1,0],"139":[0,1],"140":[0,1],"141":[0,1],"142":[0,1],"143":[0,1],"144":[1,0],"145":[1,0],"146":[1,0],"147":[0,1],"148":[1,0],"149":[1,0],"150":[1,0],"151":[1,0],"152":[0,1],"153":[0,1],"154":[1,0],"155":[1,0],"156":[1,0],"157":[0,1],"158":[0,1],"159":[0,1],"160":[0,1],"161":[0,1],"162":[0,1],"163":[0,1],"164":[0,1],"165":[0,1],"166":[0,1],"167":[0,1],"168":[1,0],"169":[1,0],"170":[1,0],"171":[1,0],"172":[1,0],"173":[0,1],"174":[0,1],"175":[0,0.8763731120431061]},"paths":[31312,70304,14397,3627,68924,3574,27400,16835,191180,192487,191608,188974,138459,45579,31199,127214,1034,1012,1022,1006,1031,1043,1038,1044,1026,994,3397,1045,132267,102446,102443,73382,1023,59211,1025,1010,1033,1015,1011,83302,136762,136755,136753,176801,176803,181983,191490,132812,187184,189326,136474,136469,136468,136239,136241,136237,136010,184793,123517,174183,136012,182119,136005,176862,135769,135768,123809,184806,72984,85192,30676,123803,135558,135557,135560,182118,135559,30600,2907,30598,72935,101945,97177,101944,97176,24685,30535,2849,134924,134927,134923,181898,134928,134926,176886,134925,174178,134590,182114,134337,185689,134336,101798,134333,134334,134150,133937,133938,133940,181990,72658,44564,101671,101636,133625,133626,44535,178419,15818,133521,133518,133522,133519,133437,133438,133436,174174,44465,133386,182126,114995,99754,52423,43666,53,191363,171521,171520,171519,52402,52398,52427,125504,52426,52425,15002,82831,125502,96643,81723,24972,67660,171375,189207,132650,11664,67645,111084,67643,195995,195991,195949,196128,196131,196133,195951,195948,195950,196132,196130,196016,196011,196012,196009,67641,96606]}] +} \ No newline at end of file diff --git a/tools/benchmarking/cypress/support/commands.js b/tools/benchmarking/cypress/support/commands.js new file mode 100644 index 0000000000..014ff9a9b9 --- /dev/null +++ b/tools/benchmarking/cypress/support/commands.js @@ -0,0 +1,214 @@ +Cypress.Commands.add('loginByCSRF', (username, password) => { + cy.request('/login/?next=/') + .its('body') + .then((body) => { + // we can use Cypress.$ to parse the string body + // thus enabling us to query into it easily + const $html = Cypress.$(body) + cy.request({ + method: 'POST', + url: '/login/?next=/', + failOnStatusCode: true, // dont fail so we can make assertions + form: true, // we are submitting a regular form body + body: { + username, + password, + "csrfmiddlewaretoken": $html.find('input[name=csrfmiddlewaretoken]').val(), // insert this as part of form body + } + }); + }); + }); + +Cypress.Commands.add('mockTiles', (username, password) => { + cy.intercept("https://*.tile.opentopomap.org/*/*/*.png", {fixture: "images/tile.png"}).as("tiles"); +}); + +Cypress.Commands.add('getMap', () => cy.get('[id="id_topology-map"]')); +Cypress.Commands.add('getPath', pathPk => cy.get(`[data-test=pathLayer-${pathPk}]`)); +Cypress.Commands.add('getRoute', stepIndex => { + if (stepIndex == null) + cy.get('[data-test^="route-step-"]') + else + cy.get(`[data-test="route-step-${stepIndex}"]`) +}); + +Cypress.Commands.add('fitPathsBounds', (pathPkList) => { + cy.window().then(win => { + const map = win.maps[0]; + const L = win.L; + let bounds = L.latLngBounds([]); + + // For each path, extend the bounds so they include it + cy.wrap(pathPkList).each(pk => { + // Get the leaflet layer whose id is the path pk + const mapLayers = map._layers; + const pathLayer = Object.values(mapLayers).find(layer => { + return layer.properties?.id && layer.properties.id == pk + }); + bounds.extend(pathLayer.getBounds()); + }).then(() => { + map.fitBounds(bounds) + }) + }) +}) + +Cypress.Commands.add('getCoordsOnMap', (pathPk, percentage) => { + cy.getPath(pathPk).then(path => { + let domPath = path.get(0); + + // Get the coordinates relative to the map element + let pathLength = domPath.getTotalLength(); + let lengthAtPercentage = percentage * pathLength / 100; + return domPath.getPointAtLength(lengthAtPercentage); + }) +}); + + +Cypress.Commands.add('getCoordsOnPath', (pathPk, percentage) => { + cy.getPath(pathPk).then(path => { + cy.getCoordsOnMap(pathPk, percentage).then(coordsOnMap => { + // Convert the coords so they are relative to the path + cy.getMap().then(map => { + let domMap = map.get(0); + let domPath = path.get(0); + + // Get the coords of the map and the path relative to the root DOM element + let mapCoords = domMap.getBoundingClientRect(); + let pathCoords = domPath.getBoundingClientRect(); + let horizontalDelta = pathCoords.x - mapCoords.x; + let verticalDelta = pathCoords.y - mapCoords.y; + + // Return the coords relative to the path element + return { + x: coordsOnMap.x - horizontalDelta, + y: coordsOnMap.y - verticalDelta, + } + }); + }) + + }) +}) + +Cypress.Commands.add('clickOnPath', (pathPk, percentage) => { + + // Zoom on the path for precision + cy.window().then(win => { + const map = win.maps[0]; + const originalMapBounds = map.getBounds(); + cy.fitPathsBounds([pathPk]).then(() => { + // Get the coordinates of the click and execute it + cy.getCoordsOnPath(pathPk, percentage).then(clickCoords => { + let startTime; + cy.getPath(pathPk) + .then((path) => {startTime = performance.now(); return path}) + .click(clickCoords.x, clickCoords.y, {force: true}) + // Return startTime so it is yielded by the command + .then(() => startTime); + }) + // Reset the map to its original bounds + .then(() => {map.fitBounds(originalMapBounds)}); + }) + }); +}); + + +Cypress.Commands.add('addViaPoint', (src, dest, stepIndex) => { + const {pathPk: srcPathPk, percentage: srcPathPercentage} = src + const {pathPk: destPathPk, percentage: destPathPercentage} = dest + + cy.window().then(win => { + const map = win.maps[0]; + const L = win.L; + // Zoom on the paths for precision + const originalMapBounds = map.getBounds(); + cy.fitPathsBounds([srcPathPk, destPathPk]).then(() => { + + // Get the coordinates of the mouse down and mouse up events + cy.getCoordsOnMap(srcPathPk, srcPathPercentage).then(srcCoords => { + cy.getCoordsOnMap(destPathPk, destPathPercentage).then(destCoords => { + + // Display the draggable marker by moving the mouse over a route layer + map.fire('mousemove', {layerPoint: {x: srcCoords.x, y: srcCoords.y}}) + + // Check that the draggable marker is displayed + cy.get('.marker-drag').then(_ => { + // Get the draggable marker layer corresponding to this step + const mapLayers = map._layers; + const draggableMarkers = Object.values(mapLayers).filter(layer => { + return layer.classname === "marker-drag" + }) + const draggableMarker = draggableMarkers[stepIndex] + + // Simulate dragging and dropping the marker onto the destination + draggableMarker.fire('dragstart') + const destLatLng = map.layerPointToLatLng(L.point(destCoords.x, destCoords.y)) + + // Measure time from the marker drop to the display of the new route layer + const startTime = performance.now(); + draggableMarker.setLatLng(destLatLng) + cy.getRoute(stepIndex + 1).then(() => { + let elapsedTime = performance.now() - startTime + cy.writeFile('time_measures/time_measures_js.txt', elapsedTime.toString() + ' ', { flag: 'a+' }) + }); + draggableMarker.fire('dragend') + }); + + }); + }) + // Reset the map to its original bounds + .then(() => map.fitBounds(originalMapBounds)); + }); + }); +}) + +Cypress.Commands.add('generateRouteTracingTimes', topologyName => { + cy.visit('/trek/add'); + cy.wait('@tiles'); + + // Wait for the path layers to be ready before clicking on the "Route" control + cy.get('[data-test^="pathLayer-"]') + cy.get("a.linetopology-control").click(); + + cy.fixture('topologies.json').then(topologies => { + // Get the paths and positions for the start and end markers + let topo = topologies[topologyName]; + const firstMarker = { + path: topo[0].paths[0], + position: topo[0].positions[0][0], + }; + const lastMarker = { + path: topo.at(-1).paths.at(-1), + position: topo.at(-1).positions[Object.keys(topo.at(-1).positions).length - 1].at(-1), + }; + + // Add the start and end markers and wait for the route to be displayed + cy.clickOnPath(firstMarker.path, firstMarker.position * 100); + cy.clickOnPath(lastMarker.path, lastMarker.position * 100) + .then((startTime) => { + cy.getRoute().then(() => { + let elapsedTime = performance.now() - startTime + cy.writeFile('time_measures/time_measures_js.txt', elapsedTime.toString() + ' ', { flag: 'a+' }) + }); + }); + + // Add the via-points: for each step, drag from previous marker + // and drop on last path and position of the current step + let prevMarker = firstMarker; + for (let step = 0; step < topo.length - 1; step++) { + const viaMarker = { + path: topo[step].paths.at(-1), + position: topo[step].positions[Object.keys(topo[step].positions).length - 1].at(-1), + }; + cy.addViaPoint( + {pathPk: prevMarker.path, percentage: prevMarker.position * 100}, + {pathPk: viaMarker.path, percentage: viaMarker.position * 100}, + step + ); + prevMarker = viaMarker; + } + }) + + // Add a newline to the time log files + cy.writeFile('time_measures/time_measures_js.txt', '\n', { flag: 'a+' }) + cy.writeFile('time_measures/time_measures_py.txt', '\n', { flag: 'a+' }) +}) diff --git a/tools/benchmarking/cypress/support/e2e.js b/tools/benchmarking/cypress/support/e2e.js new file mode 100644 index 0000000000..f65e7f7cd0 --- /dev/null +++ b/tools/benchmarking/cypress/support/e2e.js @@ -0,0 +1 @@ +import './commands' \ No newline at end of file diff --git a/tools/benchmarking/get_backend_measures.sh b/tools/benchmarking/get_backend_measures.sh new file mode 100755 index 0000000000..5f5428ccd3 --- /dev/null +++ b/tools/benchmarking/get_backend_measures.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +DATABASE=$1 # "medium" or "big" +SESSION_ID=$2 +MEASURES_DIR=./time_measures + +NB_MEASURES=10 +STEPS_MEDIUM_DB='{"steps": [{"lat": 48.78872170000001,"lng": -0.7280873000000021,"path_id": 7061},{"lat": 48.73181390000002,"lng": -0.7179939000000024,"path_id": 4139},{"lat": 48.676642,"lng": -0.6817526000000051,"path_id": 109},{"lat": 48.67861300000001,"lng": -0.6439691999999918,"path_id": 4177},{"lat": 48.7009625,"lng": -0.6448388000000005,"path_id": 4183},{"lat": 48.73732735527879,"lng": -0.6272379744023348,"path_id": 7047},{"lat": 48.766032100000004,"lng": -0.6849768999999983,"path_id": 110},{"lat": 48.81694350000001,"lng": -0.6613134000000054,"path_id": 4178},{"lat": 48.805070000000015,"lng": -0.6267835000000033,"path_id": 7021},{"lat": 48.78119819999999,"lng": -0.6334089999999981,"path_id": 4211},{"lat": 48.7650436,"lng": -0.6056722999999975,"path_id": 5611},{"lat": 48.71871339999999,"lng": -0.5552605000000099,"path_id": 637},{"lat": 48.7487884,"lng": -0.5378913000000063,"path_id": 698},{"lat": 48.78614750000001,"lng": -0.5632506999999909,"path_id": 4270},{"lat": 48.78451229999999,"lng": -0.5297335000000069,"path_id": 737},{"lat": 48.77718510751703,"lng": -0.5195043605179128,"path_id": 4300},{"lat": 48.80810350000001,"lng": -0.5094418000000012,"path_id": 6903},{"lat": 48.8110711,"lng": -0.4875585000000004,"path_id": 6862},{"lat": 48.80337130000001,"lng": -0.4788153000000062,"path_id": 6863},{"lat": 48.7722458,"lng": -0.4598700999999994,"path_id": 5522},{"lat": 48.7500188,"lng": -0.4239464000000015,"path_id": 6767},{"lat": 48.79490820408397,"lng": -0.3516193881266494,"path_id": 1439},{"lat": 48.774549100000016,"lng": -0.3201351000000052,"path_id": 6715},{"lat": 48.808130000000006,"lng": -0.29126549999999307,"path_id": 6738},{"lat": 48.745965,"lng": -0.24042960000000724,"path_id": 1501},{"lat": 48.7750731,"lng": -0.2063589000000099,"path_id": 4496},{"lat": 48.78273430000001,"lng": -0.16739840000000503,"path_id": 8093},{"lat": 48.7545855,"lng": -0.148252499999999,"path_id": 1567},{"lat": 48.7399976273847,"lng": -0.10886289938103699,"path_id": 6587},{"lat": 48.685151645123064,"lng": -0.14202597929864336,"path_id": 8088},{"lat": 48.7331811,"lng": -0.040682100000004606,"path_id": 7456},{"lat": 48.730169499999995,"lng": -0.006157399999993096,"path_id": 7466},{"lat": 48.691925299999994,"lng": -0.010618899999994547,"path_id": 6318},{"lat": 48.69748177728096,"lng": 0.09194033562911928,"path_id": 4665},{"lat": 48.692691,"lng": 0.12254079999998167,"path_id": 4689},{"lat": 48.72093480000001,"lng": 0.1583081999999969,"path_id": 4712},{"lat": 48.644576125589296,"lng": 0.20426585602449254,"path_id": 4720},{"lat": 48.70390922334383,"lng": 0.2592412032409186,"path_id": 4790},{"lat": 48.661601097217876,"lng": 0.25658600841475193,"path_id": 6376},{"lat": 48.651978400000004,"lng": 0.2847110999999858,"path_id": 6345},{"lat": 48.6566794174864,"lng": 0.32453158568303664,"path_id": 2640},{"lat": 48.659802768793455,"lng": 0.3474000596792193,"path_id": 2650},{"lat": 48.644699200000005,"lng": 0.35545089999998497,"path_id": 4841},{"lat": 48.61567840000001,"lng": 0.3517553000000051,"path_id": 4849},{"lat": 48.564724800000015,"lng": 0.2624846000000058,"path_id": 6367},{"lat": 48.5398916,"lng": 0.3795004000000013,"path_id": 7165},{"lat": 48.52095420260181,"lng": 0.46465391313148524,"path_id": 7787},{"lat": 48.464230900000004,"lng": 0.45510510000000615,"path_id": 6069},{"lat": 48.53586557886738,"lng": 0.5931378100232408,"path_id": 4995},{"lat": 48.52928500000001,"lng": 0.6260873000000133,"path_id": 5006},{"lat": 48.533391247525124,"lng": 0.6481910378033451,"path_id": 5042},{"lat": 48.577232900000006,"lng": 0.67391269999999,"path_id": 3581},{"lat": 48.5795185,"lng": 0.7066258000000092,"path_id": 3739},{"lat": 48.53995800000002,"lng": 0.6869060000000005,"path_id": 3623},{"lat": 48.5359437488372,"lng": 0.6912809757068716,"path_id": 3623},{"lat": 48.511737499999995,"lng": 0.6729164000000099,"path_id": 3593},{"lat": 48.49124379999997,"lng": 0.6834466999999876,"path_id": 3630},{"lat": 48.48738920000002,"lng": 0.6502814000000035,"path_id": 3441},{"lat": 48.5047461,"lng": 0.6321343999999929,"path_id": 3364},{"lat": 48.43746869999999,"lng": 0.5907351000000194,"path_id": 7766},{"lat": 48.43067998875949,"lng": 0.7381241776589542,"path_id": 5353},{"lat": 48.3774613,"lng": 0.620931399999991,"path_id": 6051},{"lat": 48.30171857552506,"lng": 0.6833065121897075,"path_id": 6204},{"lat": 48.3058772,"lng": 0.5374620000000174,"path_id": 6174},{"lat": 48.39975019999999,"lng": 0.30353279999998595,"path_id": 4777},{"lat": 48.61152178064436,"lng": 0.02678793256564393,"path_id": 8528},{"lat": 48.58126070000001,"lng": -0.030045000000002986,"path_id": 1732},{"lat": 48.57413842317843,"lng": -0.12063000313336845,"path_id": 1562},{"lat": 48.4704601,"lng": -0.15201150000000885,"path_id": 5394},{"lat": 48.5211142,"lng": -0.3140358,"path_id": 5629},{"lat": 48.5306078,"lng": -0.4423807000000024,"path_id": 4335},{"lat": 48.5469623,"lng": -0.48764910000000267,"path_id": 878},{"lat": 48.58734660000001,"lng": -0.5976487999999991,"path_id": 4223},{"lat": 48.61087130000001,"lng": -0.6010315000000088,"path_id": 326},{"lat": 48.61396018034764,"lng": -0.570809390607494,"path_id": 494},{"lat": 48.612043899999996,"lng": -0.5199207000000028,"path_id": 715},{"lat": 48.63959500000001,"lng": -0.4897316999999957,"path_id": 886},{"lat": 48.64058295423239,"lng": -0.45624500678267177,"path_id": 1014},{"lat": 48.616869718771966,"lng": -0.4685391699291319,"path_id": 983},{"lat": 48.57784138065519,"lng": -0.4363485819627533,"path_id": 1050},{"lat": 48.59565210000001,"lng": -0.38701859999999755,"path_id": 5961},{"lat": 48.609623400000004,"lng": -0.20698610000000617,"path_id": 5940},{"lat": 48.63182200000001,"lng": -0.21355209999999403,"path_id": 8055},{"lat": 48.6129627,"lng": -0.09591579999999711,"path_id": 5920},{"lat": 48.64295144732567,"lng": -0.040852126035595404,"path_id": 1685},{"lat": 48.68075370000001,"lng": -0.046584000000000625,"path_id": 8583},{"lat": 48.675437099999996,"lng": -0.07790740000000351,"path_id": 8564},{"lat": 48.7812038,"lng": -0.055628700000001086,"path_id": 8099},{"lat": 48.83095460000001,"lng": 0.011730200000017454,"path_id": 1867},{"lat": 48.85684071765811,"lng": 0.05253954573986696,"path_id": 1995},{"lat": 48.88185430000001,"lng": 0.12435850000001913,"path_id": 2252},{"lat": 48.930391076452246,"lng": 0.1626837010355686,"path_id": 2295},{"lat": 48.94935437081008,"lng": 0.2541650682723562,"path_id": 5761},{"lat": 48.883915499999986,"lng": 0.2652445999999919,"path_id": 6532},{"lat": 48.859749300000004,"lng": 0.3261208999999887,"path_id": 6495},{"lat": 48.835374800000004,"lng": 0.3320875999999906,"path_id": 6491},{"lat": 48.774814200000016,"lng": 0.3728876999999864,"path_id": 4863},{"lat": 48.801146400000015,"lng": 0.4462774000000147,"path_id": 6522},{"lat": 48.74335909999999,"lng": 0.5005958000000144,"path_id": 5860},{"lat": 48.748907900000006,"lng": 0.6154992000000092,"path_id": 3314},{"lat": 48.775356900000006,"lng": 0.6953322999999934,"path_id": 6442},{"lat": 48.80584295122468,"lng": 0.622037502408217,"path_id": 5024}]}' +STEPS_BIG_DB='{"steps": [{"lat": 44.40018778200495,"lng": 0.9883771908915584,"path_id": 169899},{"lat": 44.23831930000001,"lng": 1.2774443000000035,"path_id": 125826},{"lat": 44.337832899999995,"lng": 1.4496121999999945,"path_id": 51128},{"lat": 44.24025119999999,"lng": 1.6089541999999968,"path_id": 161645},{"lat": 44.369933499999995,"lng": 1.803867500000016,"path_id": 64765},{"lat": 44.46619299999999,"lng": 1.4093345999999851,"path_id": 192840},{"lat": 44.40395120000001,"lng": 1.1285949000000173,"path_id": 66258},{"lat": 44.5469963,"lng": 1.080862999999983,"path_id": 147987},{"lat": 44.65210690000001,"lng": 1.1733569000000044,"path_id": 143244},{"lat": 44.57784079999999,"lng": 1.3804053000000092,"path_id": 251},{"lat": 44.66788439999999,"lng": 1.4108565000000084,"path_id": 100640},{"lat": 44.6401763,"lng": 1.507850999999989,"path_id": 33213},{"lat": 44.521939499999974,"lng": 1.5459739999999833,"path_id": 62926},{"lat": 44.596103400000004,"lng": 1.6275356000000185,"path_id": 128221},{"lat": 44.46257740000001,"lng": 1.7491142000000037,"path_id": 152665},{"lat": 44.48786577758629,"lng": 1.9581521494801324,"path_id": 11392},{"lat": 44.589200899999994,"lng": 1.7456906999999955,"path_id": 62058},{"lat": 44.7475971,"lng": 1.6675977999999914,"path_id": 103494},{"lat": 44.74754839999998,"lng": 1.449830099999998,"path_id": 84807},{"lat": 44.891118599999984,"lng": 1.4784941000000051,"path_id": 135865},{"lat": 45.020028100000005,"lng": 1.6124456999999959,"path_id": 131624},{"lat": 44.8388804,"lng": 1.7647118999999953,"path_id": 189792},{"lat": 44.9560056,"lng": 1.9026717000000115,"path_id": 72000},{"lat": 44.80577899999999,"lng": 1.9953387000000067,"path_id": 102924},{"lat": 44.659736399999986,"lng": 1.9534054000000145,"path_id": 104388},{"lat": 44.5835307,"lng": 2.0760624000000183,"path_id": 146309},{"lat": 44.86836009999998,"lng": 2.175293200000019,"path_id": 82783}]}' + +launch_scenario() { +# $1 (boolean): if true, keep the backend cache before each spec run + + # Reset the time measure files + if [ -d "$MEASURES_DIR" ]; then + rm -f "$MEASURES_DIR"/time_measures_* + else + mkdir "$MEASURES_DIR" + fi + + for i in $(seq 1 $NB_MEASURES) + do + if ! $1; then + # Empty the pgRouting network topology + docker compose run --rm web ./manage.py dbshell -- -c "update core_path set source=null, target=null;" + fi + + if [[ "$DATABASE" == "medium" ]]; then + STEPS="$STEPS_MEDIUM_DB" + else + STEPS="$STEPS_BIG_DB" + fi + # Send a request to take the time measures + curl 'http://geotrek.local:8000/api/path/drf/paths/route-geometry' -X POST \ + -H 'X-CSRFToken: DKUegVuaV01Lsb2s4ORLTx6dqG7CWiFP' \ + -H 'Content-Type: application/json; charset=UTF-8' \ + -H "Cookie: csrftoken=DKUegVuaV01Lsb2s4ORLTx6dqG7CWiFP; sessionid=$SESSION_ID" \ + --data-raw "$STEPS" \ + -s -o /dev/null + done + + # Compute and display the average times + echo "Branch:" $(git rev-parse --abbrev-ref HEAD) >> "$MEASURES_DIR"/time_averages.txt + echo "Database:" $DATABASE >> "$MEASURES_DIR"/time_averages.txt + echo "pgr network topology:" $1 >> "$MEASURES_DIR"/time_averages.txt + echo "Number of runs:" $NB_MEASURES >> "$MEASURES_DIR"/time_averages.txt + python3 ./time_averages.py >> "$MEASURES_DIR"/time_averages.txt + echo "" >> "$MEASURES_DIR"/time_averages.txt +} + +# Reset the time averages files +if [ -d "$MEASURES_DIR" ]; then + rm -f "$MEASURES_DIR"/time_averages.txt + else + mkdir "$MEASURES_DIR" + fi +launch_scenario false +launch_scenario true \ No newline at end of file diff --git a/tools/benchmarking/package-lock.json b/tools/benchmarking/package-lock.json new file mode 100644 index 0000000000..5397ce1231 --- /dev/null +++ b/tools/benchmarking/package-lock.json @@ -0,0 +1,1956 @@ +{ + "name": "benchmarking", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "cypress": "^13.12.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "dev": true, + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cypress": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz", + "integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/tools/benchmarking/package.json b/tools/benchmarking/package.json new file mode 100644 index 0000000000..a92e654d91 --- /dev/null +++ b/tools/benchmarking/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "cypress:open": "cypress open", + "cypress:run": "cypress run" + }, + "devDependencies": { + "cypress": "^13.12.0" + } + } \ No newline at end of file diff --git a/tools/benchmarking/time_averages.py b/tools/benchmarking/time_averages.py new file mode 100644 index 0000000000..a54f30fa07 --- /dev/null +++ b/tools/benchmarking/time_averages.py @@ -0,0 +1,27 @@ +from statistics import mean +from pathlib import Path + + +def get_time_averages(measure_filepath): + file = Path(measure_filepath) + if file.is_file(): + # Generating the times matrix: + # Each row contains the time mesures of all executions for one session + # e.g. all time mesures of the request execution from 0 to 100 via-steps + with open(measure_filepath, 'r') as f: + times = [[time for time in line.split()] for line in f] + + # Computing the average of all sessions time mesures for each execution + # e.g. if time = [[1, 20, 300], [7, 80, 900]] then averages = [4, 50, 600] + averages = [mean(map(float, x)) for x in zip(*times)] + return averages + + else: + return "No data" + + +if __name__ == '__main__': + averages = get_time_averages('time_measures/time_measures_py.txt') + print('Python:', averages) + averages = get_time_averages('time_measures/time_measures_js.txt') + print('JavaScript:', averages) diff --git a/tools/install-dev.sh b/tools/install-dev.sh index d75264c8e3..7a162c8b79 100755 --- a/tools/install-dev.sh +++ b/tools/install-dev.sh @@ -22,16 +22,16 @@ if ! `localectl status | grep -q "System Locale: LANG=.*UTF-8"`; then fi if [ "$*" == "--nodb" ]; then - postgis="" + postgis_and_routing="" elif [ -n "$*" ]; then echo "Usage: $0 [--nodb]" exit 1 else - postgis="postgis" + postgis_and_routing="postgresql-pgrouting" fi sudo apt update -sudo apt install -y $postgis wget software-properties-common +sudo apt install -y $postgis_and_routing wget software-properties-common echo "deb [arch=amd64] https://packages.geotrek.fr/ubuntu $(lsb_release -sc) main dev" | sudo tee /etc/apt/sources.list.d/geotrek.list wget -O- "https://packages.geotrek.fr/geotrek.gpg.key" | sudo apt-key add - sudo apt update diff --git a/tools/install.sh b/tools/install.sh index b900aa2429..9b3ec3a360 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -11,7 +11,7 @@ else exit 1 fi -if [ "`locale charmap`" != "UTF-8" ]; then +if [ "$(locale charmap)" != "UTF-8" ]; then echo "ERROR! Your user locale charmap is not UTF-8" exit 1 fi @@ -22,16 +22,16 @@ if ! `localectl status | grep -q "System Locale: LANG=.*UTF-8"`; then fi if [ "$*" == "--nodb" ]; then - postgis="" + postgis_and_routing="" elif [ -n "$*" ]; then echo "Usage: $0 [--nodb]" exit 1 else - postgis="postgis" + postgis_and_routing="postgresql-pgrouting" fi sudo apt update -sudo apt install -y $postgis wget software-properties-common +sudo apt install -y $postgis_and_routing wget software-properties-common echo "deb [arch=amd64] https://packages.geotrek.fr/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/geotrek.list wget -O- "https://packages.geotrek.fr/geotrek.gpg.key" | sudo apt-key add - sudo apt update