From a2ede1f58249c047e280eaff42fe6c59ea2afda6 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 4 May 2024 15:50:36 +0000
Subject: [PATCH 01/33] fix/ci-optimizations (#2)

Reviewed-on: https://git.kluster.moll.re/remoll/fast-network-navigation/pulls/2
---
 .drone.yml                          | 25 -----------------------
 .gitea/workflows/build-android.yaml | 19 +++++++++++++-----
 .gitea/workflows/build-web.yaml     | 17 ++++++++++++----
 .gitea/workflows/test.yaml          | 31 +++++++++++++++++++++++++++++
 4 files changed, 58 insertions(+), 34 deletions(-)
 delete mode 100644 .drone.yml
 create mode 100644 .gitea/workflows/test.yaml

diff --git a/.drone.yml b/.drone.yml
deleted file mode 100644
index 1ea50ce..0000000
--- a/.drone.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-kind: pipeline
-type: kubernetes
-name: build-apk
-
-clone:
-  depth: 1
-
-steps:
-  - name: build
-    image: cirrusci/flutter:stable
-    pull: true
-    commands:
-      - flutter packages get
-      - flutter build apk --release --split-per-abi
-      - mkdir apks
-      - mv build/app/outputs/apk/*/*/*.apk apks
-
-  - name: Publish APK
-    image: plugins/gitea-release
-    settings:
-      api_key:
-        from_secret: GITEA_TOKEN
-trigger:
-  event:
-    - push
\ No newline at end of file
diff --git a/.gitea/workflows/build-android.yaml b/.gitea/workflows/build-android.yaml
index 76305b1..b714b6c 100644
--- a/.gitea/workflows/build-android.yaml
+++ b/.gitea/workflows/build-android.yaml
@@ -1,5 +1,10 @@
-on: push
-name: Test, Build and Release apk
+on:
+  pull_request:
+    branches:
+      - main
+
+name: Build and release APK
+
 jobs:
   build:
     name: Build APK
@@ -10,6 +15,7 @@ jobs:
       run: |
         sudo apt-get update
         sudo apt-get install -y xz-utils unzip
+
     - uses: https://gitea.com/actions/checkout@v4
 
     - uses: https://github.com/actions/setup-java@v4
@@ -22,19 +28,22 @@ jobs:
         channel: stable
         flutter-version: 3.19.6
         cache: true
+
     - name: Setup Android SDK
       uses: https://github.com/android-actions/setup-android@v3
 
     - run: flutter pub get
-    # - run: flutter test
+
     - run: flutter build apk --debug --split-per-abi
-    
+
     - name: Release APK
       uses: https://gitea.com/akkuman/gitea-release-action@v1
       with:
         files: build/app/outputs/flutter-apk/*.apk
+        name: Testing release
+        release_name: testing
         tag: testing
-        release_name: Testing release
+        tag_name: testing
         release_body: "This is a testing release."
         prerelease: true
         token: ${{ secrets.GITEA_TOKEN }}
diff --git a/.gitea/workflows/build-web.yaml b/.gitea/workflows/build-web.yaml
index cc08a5e..cdb817c 100644
--- a/.gitea/workflows/build-web.yaml
+++ b/.gitea/workflows/build-web.yaml
@@ -1,21 +1,30 @@
-on: push
-name: Test, Build and Release web
+on:
+  pull_request:
+    branches:
+      - main
+
+name: Build web
+
 jobs:
   build:
     name: Build Web
     runs-on: k8s
     steps:
+
     - name: Install prerequisites
       run: |
         sudo apt-get update
         sudo apt-get install -y xz-utils
 
     - uses: actions/checkout@v4
+
     - uses: https://github.com/subosito/flutter-action@v2
       with:
         channel: stable
         flutter-version: 3.19.6
         cache: true
-    - run: flutter pub get
-    # - run: flutter test
+
+    - run: flutter pub get
+
+    - run: flutter build web
 
diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml
new file mode 100644
index 0000000..7f22525
--- /dev/null
+++ b/.gitea/workflows/test.yaml
@@ -0,0 +1,31 @@
+on:
+  push:
+  pull_request:
+    branches:
+      - main
+
+
+name: Test code
+
+jobs:
+  test:
+    name: Test code
+    runs-on: k8s
+    steps:
+
+    - name: Install prerequisites
+      run: |
+        sudo apt-get update
+        sudo apt-get install -y xz-utils
+
+    - uses: actions/checkout@v4
+
+    - uses: https://github.com/subosito/flutter-action@v2
+      with:
+        channel: stable
+        flutter-version: 3.19.6
+        cache: true
+
+    - run: flutter pub get
+
+    - run: flutter test
-- 
2.47.2


From 3256c6d46aada13dc32e72b0c13f2c94c5b32c0c Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sun, 5 May 2024 13:14:32 +0200
Subject: [PATCH 02/33] working state with dynamic location tracker

---
 .gitea/workflows/build-android.yaml |  2 +-
 lib/modules/navigation.dart         | 53 +++++++++++++++++++-
 lib/modules/overview.dart           | 78 +++++++++++++++++++++++++----
 pubspec.lock                        | 50 +++++++++++++++++-
 pubspec.yaml                        |  7 ++-
 5 files changed, 175 insertions(+), 15 deletions(-)

diff --git a/.gitea/workflows/build-android.yaml b/.gitea/workflows/build-android.yaml
index b714b6c..e2e530a 100644
--- a/.gitea/workflows/build-android.yaml
+++ b/.gitea/workflows/build-android.yaml
@@ -34,7 +34,7 @@ jobs:
 
     - run: flutter pub get
 
-    - run: flutter build apk --debug --split-per-abi
+    - run: flutter build apk --release --split-per-abi
 
     - name: Release APK
       uses: https://gitea.com/akkuman/gitea-release-action@v1
diff --git a/lib/modules/navigation.dart b/lib/modules/navigation.dart
index f7fcbcf..7952560 100644
--- a/lib/modules/navigation.dart
+++ b/lib/modules/navigation.dart
@@ -1,13 +1,62 @@
 import 'package:flutter/material.dart';
 
-Widget singleDestination(BuildContext context, String title, String description, String image) {
+
+
+List<Widget> loadDestinations() {
+  List<Widget> cities =  [
+    singleDestination(
+      "New York",
+      "The Big Apple",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/3/34/View_of_New_York_City.jpg/800px-View_of_New_York_City.jpg"
+    ),
+    singleDestination(
+      "Los Angeles",
+      "City of Angels",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Los_Angeles_City_Hall_2013.jpg/800px-Los_Angeles_City_Hall_2013.jpg"
+    ),
+    singleDestination(
+      "Chicago",
+      "The Windy City",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Chicago_skyline%2C_viewed_from_John_Hancock_Center.jpg/800px-Chicago_skyline%2C_viewed_from_John_Hancock_Center.jpg"
+    ),
+    singleDestination(
+      "San Francisco",
+      "The Golden City",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/San_Francisco_City_Hall_2013.jpg/800px-San_Francisco_City_Hall_2013.jpg"
+    ),
+    singleDestination(
+      "Miami",
+      "The Magic City",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Miami_collage.jpg/800px-Miami_collage.jpg"
+    ),
+    singleDestination(
+      "Las Vegas",
+      "Sin City",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Las_Vegas_Strip.jpg/800px-Las_Vegas_Strip.jpg"
+    ),
+    singleDestination(
+      "Seattle",
+      "Emerald City",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Seattle_Kerry_Park_Skyline.jpg/800px-Seattle_Kerry_Park_Skyline.jpg"
+    ),
+    singleDestination(
+      "Boston",
+      "Beantown",
+      "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Boston_skyline_from_Longfellow_Bridge_September_2017_panorama_2.jpg/800px-Boston"
+    )
+  ];
+  cities.shuffle();
+  return cities;
+}
+
+Widget singleDestination(String title, String description, String image) {
   return Card(
     child: ListTile(
       leading: Icon(Icons.location_on),
       title: Text(title),
       subtitle: Text(description),
       onTap: () {
-        Navigator.pushNamed(context, '/destination');
+        // Navigator.pushNamed(context, '/destination');
       },
     ),
   );
diff --git a/lib/modules/overview.dart b/lib/modules/overview.dart
index 7d6c0ed..ff03b9b 100644
--- a/lib/modules/overview.dart
+++ b/lib/modules/overview.dart
@@ -1,14 +1,74 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_map/flutter_map.dart';
 import 'package:sliding_up_panel/sliding_up_panel.dart';
+import 'package:latlong2/latlong.dart';
+import 'package:geocoding/geocoding.dart';
+import 'package:geocode/geocode.dart';
+import 'dart:async';
+
 import 'package:fast_network_navigation/modules/navigation.dart';
 
 class MapPage extends StatefulWidget {
   @override
   _MapPageState createState() => _MapPageState();
 }
+class Debounce {
+  Duration delay;
+  Timer? _timer;
+
+  Debounce(
+    this.delay,
+  );
+
+  call(void Function() callback) {
+    _timer?.cancel();
+    _timer = Timer(delay, callback);
+  }
+
+  dispose() {
+    _timer?.cancel();
+  }
+}
+
 
 class _MapPageState extends State<MapPage> {
+  GeoCode geoCode = GeoCode();
+  final mapController = MapController();
+  String _currentCityName = "...";
+  final Debounce _debounce = Debounce(Duration(seconds: 3));
+
+  void _setCurrentCityName() async {
+    if (mapController.camera.zoom < 9) {
+      return; // Don't bother if the view is too wide
+    }
+    var currentCoordinates = mapController.camera.center;
+    String? city;
+    
+    try{
+      List<Placemark> placemarks = await placemarkFromCoordinates(currentCoordinates.latitude, currentCoordinates.longitude);
+      city = placemarks[0].locality.toString();
+    } catch (e) {
+      debugPrint("Error: $e");
+      try {
+        Address address = await geoCode.reverseGeocoding(latitude: currentCoordinates.latitude, longitude: currentCoordinates.longitude);
+        
+        if (address.city == null || address.city.toString().contains("Throttled!")){
+          throw Exception("Probably rate limited");
+        }
+        city = address.city.toString();
+      } catch (e) {
+        debugPrint("Error: $e");
+
+      }
+    }
+    if (city != null) {
+      setState(() {
+        _currentCityName = city!;
+      });
+    } else {
+      _debounce(() async {_setCurrentCityName();});
+    }
+  }
 
   @override
   Widget build(BuildContext context) {
@@ -18,8 +78,15 @@ class _MapPageState extends State<MapPage> {
         panel: _floatingPanel(theme),
         collapsed: _floatingCollapsed(theme),
         body: FlutterMap(
+          mapController: mapController,
           options: MapOptions(
             initialZoom: 11,
+            initialCenter: LatLng(51.509364, -0.128928),
+            onMapReady: () {
+                mapController.mapEventStream.listen((evt) {_debounce(() async {_setCurrentCityName();});});
+                // And any other `MapController` dependent non-movement methods
+            },
+
           ),
           children: [
             openStreetMapTileLayer,
@@ -59,14 +126,7 @@ class _MapPageState extends State<MapPage> {
             children: <Widget>[
               Greeting(theme),
               Text("Got a lot to do today! Here is a rundown:"),
-              singleDestination(context, "Location 1", "some description", "Further information"),
-              singleDestination(context, "Location 2", "some description", "Further information"),
-              singleDestination(context, "Location 3", "some description", "Further information"),
-              singleDestination(context, "Location 4", "some description", "Further information"),
-              singleDestination(context, "Location 5", "some description", "Further information"),
-              singleDestination(context, "Location 6", "some description", "Further information"),
-              singleDestination(context, "Location 7", "some description", "Further information"),
-              singleDestination(context, "Location 8", "some description", "Further information"),
+              ...loadDestinations(),
             ],
           ),
         ),
@@ -78,7 +138,7 @@ class _MapPageState extends State<MapPage> {
   Widget Greeting (ThemeData theme) {
     return Center(
         child: Text(
-          "Explore Kview",
+            "Explore ${_currentCityName}",
           style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
         ),
       );
diff --git a/pubspec.lock b/pubspec.lock
index e8a2cb6..c5548c5 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -83,6 +83,46 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  geocode:
+    dependency: "direct main"
+    description:
+      name: geocode
+      sha256: cf9727c369bb3703b97d6e440225962dc27b7f3c686662fe3cdcc91cbfb7074d
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.3"
+  geocoding:
+    dependency: "direct main"
+    description:
+      name: geocoding
+      sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
+  geocoding_android:
+    dependency: transitive
+    description:
+      name: geocoding_android
+      sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.1"
+  geocoding_ios:
+    dependency: transitive
+    description:
+      name: geocoding_ios
+      sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.1"
+  geocoding_platform_interface:
+    dependency: transitive
+    description:
+      name: geocoding_platform_interface
+      sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.0"
   http:
     dependency: transitive
     description:
@@ -108,7 +148,7 @@ packages:
     source: hosted
     version: "0.19.0"
   latlong2:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: latlong2
       sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
@@ -203,6 +243,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.9.0"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.8"
   polylabel:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 507aee5..53add6b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
-name: fast_network_navigation
-description: "A new Flutter project."
+name: "fast_network_navigation"
+description: "An interactive city navigator."
 # The following line prevents the package from being accidentally published to
 # pub.dev using `flutter pub publish`. This is preferred for private packages.
 publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@@ -37,6 +37,9 @@ dependencies:
   cupertino_icons: ^1.0.6
   flutter_map: ^6.1.0
   sliding_up_panel: ^2.0.0+1
+  latlong2: ^0.9.1
+  geocoding: ^3.0.0
+  geocode: ^1.0.3
 
 dev_dependencies:
   flutter_test:
-- 
2.47.2


From 780bf0296484798a3924c904471b38ab6ac684fe Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 16 May 2024 17:00:10 +0200
Subject: [PATCH 03/33] more evolved mockup

---
 .gitea/workflows/build-android.yaml |  2 +-
 lib/modules/destination_card.dart   | 40 +++++++++++++++++++++++++++++
 lib/modules/navigation.dart         |  2 +-
 lib/structs/destination.dart        | 31 ++++++++++++++++++++++
 pubspec.lock                        | 26 +++++++++----------
 5 files changed, 86 insertions(+), 15 deletions(-)
 create mode 100644 lib/modules/destination_card.dart
 create mode 100644 lib/structs/destination.dart

diff --git a/.gitea/workflows/build-android.yaml b/.gitea/workflows/build-android.yaml
index e2e530a..b714b6c 100644
--- a/.gitea/workflows/build-android.yaml
+++ b/.gitea/workflows/build-android.yaml
@@ -34,7 +34,7 @@ jobs:
 
     - run: flutter pub get
 
-    - run: flutter build apk --release --split-per-abi
+    - run: flutter build apk --debug --split-per-abi
 
     - name: Release APK
       uses: https://gitea.com/akkuman/gitea-release-action@v1
diff --git a/lib/modules/destination_card.dart b/lib/modules/destination_card.dart
new file mode 100644
index 0000000..5829c5e
--- /dev/null
+++ b/lib/modules/destination_card.dart
@@ -0,0 +1,40 @@
+
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+
+class DestinationCard extends StatefulWidget {
+  final String title;
+  final String description;
+  final String image;
+  bool visited;
+
+  @override
+  _DestinationCardState createState() => _DestinationCardState();
+
+
+  DestinationCard(this.title, this.description, this.image, this.visited);
+
+  Widget build() {
+    return Card(
+      child: ListTile(
+        leading: Icon(Icons.location_on),
+        title: Text(title),
+        subtitle: Text(description),
+        onTap: () {
+          // Navigator.pushNamed(context, '/destination');
+        },
+      ),
+    );
+  }
+  
+}
+
+class _DestinationCardState extends State<DestinationCard> {
+  @override
+  Widget build(BuildContext context) {
+    return Card();
+  }
+
+}
\ No newline at end of file
diff --git a/lib/modules/navigation.dart b/lib/modules/navigation.dart
index 7952560..7bd62ac 100644
--- a/lib/modules/navigation.dart
+++ b/lib/modules/navigation.dart
@@ -1,7 +1,7 @@
+import 'package:fast_network_navigation/modules/destination_card.dart';
 import 'package:flutter/material.dart';
 
 
-
 List<Widget> loadDestinations() {
   List<Widget> cities =  [
     singleDestination(
diff --git a/lib/structs/destination.dart b/lib/structs/destination.dart
new file mode 100644
index 0000000..bb8d086
--- /dev/null
+++ b/lib/structs/destination.dart
@@ -0,0 +1,31 @@
+
+class Destination {
+  final double latitude;
+  final double longitude;
+  final String name;
+  final String description;
+  final DestinationType type;
+  final Duration duration;
+  final bool visited;
+
+  Destination({
+    required this.latitude,
+    required this.longitude,
+    required this.name,
+    required this.description,
+    required this.type,
+    required this.duration,
+    required this.visited,
+  });
+}
+
+
+class DestinationType {
+  final String name;
+  final String description;
+  
+  DestinationType({
+    required this.name,
+    required this.description,
+  });
+}
diff --git a/pubspec.lock b/pubspec.lock
index c5548c5..b88a8ef 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -159,26 +159,26 @@ packages:
     dependency: transitive
     description:
       name: leak_tracker
-      sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
+      sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
       url: "https://pub.dev"
     source: hosted
-    version: "10.0.0"
+    version: "10.0.4"
   leak_tracker_flutter_testing:
     dependency: transitive
     description:
       name: leak_tracker_flutter_testing
-      sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
+      sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "3.0.3"
   leak_tracker_testing:
     dependency: transitive
     description:
       name: leak_tracker_testing
-      sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
+      sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "3.0.1"
   lints:
     dependency: transitive
     description:
@@ -223,10 +223,10 @@ packages:
     dependency: transitive
     description:
       name: meta
-      sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+      sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
       url: "https://pub.dev"
     source: hosted
-    version: "1.11.0"
+    version: "1.12.0"
   mgrs_dart:
     dependency: transitive
     description:
@@ -324,10 +324,10 @@ packages:
     dependency: transitive
     description:
       name: test_api
-      sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
+      sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
       url: "https://pub.dev"
     source: hosted
-    version: "0.6.1"
+    version: "0.7.0"
   typed_data:
     dependency: transitive
     description:
@@ -356,10 +356,10 @@ packages:
     dependency: transitive
     description:
       name: vm_service
-      sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
+      sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
       url: "https://pub.dev"
     source: hosted
-    version: "13.0.0"
+    version: "14.2.1"
   web:
     dependency: transitive
     description:
@@ -378,4 +378,4 @@ packages:
     version: "2.0.0"
 sdks:
   dart: ">=3.3.4 <4.0.0"
-  flutter: ">=3.10.0"
+  flutter: ">=3.18.0-18.0.pre.54"
-- 
2.47.2


From 713e4a6671d72231b8f110828c252a578f6458f3 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 16 May 2024 17:08:18 +0200
Subject: [PATCH 04/33] split into frontend and backend

---
 ...ild-android.yaml => frontend_build-android.yaml} |   6 +++++-
 .../{build-web.yaml => frontend_build-web.yaml}     |   4 +++-
 .gitea/workflows/{test.yaml => frontend_test.yaml}  |   2 ++
 .gitignore => frontend/.gitignore                   |   0
 .metadata => frontend/.metadata                     |   0
 .../analysis_options.yaml                           |   0
 {android => frontend/android}/.gitignore            |   0
 {android => frontend/android}/app/build.gradle      |   0
 .../android}/app/src/debug/AndroidManifest.xml      |   0
 .../android}/app/src/main/AndroidManifest.xml       |   0
 .../example/fast_network_navigation/MainActivity.kt |   0
 .../src/main/res/drawable-v21/launch_background.xml |   0
 .../app/src/main/res/drawable/launch_background.xml |   0
 .../app/src/main/res/mipmap-hdpi/ic_launcher.png    | Bin
 .../app/src/main/res/mipmap-mdpi/ic_launcher.png    | Bin
 .../app/src/main/res/mipmap-xhdpi/ic_launcher.png   | Bin
 .../app/src/main/res/mipmap-xxhdpi/ic_launcher.png  | Bin
 .../app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin
 .../app/src/main/res/values-night/styles.xml        |   0
 .../android}/app/src/main/res/values/styles.xml     |   0
 .../android}/app/src/profile/AndroidManifest.xml    |   0
 {android => frontend/android}/build.gradle          |   0
 {android => frontend/android}/gradle.properties     |   0
 .../gradle/wrapper/gradle-wrapper.properties        |   0
 {android => frontend/android}/settings.gradle       |   0
 {ios => frontend/ios}/.gitignore                    |   0
 .../ios}/Flutter/AppFrameworkInfo.plist             |   0
 {ios => frontend/ios}/Flutter/Debug.xcconfig        |   0
 {ios => frontend/ios}/Flutter/Release.xcconfig      |   0
 .../ios}/Runner.xcodeproj/project.pbxproj           |   0
 .../project.xcworkspace/contents.xcworkspacedata    |   0
 .../xcshareddata/IDEWorkspaceChecks.plist           |   0
 .../xcshareddata/WorkspaceSettings.xcsettings       |   0
 .../xcshareddata/xcschemes/Runner.xcscheme          |   0
 .../Runner.xcworkspace/contents.xcworkspacedata     |   0
 .../xcshareddata/IDEWorkspaceChecks.plist           |   0
 .../xcshareddata/WorkspaceSettings.xcsettings       |   0
 {ios => frontend/ios}/Runner/AppDelegate.swift      |   0
 .../AppIcon.appiconset/Contents.json                |   0
 .../AppIcon.appiconset/Icon-App-1024x1024@1x.png    | Bin
 .../AppIcon.appiconset/Icon-App-20x20@1x.png        | Bin
 .../AppIcon.appiconset/Icon-App-20x20@2x.png        | Bin
 .../AppIcon.appiconset/Icon-App-20x20@3x.png        | Bin
 .../AppIcon.appiconset/Icon-App-29x29@1x.png        | Bin
 .../AppIcon.appiconset/Icon-App-29x29@2x.png        | Bin
 .../AppIcon.appiconset/Icon-App-29x29@3x.png        | Bin
 .../AppIcon.appiconset/Icon-App-40x40@1x.png        | Bin
 .../AppIcon.appiconset/Icon-App-40x40@2x.png        | Bin
 .../AppIcon.appiconset/Icon-App-40x40@3x.png        | Bin
 .../AppIcon.appiconset/Icon-App-60x60@2x.png        | Bin
 .../AppIcon.appiconset/Icon-App-60x60@3x.png        | Bin
 .../AppIcon.appiconset/Icon-App-76x76@1x.png        | Bin
 .../AppIcon.appiconset/Icon-App-76x76@2x.png        | Bin
 .../AppIcon.appiconset/Icon-App-83.5x83.5@2x.png    | Bin
 .../LaunchImage.imageset/Contents.json              |   0
 .../LaunchImage.imageset/LaunchImage.png            | Bin
 .../LaunchImage.imageset/LaunchImage@2x.png         | Bin
 .../LaunchImage.imageset/LaunchImage@3x.png         | Bin
 .../Assets.xcassets/LaunchImage.imageset/README.md  |   0
 .../ios}/Runner/Base.lproj/LaunchScreen.storyboard  |   0
 .../ios}/Runner/Base.lproj/Main.storyboard          |   0
 {ios => frontend/ios}/Runner/Info.plist             |   0
 .../ios}/Runner/Runner-Bridging-Header.h            |   0
 {ios => frontend/ios}/RunnerTests/RunnerTests.swift |   0
 {lib => frontend/lib}/main.dart                     |   0
 {lib => frontend/lib}/modules/destination_card.dart |   0
 {lib => frontend/lib}/modules/navigation.dart       |   0
 {lib => frontend/lib}/modules/overview.dart         |   0
 {lib => frontend/lib}/modules/profile.dart          |   0
 {lib => frontend/lib}/modules/scaffold.dart         |   0
 {lib => frontend/lib}/structs/destination.dart      |   0
 {linux => frontend/linux}/.gitignore                |   0
 {linux => frontend/linux}/CMakeLists.txt            |   0
 {linux => frontend/linux}/flutter/CMakeLists.txt    |   0
 .../linux}/flutter/generated_plugin_registrant.cc   |   0
 .../linux}/flutter/generated_plugin_registrant.h    |   0
 .../linux}/flutter/generated_plugins.cmake          |   0
 {linux => frontend/linux}/main.cc                   |   0
 {linux => frontend/linux}/my_application.cc         |   0
 {linux => frontend/linux}/my_application.h          |   0
 {macos => frontend/macos}/.gitignore                |   0
 .../macos}/Flutter/Flutter-Debug.xcconfig           |   0
 .../macos}/Flutter/Flutter-Release.xcconfig         |   0
 .../macos}/Flutter/GeneratedPluginRegistrant.swift  |   0
 .../macos}/Runner.xcodeproj/project.pbxproj         |   0
 .../xcshareddata/IDEWorkspaceChecks.plist           |   0
 .../xcshareddata/xcschemes/Runner.xcscheme          |   0
 .../Runner.xcworkspace/contents.xcworkspacedata     |   0
 .../xcshareddata/IDEWorkspaceChecks.plist           |   0
 {macos => frontend/macos}/Runner/AppDelegate.swift  |   0
 .../AppIcon.appiconset/Contents.json                |   0
 .../AppIcon.appiconset/app_icon_1024.png            | Bin
 .../AppIcon.appiconset/app_icon_128.png             | Bin
 .../AppIcon.appiconset/app_icon_16.png              | Bin
 .../AppIcon.appiconset/app_icon_256.png             | Bin
 .../AppIcon.appiconset/app_icon_32.png              | Bin
 .../AppIcon.appiconset/app_icon_512.png             | Bin
 .../AppIcon.appiconset/app_icon_64.png              | Bin
 .../macos}/Runner/Base.lproj/MainMenu.xib           |   0
 .../macos}/Runner/Configs/AppInfo.xcconfig          |   0
 .../macos}/Runner/Configs/Debug.xcconfig            |   0
 .../macos}/Runner/Configs/Release.xcconfig          |   0
 .../macos}/Runner/Configs/Warnings.xcconfig         |   0
 .../macos}/Runner/DebugProfile.entitlements         |   0
 {macos => frontend/macos}/Runner/Info.plist         |   0
 .../macos}/Runner/MainFlutterWindow.swift           |   0
 .../macos}/Runner/Release.entitlements              |   0
 .../macos}/RunnerTests/RunnerTests.swift            |   0
 pubspec.lock => frontend/pubspec.lock               |   0
 pubspec.yaml => frontend/pubspec.yaml               |   0
 {test => frontend/test}/widget_test.dart            |   0
 {web => frontend/web}/favicon.png                   | Bin
 {web => frontend/web}/icons/Icon-192.png            | Bin
 {web => frontend/web}/icons/Icon-512.png            | Bin
 {web => frontend/web}/icons/Icon-maskable-192.png   | Bin
 {web => frontend/web}/icons/Icon-maskable-512.png   | Bin
 {web => frontend/web}/index.html                    |   0
 {web => frontend/web}/manifest.json                 |   0
 {windows => frontend/windows}/.gitignore            |   0
 {windows => frontend/windows}/CMakeLists.txt        |   0
 .../windows}/flutter/CMakeLists.txt                 |   0
 .../windows}/flutter/generated_plugin_registrant.cc |   0
 .../windows}/flutter/generated_plugin_registrant.h  |   0
 .../windows}/flutter/generated_plugins.cmake        |   0
 {windows => frontend/windows}/runner/CMakeLists.txt |   0
 {windows => frontend/windows}/runner/Runner.rc      |   0
 .../windows}/runner/flutter_window.cpp              |   0
 .../windows}/runner/flutter_window.h                |   0
 {windows => frontend/windows}/runner/main.cpp       |   0
 {windows => frontend/windows}/runner/resource.h     |   0
 .../windows}/runner/resources/app_icon.ico          | Bin
 .../windows}/runner/runner.exe.manifest             |   0
 {windows => frontend/windows}/runner/utils.cpp      |   0
 {windows => frontend/windows}/runner/utils.h        |   0
 .../windows}/runner/win32_window.cpp                |   0
 {windows => frontend/windows}/runner/win32_window.h |   0
 136 files changed, 10 insertions(+), 2 deletions(-)
 rename .gitea/workflows/{build-android.yaml => frontend_build-android.yaml} (88%)
 rename .gitea/workflows/{build-web.yaml => frontend_build-web.yaml} (87%)
 rename .gitea/workflows/{test.yaml => frontend_test.yaml} (87%)
 rename .gitignore => frontend/.gitignore (100%)
 rename .metadata => frontend/.metadata (100%)
 rename analysis_options.yaml => frontend/analysis_options.yaml (100%)
 rename {android => frontend/android}/.gitignore (100%)
 rename {android => frontend/android}/app/build.gradle (100%)
 rename {android => frontend/android}/app/src/debug/AndroidManifest.xml (100%)
 rename {android => frontend/android}/app/src/main/AndroidManifest.xml (100%)
 rename {android => frontend/android}/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt (100%)
 rename {android => frontend/android}/app/src/main/res/drawable-v21/launch_background.xml (100%)
 rename {android => frontend/android}/app/src/main/res/drawable/launch_background.xml (100%)
 rename {android => frontend/android}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%)
 rename {android => frontend/android}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%)
 rename {android => frontend/android}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%)
 rename {android => frontend/android}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%)
 rename {android => frontend/android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%)
 rename {android => frontend/android}/app/src/main/res/values-night/styles.xml (100%)
 rename {android => frontend/android}/app/src/main/res/values/styles.xml (100%)
 rename {android => frontend/android}/app/src/profile/AndroidManifest.xml (100%)
 rename {android => frontend/android}/build.gradle (100%)
 rename {android => frontend/android}/gradle.properties (100%)
 rename {android => frontend/android}/gradle/wrapper/gradle-wrapper.properties (100%)
 rename {android => frontend/android}/settings.gradle (100%)
 rename {ios => frontend/ios}/.gitignore (100%)
 rename {ios => frontend/ios}/Flutter/AppFrameworkInfo.plist (100%)
 rename {ios => frontend/ios}/Flutter/Debug.xcconfig (100%)
 rename {ios => frontend/ios}/Flutter/Release.xcconfig (100%)
 rename {ios => frontend/ios}/Runner.xcodeproj/project.pbxproj (100%)
 rename {ios => frontend/ios}/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%)
 rename {ios => frontend/ios}/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%)
 rename {ios => frontend/ios}/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%)
 rename {ios => frontend/ios}/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%)
 rename {ios => frontend/ios}/Runner.xcworkspace/contents.xcworkspacedata (100%)
 rename {ios => frontend/ios}/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%)
 rename {ios => frontend/ios}/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%)
 rename {ios => frontend/ios}/Runner/AppDelegate.swift (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%)
 rename {ios => frontend/ios}/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%)
 rename {ios => frontend/ios}/Runner/Base.lproj/LaunchScreen.storyboard (100%)
 rename {ios => frontend/ios}/Runner/Base.lproj/Main.storyboard (100%)
 rename {ios => frontend/ios}/Runner/Info.plist (100%)
 rename {ios => frontend/ios}/Runner/Runner-Bridging-Header.h (100%)
 rename {ios => frontend/ios}/RunnerTests/RunnerTests.swift (100%)
 rename {lib => frontend/lib}/main.dart (100%)
 rename {lib => frontend/lib}/modules/destination_card.dart (100%)
 rename {lib => frontend/lib}/modules/navigation.dart (100%)
 rename {lib => frontend/lib}/modules/overview.dart (100%)
 rename {lib => frontend/lib}/modules/profile.dart (100%)
 rename {lib => frontend/lib}/modules/scaffold.dart (100%)
 rename {lib => frontend/lib}/structs/destination.dart (100%)
 rename {linux => frontend/linux}/.gitignore (100%)
 rename {linux => frontend/linux}/CMakeLists.txt (100%)
 rename {linux => frontend/linux}/flutter/CMakeLists.txt (100%)
 rename {linux => frontend/linux}/flutter/generated_plugin_registrant.cc (100%)
 rename {linux => frontend/linux}/flutter/generated_plugin_registrant.h (100%)
 rename {linux => frontend/linux}/flutter/generated_plugins.cmake (100%)
 rename {linux => frontend/linux}/main.cc (100%)
 rename {linux => frontend/linux}/my_application.cc (100%)
 rename {linux => frontend/linux}/my_application.h (100%)
 rename {macos => frontend/macos}/.gitignore (100%)
 rename {macos => frontend/macos}/Flutter/Flutter-Debug.xcconfig (100%)
 rename {macos => frontend/macos}/Flutter/Flutter-Release.xcconfig (100%)
 rename {macos => frontend/macos}/Flutter/GeneratedPluginRegistrant.swift (100%)
 rename {macos => frontend/macos}/Runner.xcodeproj/project.pbxproj (100%)
 rename {macos => frontend/macos}/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%)
 rename {macos => frontend/macos}/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%)
 rename {macos => frontend/macos}/Runner.xcworkspace/contents.xcworkspacedata (100%)
 rename {macos => frontend/macos}/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%)
 rename {macos => frontend/macos}/Runner/AppDelegate.swift (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%)
 rename {macos => frontend/macos}/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%)
 rename {macos => frontend/macos}/Runner/Base.lproj/MainMenu.xib (100%)
 rename {macos => frontend/macos}/Runner/Configs/AppInfo.xcconfig (100%)
 rename {macos => frontend/macos}/Runner/Configs/Debug.xcconfig (100%)
 rename {macos => frontend/macos}/Runner/Configs/Release.xcconfig (100%)
 rename {macos => frontend/macos}/Runner/Configs/Warnings.xcconfig (100%)
 rename {macos => frontend/macos}/Runner/DebugProfile.entitlements (100%)
 rename {macos => frontend/macos}/Runner/Info.plist (100%)
 rename {macos => frontend/macos}/Runner/MainFlutterWindow.swift (100%)
 rename {macos => frontend/macos}/Runner/Release.entitlements (100%)
 rename {macos => frontend/macos}/RunnerTests/RunnerTests.swift (100%)
 rename pubspec.lock => frontend/pubspec.lock (100%)
 rename pubspec.yaml => frontend/pubspec.yaml (100%)
 rename {test => frontend/test}/widget_test.dart (100%)
 rename {web => frontend/web}/favicon.png (100%)
 rename {web => frontend/web}/icons/Icon-192.png (100%)
 rename {web => frontend/web}/icons/Icon-512.png (100%)
 rename {web => frontend/web}/icons/Icon-maskable-192.png (100%)
 rename {web => frontend/web}/icons/Icon-maskable-512.png (100%)
 rename {web => frontend/web}/index.html (100%)
 rename {web => frontend/web}/manifest.json (100%)
 rename {windows => frontend/windows}/.gitignore (100%)
 rename {windows => frontend/windows}/CMakeLists.txt (100%)
 rename {windows => frontend/windows}/flutter/CMakeLists.txt (100%)
 rename {windows => frontend/windows}/flutter/generated_plugin_registrant.cc (100%)
 rename {windows => frontend/windows}/flutter/generated_plugin_registrant.h (100%)
 rename {windows => frontend/windows}/flutter/generated_plugins.cmake (100%)
 rename {windows => frontend/windows}/runner/CMakeLists.txt (100%)
 rename {windows => frontend/windows}/runner/Runner.rc (100%)
 rename {windows => frontend/windows}/runner/flutter_window.cpp (100%)
 rename {windows => frontend/windows}/runner/flutter_window.h (100%)
 rename {windows => frontend/windows}/runner/main.cpp (100%)
 rename {windows => frontend/windows}/runner/resource.h (100%)
 rename {windows => frontend/windows}/runner/resources/app_icon.ico (100%)
 rename {windows => frontend/windows}/runner/runner.exe.manifest (100%)
 rename {windows => frontend/windows}/runner/utils.cpp (100%)
 rename {windows => frontend/windows}/runner/utils.h (100%)
 rename {windows => frontend/windows}/runner/win32_window.cpp (100%)
 rename {windows => frontend/windows}/runner/win32_window.h (100%)

diff --git a/.gitea/workflows/build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
similarity index 88%
rename from .gitea/workflows/build-android.yaml
rename to .gitea/workflows/frontend_build-android.yaml
index b714b6c..de430ce 100644
--- a/.gitea/workflows/build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -33,13 +33,16 @@ jobs:
       uses: https://github.com/android-actions/setup-android@v3
 
     - run: flutter pub get
+      working-directory: ./frontend
 
     - run: flutter build apk --debug --split-per-abi
+      working-directory: ./frontend
+
 
     - name: Release APK
       uses: https://gitea.com/akkuman/gitea-release-action@v1
       with:
-        files: build/app/outputs/flutter-apk/*.apk
+        files: ./frontendbuild/app/outputs/flutter-apk/*.apk
         name: Testing release
         release_name: testing
         tag: testing
@@ -49,3 +52,4 @@ jobs:
         token: ${{ secrets.GITEA_TOKEN }}
       env:
         NODE_OPTIONS: '--experimental-fetch'
+      
diff --git a/.gitea/workflows/build-web.yaml b/.gitea/workflows/frontend_build-web.yaml
similarity index 87%
rename from .gitea/workflows/build-web.yaml
rename to .gitea/workflows/frontend_build-web.yaml
index cdb817c..416754a 100644
--- a/.gitea/workflows/build-web.yaml
+++ b/.gitea/workflows/frontend_build-web.yaml
@@ -25,6 +25,8 @@ jobs:
         cache: true
 
     - run: flutter pub get
+      working-directory: ./frontend
+
 
     - run: flutter build web
-
+      working-directory: ./frontend
diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/frontend_test.yaml
similarity index 87%
rename from .gitea/workflows/test.yaml
rename to .gitea/workflows/frontend_test.yaml
index 7f22525..aee253d 100644
--- a/.gitea/workflows/test.yaml
+++ b/.gitea/workflows/frontend_test.yaml
@@ -27,5 +27,7 @@ jobs:
         cache: true
 
     - run: flutter pub get
+      working-directory: ./frontend
 
     - run: flutter test
+      working-directory: ./frontend
diff --git a/.gitignore b/frontend/.gitignore
similarity index 100%
rename from .gitignore
rename to frontend/.gitignore
diff --git a/.metadata b/frontend/.metadata
similarity index 100%
rename from .metadata
rename to frontend/.metadata
diff --git a/analysis_options.yaml b/frontend/analysis_options.yaml
similarity index 100%
rename from analysis_options.yaml
rename to frontend/analysis_options.yaml
diff --git a/android/.gitignore b/frontend/android/.gitignore
similarity index 100%
rename from android/.gitignore
rename to frontend/android/.gitignore
diff --git a/android/app/build.gradle b/frontend/android/app/build.gradle
similarity index 100%
rename from android/app/build.gradle
rename to frontend/android/app/build.gradle
diff --git a/android/app/src/debug/AndroidManifest.xml b/frontend/android/app/src/debug/AndroidManifest.xml
similarity index 100%
rename from android/app/src/debug/AndroidManifest.xml
rename to frontend/android/app/src/debug/AndroidManifest.xml
diff --git a/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
similarity index 100%
rename from android/app/src/main/AndroidManifest.xml
rename to frontend/android/app/src/main/AndroidManifest.xml
diff --git a/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt b/frontend/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt
similarity index 100%
rename from android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt
rename to frontend/android/app/src/main/kotlin/com/example/fast_network_navigation/MainActivity.kt
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml
similarity index 100%
rename from android/app/src/main/res/drawable-v21/launch_background.xml
rename to frontend/android/app/src/main/res/drawable-v21/launch_background.xml
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/frontend/android/app/src/main/res/drawable/launch_background.xml
similarity index 100%
rename from android/app/src/main/res/drawable/launch_background.xml
rename to frontend/android/app/src/main/res/drawable/launch_background.xml
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png
rename to frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png
rename to frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
rename to frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
rename to frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
rename to frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/android/app/src/main/res/values-night/styles.xml b/frontend/android/app/src/main/res/values-night/styles.xml
similarity index 100%
rename from android/app/src/main/res/values-night/styles.xml
rename to frontend/android/app/src/main/res/values-night/styles.xml
diff --git a/android/app/src/main/res/values/styles.xml b/frontend/android/app/src/main/res/values/styles.xml
similarity index 100%
rename from android/app/src/main/res/values/styles.xml
rename to frontend/android/app/src/main/res/values/styles.xml
diff --git a/android/app/src/profile/AndroidManifest.xml b/frontend/android/app/src/profile/AndroidManifest.xml
similarity index 100%
rename from android/app/src/profile/AndroidManifest.xml
rename to frontend/android/app/src/profile/AndroidManifest.xml
diff --git a/android/build.gradle b/frontend/android/build.gradle
similarity index 100%
rename from android/build.gradle
rename to frontend/android/build.gradle
diff --git a/android/gradle.properties b/frontend/android/gradle.properties
similarity index 100%
rename from android/gradle.properties
rename to frontend/android/gradle.properties
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/frontend/android/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from android/gradle/wrapper/gradle-wrapper.properties
rename to frontend/android/gradle/wrapper/gradle-wrapper.properties
diff --git a/android/settings.gradle b/frontend/android/settings.gradle
similarity index 100%
rename from android/settings.gradle
rename to frontend/android/settings.gradle
diff --git a/ios/.gitignore b/frontend/ios/.gitignore
similarity index 100%
rename from ios/.gitignore
rename to frontend/ios/.gitignore
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/frontend/ios/Flutter/AppFrameworkInfo.plist
similarity index 100%
rename from ios/Flutter/AppFrameworkInfo.plist
rename to frontend/ios/Flutter/AppFrameworkInfo.plist
diff --git a/ios/Flutter/Debug.xcconfig b/frontend/ios/Flutter/Debug.xcconfig
similarity index 100%
rename from ios/Flutter/Debug.xcconfig
rename to frontend/ios/Flutter/Debug.xcconfig
diff --git a/ios/Flutter/Release.xcconfig b/frontend/ios/Flutter/Release.xcconfig
similarity index 100%
rename from ios/Flutter/Release.xcconfig
rename to frontend/ios/Flutter/Release.xcconfig
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/frontend/ios/Runner.xcodeproj/project.pbxproj
similarity index 100%
rename from ios/Runner.xcodeproj/project.pbxproj
rename to frontend/ios/Runner.xcodeproj/project.pbxproj
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename to frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
similarity index 100%
rename from ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
rename to frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
similarity index 100%
rename from ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
rename to frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
similarity index 100%
rename from ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
rename to frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from ios/Runner.xcworkspace/contents.xcworkspacedata
rename to frontend/ios/Runner.xcworkspace/contents.xcworkspacedata
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
similarity index 100%
rename from ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
rename to frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
similarity index 100%
rename from ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
rename to frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
diff --git a/ios/Runner/AppDelegate.swift b/frontend/ios/Runner/AppDelegate.swift
similarity index 100%
rename from ios/Runner/AppDelegate.swift
rename to frontend/ios/Runner/AppDelegate.swift
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
rename to frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
similarity index 100%
rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
rename to frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
rename to frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
rename to frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
similarity index 100%
rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
rename to frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
similarity index 100%
rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
rename to frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard
similarity index 100%
rename from ios/Runner/Base.lproj/LaunchScreen.storyboard
rename to frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/frontend/ios/Runner/Base.lproj/Main.storyboard
similarity index 100%
rename from ios/Runner/Base.lproj/Main.storyboard
rename to frontend/ios/Runner/Base.lproj/Main.storyboard
diff --git a/ios/Runner/Info.plist b/frontend/ios/Runner/Info.plist
similarity index 100%
rename from ios/Runner/Info.plist
rename to frontend/ios/Runner/Info.plist
diff --git a/ios/Runner/Runner-Bridging-Header.h b/frontend/ios/Runner/Runner-Bridging-Header.h
similarity index 100%
rename from ios/Runner/Runner-Bridging-Header.h
rename to frontend/ios/Runner/Runner-Bridging-Header.h
diff --git a/ios/RunnerTests/RunnerTests.swift b/frontend/ios/RunnerTests/RunnerTests.swift
similarity index 100%
rename from ios/RunnerTests/RunnerTests.swift
rename to frontend/ios/RunnerTests/RunnerTests.swift
diff --git a/lib/main.dart b/frontend/lib/main.dart
similarity index 100%
rename from lib/main.dart
rename to frontend/lib/main.dart
diff --git a/lib/modules/destination_card.dart b/frontend/lib/modules/destination_card.dart
similarity index 100%
rename from lib/modules/destination_card.dart
rename to frontend/lib/modules/destination_card.dart
diff --git a/lib/modules/navigation.dart b/frontend/lib/modules/navigation.dart
similarity index 100%
rename from lib/modules/navigation.dart
rename to frontend/lib/modules/navigation.dart
diff --git a/lib/modules/overview.dart b/frontend/lib/modules/overview.dart
similarity index 100%
rename from lib/modules/overview.dart
rename to frontend/lib/modules/overview.dart
diff --git a/lib/modules/profile.dart b/frontend/lib/modules/profile.dart
similarity index 100%
rename from lib/modules/profile.dart
rename to frontend/lib/modules/profile.dart
diff --git a/lib/modules/scaffold.dart b/frontend/lib/modules/scaffold.dart
similarity index 100%
rename from lib/modules/scaffold.dart
rename to frontend/lib/modules/scaffold.dart
diff --git a/lib/structs/destination.dart b/frontend/lib/structs/destination.dart
similarity index 100%
rename from lib/structs/destination.dart
rename to frontend/lib/structs/destination.dart
diff --git a/linux/.gitignore b/frontend/linux/.gitignore
similarity index 100%
rename from linux/.gitignore
rename to frontend/linux/.gitignore
diff --git a/linux/CMakeLists.txt b/frontend/linux/CMakeLists.txt
similarity index 100%
rename from linux/CMakeLists.txt
rename to frontend/linux/CMakeLists.txt
diff --git a/linux/flutter/CMakeLists.txt b/frontend/linux/flutter/CMakeLists.txt
similarity index 100%
rename from linux/flutter/CMakeLists.txt
rename to frontend/linux/flutter/CMakeLists.txt
diff --git a/linux/flutter/generated_plugin_registrant.cc b/frontend/linux/flutter/generated_plugin_registrant.cc
similarity index 100%
rename from linux/flutter/generated_plugin_registrant.cc
rename to frontend/linux/flutter/generated_plugin_registrant.cc
diff --git a/linux/flutter/generated_plugin_registrant.h b/frontend/linux/flutter/generated_plugin_registrant.h
similarity index 100%
rename from linux/flutter/generated_plugin_registrant.h
rename to frontend/linux/flutter/generated_plugin_registrant.h
diff --git a/linux/flutter/generated_plugins.cmake b/frontend/linux/flutter/generated_plugins.cmake
similarity index 100%
rename from linux/flutter/generated_plugins.cmake
rename to frontend/linux/flutter/generated_plugins.cmake
diff --git a/linux/main.cc b/frontend/linux/main.cc
similarity index 100%
rename from linux/main.cc
rename to frontend/linux/main.cc
diff --git a/linux/my_application.cc b/frontend/linux/my_application.cc
similarity index 100%
rename from linux/my_application.cc
rename to frontend/linux/my_application.cc
diff --git a/linux/my_application.h b/frontend/linux/my_application.h
similarity index 100%
rename from linux/my_application.h
rename to frontend/linux/my_application.h
diff --git a/macos/.gitignore b/frontend/macos/.gitignore
similarity index 100%
rename from macos/.gitignore
rename to frontend/macos/.gitignore
diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/frontend/macos/Flutter/Flutter-Debug.xcconfig
similarity index 100%
rename from macos/Flutter/Flutter-Debug.xcconfig
rename to frontend/macos/Flutter/Flutter-Debug.xcconfig
diff --git a/macos/Flutter/Flutter-Release.xcconfig b/frontend/macos/Flutter/Flutter-Release.xcconfig
similarity index 100%
rename from macos/Flutter/Flutter-Release.xcconfig
rename to frontend/macos/Flutter/Flutter-Release.xcconfig
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
similarity index 100%
rename from macos/Flutter/GeneratedPluginRegistrant.swift
rename to frontend/macos/Flutter/GeneratedPluginRegistrant.swift
diff --git a/macos/Runner.xcodeproj/project.pbxproj b/frontend/macos/Runner.xcodeproj/project.pbxproj
similarity index 100%
rename from macos/Runner.xcodeproj/project.pbxproj
rename to frontend/macos/Runner.xcodeproj/project.pbxproj
diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
similarity index 100%
rename from macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
rename to frontend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
similarity index 100%
rename from macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
rename to frontend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/macos/Runner.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from macos/Runner.xcworkspace/contents.xcworkspacedata
rename to frontend/macos/Runner.xcworkspace/contents.xcworkspacedata
diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
similarity index 100%
rename from macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
rename to frontend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
diff --git a/macos/Runner/AppDelegate.swift b/frontend/macos/Runner/AppDelegate.swift
similarity index 100%
rename from macos/Runner/AppDelegate.swift
rename to frontend/macos/Runner/AppDelegate.swift
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
similarity index 100%
rename from macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
rename to frontend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/frontend/macos/Runner/Base.lproj/MainMenu.xib
similarity index 100%
rename from macos/Runner/Base.lproj/MainMenu.xib
rename to frontend/macos/Runner/Base.lproj/MainMenu.xib
diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/frontend/macos/Runner/Configs/AppInfo.xcconfig
similarity index 100%
rename from macos/Runner/Configs/AppInfo.xcconfig
rename to frontend/macos/Runner/Configs/AppInfo.xcconfig
diff --git a/macos/Runner/Configs/Debug.xcconfig b/frontend/macos/Runner/Configs/Debug.xcconfig
similarity index 100%
rename from macos/Runner/Configs/Debug.xcconfig
rename to frontend/macos/Runner/Configs/Debug.xcconfig
diff --git a/macos/Runner/Configs/Release.xcconfig b/frontend/macos/Runner/Configs/Release.xcconfig
similarity index 100%
rename from macos/Runner/Configs/Release.xcconfig
rename to frontend/macos/Runner/Configs/Release.xcconfig
diff --git a/macos/Runner/Configs/Warnings.xcconfig b/frontend/macos/Runner/Configs/Warnings.xcconfig
similarity index 100%
rename from macos/Runner/Configs/Warnings.xcconfig
rename to frontend/macos/Runner/Configs/Warnings.xcconfig
diff --git a/macos/Runner/DebugProfile.entitlements b/frontend/macos/Runner/DebugProfile.entitlements
similarity index 100%
rename from macos/Runner/DebugProfile.entitlements
rename to frontend/macos/Runner/DebugProfile.entitlements
diff --git a/macos/Runner/Info.plist b/frontend/macos/Runner/Info.plist
similarity index 100%
rename from macos/Runner/Info.plist
rename to frontend/macos/Runner/Info.plist
diff --git a/macos/Runner/MainFlutterWindow.swift b/frontend/macos/Runner/MainFlutterWindow.swift
similarity index 100%
rename from macos/Runner/MainFlutterWindow.swift
rename to frontend/macos/Runner/MainFlutterWindow.swift
diff --git a/macos/Runner/Release.entitlements b/frontend/macos/Runner/Release.entitlements
similarity index 100%
rename from macos/Runner/Release.entitlements
rename to frontend/macos/Runner/Release.entitlements
diff --git a/macos/RunnerTests/RunnerTests.swift b/frontend/macos/RunnerTests/RunnerTests.swift
similarity index 100%
rename from macos/RunnerTests/RunnerTests.swift
rename to frontend/macos/RunnerTests/RunnerTests.swift
diff --git a/pubspec.lock b/frontend/pubspec.lock
similarity index 100%
rename from pubspec.lock
rename to frontend/pubspec.lock
diff --git a/pubspec.yaml b/frontend/pubspec.yaml
similarity index 100%
rename from pubspec.yaml
rename to frontend/pubspec.yaml
diff --git a/test/widget_test.dart b/frontend/test/widget_test.dart
similarity index 100%
rename from test/widget_test.dart
rename to frontend/test/widget_test.dart
diff --git a/web/favicon.png b/frontend/web/favicon.png
similarity index 100%
rename from web/favicon.png
rename to frontend/web/favicon.png
diff --git a/web/icons/Icon-192.png b/frontend/web/icons/Icon-192.png
similarity index 100%
rename from web/icons/Icon-192.png
rename to frontend/web/icons/Icon-192.png
diff --git a/web/icons/Icon-512.png b/frontend/web/icons/Icon-512.png
similarity index 100%
rename from web/icons/Icon-512.png
rename to frontend/web/icons/Icon-512.png
diff --git a/web/icons/Icon-maskable-192.png b/frontend/web/icons/Icon-maskable-192.png
similarity index 100%
rename from web/icons/Icon-maskable-192.png
rename to frontend/web/icons/Icon-maskable-192.png
diff --git a/web/icons/Icon-maskable-512.png b/frontend/web/icons/Icon-maskable-512.png
similarity index 100%
rename from web/icons/Icon-maskable-512.png
rename to frontend/web/icons/Icon-maskable-512.png
diff --git a/web/index.html b/frontend/web/index.html
similarity index 100%
rename from web/index.html
rename to frontend/web/index.html
diff --git a/web/manifest.json b/frontend/web/manifest.json
similarity index 100%
rename from web/manifest.json
rename to frontend/web/manifest.json
diff --git a/windows/.gitignore b/frontend/windows/.gitignore
similarity index 100%
rename from windows/.gitignore
rename to frontend/windows/.gitignore
diff --git a/windows/CMakeLists.txt b/frontend/windows/CMakeLists.txt
similarity index 100%
rename from windows/CMakeLists.txt
rename to frontend/windows/CMakeLists.txt
diff --git a/windows/flutter/CMakeLists.txt b/frontend/windows/flutter/CMakeLists.txt
similarity index 100%
rename from windows/flutter/CMakeLists.txt
rename to frontend/windows/flutter/CMakeLists.txt
diff --git a/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc
similarity index 100%
rename from windows/flutter/generated_plugin_registrant.cc
rename to frontend/windows/flutter/generated_plugin_registrant.cc
diff --git a/windows/flutter/generated_plugin_registrant.h b/frontend/windows/flutter/generated_plugin_registrant.h
similarity index 100%
rename from windows/flutter/generated_plugin_registrant.h
rename to frontend/windows/flutter/generated_plugin_registrant.h
diff --git a/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake
similarity index 100%
rename from windows/flutter/generated_plugins.cmake
rename to frontend/windows/flutter/generated_plugins.cmake
diff --git a/windows/runner/CMakeLists.txt b/frontend/windows/runner/CMakeLists.txt
similarity index 100%
rename from windows/runner/CMakeLists.txt
rename to frontend/windows/runner/CMakeLists.txt
diff --git a/windows/runner/Runner.rc b/frontend/windows/runner/Runner.rc
similarity index 100%
rename from windows/runner/Runner.rc
rename to frontend/windows/runner/Runner.rc
diff --git a/windows/runner/flutter_window.cpp b/frontend/windows/runner/flutter_window.cpp
similarity index 100%
rename from windows/runner/flutter_window.cpp
rename to frontend/windows/runner/flutter_window.cpp
diff --git a/windows/runner/flutter_window.h b/frontend/windows/runner/flutter_window.h
similarity index 100%
rename from windows/runner/flutter_window.h
rename to frontend/windows/runner/flutter_window.h
diff --git a/windows/runner/main.cpp b/frontend/windows/runner/main.cpp
similarity index 100%
rename from windows/runner/main.cpp
rename to frontend/windows/runner/main.cpp
diff --git a/windows/runner/resource.h b/frontend/windows/runner/resource.h
similarity index 100%
rename from windows/runner/resource.h
rename to frontend/windows/runner/resource.h
diff --git a/windows/runner/resources/app_icon.ico b/frontend/windows/runner/resources/app_icon.ico
similarity index 100%
rename from windows/runner/resources/app_icon.ico
rename to frontend/windows/runner/resources/app_icon.ico
diff --git a/windows/runner/runner.exe.manifest b/frontend/windows/runner/runner.exe.manifest
similarity index 100%
rename from windows/runner/runner.exe.manifest
rename to frontend/windows/runner/runner.exe.manifest
diff --git a/windows/runner/utils.cpp b/frontend/windows/runner/utils.cpp
similarity index 100%
rename from windows/runner/utils.cpp
rename to frontend/windows/runner/utils.cpp
diff --git a/windows/runner/utils.h b/frontend/windows/runner/utils.h
similarity index 100%
rename from windows/runner/utils.h
rename to frontend/windows/runner/utils.h
diff --git a/windows/runner/win32_window.cpp b/frontend/windows/runner/win32_window.cpp
similarity index 100%
rename from windows/runner/win32_window.cpp
rename to frontend/windows/runner/win32_window.cpp
diff --git a/windows/runner/win32_window.h b/frontend/windows/runner/win32_window.h
similarity index 100%
rename from windows/runner/win32_window.h
rename to frontend/windows/runner/win32_window.h
-- 
2.47.2


From 7f4f707ab522e1f735ff5d2a6a33daf15fdd31ff Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Thu, 16 May 2024 17:34:54 +0200
Subject: [PATCH 05/33] sample backend deployment

---
 backend/Dockerfile   |  11 +
 backend/Pipfile      |  14 +
 backend/Pipfile.lock | 884 +++++++++++++++++++++++++++++++++++++++++++
 backend/src/main.py  |  23 ++
 4 files changed, 932 insertions(+)
 create mode 100644 backend/Dockerfile
 create mode 100644 backend/Pipfile
 create mode 100644 backend/Pipfile.lock
 create mode 100644 backend/src/main.py

diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..1535818
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,11 @@
+FROM python:3
+
+WORKDIR /app
+COPY Pipfile Pipfile.lock /app/
+
+RUN pip install pipenv
+RUN pipenv install --deploy --ignore-pipfile
+
+COPY . /src
+
+CMD ["pipenv", "run", "python", "/app/src/main.py"]
diff --git a/backend/Pipfile b/backend/Pipfile
new file mode 100644
index 0000000..dfcd2a4
--- /dev/null
+++ b/backend/Pipfile
@@ -0,0 +1,14 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+numpy = "*"
+scipy = "*"
+fastapi = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.12"
diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock
new file mode 100644
index 0000000..a4c1f54
--- /dev/null
+++ b/backend/Pipfile.lock
@@ -0,0 +1,884 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "338629802198c23b4efab9dde90a89e178caf0840d99ff5c5936550b91d64d5e"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.12"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "annotated-types": {
+            "hashes": [
+                "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43",
+                "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.6.0"
+        },
+        "anyio": {
+            "hashes": [
+                "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8",
+                "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.3.0"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
+                "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2024.2.2"
+        },
+        "click": {
+            "hashes": [
+                "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+                "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.7"
+        },
+        "dnspython": {
+            "hashes": [
+                "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50",
+                "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.6.1"
+        },
+        "email-validator": {
+            "hashes": [
+                "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84",
+                "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.1.1"
+        },
+        "fastapi": {
+            "hashes": [
+                "sha256:97ecbf994be0bcbdadedf88c3150252bed7b2087075ac99735403b1b76cc8fc0",
+                "sha256:b9db9dd147c91cb8b769f7183535773d8741dd46f9dc6676cd82eab510228cd7"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.8'",
+            "version": "==0.111.0"
+        },
+        "fastapi-cli": {
+            "hashes": [
+                "sha256:3b6e4d2c4daee940fb8db59ebbfd60a72c4b962bcf593e263e4cc69da4ea3d7f",
+                "sha256:ae233115f729945479044917d949095e829d2d84f56f55ce1ca17627872825a5"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.0.3"
+        },
+        "h11": {
+            "hashes": [
+                "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
+                "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.14.0"
+        },
+        "httpcore": {
+            "hashes": [
+                "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61",
+                "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.0.5"
+        },
+        "httptools": {
+            "hashes": [
+                "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563",
+                "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142",
+                "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d",
+                "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b",
+                "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4",
+                "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb",
+                "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658",
+                "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084",
+                "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2",
+                "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97",
+                "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837",
+                "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3",
+                "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58",
+                "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da",
+                "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d",
+                "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90",
+                "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0",
+                "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1",
+                "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2",
+                "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e",
+                "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0",
+                "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf",
+                "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc",
+                "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3",
+                "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503",
+                "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a",
+                "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3",
+                "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949",
+                "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84",
+                "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb",
+                "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a",
+                "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f",
+                "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e",
+                "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81",
+                "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185",
+                "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"
+            ],
+            "version": "==0.6.1"
+        },
+        "httpx": {
+            "hashes": [
+                "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
+                "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.27.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
+                "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.7"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
+                "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.4"
+        },
+        "markdown-it-py": {
+            "hashes": [
+                "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
+                "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.0.0"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+                "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+                "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+                "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+                "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+                "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+                "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+                "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+                "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+                "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+                "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+                "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+                "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+                "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+                "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+                "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+                "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+                "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+                "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+                "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+                "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+                "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+                "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+                "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+                "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+                "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+                "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+                "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+                "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+                "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+                "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+                "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+                "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+                "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+                "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+                "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+                "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+                "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+                "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+                "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+                "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+                "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+                "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+                "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+                "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+                "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+                "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+                "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+                "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+                "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+                "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+                "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+                "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+                "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+                "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+                "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+                "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+                "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+                "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+                "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.5"
+        },
+        "mdurl": {
+            "hashes": [
+                "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
+                "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.1.2"
+        },
+        "numpy": {
+            "hashes": [
+                "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b",
+                "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818",
+                "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20",
+                "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0",
+                "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010",
+                "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a",
+                "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea",
+                "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c",
+                "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71",
+                "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110",
+                "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be",
+                "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a",
+                "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a",
+                "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5",
+                "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed",
+                "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd",
+                "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c",
+                "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e",
+                "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0",
+                "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c",
+                "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a",
+                "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b",
+                "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0",
+                "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6",
+                "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2",
+                "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a",
+                "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30",
+                "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218",
+                "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5",
+                "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07",
+                "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2",
+                "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4",
+                "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764",
+                "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef",
+                "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3",
+                "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.9'",
+            "version": "==1.26.4"
+        },
+        "orjson": {
+            "hashes": [
+                "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2",
+                "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b",
+                "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5",
+                "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b",
+                "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0",
+                "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0",
+                "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7",
+                "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42",
+                "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5",
+                "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa",
+                "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818",
+                "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25",
+                "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08",
+                "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7",
+                "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b",
+                "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754",
+                "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0",
+                "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912",
+                "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b",
+                "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3",
+                "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700",
+                "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78",
+                "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290",
+                "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134",
+                "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294",
+                "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0",
+                "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5",
+                "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d",
+                "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d",
+                "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16",
+                "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195",
+                "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da",
+                "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8",
+                "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098",
+                "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8",
+                "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063",
+                "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534",
+                "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d",
+                "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109",
+                "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d",
+                "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3",
+                "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2",
+                "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf",
+                "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069",
+                "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7",
+                "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.10.3"
+        },
+        "pydantic": {
+            "hashes": [
+                "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5",
+                "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.7.1"
+        },
+        "pydantic-core": {
+            "hashes": [
+                "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b",
+                "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a",
+                "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90",
+                "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d",
+                "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e",
+                "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d",
+                "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027",
+                "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804",
+                "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347",
+                "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400",
+                "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3",
+                "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399",
+                "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349",
+                "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd",
+                "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c",
+                "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e",
+                "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413",
+                "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3",
+                "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e",
+                "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3",
+                "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91",
+                "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce",
+                "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c",
+                "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb",
+                "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664",
+                "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6",
+                "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd",
+                "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3",
+                "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af",
+                "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043",
+                "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350",
+                "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7",
+                "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0",
+                "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563",
+                "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761",
+                "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72",
+                "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3",
+                "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb",
+                "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788",
+                "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b",
+                "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c",
+                "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038",
+                "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250",
+                "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec",
+                "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c",
+                "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74",
+                "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81",
+                "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439",
+                "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75",
+                "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0",
+                "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8",
+                "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150",
+                "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438",
+                "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae",
+                "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857",
+                "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038",
+                "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374",
+                "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f",
+                "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241",
+                "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592",
+                "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4",
+                "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d",
+                "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b",
+                "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b",
+                "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182",
+                "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e",
+                "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641",
+                "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70",
+                "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9",
+                "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a",
+                "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543",
+                "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b",
+                "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f",
+                "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38",
+                "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845",
+                "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2",
+                "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0",
+                "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4",
+                "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.18.2"
+        },
+        "pygments": {
+            "hashes": [
+                "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
+                "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.18.0"
+        },
+        "python-dotenv": {
+            "hashes": [
+                "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
+                "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
+            ],
+            "version": "==1.0.1"
+        },
+        "python-multipart": {
+            "hashes": [
+                "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026",
+                "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.0.9"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+                "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+                "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "version": "==6.0.1"
+        },
+        "rich": {
+            "hashes": [
+                "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222",
+                "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==13.7.1"
+        },
+        "scipy": {
+            "hashes": [
+                "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922",
+                "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5",
+                "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa",
+                "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820",
+                "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd",
+                "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42",
+                "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e",
+                "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d",
+                "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86",
+                "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e",
+                "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c",
+                "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602",
+                "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e",
+                "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5",
+                "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a",
+                "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21",
+                "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d",
+                "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6",
+                "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78",
+                "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551",
+                "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7",
+                "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4",
+                "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d",
+                "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b",
+                "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.9'",
+            "version": "==1.13.0"
+        },
+        "shellingham": {
+            "hashes": [
+                "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
+                "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.5.4"
+        },
+        "sniffio": {
+            "hashes": [
+                "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
+                "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.3.1"
+        },
+        "starlette": {
+            "hashes": [
+                "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee",
+                "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.37.2"
+        },
+        "typer": {
+            "hashes": [
+                "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914",
+                "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.12.3"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
+                "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.11.0"
+        },
+        "ujson": {
+            "hashes": [
+                "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e",
+                "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b",
+                "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6",
+                "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7",
+                "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9",
+                "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd",
+                "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569",
+                "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f",
+                "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51",
+                "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20",
+                "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1",
+                "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf",
+                "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc",
+                "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e",
+                "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a",
+                "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539",
+                "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27",
+                "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165",
+                "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126",
+                "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1",
+                "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816",
+                "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64",
+                "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8",
+                "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e",
+                "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287",
+                "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3",
+                "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb",
+                "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0",
+                "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043",
+                "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557",
+                "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e",
+                "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21",
+                "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d",
+                "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd",
+                "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0",
+                "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337",
+                "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753",
+                "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804",
+                "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f",
+                "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f",
+                "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5",
+                "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5",
+                "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1",
+                "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00",
+                "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2",
+                "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050",
+                "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e",
+                "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4",
+                "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8",
+                "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996",
+                "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6",
+                "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1",
+                "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f",
+                "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1",
+                "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4",
+                "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b",
+                "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88",
+                "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518",
+                "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5",
+                "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770",
+                "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4",
+                "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a",
+                "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76",
+                "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe",
+                "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988",
+                "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1",
+                "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5",
+                "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b",
+                "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7",
+                "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8",
+                "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc",
+                "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a",
+                "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720",
+                "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3",
+                "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b",
+                "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9",
+                "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1",
+                "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==5.10.0"
+        },
+        "uvicorn": {
+            "extras": [
+                "standard"
+            ],
+            "hashes": [
+                "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de",
+                "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.29.0"
+        },
+        "uvloop": {
+            "hashes": [
+                "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd",
+                "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec",
+                "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b",
+                "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc",
+                "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797",
+                "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5",
+                "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2",
+                "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d",
+                "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be",
+                "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd",
+                "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12",
+                "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17",
+                "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef",
+                "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24",
+                "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428",
+                "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1",
+                "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849",
+                "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593",
+                "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd",
+                "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67",
+                "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6",
+                "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3",
+                "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd",
+                "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8",
+                "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7",
+                "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533",
+                "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957",
+                "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650",
+                "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e",
+                "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7",
+                "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"
+            ],
+            "version": "==0.19.0"
+        },
+        "watchfiles": {
+            "hashes": [
+                "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc",
+                "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365",
+                "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0",
+                "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e",
+                "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124",
+                "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c",
+                "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317",
+                "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094",
+                "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7",
+                "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235",
+                "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c",
+                "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c",
+                "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c",
+                "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235",
+                "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293",
+                "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa",
+                "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef",
+                "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19",
+                "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8",
+                "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d",
+                "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915",
+                "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429",
+                "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097",
+                "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe",
+                "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0",
+                "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d",
+                "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99",
+                "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1",
+                "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a",
+                "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895",
+                "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94",
+                "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562",
+                "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab",
+                "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360",
+                "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1",
+                "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7",
+                "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f",
+                "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03",
+                "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01",
+                "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58",
+                "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052",
+                "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e",
+                "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765",
+                "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6",
+                "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137",
+                "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85",
+                "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca",
+                "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f",
+                "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214",
+                "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7",
+                "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7",
+                "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3",
+                "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b",
+                "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7",
+                "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6",
+                "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994",
+                "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9",
+                "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec",
+                "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128",
+                "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c",
+                "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2",
+                "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078",
+                "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3",
+                "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e",
+                "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a",
+                "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6",
+                "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49",
+                "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b",
+                "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28",
+                "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9",
+                "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586",
+                "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400",
+                "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165",
+                "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303",
+                "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"
+            ],
+            "version": "==0.21.0"
+        },
+        "websockets": {
+            "hashes": [
+                "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b",
+                "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6",
+                "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df",
+                "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b",
+                "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205",
+                "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892",
+                "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53",
+                "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2",
+                "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed",
+                "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c",
+                "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd",
+                "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b",
+                "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931",
+                "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30",
+                "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370",
+                "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be",
+                "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec",
+                "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf",
+                "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62",
+                "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b",
+                "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402",
+                "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f",
+                "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123",
+                "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9",
+                "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603",
+                "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45",
+                "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558",
+                "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4",
+                "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438",
+                "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137",
+                "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480",
+                "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447",
+                "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8",
+                "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04",
+                "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c",
+                "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb",
+                "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967",
+                "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b",
+                "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d",
+                "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def",
+                "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c",
+                "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92",
+                "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2",
+                "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113",
+                "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b",
+                "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28",
+                "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7",
+                "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d",
+                "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f",
+                "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468",
+                "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8",
+                "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae",
+                "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611",
+                "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d",
+                "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9",
+                "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca",
+                "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f",
+                "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2",
+                "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077",
+                "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2",
+                "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6",
+                "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374",
+                "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc",
+                "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e",
+                "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53",
+                "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399",
+                "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547",
+                "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3",
+                "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870",
+                "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5",
+                "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8",
+                "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"
+            ],
+            "version": "==12.0"
+        }
+    },
+    "develop": {}
+}
diff --git a/backend/src/main.py b/backend/src/main.py
new file mode 100644
index 0000000..e0072b0
--- /dev/null
+++ b/backend/src/main.py
@@ -0,0 +1,23 @@
+import fastapi
+from dataclasses import dataclass
+
+
+@dataclass
+class Destination:
+    name: str
+    location: tuple
+    attractiveness: int
+
+
+
+d = Destination()
+
+
+
+def get_route() -> list[Destination]:
+    return {"route": "Hello World"}
+
+endpoint = ("/get_route", get_route)
+end
+if __name__ == "__main__":
+    fastapi.run()
-- 
2.47.2


From 3f1c16b575d187d468562920b2744a3b23421a0f Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Thu, 16 May 2024 17:36:12 +0200
Subject: [PATCH 06/33] added the optimizer

---
 backend/src/optimizer.py | 206 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 206 insertions(+)
 create mode 100644 backend/src/optimizer.py

diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py
new file mode 100644
index 0000000..dac0df5
--- /dev/null
+++ b/backend/src/optimizer.py
@@ -0,0 +1,206 @@
+from scipy.optimize import linprog
+import numpy as np
+from scipy.linalg import block_diag
+
+
+# Defines the landmark class (aka some place there is to visit)
+class landmark :
+    def __init__(self, name: str, attractiveness: int, loc: tuple):
+        self.name = name
+        self.attractiveness = attractiveness
+        self.loc = loc
+
+# Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks
+def untangle(res: list) :
+    N = len(res)                # length of res
+    L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
+    n_landmarks = res.sum()     # number of visited landmarks
+    visit_order = []
+    cnt = 0
+
+    if n_landmarks % 2 == 1 :                                     # if odd number of visited checkpoints
+        for i in range(L) :
+            for j in range(L) :
+                if res[i*L + j] == 1 :              # if index is 1
+                    cnt += 1                        # increment counter
+                    if cnt % 2 == 1 :               # if counter odd
+                        visit_order.append(i)
+                        visit_order.append(j)
+    else :                                   # if even number of ones
+        for i in range(L) :
+            for j in range(L) :
+                if res[i*L + j] == 1 :              # if index is one
+                    cnt += 1                        # increment counter
+                    if j % (L-1) == 0 :             # if last node
+                        visit_order.append(j)       # append only the last index
+                        return visit_order          # return
+                    if cnt % 2 == 1 : 
+                        visit_order.append(i)
+                        visit_order.append(j)
+    return visit_order
+
+# Just to print the result
+def print_res(res: list, P) :
+    X = abs(res.x)
+    order = untangle(X)
+
+    # print("Optimal value:", -res.fun)  # Minimization, so we negate to get the maximum
+    # print("Optimal point:", res.x)
+    # N = int(np.sqrt(len(X)))
+    # for i in range(N):
+    #     print(X[i*N:i*N+N])
+    # print(order)
+
+    if (X.sum()+1)**2 == len(X) : 
+        print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest')
+    else :
+        print('Could not visit all the landmarks, the following order could be the fastest but not sure')
+    print("Order of visit :")
+    for i, elem in enumerate(landmarks) : 
+        if i in order : print('- ' + elem.name)
+
+    steps = path_length(P, abs(res.x))
+    print("\nSteps walked : " + str(steps))
+
+# Constraint to use only the upper triangular indices for travel
+def break_sym(landmarks, A_eq, b_eq):
+    L = len(landmarks)
+    l = [0]*L*L
+    for i in range(L) :
+        for j in range(L) :
+            if i >= j :
+                l[j+i*L] = 1
+
+    A_eq = np.vstack((A_eq,l))
+    b_eq.append(0)
+
+    return A_eq, b_eq
+
+# Constraint to respect max number of travels
+def respect_number(landmarks, A_ub, b_ub):
+    h = []
+    for i in range(len(landmarks)) : h.append([1]*len(landmarks))
+    T = block_diag(*h)
+    return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
+
+# Constraint to tie the problem together and have a connected path
+def respect_order(landmarks: list, A_eq, b_eq): 
+    N = len(landmarks)
+    for i in range(N-1) :     # Prevent stacked ones
+        if i == 0 :
+            continue
+        else : 
+            l = [0]*N
+            l[i] = -1
+            l = l*N
+            for j in range(N) :
+                l[i*N + j] = 1
+
+            A_eq = np.vstack((A_eq,l))
+            b_eq.append(0)
+
+    return A_eq, b_eq
+
+# Compute manhattan distance between 2 locations
+def manhattan_distance(loc1: tuple, loc2: tuple):
+    x1, y1 = loc1
+    x2, y2 = loc2
+    return abs(x1 - x2) + abs(y1 - y2)
+
+# Constraint to not stay in position
+def init_eq_not_stay(landmarks): 
+    L = len(landmarks)
+    l = [0]*L*L
+    for i in range(L) :
+        for j in range(L) :
+            if j == i :
+                l[j + i*L] = 1
+    #A_eq = np.array([np.array(xi) for xi in A_eq])                  # Must convert A_eq into an np array
+    l = np.array(np.array(l))
+    return [l], [0]
+
+# Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
+# We want to maximize the sightseeing :  max(c) st. A*x < b   and   A_eq*x = b_eq
+def init_ub_dist(landmarks: list, max_steps: int):
+    # Objective function coefficients. a*x1 + b*x2 + c*x3 + ...
+    c = []
+    # Coefficients of inequality constraints (left-hand side)
+    A = []
+    for i, spot1 in enumerate(landmarks) :
+        dist_table = [0]*len(landmarks)
+        c.append(-spot1.attractiveness)
+        for j, spot2 in enumerate(landmarks) :
+            dist_table[j] = manhattan_distance(spot1.loc, spot2.loc)
+        A.append(dist_table)
+    c = c*len(landmarks)
+    A_ub = []
+    for line in A :
+        A_ub += line
+    return c, A_ub, [max_steps]
+
+# Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
+def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
+    L = len(landmarks)
+    for i, elem in enumerate(landmarks) :
+        if elem.attractiveness == -1 :
+            l = [0]*L*L
+            if elem.name != "arrivée" :
+                for j in range(L) :
+                    l[j +i*L] = 1
+            else :                          # This ensures we go to goal
+                for k in range(L-1) :
+                        l[k*L+L-1] = 1
+            A_eq = np.vstack((A_eq,l))
+            b_eq.append(1)
+    return A_eq, b_eq
+
+# Computes the path length given path matrix (dist_table) and a result
+def path_length(P: list, resx: list) :
+    return np.dot(P, resx)
+
+# Initialize all landmarks (+ start and goal). Order matters here
+landmarks = []
+landmarks.append(landmark("départ", -1, (0, 0)))
+landmarks.append(landmark("concorde", -1, (5,5)))
+landmarks.append(landmark("tour eiffel", 99, (1,1)))                           # PUT IN JSON
+landmarks.append(landmark("arc de triomphe", 99, (2,3)))
+landmarks.append(landmark("louvre", 70, (4,2)))
+landmarks.append(landmark("montmartre", 20, (0,2)))
+landmarks.append(landmark("arrivée", -1, (0, 0)))
+
+
+
+# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
+max_steps = 25
+
+# SET CONSTRAINTS FOR INEQUALITY
+c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other
+P = A_ub                                                        # store the paths for later. Needed to compute path length
+A_ub, b_ub = respect_number(landmarks, A_ub, b_ub)              # Respect max number of visits. 
+
+# SET CONSTRAINTS FOR EQUALITY
+A_eq, b_eq = init_eq_not_stay(landmarks)                       # Force solution not to stay in same place
+A_eq, b_eq = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal
+A_eq, b_eq = break_sym(landmarks, A_eq, b_eq)                  # break the symmetry. Only use the upper diagonal values
+A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order of visit (only works when max_steps is limiting factor)
+
+# Bounds for variables (x can only be 0 or 1)
+x_bounds = [(0, 1)] * len(c)
+
+# Solve linear programming problem
+res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
+
+# Raise error if no solution is found
+if not res.success :
+    raise ValueError("No solution has been found, please adapt your max steps")
+
+# Print result
+print_res(res, P)
+
+
+
+
+
+
+
+
-- 
2.47.2


From 3854cef54a062ca68308a1b74d4d4ea04e3bc590 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Fri, 17 May 2024 20:12:59 +0200
Subject: [PATCH 07/33] adding google maps baby!

---
 .gitea/workflows/frontend_build-android.yaml  |   2 +-
 frontend/android/app/build.gradle             |   3 +
 .../android/app/src/main/AndroidManifest.xml  |   4 +-
 frontend/lib/modules/map.dart                 |  81 +++++++++++++
 frontend/lib/modules/overview.dart            |  78 ++----------
 frontend/pubspec.lock                         | 111 +++++++++++++++++-
 frontend/pubspec.yaml                         |   1 +
 frontend/web/index.html                       |   1 +
 8 files changed, 212 insertions(+), 69 deletions(-)
 create mode 100644 frontend/lib/modules/map.dart

diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index de430ce..02bf3bf 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -35,7 +35,7 @@ jobs:
     - run: flutter pub get
       working-directory: ./frontend
 
-    - run: flutter build apk --debug --split-per-abi
+    - run: flutter build apk --release --split-per-abi
       working-directory: ./frontend
 
 
diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle
index 21a5169..f6b499e 100644
--- a/frontend/android/app/build.gradle
+++ b/frontend/android/app/build.gradle
@@ -45,6 +45,9 @@ android {
         applicationId "com.example.fast_network_navigation"
         // You can update the following values to match your application needs.
         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
+        // Minimum Android version for Google Maps SDK
+        // https://developers.google.com/maps/flutter-package/config#android
+        minSdk = 21
         minSdkVersion flutter.minSdkVersion
         targetSdkVersion flutter.targetSdkVersion
         versionCode flutterVersionCode.toInteger()
diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
index fc057fe..54f939a 100644
--- a/frontend/android/app/src/main/AndroidManifest.xml
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -28,7 +28,9 @@
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
         <meta-data
             android:name="flutterEmbedding"
-            android:value="2" />
+            android:value="2"
+            android:name="com.google.android.geo.API_KEY"
+            android:value="AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"/> />
     </application>
     <!-- Required to query activities that can process text, see:
          https://developer.android.com/training/package-visibility?hl=en and
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
new file mode 100644
index 0000000..9f7a22d
--- /dev/null
+++ b/frontend/lib/modules/map.dart
@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+
+class MapWidget extends StatefulWidget {
+  @override
+  State<MapWidget> createState() => _MapWidgetState();
+}
+
+class _MapWidgetState extends State<MapWidget> {
+  late GoogleMapController mapController;
+
+  final LatLng _center = const LatLng(45.521563, -122.677433);
+
+  void _onMapCreated(GoogleMapController controller) {
+    mapController = controller;
+  }
+  void _onCameraIdle() {
+    // print(mapController.getLatLng());
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GoogleMap(
+      onMapCreated: _onMapCreated,
+      initialCameraPosition: CameraPosition(
+        target: _center,
+        zoom: 11.0,
+      ),
+      onCameraIdle: _onCameraIdle,
+    );
+  }
+}
+
+
+
+  // GeoCode geoCode = GeoCode();
+  // String _currentCityName = "...";
+  // final Debounce _debounce = Debounce(Duration(seconds: 3));
+
+  // final LatLng _center = const LatLng(45.521563, -122.677433);
+  // late GoogleMapController mapController;
+
+  // void _onMapCreated(GoogleMapController controller) {
+  //   mapController = controller;
+  // }
+
+  // // void _setCurrentCityName() async {
+  //   if (mapController.camera.zoom < 9) {
+  //     return; // Don't bother if the view is too wide
+  //   }
+  //   var currentCoordinates = mapController.camera.center;
+  //   String? city;
+    
+  //   try{
+  //     List<Placemark> placemarks = await placemarkFromCoordinates(currentCoordinates.latitude, currentCoordinates.longitude);
+  //     city = placemarks[0].locality.toString();
+  //   } catch (e) {
+  //     debugPrint("Error: $e");
+  //     try {
+  //       Address address = await geoCode.reverseGeocoding(latitude: currentCoordinates.latitude, longitude: currentCoordinates.longitude);
+        
+  //       if (address.city == null || address.city.toString().contains("Throttled!")){
+  //         throw Exception("Probably rate limited");
+  //       }
+  //       city = address.city.toString();
+  //     } catch (e) {
+  //       debugPrint("Error: $e");
+
+  //     }
+  //   }
+  //   if (city != null) {
+  //     setState(() {
+  //       _currentCityName = city!;
+  //     });
+  //   } else {
+  //     _debounce(() async {_setCurrentCityName();});
+  //   }
+  // }
+
+  
\ No newline at end of file
diff --git a/frontend/lib/modules/overview.dart b/frontend/lib/modules/overview.dart
index ff03b9b..d9bc126 100644
--- a/frontend/lib/modules/overview.dart
+++ b/frontend/lib/modules/overview.dart
@@ -1,17 +1,21 @@
 import 'package:flutter/material.dart';
-import 'package:flutter_map/flutter_map.dart';
 import 'package:sliding_up_panel/sliding_up_panel.dart';
-import 'package:latlong2/latlong.dart';
-import 'package:geocoding/geocoding.dart';
 import 'package:geocode/geocode.dart';
 import 'dart:async';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
 
 import 'package:fast_network_navigation/modules/navigation.dart';
+import 'package:fast_network_navigation/modules/map.dart';
 
-class MapPage extends StatefulWidget {
+
+
+class MainPage extends StatefulWidget {
   @override
-  _MapPageState createState() => _MapPageState();
+  State<MainPage> createState() => _MainPageState();
 }
+
+
+
 class Debounce {
   Duration delay;
   Timer? _timer;
@@ -31,44 +35,7 @@ class Debounce {
 }
 
 
-class _MapPageState extends State<MapPage> {
-  GeoCode geoCode = GeoCode();
-  final mapController = MapController();
-  String _currentCityName = "...";
-  final Debounce _debounce = Debounce(Duration(seconds: 3));
-
-  void _setCurrentCityName() async {
-    if (mapController.camera.zoom < 9) {
-      return; // Don't bother if the view is too wide
-    }
-    var currentCoordinates = mapController.camera.center;
-    String? city;
-    
-    try{
-      List<Placemark> placemarks = await placemarkFromCoordinates(currentCoordinates.latitude, currentCoordinates.longitude);
-      city = placemarks[0].locality.toString();
-    } catch (e) {
-      debugPrint("Error: $e");
-      try {
-        Address address = await geoCode.reverseGeocoding(latitude: currentCoordinates.latitude, longitude: currentCoordinates.longitude);
-        
-        if (address.city == null || address.city.toString().contains("Throttled!")){
-          throw Exception("Probably rate limited");
-        }
-        city = address.city.toString();
-      } catch (e) {
-        debugPrint("Error: $e");
-
-      }
-    }
-    if (city != null) {
-      setState(() {
-        _currentCityName = city!;
-      });
-    } else {
-      _debounce(() async {_setCurrentCityName();});
-    }
-  }
+class _MainPageState extends State<MainPage> {
 
   @override
   Widget build(BuildContext context) {
@@ -77,21 +44,7 @@ class _MapPageState extends State<MapPage> {
         renderPanelSheet: false,
         panel: _floatingPanel(theme),
         collapsed: _floatingCollapsed(theme),
-        body: FlutterMap(
-          mapController: mapController,
-          options: MapOptions(
-            initialZoom: 11,
-            initialCenter: LatLng(51.509364, -0.128928),
-            onMapReady: () {
-                mapController.mapEventStream.listen((evt) {_debounce(() async {_setCurrentCityName();});});
-                // And any other `MapController` dependent non-movement methods
-            },
-
-          ),
-          children: [
-            openStreetMapTileLayer,
-          ],
-        ),
+        body: MapWidget()
     );
   }
 
@@ -138,16 +91,9 @@ class _MapPageState extends State<MapPage> {
   Widget Greeting (ThemeData theme) {
     return Center(
         child: Text(
-            "Explore ${_currentCityName}",
+            "Explore #todo",
           style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
         ),
       );
   }
 }
-
-TileLayer get openStreetMapTileLayer => TileLayer(
-  urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
-  userAgentPackageName: 'flutter_map',
-);
-
-// Add a pin to the map
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index b88a8ef..f72974c 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -41,6 +41,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  csslib:
+    dependency: transitive
+    description:
+      name: csslib
+      sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -78,11 +86,24 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "6.1.0"
+  flutter_plugin_android_lifecycle:
+    dependency: transitive
+    description:
+      name: flutter_plugin_android_lifecycle
+      sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.19"
   flutter_test:
     dependency: "direct dev"
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
   geocode:
     dependency: "direct main"
     description:
@@ -123,6 +144,62 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.2.0"
+  google_maps:
+    dependency: transitive
+    description:
+      name: google_maps
+      sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.1.0"
+  google_maps_flutter:
+    dependency: "direct main"
+    description:
+      name: google_maps_flutter
+      sha256: c1972cbad779bc5346c49045f26ae45550a0958b1cbca5b524dd3c8954995d28
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.6.1"
+  google_maps_flutter_android:
+    dependency: transitive
+    description:
+      name: google_maps_flutter_android
+      sha256: "0bcadb80eba39afda77dede89a6caafd3b68f2786b90491eceea4a01c3db181c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.8.0"
+  google_maps_flutter_ios:
+    dependency: transitive
+    description:
+      name: google_maps_flutter_ios
+      sha256: e5132d17f051600d90d79d9f574b177c24231da702453a036db2490f9ced4646
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.6.0"
+  google_maps_flutter_platform_interface:
+    dependency: transitive
+    description:
+      name: google_maps_flutter_platform_interface
+      sha256: "167af879da4d004cd58771f1469b91dcc3b9b0a2c5334cc6bf71fd41d4b35403"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.6.0"
+  google_maps_flutter_web:
+    dependency: transitive
+    description:
+      name: google_maps_flutter_web
+      sha256: "0c0d5c723d94b295cf86dd1c45ff91d2ac1fff7c05ddca4f01bef9fa0a014690"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.5.7"
+  html:
+    dependency: transitive
+    description:
+      name: html
+      sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.15.4"
   http:
     dependency: transitive
     description:
@@ -147,6 +224,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.19.0"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.7"
+  js_wrapping:
+    dependency: transitive
+    description:
+      name: js_wrapping
+      sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.4"
   latlong2:
     dependency: "direct main"
     description:
@@ -267,6 +360,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.0"
+  sanitize_html:
+    dependency: transitive
+    description:
+      name: sanitize_html
+      sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -304,6 +405,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.2"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   string_scanner:
     dependency: transitive
     description:
@@ -378,4 +487,4 @@ packages:
     version: "2.0.0"
 sdks:
   dart: ">=3.3.4 <4.0.0"
-  flutter: ">=3.18.0-18.0.pre.54"
+  flutter: ">=3.19.0"
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 53add6b..4c794d1 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -40,6 +40,7 @@ dependencies:
   latlong2: ^0.9.1
   geocoding: ^3.0.0
   geocode: ^1.0.3
+  google_maps_flutter: ^2.6.1
 
 dev_dependencies:
   flutter_test:
diff --git a/frontend/web/index.html b/frontend/web/index.html
index 30e08e2..5cbcc91 100644
--- a/frontend/web/index.html
+++ b/frontend/web/index.html
@@ -14,6 +14,7 @@
     This is a placeholder for base href that will be replaced by the value of
     the `--base-href` argument provided to `flutter build`.
   -->
+  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"></script>
   <base href="$FLUTTER_BASE_HREF">
 
   <meta charset="UTF-8">
-- 
2.47.2


From 66fc4e7d33712e6b202ea933e381f3efb501792c Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 18 May 2024 10:56:04 +0200
Subject: [PATCH 08/33] update deps

---
 frontend/pubspec.lock | 88 ++-----------------------------------------
 frontend/pubspec.yaml |  4 +-
 2 files changed, 5 insertions(+), 87 deletions(-)

diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index f72974c..21fb46f 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -74,18 +74,10 @@ packages:
     dependency: "direct dev"
     description:
       name: flutter_lints
-      sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
+      sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.2"
-  flutter_map:
-    dependency: "direct main"
-    description:
-      name: flutter_map
-      sha256: cda8d72135b697f519287258b5294a57ce2f2a5ebf234f0e406aad4dc14c9399
-      url: "https://pub.dev"
-    source: hosted
-    version: "6.1.0"
+    version: "4.0.0"
   flutter_plugin_android_lifecycle:
     dependency: transitive
     description:
@@ -216,14 +208,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.0.2"
-  intl:
-    dependency: transitive
-    description:
-      name: intl
-      sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.19.0"
   js:
     dependency: transitive
     description:
@@ -240,14 +224,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.7.4"
-  latlong2:
-    dependency: "direct main"
-    description:
-      name: latlong2
-      sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe"
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.9.1"
   leak_tracker:
     dependency: transitive
     description:
@@ -276,26 +252,10 @@ packages:
     dependency: transitive
     description:
       name: lints
-      sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
+      sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.0"
-  lists:
-    dependency: transitive
-    description:
-      name: lists
-      sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
-  logger:
-    dependency: transitive
-    description:
-      name: logger
-      sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.2.0"
+    version: "4.0.0"
   matcher:
     dependency: transitive
     description:
@@ -320,14 +280,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.12.0"
-  mgrs_dart:
-    dependency: transitive
-    description:
-      name: mgrs_dart
-      sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.0"
   path:
     dependency: transitive
     description:
@@ -344,22 +296,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.8"
-  polylabel:
-    dependency: transitive
-    description:
-      name: polylabel
-      sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
-  proj4dart:
-    dependency: transitive
-    description:
-      name: proj4dart
-      sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.1.0"
   sanitize_html:
     dependency: transitive
     description:
@@ -445,14 +381,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.2"
-  unicode:
-    dependency: transitive
-    description:
-      name: unicode
-      sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.3.1"
   vector_math:
     dependency: transitive
     description:
@@ -477,14 +405,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.5.1"
-  wkt_parser:
-    dependency: transitive
-    description:
-      name: wkt_parser
-      sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.0"
 sdks:
   dart: ">=3.3.4 <4.0.0"
   flutter: ">=3.19.0"
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 4c794d1..b0409b2 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -35,9 +35,7 @@ dependencies:
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.6
-  flutter_map: ^6.1.0
   sliding_up_panel: ^2.0.0+1
-  latlong2: ^0.9.1
   geocoding: ^3.0.0
   geocode: ^1.0.3
   google_maps_flutter: ^2.6.1
@@ -51,7 +49,7 @@ dev_dependencies:
   # activated in the `analysis_options.yaml` file located at the root of your
   # package. See that file for information about deactivating specific lint
   # rules and activating additional ones.
-  flutter_lints: ^3.0.0
+  flutter_lints: ^4.0.0
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec
-- 
2.47.2


From e600f40c1a991844730d8cf7352185ef02c360a0 Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Mon, 20 May 2024 01:37:35 +0200
Subject: [PATCH 09/33] corrected symmetry cosntraint

---
 backend/src/optimizer.py | 153 ++++++++++++++++++++++++++++++---------
 1 file changed, 120 insertions(+), 33 deletions(-)

diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py
index dac0df5..82d136a 100644
--- a/backend/src/optimizer.py
+++ b/backend/src/optimizer.py
@@ -10,11 +10,44 @@ class landmark :
         self.attractiveness = attractiveness
         self.loc = loc
 
+
+
+
+def untangle2(resx: list) :
+    N = len(resx)                   # length of res
+    L = int(np.sqrt(N))             # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
+    n_edges = resx.sum()      # number of edges
+
+    order = []
+    nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
+
+    nonzero_tup = np.unravel_index(nonzeroind, (L,L))
+
+    indx = nonzero_tup[0].tolist()
+    indy = nonzero_tup[1].tolist()
+
+    vert = (indx[0], indy[0])
+
+    order.append(vert[0])
+    order.append(vert[1])
+    
+    while len(order) < n_edges + 1 :
+        ind = indx.index(vert[1])
+
+        vert = (indx[ind], indy[ind])
+
+        order.append(vert[1])
+
+    return order
+
+
+
+
 # Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks
-def untangle(res: list) :
-    N = len(res)                # length of res
+def untangle(resx: list) :
+    N = len(resx)                # length of res
     L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
-    n_landmarks = res.sum()     # number of visited landmarks
+    n_landmarks = resx.sum()     # number of edges
     visit_order = []
     cnt = 0
 
@@ -40,47 +73,67 @@ def untangle(res: list) :
     return visit_order
 
 # Just to print the result
-def print_res(res: list, P) :
+def print_res(res: list, landmarks: list, P) :
     X = abs(res.x)
-    order = untangle(X)
+
+    N = int(np.sqrt(len(X)))
+    for i in range(N):
+        print(X[i*N:i*N+N])
+
+    order = untangle2(X)
+
+    order_ideal = [0, 0, 0, 0, 0, 0, 1, 0]
 
     # print("Optimal value:", -res.fun)  # Minimization, so we negate to get the maximum
     # print("Optimal point:", res.x)
-    # N = int(np.sqrt(len(X)))
-    # for i in range(N):
-    #     print(X[i*N:i*N+N])
-    # print(order)
+    
+    #for i,x in enumerate(X) : X[i] = round(x,0)
+    
+    #print(order)
 
     if (X.sum()+1)**2 == len(X) : 
         print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest')
     else :
         print('Could not visit all the landmarks, the following order could be the fastest but not sure')
     print("Order of visit :")
-    for i, elem in enumerate(landmarks) : 
-        if i in order : print('- ' + elem.name)
+    for idx in order : 
+        print('- ' + landmarks[idx].name)
 
     steps = path_length(P, abs(res.x))
     print("\nSteps walked : " + str(steps))
 
-# Constraint to use only the upper triangular indices for travel
-def break_sym(landmarks, A_eq, b_eq):
+# Constraint to not have d14 and d41 simultaneously
+def break_sym(landmarks, A_ub, b_ub):
     L = len(landmarks)
-    l = [0]*L*L
-    for i in range(L) :
-        for j in range(L) :
-            if i >= j :
-                l[j+i*L] = 1
+    upper_ind = np.triu_indices(L,0,L)
 
-    A_eq = np.vstack((A_eq,l))
-    b_eq.append(0)
+    up_ind_x = upper_ind[0]
+    up_ind_y = upper_ind[1]
 
-    return A_eq, b_eq
+    for i, _ in enumerate(up_ind_x) :
+        l = [0]*L*L
+        if up_ind_x[i] != up_ind_y[i] :
+            l[up_ind_x[i]*L + up_ind_y[i]] = 1
+            l[up_ind_y[i]*L + up_ind_x[i]] = 1
+
+            A_ub = np.vstack((A_ub,l))
+            b_ub.append(1)
+
+            """for i in range(7):
+                print(l[i*7:i*7+7])
+            print("\n")"""
+
+    return A_ub, b_ub
 
 # Constraint to respect max number of travels
 def respect_number(landmarks, A_ub, b_ub):
     h = []
     for i in range(len(landmarks)) : h.append([1]*len(landmarks))
     T = block_diag(*h)
+    """for l in T :
+        for i in range(7):
+            print(l[i*7:i*7+7])
+        print("\n")"""
     return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
 
 # Constraint to tie the problem together and have a connected path
@@ -99,6 +152,10 @@ def respect_order(landmarks: list, A_eq, b_eq):
             A_eq = np.vstack((A_eq,l))
             b_eq.append(0)
 
+            for i in range(7):
+                print(l[i*7:i*7+7])
+            print("\n")
+
     return A_eq, b_eq
 
 # Compute manhattan distance between 2 locations
@@ -111,12 +168,19 @@ def manhattan_distance(loc1: tuple, loc2: tuple):
 def init_eq_not_stay(landmarks): 
     L = len(landmarks)
     l = [0]*L*L
+
+
     for i in range(L) :
         for j in range(L) :
             if j == i :
                 l[j + i*L] = 1
+    l[L-1] = 1      # cannot skip from start to finish
     #A_eq = np.array([np.array(xi) for xi in A_eq])                  # Must convert A_eq into an np array
     l = np.array(np.array(l))
+
+    """for i in range(7):
+        print(l[i*7:i*7+7])"""
+
     return [l], [0]
 
 # Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness
@@ -135,24 +199,37 @@ def init_ub_dist(landmarks: list, max_steps: int):
     c = c*len(landmarks)
     A_ub = []
     for line in A :
+        #print(line)
         A_ub += line
     return c, A_ub, [max_steps]
 
 # Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1
 def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
     L = len(landmarks)
+    H = 0       # sort of heuristic to get an idea of the number of steps needed
+    for i in landmarks : 
+        if i.name == "départ" : elem_prev = i              # list of all matches
     for i, elem in enumerate(landmarks) :
         if elem.attractiveness == -1 :
             l = [0]*L*L
             if elem.name != "arrivée" :
                 for j in range(L) :
                     l[j +i*L] = 1
+                    
             else :                          # This ensures we go to goal
                 for k in range(L-1) :
-                        l[k*L+L-1] = 1
+                        l[k*L+L-1] = 1  
+
+            H += manhattan_distance(elem.loc, elem_prev.loc)
+            elem_prev = elem
+
+            """for i in range(7):
+                print(l[i*7:i*7+7])
+            print("\n")"""
+
             A_eq = np.vstack((A_eq,l))
             b_eq.append(1)
-    return A_eq, b_eq
+    return A_eq, b_eq, H
 
 # Computes the path length given path matrix (dist_table) and a result
 def path_length(P: list, resx: list) :
@@ -161,27 +238,30 @@ def path_length(P: list, resx: list) :
 # Initialize all landmarks (+ start and goal). Order matters here
 landmarks = []
 landmarks.append(landmark("départ", -1, (0, 0)))
-landmarks.append(landmark("concorde", -1, (5,5)))
-landmarks.append(landmark("tour eiffel", 99, (1,1)))                           # PUT IN JSON
-landmarks.append(landmark("arc de triomphe", 99, (2,3)))
-landmarks.append(landmark("louvre", 70, (4,2)))
-landmarks.append(landmark("montmartre", 20, (0,2)))
+landmarks.append(landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
+landmarks.append(landmark("arc de triomphe", 99, (0,4)))
+landmarks.append(landmark("louvre", 99, (0,6)))
+landmarks.append(landmark("montmartre", 99, (0,10)))
+landmarks.append(landmark("concorde", 99, (0,8)))
 landmarks.append(landmark("arrivée", -1, (0, 0)))
 
 
 
 # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
-max_steps = 25
+max_steps = 16
 
 # SET CONSTRAINTS FOR INEQUALITY
 c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other
 P = A_ub                                                        # store the paths for later. Needed to compute path length
 A_ub, b_ub = respect_number(landmarks, A_ub, b_ub)              # Respect max number of visits. 
 
+# TODO : Problems with circular symmetry
+A_ub, b_ub = break_sym(landmarks, A_ub, b_ub)                  # break the symmetry. Only use the upper diagonal values
+
 # SET CONSTRAINTS FOR EQUALITY
 A_eq, b_eq = init_eq_not_stay(landmarks)                       # Force solution not to stay in same place
-A_eq, b_eq = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal
-A_eq, b_eq = break_sym(landmarks, A_eq, b_eq)                  # break the symmetry. Only use the upper diagonal values
+A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal
+
 A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order of visit (only works when max_steps is limiting factor)
 
 # Bounds for variables (x can only be 0 or 1)
@@ -192,10 +272,17 @@ res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds,
 
 # Raise error if no solution is found
 if not res.success :
-    raise ValueError("No solution has been found, please adapt your max steps")
+    print(f"No solution has been found within given timeframe.\nMinimum steps to visit all must_see is : {H}")
+    # Override the max_steps using the heuristic
+    for i, val in enumerate(b_ub) :
+        if val == max_steps : b_ub[i] = H
+
+    # Solve problem again :
+    res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
+
 
 # Print result
-print_res(res, P)
+print_res(res, landmarks, P)
 
 
 
-- 
2.47.2


From f6e3cfc8a0482f3ad95a0f5d86b4f253f1c639fb Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 20 May 2024 12:09:33 +0200
Subject: [PATCH 10/33] build like this

---
 .gitea/workflows/frontend_build-android.yaml | 6 +++---
 deployment/kustomization.yaml                | 0
 frontend/lib/modules/scaffold.dart           | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)
 create mode 100644 deployment/kustomization.yaml

diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index 02bf3bf..b255196 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -8,13 +8,13 @@ name: Build and release APK
 jobs:
   build:
     name: Build APK
-    runs-on: k8s
+    runs-on: ubuntu-latest
     steps:
 
     - name: Install prerequisites
       run: |
-        sudo apt-get update
-        sudo apt-get install -y xz-utils unzip
+        apt-get update
+        apt-get install -y jq
 
     - uses: https://gitea.com/actions/checkout@v4
 
diff --git a/deployment/kustomization.yaml b/deployment/kustomization.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/lib/modules/scaffold.dart b/frontend/lib/modules/scaffold.dart
index 34c603c..822f208 100644
--- a/frontend/lib/modules/scaffold.dart
+++ b/frontend/lib/modules/scaffold.dart
@@ -22,7 +22,7 @@ class _BasePageState extends State<BasePage> {
     });
   }
 
-  Widget currentView = MapPage();
+  Widget currentView = MainPage();
   @override
   Widget build(BuildContext context) {
     final ThemeData theme = Theme.of(context);
@@ -51,7 +51,7 @@ class _BasePageState extends State<BasePage> {
                 // Update the state of the app
                 _onItemTapped(0);
                 // Then close the drawer
-                currentView = MapPage();
+                currentView = MainPage();
                 Navigator.pop(context);
               },
             ),
-- 
2.47.2


From 6405f33a34ff28e9404dde72d0095d2b4477ef8f Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 20 May 2024 21:07:10 +0200
Subject: [PATCH 11/33] update flutter builder

---
 .gitea/workflows/frontend_build-android.yaml | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index b255196..e510298 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -18,15 +18,19 @@ jobs:
 
     - uses: https://gitea.com/actions/checkout@v4
 
+
     - uses: https://github.com/actions/setup-java@v4
       with:
         java-version: '17'
         distribution: 'zulu'
 
+    - name: Fix flutter SDK folder permission
+      run: git config --global --add safe.directory "*"
+
     - uses: https://github.com/subosito/flutter-action@v2
       with:
         channel: stable
-        flutter-version: 3.19.6
+        flutter-version: 3.22.0
         cache: true
 
     - name: Setup Android SDK
-- 
2.47.2


From 3688229d7b159f07ee182c20af127a4a7c7ebf2c Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 20 May 2024 22:18:08 +0000
Subject: [PATCH 12/33] Update
 frontend/android/app/src/main/AndroidManifest.xml

---
 frontend/android/app/src/main/AndroidManifest.xml | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
index 54f939a..58e6f22 100644
--- a/frontend/android/app/src/main/AndroidManifest.xml
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -29,8 +29,13 @@
         <meta-data
             android:name="flutterEmbedding"
             android:value="2"
+        />
+        <meta-data
             android:name="com.google.android.geo.API_KEY"
-            android:value="AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"/> />
+            android:value="AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"
+        />
+
+
     </application>
     <!-- Required to query activities that can process text, see:
          https://developer.android.com/training/package-visibility?hl=en and
-- 
2.47.2


From 07830de1b2429a23967a1bd86db263799076079d Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Tue, 21 May 2024 06:29:10 +0000
Subject: [PATCH 13/33] Update .gitea/workflows/frontend_build-android.yaml

---
 .gitea/workflows/frontend_build-android.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index e510298..456a138 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -46,7 +46,7 @@ jobs:
     - name: Release APK
       uses: https://gitea.com/akkuman/gitea-release-action@v1
       with:
-        files: ./frontendbuild/app/outputs/flutter-apk/*.apk
+        files: ./frontend/build/app/outputs/flutter-apk/*.apk
         name: Testing release
         release_name: testing
         tag: testing
-- 
2.47.2


From 82a864e57f5e9876103252993b76b9dc81705ed2 Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Wed, 22 May 2024 00:40:57 +0200
Subject: [PATCH 14/33] fixed circular symmetry

---
 backend/src/optimizer.py | 83 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 77 insertions(+), 6 deletions(-)

diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py
index 82d136a..aaf214b 100644
--- a/backend/src/optimizer.py
+++ b/backend/src/optimizer.py
@@ -40,9 +40,6 @@ def untangle2(resx: list) :
 
     return order
 
-
-
-
 # Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks
 def untangle(resx: list) :
     N = len(resx)                # length of res
@@ -102,7 +99,49 @@ def print_res(res: list, landmarks: list, P) :
     steps = path_length(P, abs(res.x))
     print("\nSteps walked : " + str(steps))
 
-# Constraint to not have d14 and d41 simultaneously
+
+# Checks for cases of circular symmetry in the result
+def has_circle(resx: list) :
+    N = len(resx)                   # length of res
+    L = int(np.sqrt(N))             # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
+    n_edges = resx.sum()      # number of edges
+
+    
+    nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
+
+    nonzero_tup = np.unravel_index(nonzeroind, (L,L))
+
+    indx = nonzero_tup[0].tolist()
+    indy = nonzero_tup[1].tolist()
+    
+
+    verts = []
+
+    for i, x in enumerate(indx) :
+        verts.append((x, indy[i]))
+
+    
+    for vert in verts :
+        visited = []
+        visited.append(vert)
+
+        while len(visited) < n_edges + 1 :
+
+            try : 
+                ind = indx.index(vert[1])
+
+                vert = (indx[ind], indy[ind])
+
+                if vert in visited :
+                    return visited
+                else :
+                    visited.append(vert)
+            except :
+                break
+
+    return []
+
+# Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements
 def break_sym(landmarks, A_ub, b_ub):
     L = len(landmarks)
     upper_ind = np.triu_indices(L,0,L)
@@ -125,6 +164,26 @@ def break_sym(landmarks, A_ub, b_ub):
 
     return A_ub, b_ub
 
+
+def prevent_circle(landmarks, A_ub, b_ub, circle) :
+    N = len(landmarks)
+    l = [0]*N*N
+
+    for index in circle :
+        x = index[0]
+        y = index[1]
+        l[x*N+y] = 1
+
+    A_ub = np.vstack((A_ub,l))
+    b_ub.append(len(circle)-1)
+
+    """print("\n\nPREVENT CIRCLE")
+    for i in range(7):
+        print(l[i*7:i*7+7])
+    print("\n")"""
+
+    return A_ub, b_ub
+
 # Constraint to respect max number of travels
 def respect_number(landmarks, A_ub, b_ub):
     h = []
@@ -152,9 +211,9 @@ def respect_order(landmarks: list, A_eq, b_eq):
             A_eq = np.vstack((A_eq,l))
             b_eq.append(0)
 
-            for i in range(7):
+            """for i in range(7):
                 print(l[i*7:i*7+7])
-            print("\n")
+            print("\n")"""
 
     return A_eq, b_eq
 
@@ -268,7 +327,19 @@ A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order o
 x_bounds = [(0, 1)] * len(c)
 
 # Solve linear programming problem
+
 res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
+circle = has_circle(res.x)
+
+while len(circle) != 0 :
+    print("The solution has a circular path. Not interpretable.")
+    print("Need to add constraints until no circle ")
+
+    A_ub, b_ub = prevent_circle(landmarks, A_ub, b_ub, circle)
+    res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
+    circle = has_circle(res.x)
+
+
 
 # Raise error if no solution is found
 if not res.success :
-- 
2.47.2


From 2b31ce5f6b057c985bc22caa97d5d299a5e8392c Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Wed, 22 May 2024 10:16:33 +0200
Subject: [PATCH 15/33] Cleanup and created main

---
 .../src/__pycache__/optimizer.cpython-310.pyc | Bin 0 -> 6065 bytes
 backend/src/main.py                           |  37 +++--
 backend/src/main_example.py                   |  23 +++
 backend/src/optimizer.py                      | 155 +++++++-----------
 4 files changed, 100 insertions(+), 115 deletions(-)
 create mode 100644 backend/src/__pycache__/optimizer.cpython-310.pyc
 create mode 100644 backend/src/main_example.py

diff --git a/backend/src/__pycache__/optimizer.cpython-310.pyc b/backend/src/__pycache__/optimizer.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2446c047e04e4aa4cec9c2a5628a2c89ec72a9c7
GIT binary patch
literal 6065
zcma)AO>o=B6~-=p2!bF*%d#XpZi4(dVH!KJ(>7`1ruO9jPfhBiX<2nJ5Er5#kpQ&-
zWk~~OJXJcAOMB?8({@Ih>7l-Ndg!S$x%ZSq2ZvsI==k1K|I+U*NJ><HsnKHhVRwOj
z?|t8U4@tf5X!w0{_nr0MJ+5hgq`}Fbg~1K{r++{rG{JgWpS2mIKJW3ij;r1?`exf?
z+H0CHg!!>1%md!GgcVlSxu^*HW20@0sv0?>hEa8V&lPoy9MKSSA8YNJXo@qqx?*0O
z#kD?Ovmnl4Rzoa?b7Co+TQ|hAI1fovToCtR-Wi%FE{5~C^96A~?wnPU2O#zER7*T4
z9)fg1JPZlFl_4Gxk3w=zToR9Aheg3}X|2ca;fz{~6_s8T4`k9^LEl;HC7mr_L_xPh
zw@?12%6SOVP-`=xwYgxSE_i4NJv8xXQiwsyzym6U-3wyT59C&1#6dqS8bOxHpp!*A
zVH~Duq2pm4<HR$l&+-2f{?kP?Im<O)%Q;$>>u9;qZfSSfN=ug}?n`<pSwYh>3L_1B
z>!-wbHAyD_%U0+4)eq88rmI^~FA7#)PCCPW7-#9~yDxcfz47)NH&?vNUb+`|R^JH*
zsh7lFnuRinM7p{jq}k;-%x))g>vA0IMBN~Zl6ZA3=xl|tSWRVTH5p`4KiUuFmBC(N
z`+gKhneW?pbV{#iF^f5D|I9=oS0*!7NKL*^>hb-;@%?@xhCS-LzP~*TdgD8Kn#n?s
z;;b;U;h+~v7rUyXooH$#BOQ`LQtH^+1*Rm0A;WaH!yuddXg29kDf=^YGA-8**ob8;
z<A-|Aju=Kz{h@KBjr6WIG9Wi|?T86>UK^PhS-A=0@O6H~M^<i;{IG)A74-SIZ|4<~
z=Qg$Ld{oViLp!h1=m^jnk1(4?cn9MErWSlj+tTDlUOB8{uDUyN@=ET|PSl>*>EEtR
z-^>lTioPQ0(D$fmIdHi!mS@l}cszi|Y|E_B(_z1;#7PYEB}IK0%U~z$!Gt2*Evzi*
zMQK)8!C(Mm7p4-oYVs^?!d43QHrD!~=!R)wCQ^j5aK?LKYEc{aeVDB<AlX&zUSaHn
z^lV5@S+KB&aTdhgUO0zGq=Yt`HJHr|-e67cau;nw^=aJT&A%EI*`n8}PW&tF=on9a
z<PK9ye}n;9dXucHXFS(&HL6;!AL>V-yd_ZGSuHb2zQyHznJLg?T5O;P+mOXx*1lJR
z{Y_y4pyqjP(^`V(Fm3z6tdjG|?g#J*%&{^%w+^sxxt5q=MV`Z3+#{&(-*RsDdfxQK
zy-pB&YoWIjrBN0N?{<`JM6uToc6|VHkUr^U8=<$J^m@tdDDHYn<6e|{>97kE(30o9
z{a0T~hCSiMNmg#;1$YK6oIdCO+lOEF_M6Tvda!pp=xwRpTh&4j*3yFQDy_@&IEEy)
zD$IcdL=^=UwLy?=_;9ywwjrCi3#97+p?rd7vUdyiZArFm>yg-Po9b|<J)x@h^ReoS
zn5h`ifg^CfY~#vpz5oYk!ZGACxIcsL1_Wexg>DY-|L^FYK{nys%8e<q&D@+K8;GAG
zy8=$IOY6cT>Z`yuFlqy_gw!fwlrX*hbZ+DRi=!ImySa<|wK3M+6IlOf|AE{(bn@C5
z?e*Ns>p**jTA;m_*K&79lbgBwA)*9BAUNLZLoG%~roZoKv5m8y*g1V~`$iYD<pT5p
z<dUfr2goJrEmNaxc!7HNQF9SZQ4M3n2pJ#_e1U}bqiHo1@Z|#}@Thr^nun=*gqlaG
zxrC-|62Rdlk-SW@$4FKgyR}62agx4?iwkEXNc~PEJH7A~jMHmqO5`{6688qK3(VFH
z1d}G`iUVx!um-o7!J2>OcZ`Po3YN{lp8_BSA>|6cL}%;@dd3a`e*wBZ(nm&akda3w
zt{_xESI^A@2E#oE!&XM1?=EI8fG8F;khqywU>u9uEnR*?@O&bpIZD2yT?d&!u10FD
zT19d^+W>h~$wfviM_>`kS*qIh666Re6N6c$!N^u^H<Qtjm^tbINne7fuy)c6fmoTS
z<&@|$Zu-Nu!dOGAjMT0U!QPS<w(a47uKw<{w<o_$8!^9Ny?;UAnhb+2Ki%ucn3IxU
zD3frTsFyX_GX5Gc0jzPyHsp0om`Q1>5KOsu6N9nxp=_wrfU58hrRz}i{3M1>;wO~O
zNzrPaO={b&E2DvRr;5|DBN$rnWfeie<TL6sj@2ih#q&!=Rh6RTb0qjGn!+j_uXOAe
zNKEdoT(@9*r`6(?YAM(J025Qf)<iQln}<&3p%dGbU&oCZcU1WhgvuS?M+dBcGh|5h
z4iJ-|2N%w$A0oHxA{Qa)f;!o@4VTn^1Z`CjpOA@+wUE!uTmn}h#_%%6w5-CE2&Q4F
z1%+cB&n%%&`e<3FRlzpK4)Ol!V>iIWgRqnNAcnP2{shZYLYKiV5iK*%kaVh<;}Qu^
z(KiXcD)dsC!<KP89QkR5KaPNVX4?_GMA`_U7v?vvYiJ2|Ce#5_W4<#&vPJh4st|tR
z8V`ZCk+z+14Ir6_7MOA-PSG+-#gtjm=lHeEQa3m7qQ`0}-J~+bQ@Bld3b#uKA;3uT
zl7elXR#JVUBo&%}fk`P5D*&T(<N!K0bCruY2wecp>$H)9V&JN3uXQLfnEXh0l*%f1
zA==C}@DU&3q_`T;3DpM%>5+hto77t(y0<a8#pIK8vE&=L$T!in`R>&=-^2gzHC(Tq
ze*gJ?5O1JT31ZYi;Fh=({szlb9#5~n%;Z}b&qP~7_%zyP7~oa#8Y5)NFeUt#{zn|<
zD?R)iJbuz|km++Brhgb{$5C(y%vL5nw>_t13f&-07+4t@W(93>U>&YP&p4*bG2U<B
zC6qPWW*}v-2P;28B99t{Z)L;RXh<$xHYyG^Rq+yT`$(UVbOn3A#r>4RBmvyjT_~Z+
z_#OTa7x9R5SC?<&W>K9cX)^|-J*N!#V+??IUSghf1ZQrQhCGRPFd;{9GB@?wq%J7u
zQkH>Wg#6UpG;@0s#W#f)xc~jc8M&o04jWnMm^sw5s)}DNWGBc?o<;cDz79=SbJCn%
zvV|%TEEQ8ua0PjvGIv>I)=DM5L&s&Eg56Y1f$VLNWLHtz!(aR?SVMhAiY?{W6wAW1
zqc}XRYFDXxxTZYqGb~Y1F}Q)Ou7STMTL4sC1b3S`Nc`n{^ggdp^D3Gdq9Hv`eem}f
zjC~L!0}s=n=PKfixd~q)n#s+P1xDp#8@vm>nut4<O<j;Lt}D+4=_uks4Tp6G0{shR
zWuPdiiu@myTuDyKU!RK4(tV*oh&k@aQF9zcQ#xu3+<~ob=BOsHCu+t`>wv=|MqU|N
z;Pnb>Lzdgq=!Rz~i!Ks@<%Y7BRn~MvI<5Hh?jVqvd>Q>AAs*fy?R<JSR3!!y0+sEN
z|COdy2E9Q;0<p5{Y0?`Td8%cFnS#Au+PWI{!oEL{;Z9*tw*(WJyI@<V?Yl7H!Xdsr
z`2As;rD6Cd%uNZ5DCuC=2B6eLLf61Q(IWr6X)LmNZXkX(3}hB}940@Ym%WN+23o|E
zQ*NX$0-1IQw@2wt*^}Zwu1;6zBFS1@?NBDLvzV7pVX+Pfq_idcjK(wGMHLLKQ8c_W
zt|!1|d{rMId8D)vDI+}`$UM-;)C}*M8RHYq0nv7eV9M=n%++Z;s=^~Cl4-)3SrqWV
zoenBXo%W(NIYOk+h4DjsR73qy+pL0XU15&un2*d~nDEqL=9Eu|#2CNN|J)<53k%+O
zVxROq>|aZNmbv3}*u`G;Q6q2UR3Cq${J##L&i~UJj@Q>k<$!%3UM%dPCV!15(Odp1
zYlv!I|EVUNpQ}1Mn?s$01o}W9&E<2qH2UC`HOn)CscI`QBXO1T@3y%SbvM#`l+v^+
z$LZWLMYOH8WEhLIu=-)Pk%)E;6>8X(K`+YojyKw0SxLM!=?&@2me*0$Gpg`)TKl9I
z;)9)cdjm}rcaRXLRC!O?-W#ehmDd~LP=W)a&ini4S3-euIZHelc9Q;Jn1vpd#HqJ`
z$-~F8Fh!Z)2>=W)S@(u<Cy8UER#?(Yl0kaq%9YlclJ3amRAQ~dObLb8Nv5Dp4y$UO
zvdp4%vt3Dt9TW)T3c>kS&>M!YNSR1^g=RlP4Sg_aHIMncZL4A%CzC65f0`PXpgCrP
zZ{lv--t|kp%BN}a*QildguWB>Bvo#wsbC)3-3k5iheGuQu|j}*5p$cU=$fplyZStp
z75Xw@Zy?gnBiW#W-~F3qH+71+%eqDRlTHzs!h-R4$CBU07OkarBke?ky(`mCk))Zn
ziBxs4S7rikIRSwv=yfIe%=p_Nec>v<Whm891z9Sbw5d{Bm7~dd8Y<qTfM3?oDk3R*
zBF<3na^mQUW+*Zh_Vx19=8NRK>Z_Iu0(4o!XxNR5jSF_mUa}V&wfRNcnqOEvJActN
F{s}4|`=0;+

literal 0
HcmV?d00001

diff --git a/backend/src/main.py b/backend/src/main.py
index e0072b0..772daca 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -1,23 +1,26 @@
-import fastapi
-from dataclasses import dataclass
+from optimizer import solve_optimization
+from optimizer import landmark
+
+def main():
+    
+    # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
+    max_steps = 16
 
 
-@dataclass
-class Destination:
-    name: str
-    location: tuple
-    attractiveness: int
+    # Initialize all landmarks (+ start and goal). Order matters here
+    landmarks = []
+    landmarks.append(landmark("départ", -1, (0, 0)))
+    landmarks.append(landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
+    landmarks.append(landmark("arc de triomphe", 99, (0,4)))
+    landmarks.append(landmark("louvre", 99, (0,6)))
+    landmarks.append(landmark("montmartre", 99, (0,10)))
+    landmarks.append(landmark("concorde", 99, (0,8)))
+    landmarks.append(landmark("arrivée", -1, (0, 0)))
+
+
+    visiting_order = solve_optimization(landmarks, max_steps, True)
 
 
 
-d = Destination()
-
-
-
-def get_route() -> list[Destination]:
-    return {"route": "Hello World"}
-
-endpoint = ("/get_route", get_route)
-end
 if __name__ == "__main__":
-    fastapi.run()
+    main()
\ No newline at end of file
diff --git a/backend/src/main_example.py b/backend/src/main_example.py
new file mode 100644
index 0000000..e0072b0
--- /dev/null
+++ b/backend/src/main_example.py
@@ -0,0 +1,23 @@
+import fastapi
+from dataclasses import dataclass
+
+
+@dataclass
+class Destination:
+    name: str
+    location: tuple
+    attractiveness: int
+
+
+
+d = Destination()
+
+
+
+def get_route() -> list[Destination]:
+    return {"route": "Hello World"}
+
+endpoint = ("/get_route", get_route)
+end
+if __name__ == "__main__":
+    fastapi.run()
diff --git a/backend/src/optimizer.py b/backend/src/optimizer.py
index aaf214b..66efaeb 100644
--- a/backend/src/optimizer.py
+++ b/backend/src/optimizer.py
@@ -11,9 +11,8 @@ class landmark :
         self.loc = loc
 
 
-
-
-def untangle2(resx: list) :
+# Convert the solution of the optimization into the list of edges to follow. Order is taken into account
+def untangle(resx: list) :
     N = len(resx)                   # length of res
     L = int(np.sqrt(N))             # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
     n_edges = resx.sum()      # number of edges
@@ -40,65 +39,31 @@ def untangle2(resx: list) :
 
     return order
 
-# Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks
-def untangle(resx: list) :
-    N = len(resx)                # length of res
-    L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
-    n_landmarks = resx.sum()     # number of edges
-    visit_order = []
-    cnt = 0
-
-    if n_landmarks % 2 == 1 :                                     # if odd number of visited checkpoints
-        for i in range(L) :
-            for j in range(L) :
-                if res[i*L + j] == 1 :              # if index is 1
-                    cnt += 1                        # increment counter
-                    if cnt % 2 == 1 :               # if counter odd
-                        visit_order.append(i)
-                        visit_order.append(j)
-    else :                                   # if even number of ones
-        for i in range(L) :
-            for j in range(L) :
-                if res[i*L + j] == 1 :              # if index is one
-                    cnt += 1                        # increment counter
-                    if j % (L-1) == 0 :             # if last node
-                        visit_order.append(j)       # append only the last index
-                        return visit_order          # return
-                    if cnt % 2 == 1 : 
-                        visit_order.append(i)
-                        visit_order.append(j)
-    return visit_order
-
 # Just to print the result
-def print_res(res: list, landmarks: list, P) :
+def print_res(res, landmarks: list, P) :
     X = abs(res.x)
+    order = untangle(X)
 
-    N = int(np.sqrt(len(X)))
+    """N = int(np.sqrt(len(X)))
     for i in range(N):
         print(X[i*N:i*N+N])
-
-    order = untangle2(X)
-
-    order_ideal = [0, 0, 0, 0, 0, 0, 1, 0]
-
-    # print("Optimal value:", -res.fun)  # Minimization, so we negate to get the maximum
-    # print("Optimal point:", res.x)
-    
-    #for i,x in enumerate(X) : X[i] = round(x,0)
-    
-    #print(order)
+    print("Optimal value:", -res.fun)  # Minimization, so we negate to get the maximum
+    print("Optimal point:", res.x)
+    for i,x in enumerate(X) : X[i] = round(x,0)
+    print(order)"""
 
     if (X.sum()+1)**2 == len(X) : 
-        print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest')
+        print('\nAll landmarks can be visited within max_steps, the following order is suggested : ')
     else :
-        print('Could not visit all the landmarks, the following order could be the fastest but not sure')
-    print("Order of visit :")
+        print('Could not visit all the landmarks, the following order is suggested : ')
+
     for idx in order : 
         print('- ' + landmarks[idx].name)
 
     steps = path_length(P, abs(res.x))
     print("\nSteps walked : " + str(steps))
 
+    return order
 
 # Checks for cases of circular symmetry in the result
 def has_circle(resx: list) :
@@ -164,8 +129,8 @@ def break_sym(landmarks, A_ub, b_ub):
 
     return A_ub, b_ub
 
-
-def prevent_circle(landmarks, A_ub, b_ub, circle) :
+# Constraint to not have circular paths. Want to go from start -> finish without unconnected loops
+def break_circle(landmarks, A_ub, b_ub, circle) :
     N = len(landmarks)
     l = [0]*N*N
 
@@ -195,7 +160,7 @@ def respect_number(landmarks, A_ub, b_ub):
         print("\n")"""
     return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks)
 
-# Constraint to tie the problem together and have a connected path
+# Constraint to tie the problem together. Necessary but not sufficient to avoid circles
 def respect_order(landmarks: list, A_eq, b_eq): 
     N = len(landmarks)
     for i in range(N-1) :     # Prevent stacked ones
@@ -294,68 +259,62 @@ def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) :
 def path_length(P: list, resx: list) :
     return np.dot(P, resx)
 
-# Initialize all landmarks (+ start and goal). Order matters here
-landmarks = []
-landmarks.append(landmark("départ", -1, (0, 0)))
-landmarks.append(landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
-landmarks.append(landmark("arc de triomphe", 99, (0,4)))
-landmarks.append(landmark("louvre", 99, (0,6)))
-landmarks.append(landmark("montmartre", 99, (0,10)))
-landmarks.append(landmark("concorde", 99, (0,8)))
-landmarks.append(landmark("arrivée", -1, (0, 0)))
+# Main optimization pipeline
+def solve_optimization (landmarks, max_steps, printing) :
 
+    # SET CONSTRAINTS FOR INEQUALITY
+    c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other
+    P = A_ub                                                        # store the paths for later. Needed to compute path length
+    A_ub, b_ub = respect_number(landmarks, A_ub, b_ub)              # Respect max number of visits. 
 
+    # TODO : Problems with circular symmetry
+    A_ub, b_ub = break_sym(landmarks, A_ub, b_ub)                  # break the symmetry. Only use the upper diagonal values
 
-# CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
-max_steps = 16
+    # SET CONSTRAINTS FOR EQUALITY
+    A_eq, b_eq = init_eq_not_stay(landmarks)                       # Force solution not to stay in same place
+    A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal
 
-# SET CONSTRAINTS FOR INEQUALITY
-c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other
-P = A_ub                                                        # store the paths for later. Needed to compute path length
-A_ub, b_ub = respect_number(landmarks, A_ub, b_ub)              # Respect max number of visits. 
+    A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order of visit (only works when max_steps is limiting factor)
 
-# TODO : Problems with circular symmetry
-A_ub, b_ub = break_sym(landmarks, A_ub, b_ub)                  # break the symmetry. Only use the upper diagonal values
+    # Bounds for variables (x can only be 0 or 1)
+    x_bounds = [(0, 1)] * len(c)
 
-# SET CONSTRAINTS FOR EQUALITY
-A_eq, b_eq = init_eq_not_stay(landmarks)                       # Force solution not to stay in same place
-A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal
+    # Solve linear programming problem
 
-A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order of visit (only works when max_steps is limiting factor)
-
-# Bounds for variables (x can only be 0 or 1)
-x_bounds = [(0, 1)] * len(c)
-
-# Solve linear programming problem
-
-res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
-circle = has_circle(res.x)
-
-while len(circle) != 0 :
-    print("The solution has a circular path. Not interpretable.")
-    print("Need to add constraints until no circle ")
-
-    A_ub, b_ub = prevent_circle(landmarks, A_ub, b_ub, circle)
     res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
     circle = has_circle(res.x)
+    i = 0
+
+    # Break the circular symmetry if needed
+    while len(circle) != 0 :
+        A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
+        res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
+        circle = has_circle(res.x)
+        i += 1
 
 
+    # Raise error if no solution is found
+    if not res.success :
 
-# Raise error if no solution is found
-if not res.success :
-    print(f"No solution has been found within given timeframe.\nMinimum steps to visit all must_see is : {H}")
-    # Override the max_steps using the heuristic
-    for i, val in enumerate(b_ub) :
-        if val == max_steps : b_ub[i] = H
+        # Override the max_steps using the heuristic
+        for i, val in enumerate(b_ub) :
+            if val == max_steps : b_ub[i] = H
 
-    # Solve problem again :
-    res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
-
-
-# Print result
-print_res(res, landmarks, P)
+        # Solve problem again :
+        res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
 
+        if not res.success :
+            raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
+    
 
+    if printing is True :
+        if i != 0 :
+            print(f"Neded to recompute paths {i} times because of unconnected loops...")
+            X = print_res(res, landmarks, P)
+            return X
+
+    else :
+        return untangle(res.x)
 
 
 
-- 
2.47.2


From 101af0ebc62cd89f5378b29559b336013c147e2e Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Wed, 22 May 2024 15:01:46 +0200
Subject: [PATCH 16/33] included fastapi

---
 backend/app/__init__.py                       |   0
 .../app/__pycache__/__init__.cpython-310.pyc  | Bin 0 -> 199 bytes
 backend/app/__pycache__/main.cpython-310.pyc  | Bin 0 -> 986 bytes
 backend/app/dependencies.py                   |   1 +
 backend/{src => app}/main.py                  |  18 +++++++++++++-----
 backend/app/src/__init__.py                   |   0
 .../src/__pycache__/__init__.cpython-310.pyc  | Bin 0 -> 203 bytes
 .../src/__pycache__/optimizer.cpython-310.pyc | Bin 6065 -> 6114 bytes
 backend/{ => app}/src/main_example.py         |   0
 backend/{ => app}/src/optimizer.py            |  10 +++++-----
 10 files changed, 19 insertions(+), 10 deletions(-)
 create mode 100644 backend/app/__init__.py
 create mode 100644 backend/app/__pycache__/__init__.cpython-310.pyc
 create mode 100644 backend/app/__pycache__/main.cpython-310.pyc
 create mode 100644 backend/app/dependencies.py
 rename backend/{src => app}/main.py (59%)
 create mode 100644 backend/app/src/__init__.py
 create mode 100644 backend/app/src/__pycache__/__init__.cpython-310.pyc
 rename backend/{ => app}/src/__pycache__/optimizer.cpython-310.pyc (51%)
 rename backend/{ => app}/src/main_example.py (100%)
 rename backend/{ => app}/src/optimizer.py (98%)

diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/__pycache__/__init__.cpython-310.pyc b/backend/app/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..678d7d066fb628b94456ab8b9b89e3cd338ff648
GIT binary patch
literal 199
zcmd1j<>g`kg7aH^(?IlN5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;x_kvsFxJacWU<
zOm=2YW@3y>esXDUYF<fkOn|e3x2KP%qo0DVLUCnYa*S_cL9s%9o<ea+YEgb>N^wkD
zVsVLXUTR5seo?k=USe5hdSXdteqKycVsdtBUP?@2K|xG>d}dx|NqoFsLFFwDo80`A
Q(wtN~kQ<7bfCLKz08t+`9{>OV

literal 0
HcmV?d00001

diff --git a/backend/app/__pycache__/main.cpython-310.pyc b/backend/app/__pycache__/main.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..21f3cee4583cade7331b40935c4fb79e719f22cd
GIT binary patch
literal 986
zcmZ8f%}(1u5Z<*niSt(=;?SdybyeVws;Y>p5^6z6s29tUwRqR*Dz?|!-J~TXr}Qa$
zf+NqtTlm^j)HmQlXRQ!O$J){CH{<>0PxkvA1oH6rG<{1D`eQFIr2&_ZaC8KUA%-)Q
z;}BzOJ77DZQ`>Il<|HH-ak4;RgEwxPjND?@;4RkVUP@StdB4a4hi%?r?UAG2@%942
zznzQv1l=Vtpa53`Ewc%aWobk%rql>o0P1D5U^!JIuv?$0Hb>_t(>DjJD1LCA)1R?6
zywvl9>r#or#74%6EZ{Xhui9&Owf(<0aV+ynpb4l6=NOLu4Qhzy&dj+%1~beY<}Tb@
zD1U_z)?m#Iqy>n#fwTeXY#?1gdK*X|kXIYX)-#s@{u!F#g*$iI_6)I|nVXUe6b#PB
zEy$4F-<4FE<s(SoC=5`ord=b)%I6|YdA3XdUU`?c;OQ1siO;xil#qG(oj*Bk2f}71
z#}l>2Y)8UQE(-%AFweMMp3s9XCsMI>v<K+eDyo#2+~4tO7uxUF)A0{0+zH&ONz0NK
zOt}!IY(4=|cG*--)qa|`4qr`#7Dg1q*ixuFq_!dKAw52fzG<#>G!mJh(I=UVb6yx7
zoge#OPQIQTo%wsdzABRFl$P3;h2>XLFde0^K6?c>KcpJ%6?7to^_E4KG#Noo5u_I7
zR22K=RYj~}Ytg%)h&RA5zn<G8Y5<PPi(?xZ$7ew|>}!?mKdq*!%c~0wTb7!ZLfIx)
zE)Z4jkekYdeXYo)l-c^Vg62Gq>tbK1oto=?F4;KaAMB{LrSG90?%}56xx3hV-NEGX
EA3sR{+W-In

literal 0
HcmV?d00001

diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py
new file mode 100644
index 0000000..2cb6801
--- /dev/null
+++ b/backend/app/dependencies.py
@@ -0,0 +1 @@
+import app.src
\ No newline at end of file
diff --git a/backend/src/main.py b/backend/app/main.py
similarity index 59%
rename from backend/src/main.py
rename to backend/app/main.py
index 772daca..84b3141 100644
--- a/backend/src/main.py
+++ b/backend/app/main.py
@@ -1,10 +1,15 @@
-from optimizer import solve_optimization
-from optimizer import landmark
+from src.optimizer import solve_optimization
+from src.optimizer import landmark
+from fastapi import FastAPI
 
-def main():
+app = FastAPI()
+
+
+@app.get("/optimize/{max_steps}/{print_to_console}")
+def main(max_steps: int, print_to_console: bool):
     
     # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
-    max_steps = 16
+    #max_steps = 16
 
 
     # Initialize all landmarks (+ start and goal). Order matters here
@@ -18,8 +23,11 @@ def main():
     landmarks.append(landmark("arrivée", -1, (0, 0)))
 
 
-    visiting_order = solve_optimization(landmarks, max_steps, True)
+    visiting_order = solve_optimization(landmarks, max_steps, print_to_console)
 
+    #return visiting_order
+
+    return("max steps :", max_steps, "\n", visiting_order)
 
 
 if __name__ == "__main__":
diff --git a/backend/app/src/__init__.py b/backend/app/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/src/__pycache__/__init__.cpython-310.pyc b/backend/app/src/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3c06a0824710e098468b6c39d10d3d27ee83ac35
GIT binary patch
literal 203
zcmYk0u?oU47=%;lAVS|Et3!PN5m9t0+9HVC4<)9lgtmW5{#5BBx%)J(j=q7D>7)nu
z9e2~2D4H;@_aiNbd=2<l;LFOJLXUzS*)+)R%ntJpACz-Ak+KApuC;)zkzJ$E5gayr
zm+X^u#$!%BN=QXZ+#n~XtkDHQDaggB+|Af3Msd{_;j}?G3t1^FK(sB8m5}EewafEa
T+ds#P#uQzxmctJkd4%i>W2QF)

literal 0
HcmV?d00001

diff --git a/backend/src/__pycache__/optimizer.cpython-310.pyc b/backend/app/src/__pycache__/optimizer.cpython-310.pyc
similarity index 51%
rename from backend/src/__pycache__/optimizer.cpython-310.pyc
rename to backend/app/src/__pycache__/optimizer.cpython-310.pyc
index 2446c047e04e4aa4cec9c2a5628a2c89ec72a9c7..55a6337409776ed6c3207b9db525d84153adfb21 100644
GIT binary patch
delta 1087
zcmZ9K-%Aux6vy{mXJ*G8cN}MJS8XM2(-@;n!^}iO%luJ+7FkS%Zo8Wrvb)_G!(gRA
zj9%+NP!UwnQ%~Yk)Qj}gLqQKcG}KEk734#IK+rwcU(mvQ_TF>P`QGn2Gs~^tTg^f=
z8VQNt((g0ZYMf<r4Orng{sp%zT`aDQGnavh%gW$hT_maEIW8XWfq)+>g|5RQER^}I
zR3`qFsnCLoJJs5By{~3P1VVX5tQ1<6loO#Vu?0=+)e@An+R$|^5t1kBv+!o*xyZbc
z^7RtiR9|EZVNs*`<)!c<Y*yfs`VJyEtfgvsz3>tk#k4;?F`LUz`P1AI5l6>6+K%!-
z^QUL>Q(k}%wWg*P@?Iwp0UZ=ztVS0rrZXFCu$To3;~gtswT}QN@s-{UNAQRK3l8I_
z@*(KNx=0EN*c)krUd%?GI$c6ZUhkB1lIBMV-2#D@nc<n)9N$He9&9o8!(F^+)I%TM
zG@jH^rK9~{1k=0|_f<5(V;rl9!2sqeUK{<SkRWuS6K&Kw*A+*jd*CI`MN=?>kD}T5
zDGHw!2-tjEz;3m<Y`#6fnAu$QMnpYLKo?!t03XE>^9A(dnwd0us7y)@V|C>oSjDc&
zMi|3OmG93Vrn5MKM$Acpj}pQHj<N3ktXKz-37}8PxT;qy_-$?^JLCBie)_7I!LUeK
zkGJ!qc*|;r&-lzr8RMiiNEllAXqhzgZ&kbd<+buM+4|Ov8$+Oc^0R4RU4Wtu0s#z(
z8TQ#bG}+lOUbgqaPrPRz0vF%f8BkUlVyj?ny{QcY_@{dHwlp+AQoV#Rf=d`7$Vo6-
z9U<%|^a;3OZ!VL`c^)U{L1a8VJC_^ayukSZ%IP3D0*<vkGS@2dnY=GP&52AQ?-j(D
z&dJxXZLE#zw!6C^wyr)uakVg)&wBhIogO1};$-|F=y*RqP^R)p{1PAOmv>5=vJ<k&
zWn;1vo3?VUvc)Re%HCzOt+buQ4=jPDM6xz)Ce5Vo=n1{rH0-#p+12rwovcv)0rJ7*
A^Z)<=

delta 1147
zcmZ9LO=uNY6oBX4`FZpHCgV$d6Jx~aGr>_bCNchy1Y={G_*ZPJq7x)M-y4%?e2I4^
zC4m=fNri4&C}&k9+DbPrT!hd?+fB7AH$uVHEQM0K=(<p-c+P!lz=8YCIp>~x{^rih
z?zL`bE}ze6^nCT`?6v2+*PMSq+^FFH5Ufv#%L1D4Cu3|VMGhvd5n9o0Laf!b>#zdL
zdMxTXy^XY1*T2_)(5m2RRne@)2HS54xfO>@?`U?}jLoW9hq}2%yKZ^f3V?QNN1UpQ
zx3N~lvPC{tY}c(7iozas16R%8A&q6Lv?s~Vm*eTl*|0VhPfM4|@KcE{!$|HSIe~Kt
zT#`FU4DeU0bH`qiq9P#!HVDyb(_7f01zh2RA!OgWnR*T|iZAQ|ID~HcZ#anmrN^Nk
zM=~X-;Z&v*2J!36s(+ZG$ZZ5at#ZjDY&c3ttjdh6%!YD1TL$pc>^``QUu3tzF?^7{
z*TyG(>-|G8E&K6&u5<Huv>oyM4d~)3p~H|P_)YHT-eEFIkFk+F)o9c>J{jIhjH|>(
z+J1H$z@_{y_z~~tOE8X4^VQ->HlHE{VzDO>OSBey0~~a^HvK}@$RgO3wH4%PjGTwN
zhsmIF24-2x@{(^h3h!Y107GRO!=a{KrQb-8S_w8dkkfM%$OUmnyvpgW5F)3`Gx%lG
zvz~)|jz5DMpyZ_TG=nddpKW9~OL7901dh{SZo6K7j0}Mp&CkWX@(4b3yWj;rb4%He
z`Q#|$!}Wh%2jB!+g`HPa5*2!w_uj>Se}Zq)H3G0_2O)uR)mV@YXTzJ5^D_J_F<5N$
zJcf4*d*Lr!EA+ty{IgI2ef?zf6L8;Mf;R<W+vX>qs{;8yk%Npgj0=o$hMI$5=@4Td
z;}{`GMGKWm7)6phNirA9vkT#<lyg$<XP-WXPw?H1fd%%woT<e#wJG@#?{huf>Q>4g
z);GHRgw()ZoV+@>P^(7r0PhYl`tjT10Z8G~;;3%P3Cwt>K2W(-d8z|0PpXDg2L)Eu
ud{t|kSXDi%Cc9akz(0hCkG=LSw$tvk*}m=BEl##jw5>u*vANKmGhPD<Q0Jxq

diff --git a/backend/src/main_example.py b/backend/app/src/main_example.py
similarity index 100%
rename from backend/src/main_example.py
rename to backend/app/src/main_example.py
diff --git a/backend/src/optimizer.py b/backend/app/src/optimizer.py
similarity index 98%
rename from backend/src/optimizer.py
rename to backend/app/src/optimizer.py
index 66efaeb..4c1b631 100644
--- a/backend/src/optimizer.py
+++ b/backend/app/src/optimizer.py
@@ -10,7 +10,6 @@ class landmark :
         self.attractiveness = attractiveness
         self.loc = loc
 
-
 # Convert the solution of the optimization into the list of edges to follow. Order is taken into account
 def untangle(resx: list) :
     N = len(resx)                   # length of res
@@ -43,6 +42,7 @@ def untangle(resx: list) :
 def print_res(res, landmarks: list, P) :
     X = abs(res.x)
     order = untangle(X)
+    things = []
 
     """N = int(np.sqrt(len(X)))
     for i in range(N):
@@ -59,11 +59,12 @@ def print_res(res, landmarks: list, P) :
 
     for idx in order : 
         print('- ' + landmarks[idx].name)
+        things.append(landmarks[idx].name)
 
     steps = path_length(P, abs(res.x))
     print("\nSteps walked : " + str(steps))
 
-    return order
+    return things
 
 # Checks for cases of circular symmetry in the result
 def has_circle(resx: list) :
@@ -260,7 +261,7 @@ def path_length(P: list, resx: list) :
     return np.dot(P, resx)
 
 # Main optimization pipeline
-def solve_optimization (landmarks, max_steps, printing) :
+def solve_optimization (landmarks, max_steps, printing_console) :
 
     # SET CONSTRAINTS FOR INEQUALITY
     c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other
@@ -307,12 +308,11 @@ def solve_optimization (landmarks, max_steps, printing) :
             raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
     
 
-    if printing is True :
+    if printing_console is True :
         if i != 0 :
             print(f"Neded to recompute paths {i} times because of unconnected loops...")
             X = print_res(res, landmarks, P)
             return X
-
     else :
         return untangle(res.x)
 
-- 
2.47.2


From 70cebc0aa19f900cac6bf0bca8cee79fd2e9934a Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Wed, 22 May 2024 15:12:54 +0200
Subject: [PATCH 17/33] fixed return order

---
 backend/app/__pycache__/main.cpython-310.pyc  | Bin 986 -> 980 bytes
 backend/app/main.py                           |   6 +--
 .../src/__pycache__/optimizer.cpython-310.pyc | Bin 6114 -> 6107 bytes
 backend/app/src/optimizer.py                  |  37 ++++++++++--------
 4 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/backend/app/__pycache__/main.cpython-310.pyc b/backend/app/__pycache__/main.cpython-310.pyc
index 21f3cee4583cade7331b40935c4fb79e719f22cd..cd67059540ba463b9a64189908a944b49246cc02 100644
GIT binary patch
delta 66
zcmcb`eubSkpO=@50SIK@`lcyP<lV%mI`M$4Qc7w`VrEWp?JdsS#ESUhlGK9YTf7BD
PnRzAgP^rzVj8~Ze$R8I(

delta 71
zcmcb@ev6$qpO=@50SJD*_D$28$h(PAW8wi>jgtKM<ovwi{G8O<Tb#Lx74gL-sRhNi
S1PY2W^Gf0ovK#+jWdZ<!dK<|A

diff --git a/backend/app/main.py b/backend/app/main.py
index 84b3141..d7e153c 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -5,8 +5,8 @@ from fastapi import FastAPI
 app = FastAPI()
 
 
-@app.get("/optimize/{max_steps}/{print_to_console}")
-def main(max_steps: int, print_to_console: bool):
+@app.get("/optimize/{max_steps}/{print_details}")
+def main(max_steps: int, print_details: bool):
     
     # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
     #max_steps = 16
@@ -23,7 +23,7 @@ def main(max_steps: int, print_to_console: bool):
     landmarks.append(landmark("arrivée", -1, (0, 0)))
 
 
-    visiting_order = solve_optimization(landmarks, max_steps, print_to_console)
+    visiting_order = solve_optimization(landmarks, max_steps, print_details)
 
     #return visiting_order
 
diff --git a/backend/app/src/__pycache__/optimizer.cpython-310.pyc b/backend/app/src/__pycache__/optimizer.cpython-310.pyc
index 55a6337409776ed6c3207b9db525d84153adfb21..e60b82c34deb09c2e891f1f4f1cc3ba6d701d1f9 100644
GIT binary patch
delta 488
zcmZ{gK~EDw6vyYyY<IW2v)d`9r60jIK@JAtKme0+(P)f4)HE7H7%;VfIy-P6Z&Q*z
z5>LwkLi8h$aPh;`n;*bWK#Xtdp<bMq{PN!W=l$Qy{9O65l738*h|#<F_55I|dzTIx
zP%jS}R}+jsgxQg;Vx@o{dn@1YT;>TT2xD#rNPUzW?DI1lpg@nR07Hy4*Ft+=!aSbM
zk98$aG3_(W`pibuiwnqW+Rvr+u~vZg$HIKG64}h7R(Q0>%PV&BAKSSJN*%1Re~q#;
z<T^ZI<~wQ5_|_)6it>i(=&L9Im`yRJRem9Ig^Et9{XMDAI`NARH$XpOnuK9_zqncU
znlGbyue@yTfGVfW2TzAVcCxp(pJnE;n^<>Pc6j3O&|%e~L+I2^*X6GWL-omwj_v69
z{X4yHcSna=`MUL}zDCkiobDb@Av0T4l-I50-Zmvlc%VQDDHcHSRJ1+6qJ#o|-WERh
bfZ}%)c;NIlL=b|!5B-p4Bxw;Z!=>pTJ|}Fx

delta 411
zcmYk1F-yZh6vyx7k~B?|^t4UQDmd7oRuHWeQ4|Xf3Ti<?bSO<b*kbUKIK6-v1V`Z<
z1ocA*;)jWwlY<{Y!3&6Z!w>$vAOCyrt@++`0^2r;eAk~lL$Chq99DpdVdZ`%GO%=7
zOd+v+EwLjVtrUE1M0p&UNKi!$EfH0xJAWS9zTP7L-EdEKl)}fzLM??&a$+4TiJds;
zAdD1#gGJe5uaI*TSoOAkW!o>La0RINMojpttj)iDi5$O(_5<<FCz=9~sEj4Kh@R4<
zjO8xzNtZ+{Us`-r_e~D+D(*^%w!16t+zw2L%$;rX<$Tb{u`Y2`2;+W#5Qcn7rp!F(
zjpIRw^MLcY+*!;~m#8x_Uvmrn;4BQr1F>COaMto!OCqBey|K8hH8xgdVpIhNj9PRO
onBq`RH9>_ud0>J8)|YND%0RjFsIC}bP(%8bbkuKC6EKzi0M12SO8@`>

diff --git a/backend/app/src/optimizer.py b/backend/app/src/optimizer.py
index 4c1b631..2681469 100644
--- a/backend/app/src/optimizer.py
+++ b/backend/app/src/optimizer.py
@@ -261,7 +261,7 @@ def path_length(P: list, resx: list) :
     return np.dot(P, resx)
 
 # Main optimization pipeline
-def solve_optimization (landmarks, max_steps, printing_console) :
+def solve_optimization (landmarks, max_steps, printing_details) :
 
     # SET CONSTRAINTS FOR INEQUALITY
     c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other
@@ -283,15 +283,7 @@ def solve_optimization (landmarks, max_steps, printing_console) :
     # Solve linear programming problem
 
     res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
-    circle = has_circle(res.x)
-    i = 0
-
-    # Break the circular symmetry if needed
-    while len(circle) != 0 :
-        A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
-        res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
-        circle = has_circle(res.x)
-        i += 1
+    
 
 
     # Raise error if no solution is found
@@ -305,16 +297,29 @@ def solve_optimization (landmarks, max_steps, printing_console) :
         res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
 
         if not res.success :
-            raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
+            s = "No solution could be found, even when increasing max_steps using the heuristic"
+            return s
+            #raise ValueError("No solution could be found, even when increasing max_steps using the heuristic")
     
+    # If there is a solution, we're good to go, just check for
+    else :
+        circle = has_circle(res.x)
+        i = 0
 
-    if printing_console is True :
-        if i != 0 :
-            print(f"Neded to recompute paths {i} times because of unconnected loops...")
+        # Break the circular symmetry if needed
+        while len(circle) != 0 :
+            A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle)
+            res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3)
+            circle = has_circle(res.x)
+            i += 1
+
+        if printing_details is True :
+            if i != 0 :
+                print(f"Neded to recompute paths {i} times because of unconnected loops...")
             X = print_res(res, landmarks, P)
             return X
-    else :
-        return untangle(res.x)
+        else :
+            return untangle(res.x)
 
 
 
-- 
2.47.2


From d8d425a922f139a52914c0beae1755cb1aa590a0 Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Thu, 23 May 2024 10:39:14 +0200
Subject: [PATCH 18/33] added dataclass

---
 backend/app/main.py          | 22 +++++++++++-----------
 backend/app/src/optimizer.py | 11 ++++++-----
 2 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/backend/app/main.py b/backend/app/main.py
index d7e153c..17ba5c6 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,5 +1,5 @@
 from src.optimizer import solve_optimization
-from src.optimizer import landmark
+from src.optimizer import Landmark
 from fastapi import FastAPI
 
 app = FastAPI()
@@ -12,18 +12,18 @@ def main(max_steps: int, print_details: bool):
     #max_steps = 16
 
 
-    # Initialize all landmarks (+ start and goal). Order matters here
-    landmarks = []
-    landmarks.append(landmark("départ", -1, (0, 0)))
-    landmarks.append(landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
-    landmarks.append(landmark("arc de triomphe", 99, (0,4)))
-    landmarks.append(landmark("louvre", 99, (0,6)))
-    landmarks.append(landmark("montmartre", 99, (0,10)))
-    landmarks.append(landmark("concorde", 99, (0,8)))
-    landmarks.append(landmark("arrivée", -1, (0, 0)))
+    # Initialize all Landmarks (+ start and goal). Order matters here
+    Landmarks = []
+    Landmarks.append(Landmark("départ", -1, (0, 0)))
+    Landmarks.append(Landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
+    Landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
+    Landmarks.append(Landmark("louvre", 99, (0,6)))
+    Landmarks.append(Landmark("montmartre", 99, (0,10)))
+    Landmarks.append(Landmark("concorde", 99, (0,8)))
+    Landmarks.append(Landmark("arrivée", -1, (0, 0)))
 
 
-    visiting_order = solve_optimization(landmarks, max_steps, print_details)
+    visiting_order = solve_optimization(Landmarks, max_steps, print_details)
 
     #return visiting_order
 
diff --git a/backend/app/src/optimizer.py b/backend/app/src/optimizer.py
index 2681469..527b8eb 100644
--- a/backend/app/src/optimizer.py
+++ b/backend/app/src/optimizer.py
@@ -1,14 +1,15 @@
 from scipy.optimize import linprog
 import numpy as np
 from scipy.linalg import block_diag
+from dataclasses import dataclass
 
 
 # Defines the landmark class (aka some place there is to visit)
-class landmark :
-    def __init__(self, name: str, attractiveness: int, loc: tuple):
-        self.name = name
-        self.attractiveness = attractiveness
-        self.loc = loc
+@dataclass
+class Landmark :
+    name : str
+    attractiveness : int
+    loc : tuple
 
 # Convert the solution of the optimization into the list of edges to follow. Order is taken into account
 def untangle(resx: list) :
-- 
2.47.2


From 99dc8b5e6780766693f8cd07c37b17b68371eedd Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Thu, 23 May 2024 12:20:32 +0200
Subject: [PATCH 19/33] added OSM api

---
 backend/app/main.py                  | 22 ++++++------
 backend/app/src/landmarks_manager.py | 52 ++++++++++++++++++++++++++++
 backend/app/src/optimizer.py         |  8 +----
 3 files changed, 64 insertions(+), 18 deletions(-)
 create mode 100644 backend/app/src/landmarks_manager.py

diff --git a/backend/app/main.py b/backend/app/main.py
index 17ba5c6..38863e2 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,5 +1,5 @@
 from src.optimizer import solve_optimization
-from src.optimizer import Landmark
+from src.landmarks_manager import Landmark
 from fastapi import FastAPI
 
 app = FastAPI()
@@ -12,18 +12,18 @@ def main(max_steps: int, print_details: bool):
     #max_steps = 16
 
 
-    # Initialize all Landmarks (+ start and goal). Order matters here
-    Landmarks = []
-    Landmarks.append(Landmark("départ", -1, (0, 0)))
-    Landmarks.append(Landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
-    Landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
-    Landmarks.append(Landmark("louvre", 99, (0,6)))
-    Landmarks.append(Landmark("montmartre", 99, (0,10)))
-    Landmarks.append(Landmark("concorde", 99, (0,8)))
-    Landmarks.append(Landmark("arrivée", -1, (0, 0)))
+    # Initialize all landmarks (+ start and goal). Order matters here
+    landmarks = []
+    landmarks.append(Landmark("départ", -1, (0, 0)))
+    landmarks.append(Landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
+    landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
+    landmarks.append(Landmark("louvre", 99, (0,6)))
+    landmarks.append(Landmark("montmartre", 99, (0,10)))
+    landmarks.append(Landmark("concorde", 99, (0,8)))
+    landmarks.append(Landmark("arrivée", -1, (0, 0)))
 
 
-    visiting_order = solve_optimization(Landmarks, max_steps, print_details)
+    visiting_order = solve_optimization(landmarks, max_steps, print_details)
 
     #return visiting_order
 
diff --git a/backend/app/src/landmarks_manager.py b/backend/app/src/landmarks_manager.py
new file mode 100644
index 0000000..df2bb9f
--- /dev/null
+++ b/backend/app/src/landmarks_manager.py
@@ -0,0 +1,52 @@
+from OSMPythonTools.api import Api
+from OSMPythonTools.overpass import Overpass
+from dataclasses import dataclass
+
+
+# Defines the landmark class (aka some place there is to visit)
+@dataclass
+class Landmark :
+    name : str
+    attractiveness : int
+    id : int
+
+
+# Converts a OSM id to a landmark
+def add_from_id(id: int, score: int) :
+
+    try :
+        s = 'way/' + str(id)           # prepare string for query
+        obj =  api.query(s)                             # object to add
+    except :
+        s = 'relation/' + str(id)           # prepare string for query
+        obj =  api.query(s)                             # object to add
+
+    return Landmark(obj.tag('name:fr'), score, id)      # create Landmark out of it
+
+
+# take a lsit of tuples (id, score) to generate a list of landmarks
+def generate_landmarks(ids_and_scores: list) :
+
+    L = []
+    for tup in ids_and_scores :
+        L.append(add_from_id(tup[0], tup[1]))
+
+    return L
+
+api = Api()
+
+
+l = (7515426, 70)
+t = (5013364, 100)
+n = (201611261, 99)
+a = (226413508, 50)
+m = (23762981, 30)
+
+
+ids_and_scores = [t, l, n, a, m]
+
+landmarks = generate_landmarks(ids_and_scores)
+
+
+for obj in landmarks :
+    print(obj)
\ No newline at end of file
diff --git a/backend/app/src/optimizer.py b/backend/app/src/optimizer.py
index 527b8eb..d74d1b9 100644
--- a/backend/app/src/optimizer.py
+++ b/backend/app/src/optimizer.py
@@ -1,15 +1,9 @@
 from scipy.optimize import linprog
 import numpy as np
 from scipy.linalg import block_diag
-from dataclasses import dataclass
 
 
-# Defines the landmark class (aka some place there is to visit)
-@dataclass
-class Landmark :
-    name : str
-    attractiveness : int
-    loc : tuple
+# landmarks = [Landmark_1, Landmark_2, ...]
 
 # Convert the solution of the optimization into the list of edges to follow. Order is taken into account
 def untangle(resx: list) :
-- 
2.47.2


From 656c606493367abe6ce1f394b137ca95972461d6 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 25 May 2024 13:32:53 +0200
Subject: [PATCH 20/33] first try of automated docker builds

---
 .gitea/workflows/backed_build-image.yaml     | 30 ++++++++++++++++++++
 .gitea/workflows/frontend_build-android.yaml |  2 +-
 backend/Dockerfile                           |  4 +--
 3 files changed, 33 insertions(+), 3 deletions(-)
 create mode 100644 .gitea/workflows/backed_build-image.yaml

diff --git a/.gitea/workflows/backed_build-image.yaml b/.gitea/workflows/backed_build-image.yaml
new file mode 100644
index 0000000..9f062e5
--- /dev/null
+++ b/.gitea/workflows/backed_build-image.yaml
@@ -0,0 +1,30 @@
+on:
+  pull_request:
+    branches:
+      - main
+
+name: Build and push docker image
+
+jobs:
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+
+    - uses: https://gitea.com/actions/checkout@v4
+    
+    - name: Login to Docker Registry
+      uses: docker/login-action@v3
+      with:
+        registry: git.kluster.moll.re
+        username: ${{ gitea.repository_owner }}
+        password: ${{ secrets.GITEA_TOKEN}}
+
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v3
+
+    - name: Build and push
+      uses: docker/build-push-action@v5
+      with:
+        context: backend
+        tags: git.kluster.moll.re/renoll/fast_network_navigation/backend:latest
diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index de430ce..99ce9b1 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -8,7 +8,7 @@ name: Build and release APK
 jobs:
   build:
     name: Build APK
-    runs-on: k8s
+    runs-on: ubuntu-latest
     steps:
 
     - name: Install prerequisites
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 1535818..79dbe76 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,10 +1,10 @@
 FROM python:3
 
 WORKDIR /app
-COPY Pipfile Pipfile.lock /app/
+COPY Pipfile Pipfile.lock .
 
 RUN pip install pipenv
-RUN pipenv install --deploy --ignore-pipfile
+RUN pipenv install --deploy --system
 
 COPY . /src
 
-- 
2.47.2


From cb33f315a05fe913ed6a3b8db85f815072d8f1ef Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 25 May 2024 14:19:57 +0200
Subject: [PATCH 21/33] final building cleanup

---
 .gitea/workflows/backed_build-image.yaml     |  1 +
 .gitea/workflows/frontend_build-android.yaml |  4 ++-
 .gitea/workflows/frontend_build-web.yaml     |  6 ++--
 .gitea/workflows/frontend_test.yaml          | 33 --------------------
 4 files changed, 8 insertions(+), 36 deletions(-)
 delete mode 100644 .gitea/workflows/frontend_test.yaml

diff --git a/.gitea/workflows/backed_build-image.yaml b/.gitea/workflows/backed_build-image.yaml
index 9f062e5..52343d5 100644
--- a/.gitea/workflows/backed_build-image.yaml
+++ b/.gitea/workflows/backed_build-image.yaml
@@ -28,3 +28,4 @@ jobs:
       with:
         context: backend
         tags: git.kluster.moll.re/renoll/fast_network_navigation/backend:latest
+        push: true
diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index 99ce9b1..a035322 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -2,6 +2,9 @@ on:
   pull_request:
     branches:
       - main
+    paths:
+      - frontend/**
+
 
 name: Build and release APK
 
@@ -38,7 +41,6 @@ jobs:
     - run: flutter build apk --debug --split-per-abi
       working-directory: ./frontend
 
-
     - name: Release APK
       uses: https://gitea.com/akkuman/gitea-release-action@v1
       with:
diff --git a/.gitea/workflows/frontend_build-web.yaml b/.gitea/workflows/frontend_build-web.yaml
index 416754a..f5980fe 100644
--- a/.gitea/workflows/frontend_build-web.yaml
+++ b/.gitea/workflows/frontend_build-web.yaml
@@ -2,13 +2,16 @@ on:
   pull_request:
     branches:
       - main
+    paths:
+      - frontend/**
+
 
 name: Build web
 
 jobs:
   build:
     name: Build Web
-    runs-on: k8s
+    runs-on: ubuntu-latest
     steps:
 
     - name: Install prerequisites
@@ -27,6 +30,5 @@ jobs:
     - run: flutter pub get
       working-directory: ./frontend
 
-
     - run: flutter build web
       working-directory: ./frontend
diff --git a/.gitea/workflows/frontend_test.yaml b/.gitea/workflows/frontend_test.yaml
deleted file mode 100644
index aee253d..0000000
--- a/.gitea/workflows/frontend_test.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-on:
-  push:
-  pull_request:
-    branches:
-      - main
-
-
-name: Test code
-
-jobs:
-  test:
-    name: Test code
-    runs-on: k8s
-    steps:
-
-    - name: Install prerequisites
-      run: |
-        sudo apt-get update
-        sudo apt-get install -y xz-utils
-
-    - uses: actions/checkout@v4
-
-    - uses: https://github.com/subosito/flutter-action@v2
-      with:
-        channel: stable
-        flutter-version: 3.19.6
-        cache: true
-
-    - run: flutter pub get
-      working-directory: ./frontend
-
-    - run: flutter test
-      working-directory: ./frontend
-- 
2.47.2


From f292b2a3756ecc279de146f6e59a42ff37f29fd7 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 25 May 2024 12:23:22 +0000
Subject: [PATCH 22/33] Update .gitea/workflows/backed_build-image.yaml

---
 .gitea/workflows/backed_build-image.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitea/workflows/backed_build-image.yaml b/.gitea/workflows/backed_build-image.yaml
index 52343d5..2f1d21c 100644
--- a/.gitea/workflows/backed_build-image.yaml
+++ b/.gitea/workflows/backed_build-image.yaml
@@ -27,5 +27,5 @@ jobs:
       uses: docker/build-push-action@v5
       with:
         context: backend
-        tags: git.kluster.moll.re/renoll/fast_network_navigation/backend:latest
+        tags: git.kluster.moll.re/remoll/fast_network_navigation/backend:latest
         push: true
-- 
2.47.2


From 3029fb85374ded3b3f713b9f6d4ad4c5c96ecfa7 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 25 May 2024 18:55:58 +0200
Subject: [PATCH 23/33] some preference improvements

---
 .../android/app/src/main/AndroidManifest.xml  |   3 +
 .../{modules/scaffold.dart => layout.dart}    |   8 +-
 frontend/lib/main.dart                        |   2 +-
 frontend/lib/modules/overview.dart            |   6 +-
 frontend/lib/modules/profile.dart             | 114 ++++++++++------
 frontend/lib/structs/destination.dart         |  39 +++++-
 frontend/lib/structs/preferences.dart         |  82 ++++++++++++
 frontend/lib/structs/route.dart               |  14 ++
 frontend/lib/utils/get_route.dart             |  18 +++
 .../Flutter/GeneratedPluginRegistrant.swift   |   2 +
 frontend/pubspec.lock                         | 124 +++++++++++++++++-
 frontend/pubspec.yaml                         |   2 +
 frontend/test/widget_test.dart                |   7 +-
 13 files changed, 365 insertions(+), 56 deletions(-)
 rename frontend/lib/{modules/scaffold.dart => layout.dart} (92%)
 create mode 100644 frontend/lib/structs/preferences.dart
 create mode 100644 frontend/lib/structs/route.dart
 create mode 100644 frontend/lib/utils/get_route.dart

diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
index 58e6f22..f144d58 100644
--- a/frontend/android/app/src/main/AndroidManifest.xml
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -48,4 +48,7 @@
             <data android:mimeType="text/plain"/>
         </intent>
     </queries>
+
+    <!-- Required to fetch data from the internet. -->
+    <uses-permission android:name="android.permission.INTERNET" />
 </manifest>
diff --git a/frontend/lib/modules/scaffold.dart b/frontend/lib/layout.dart
similarity index 92%
rename from frontend/lib/modules/scaffold.dart
rename to frontend/lib/layout.dart
index 822f208..875ccbc 100644
--- a/frontend/lib/modules/scaffold.dart
+++ b/frontend/lib/layout.dart
@@ -4,9 +4,10 @@ import 'package:fast_network_navigation/modules/overview.dart';
 import 'package:fast_network_navigation/modules/profile.dart';
 
 
+// BasePage is the scaffold that holds all other pages
+// A side drawer is used to switch between pages
 class BasePage extends StatefulWidget {
   const BasePage({super.key, required this.title});
-
   final String title;
 
   @override
@@ -22,7 +23,7 @@ class _BasePageState extends State<BasePage> {
     });
   }
 
-  Widget currentView = MainPage();
+  Widget currentView = NavigationOverview();
   @override
   Widget build(BuildContext context) {
     final ThemeData theme = Theme.of(context);
@@ -51,7 +52,7 @@ class _BasePageState extends State<BasePage> {
                 // Update the state of the app
                 _onItemTapped(0);
                 // Then close the drawer
-                currentView = MainPage();
+                currentView = NavigationOverview();
                 Navigator.pop(context);
               },
             ),
@@ -87,3 +88,4 @@ class _BasePageState extends State<BasePage> {
     );
   }
 }
+
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index 424c0d5..d818628 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -1,5 +1,5 @@
 import 'package:flutter/material.dart';
-import 'package:fast_network_navigation/modules/scaffold.dart';
+import 'package:fast_network_navigation/layout.dart';
 
 void main() => runApp(const App());
 
diff --git a/frontend/lib/modules/overview.dart b/frontend/lib/modules/overview.dart
index d9bc126..bfcf18f 100644
--- a/frontend/lib/modules/overview.dart
+++ b/frontend/lib/modules/overview.dart
@@ -9,9 +9,9 @@ import 'package:fast_network_navigation/modules/map.dart';
 
 
 
-class MainPage extends StatefulWidget {
+class NavigationOverview extends StatefulWidget {
   @override
-  State<MainPage> createState() => _MainPageState();
+  State<NavigationOverview> createState() => _NavigationOverviewState();
 }
 
 
@@ -35,7 +35,7 @@ class Debounce {
 }
 
 
-class _MainPageState extends State<MainPage> {
+class _NavigationOverviewState extends State<NavigationOverview> {
 
   @override
   Widget build(BuildContext context) {
diff --git a/frontend/lib/modules/profile.dart b/frontend/lib/modules/profile.dart
index 69bfba4..e8beb08 100644
--- a/frontend/lib/modules/profile.dart
+++ b/frontend/lib/modules/profile.dart
@@ -1,3 +1,4 @@
+import 'package:fast_network_navigation/structs/preferences.dart';
 import 'package:flutter/material.dart';
 
 
@@ -8,48 +9,83 @@ class ProfilePage extends StatefulWidget {
 }
 
 class _ProfilePageState extends State<ProfilePage> {
-  
-  double value = 0.0;
-  void onChanged(double newValue) {
-    setState(() {
-      value = newValue;
-    });
-  }
   @override
   Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: AppBar(
-        title: Text('Profile'),
-      ),
-      body: Padding(
-        padding: EdgeInsets.all(8.0),
-        child: Column(
-          children: <Widget>[
-            Card(
-              child: ListTile(
-                leading: Icon(Icons.notifications_sharp),
-                title: Text('Notification 1'),
-                subtitle: Text('This is a first notification'),
-              ),
-            ),
-            Card(
-              child: ListTile(
-                leading: Icon(Icons.notifications_sharp),
-                title: Text('Notification 2'),
-                subtitle: Text('This is a notification'),
-              ),
-            ),
-            Card(
-              child: ListTile(
-                leading: Icon(Icons.outdoor_grill),
-                title: Text("Eating preference"),
-                subtitle: Slider.adaptive(value: value, onChanged: onChanged, min: 0, max: 5, divisions: 5, label: value.toInt().toString(),)
-                
-              )
-            )
-          ],
+    return ListView(
+      children: [
+        // First a round, centered image
+        Center(
+          child: CircleAvatar(
+            radius: 100,
+            child: Icon(Icons.person, size: 100),
+          )
         ),
-      )
+        Center(
+          child: Text('Curious traveler', style: TextStyle(fontSize: 24))
+        ),
+
+        Padding(
+          padding: EdgeInsets.all(10),
+        ),
+
+        Text('Please rate your preferences for the following activities:'),
+
+        // Now the sliders
+        ImportanceSliders()
+      ]
     );
   }
 }
+
+
+
+class ImportanceSliders extends StatefulWidget {
+
+  @override
+  State<ImportanceSliders> createState() => _ImportanceSlidersState();
+}
+
+
+class _ImportanceSlidersState extends State<ImportanceSliders> {
+
+  UserPreferences _prefs = UserPreferences();
+
+  @override
+  void initState() {
+    super.initState();
+    _prefs.load();
+  }
+
+  List<Card> _createSliders() {
+    List<Card> sliders = [];
+    for (SinglePreference pref in _prefs.preferences) {
+      sliders.add(Card(
+        child: ListTile(
+          leading: pref.icon,
+          title: Text(pref.name),
+          subtitle: Slider(
+            value: pref.value.toDouble(),
+            min: 0,
+            max: 10,
+            divisions: 10,
+            label: pref.value.toString(),
+            onChanged: (double newValue) {
+              setState(() {
+                pref.value = newValue.toInt();
+                _prefs.save();
+              });
+            },
+          )
+        ),
+        margin: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
+        shadowColor: Colors.grey,
+      ));
+    }
+    return sliders;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(children: _createSliders());
+  }
+}
diff --git a/frontend/lib/structs/destination.dart b/frontend/lib/structs/destination.dart
index bb8d086..c44fbf9 100644
--- a/frontend/lib/structs/destination.dart
+++ b/frontend/lib/structs/destination.dart
@@ -1,31 +1,62 @@
+import "package:flutter/material.dart";
 
 class Destination {
   final double latitude;
   final double longitude;
   final String name;
   final String description;
-  final DestinationType type;
+  // final DestinationType type;
   final Duration duration;
   final bool visited;
 
-  Destination({
+  const Destination({
     required this.latitude,
     required this.longitude,
     required this.name,
     required this.description,
-    required this.type,
+    // required this.type,
     required this.duration,
     required this.visited,
   });
+
+  factory Destination.fromJson(Map<String, dynamic> json) {
+  return switch (json) {
+    {
+      'lat': double latitude,
+      'lon': double longitude,
+      'name': String name,
+      'description': String description,
+      // 'type': String type,
+      'duration': int duration,
+      'visited': bool visited
+
+    } =>
+      Destination(
+        latitude: latitude,
+        longitude: longitude,
+        name: name,
+        description: description,
+        // type: "DestinationType.values.firstWhere((element) => element.name == type)",
+        duration: Duration(minutes: duration),
+        visited: visited
+      ),
+    _ => throw const FormatException('Failed to load destination.'),
+  };
+}
+
 }
 
 
 class DestinationType {
   final String name;
   final String description;
+  final Icon icon;
   
-  DestinationType({
+  const DestinationType({
     required this.name,
     required this.description,
+    required this.icon,
   });
 }
+
+
diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart
new file mode 100644
index 0000000..7746393
--- /dev/null
+++ b/frontend/lib/structs/preferences.dart
@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+
+class SinglePreference {
+  String name;
+  String description;
+  int value;
+  Icon icon;
+  String key;
+
+  SinglePreference({
+    required this.name,
+    required this.description,
+    required this.value,
+    required this.icon,
+    required this.key,
+  });
+}
+
+
+class UserPreferences {
+  List<SinglePreference> preferences = [
+    SinglePreference(
+      name: "Sightseeing",
+      description: "How much do you like sightseeing?",
+      value: 0,
+      icon: Icon(Icons.church),
+      key: "sightseeing",
+    ),
+    SinglePreference(
+      name: "Shopping",
+      description: "How much do you like shopping?",
+      value: 0,
+      icon: Icon(Icons.shopping_bag),
+      key: "shopping",
+    ),
+    SinglePreference(
+      name: "Foods & Drinks",
+      description: "How much do you like eating?",
+      value: 0,
+      icon: Icon(Icons.restaurant),
+      key: "eating",
+    ),
+    SinglePreference(
+      name: "Nightlife",
+      description: "How much do you like nightlife?",
+      value: 0,
+      icon: Icon(Icons.wine_bar),
+      key: "nightlife",
+    ),
+    SinglePreference(
+      name: "Nature",
+      description: "How much do you like nature?",
+      value: 0,
+      icon: Icon(Icons.landscape),
+      key: "nature",
+    ),
+    SinglePreference(
+      name: "Culture",
+      description: "How much do you like culture?",
+      value: 0,
+      icon: Icon(Icons.palette),
+      key: "culture",
+    ),
+  ];
+
+
+  void save() async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    for (SinglePreference pref in preferences) {
+      prefs.setInt(pref.key, pref.value);
+    }
+  }
+
+  void load() async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    for (SinglePreference pref in preferences) {
+      pref.value = prefs.getInt(pref.key) ?? 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/frontend/lib/structs/route.dart b/frontend/lib/structs/route.dart
new file mode 100644
index 0000000..8b46787
--- /dev/null
+++ b/frontend/lib/structs/route.dart
@@ -0,0 +1,14 @@
+import "package:fast_network_navigation/structs/destination.dart";
+
+
+class Route {
+  final String name;
+  final Duration duration;
+  final List<Destination> destinations;
+  
+  Route({
+    required this.name,
+    required this.duration,
+    required this.destinations
+  });
+}
\ No newline at end of file
diff --git a/frontend/lib/utils/get_route.dart b/frontend/lib/utils/get_route.dart
new file mode 100644
index 0000000..fccf6ac
--- /dev/null
+++ b/frontend/lib/utils/get_route.dart
@@ -0,0 +1,18 @@
+import "package:fast_network_navigation/structs/destination.dart";
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+
+Future<Destination> fetchDestination() async {
+  final response = await http
+      .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
+
+  if (response.statusCode == 200) {
+    // If the server did return a 200 OK response,
+    // then parse the JSON.
+    return Destination.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
+  } else {
+    // If the server did not return a 200 OK response,
+    // then throw an exception.
+    throw Exception('Failed to load destination');
+  }
+}
\ No newline at end of file
diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
index cccf817..724bb2a 100644
--- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,8 @@
 import FlutterMacOS
 import Foundation
 
+import shared_preferences_foundation
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
 }
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index 21fb46f..7c2f396 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -65,6 +65,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.1"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -193,7 +209,7 @@ packages:
     source: hosted
     version: "0.15.4"
   http:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: http
       sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
@@ -288,6 +304,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.9.0"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.4"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -304,6 +352,62 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.0"
+  shared_preferences:
+    dependency: "direct main"
+    description:
+      name: shared_preferences
+      sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.3"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.2"
+  shared_preferences_foundation:
+    dependency: transitive
+    description:
+      name: shared_preferences_foundation
+      sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.0"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -405,6 +509,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.5.1"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.5.1"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.4"
 sdks:
-  dart: ">=3.3.4 <4.0.0"
+  dart: ">=3.4.0 <4.0.0"
   flutter: ">=3.19.0"
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index b0409b2..9f1732c 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -39,6 +39,8 @@ dependencies:
   geocoding: ^3.0.0
   geocode: ^1.0.3
   google_maps_flutter: ^2.6.1
+  http: ^1.2.1
+  shared_preferences: ^2.2.3
 
 dev_dependencies:
   flutter_test:
diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart
index 1eacbc4..143b24f 100644
--- a/frontend/test/widget_test.dart
+++ b/frontend/test/widget_test.dart
@@ -9,16 +9,15 @@ import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 // import 'package:fast_network_navigation/main.dart';
-import 'package:fast_network_navigation/modules/scaffold.dart';
+import 'package:fast_network_navigation/layout.dart';
 
 void main() {
   testWidgets('Counter increments smoke test', (WidgetTester tester) async {
     // Build our app and trigger a frame.
     await tester.pumpWidget(BasePage(title: "City Nav"));
 
-    // Verify that our counter starts at 0.
-    expect(find.text('0'), findsOneWidget);
-    expect(find.text('1'), findsNothing);
+    // Verfiy that the title is displayed
+    expect(find.text('City Nav'), findsOneWidget);
 
     // Tap the '+' icon and trigger a frame.
     await tester.tap(find.byIcon(Icons.add));
-- 
2.47.2


From ae9860cc8ec44adba9d7f55c54b59af075746bc6 Mon Sep 17 00:00:00 2001
From: Kilian Scheidecker <kilian.scheidecker@orange.fr>
Date: Wed, 29 May 2024 18:14:40 +0200
Subject: [PATCH 24/33] proper fast api suage

---
 backend/.gitignore                   |  164 ++
 backend/Pipfile                      |   26 +-
 backend/Pipfile.lock                 | 2369 ++++++++++++++++----------
 backend/app/main.py                  |   68 +-
 backend/app/src/landmarks_manager.py |  107 +-
 backend/app/src/main_example.py      |   46 +-
 6 files changed, 1774 insertions(+), 1006 deletions(-)
 create mode 100644 backend/.gitignore

diff --git a/backend/.gitignore b/backend/.gitignore
new file mode 100644
index 0000000..2e722c1
--- /dev/null
+++ b/backend/.gitignore
@@ -0,0 +1,164 @@
+# osm-cache
+cache/
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
\ No newline at end of file
diff --git a/backend/Pipfile b/backend/Pipfile
index dfcd2a4..f02576b 100644
--- a/backend/Pipfile
+++ b/backend/Pipfile
@@ -1,14 +1,12 @@
-[[source]]
-url = "https://pypi.org/simple"
-verify_ssl = true
-name = "pypi"
-
-[packages]
-numpy = "*"
-scipy = "*"
-fastapi = "*"
-
-[dev-packages]
-
-[requires]
-python_version = "3.12"
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+numpy = "*"
+scipy = "*"
+fastapi = "*"
+osmpythontools = "*"
+
+[dev-packages]
diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock
index a4c1f54..f07cd8e 100644
--- a/backend/Pipfile.lock
+++ b/backend/Pipfile.lock
@@ -1,884 +1,1485 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "338629802198c23b4efab9dde90a89e178caf0840d99ff5c5936550b91d64d5e"
-        },
-        "pipfile-spec": 6,
-        "requires": {
-            "python_version": "3.12"
-        },
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "annotated-types": {
-            "hashes": [
-                "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43",
-                "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==0.6.0"
-        },
-        "anyio": {
-            "hashes": [
-                "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8",
-                "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==4.3.0"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
-                "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==2024.2.2"
-        },
-        "click": {
-            "hashes": [
-                "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
-                "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==8.1.7"
-        },
-        "dnspython": {
-            "hashes": [
-                "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50",
-                "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==2.6.1"
-        },
-        "email-validator": {
-            "hashes": [
-                "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84",
-                "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==2.1.1"
-        },
-        "fastapi": {
-            "hashes": [
-                "sha256:97ecbf994be0bcbdadedf88c3150252bed7b2087075ac99735403b1b76cc8fc0",
-                "sha256:b9db9dd147c91cb8b769f7183535773d8741dd46f9dc6676cd82eab510228cd7"
-            ],
-            "index": "pypi",
-            "markers": "python_version >= '3.8'",
-            "version": "==0.111.0"
-        },
-        "fastapi-cli": {
-            "hashes": [
-                "sha256:3b6e4d2c4daee940fb8db59ebbfd60a72c4b962bcf593e263e4cc69da4ea3d7f",
-                "sha256:ae233115f729945479044917d949095e829d2d84f56f55ce1ca17627872825a5"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==0.0.3"
-        },
-        "h11": {
-            "hashes": [
-                "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
-                "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==0.14.0"
-        },
-        "httpcore": {
-            "hashes": [
-                "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61",
-                "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==1.0.5"
-        },
-        "httptools": {
-            "hashes": [
-                "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563",
-                "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142",
-                "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d",
-                "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b",
-                "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4",
-                "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb",
-                "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658",
-                "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084",
-                "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2",
-                "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97",
-                "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837",
-                "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3",
-                "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58",
-                "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da",
-                "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d",
-                "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90",
-                "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0",
-                "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1",
-                "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2",
-                "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e",
-                "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0",
-                "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf",
-                "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc",
-                "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3",
-                "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503",
-                "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a",
-                "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3",
-                "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949",
-                "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84",
-                "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb",
-                "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a",
-                "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f",
-                "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e",
-                "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81",
-                "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185",
-                "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"
-            ],
-            "version": "==0.6.1"
-        },
-        "httpx": {
-            "hashes": [
-                "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
-                "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==0.27.0"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
-                "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==3.7"
-        },
-        "jinja2": {
-            "hashes": [
-                "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
-                "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==3.1.4"
-        },
-        "markdown-it-py": {
-            "hashes": [
-                "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
-                "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==3.0.0"
-        },
-        "markupsafe": {
-            "hashes": [
-                "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
-                "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
-                "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
-                "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
-                "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
-                "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
-                "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
-                "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
-                "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
-                "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
-                "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
-                "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
-                "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
-                "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
-                "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
-                "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
-                "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
-                "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
-                "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
-                "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
-                "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
-                "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
-                "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
-                "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
-                "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
-                "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
-                "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
-                "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
-                "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
-                "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
-                "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
-                "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
-                "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
-                "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
-                "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
-                "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
-                "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
-                "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
-                "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
-                "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
-                "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
-                "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
-                "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
-                "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
-                "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
-                "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
-                "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
-                "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
-                "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
-                "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
-                "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
-                "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
-                "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
-                "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
-                "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
-                "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
-                "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
-                "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
-                "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
-                "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==2.1.5"
-        },
-        "mdurl": {
-            "hashes": [
-                "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
-                "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==0.1.2"
-        },
-        "numpy": {
-            "hashes": [
-                "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b",
-                "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818",
-                "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20",
-                "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0",
-                "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010",
-                "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a",
-                "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea",
-                "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c",
-                "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71",
-                "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110",
-                "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be",
-                "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a",
-                "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a",
-                "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5",
-                "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed",
-                "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd",
-                "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c",
-                "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e",
-                "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0",
-                "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c",
-                "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a",
-                "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b",
-                "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0",
-                "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6",
-                "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2",
-                "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a",
-                "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30",
-                "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218",
-                "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5",
-                "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07",
-                "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2",
-                "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4",
-                "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764",
-                "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef",
-                "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3",
-                "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"
-            ],
-            "index": "pypi",
-            "markers": "python_version >= '3.9'",
-            "version": "==1.26.4"
-        },
-        "orjson": {
-            "hashes": [
-                "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2",
-                "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b",
-                "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5",
-                "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b",
-                "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0",
-                "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0",
-                "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7",
-                "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42",
-                "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5",
-                "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa",
-                "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818",
-                "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25",
-                "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08",
-                "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7",
-                "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b",
-                "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754",
-                "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0",
-                "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912",
-                "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b",
-                "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3",
-                "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700",
-                "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78",
-                "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290",
-                "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134",
-                "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294",
-                "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0",
-                "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5",
-                "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d",
-                "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d",
-                "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16",
-                "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195",
-                "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da",
-                "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8",
-                "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098",
-                "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8",
-                "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063",
-                "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534",
-                "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d",
-                "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109",
-                "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d",
-                "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3",
-                "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2",
-                "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf",
-                "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069",
-                "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7",
-                "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==3.10.3"
-        },
-        "pydantic": {
-            "hashes": [
-                "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5",
-                "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==2.7.1"
-        },
-        "pydantic-core": {
-            "hashes": [
-                "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b",
-                "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a",
-                "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90",
-                "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d",
-                "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e",
-                "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d",
-                "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027",
-                "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804",
-                "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347",
-                "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400",
-                "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3",
-                "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399",
-                "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349",
-                "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd",
-                "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c",
-                "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e",
-                "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413",
-                "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3",
-                "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e",
-                "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3",
-                "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91",
-                "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce",
-                "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c",
-                "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb",
-                "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664",
-                "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6",
-                "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd",
-                "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3",
-                "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af",
-                "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043",
-                "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350",
-                "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7",
-                "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0",
-                "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563",
-                "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761",
-                "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72",
-                "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3",
-                "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb",
-                "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788",
-                "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b",
-                "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c",
-                "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038",
-                "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250",
-                "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec",
-                "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c",
-                "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74",
-                "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81",
-                "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439",
-                "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75",
-                "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0",
-                "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8",
-                "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150",
-                "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438",
-                "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae",
-                "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857",
-                "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038",
-                "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374",
-                "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f",
-                "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241",
-                "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592",
-                "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4",
-                "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d",
-                "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b",
-                "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b",
-                "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182",
-                "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e",
-                "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641",
-                "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70",
-                "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9",
-                "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a",
-                "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543",
-                "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b",
-                "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f",
-                "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38",
-                "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845",
-                "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2",
-                "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0",
-                "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4",
-                "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==2.18.2"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
-                "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==2.18.0"
-        },
-        "python-dotenv": {
-            "hashes": [
-                "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
-                "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
-            ],
-            "version": "==1.0.1"
-        },
-        "python-multipart": {
-            "hashes": [
-                "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026",
-                "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==0.0.9"
-        },
-        "pyyaml": {
-            "hashes": [
-                "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
-                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
-                "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
-                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
-                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
-                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
-                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
-                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
-                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
-                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
-                "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
-                "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
-                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
-                "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
-                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
-                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
-                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
-                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
-                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
-                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
-                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
-                "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
-                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
-                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
-                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
-                "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
-                "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
-                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
-                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
-                "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
-                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
-                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
-                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
-                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
-                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
-                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
-                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
-                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
-                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
-                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
-                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
-                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
-                "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
-                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
-                "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
-                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
-                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
-                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
-                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
-                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
-                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
-            ],
-            "version": "==6.0.1"
-        },
-        "rich": {
-            "hashes": [
-                "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222",
-                "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"
-            ],
-            "markers": "python_full_version >= '3.7.0'",
-            "version": "==13.7.1"
-        },
-        "scipy": {
-            "hashes": [
-                "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922",
-                "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5",
-                "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa",
-                "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820",
-                "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd",
-                "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42",
-                "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e",
-                "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d",
-                "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86",
-                "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e",
-                "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c",
-                "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602",
-                "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e",
-                "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5",
-                "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a",
-                "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21",
-                "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d",
-                "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6",
-                "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78",
-                "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551",
-                "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7",
-                "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4",
-                "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d",
-                "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b",
-                "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"
-            ],
-            "index": "pypi",
-            "markers": "python_version >= '3.9'",
-            "version": "==1.13.0"
-        },
-        "shellingham": {
-            "hashes": [
-                "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
-                "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.5.4"
-        },
-        "sniffio": {
-            "hashes": [
-                "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
-                "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==1.3.1"
-        },
-        "starlette": {
-            "hashes": [
-                "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee",
-                "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==0.37.2"
-        },
-        "typer": {
-            "hashes": [
-                "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914",
-                "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==0.12.3"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0",
-                "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==4.11.0"
-        },
-        "ujson": {
-            "hashes": [
-                "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e",
-                "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b",
-                "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6",
-                "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7",
-                "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9",
-                "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd",
-                "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569",
-                "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f",
-                "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51",
-                "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20",
-                "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1",
-                "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf",
-                "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc",
-                "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e",
-                "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a",
-                "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539",
-                "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27",
-                "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165",
-                "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126",
-                "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1",
-                "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816",
-                "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64",
-                "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8",
-                "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e",
-                "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287",
-                "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3",
-                "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb",
-                "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0",
-                "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043",
-                "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557",
-                "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e",
-                "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21",
-                "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d",
-                "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd",
-                "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0",
-                "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337",
-                "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753",
-                "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804",
-                "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f",
-                "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f",
-                "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5",
-                "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5",
-                "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1",
-                "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00",
-                "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2",
-                "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050",
-                "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e",
-                "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4",
-                "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8",
-                "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996",
-                "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6",
-                "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1",
-                "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f",
-                "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1",
-                "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4",
-                "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b",
-                "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88",
-                "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518",
-                "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5",
-                "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770",
-                "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4",
-                "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a",
-                "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76",
-                "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe",
-                "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988",
-                "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1",
-                "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5",
-                "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b",
-                "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7",
-                "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8",
-                "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc",
-                "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a",
-                "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720",
-                "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3",
-                "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b",
-                "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9",
-                "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1",
-                "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==5.10.0"
-        },
-        "uvicorn": {
-            "extras": [
-                "standard"
-            ],
-            "hashes": [
-                "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de",
-                "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"
-            ],
-            "markers": "python_version >= '3.8'",
-            "version": "==0.29.0"
-        },
-        "uvloop": {
-            "hashes": [
-                "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd",
-                "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec",
-                "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b",
-                "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc",
-                "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797",
-                "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5",
-                "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2",
-                "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d",
-                "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be",
-                "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd",
-                "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12",
-                "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17",
-                "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef",
-                "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24",
-                "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428",
-                "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1",
-                "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849",
-                "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593",
-                "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd",
-                "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67",
-                "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6",
-                "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3",
-                "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd",
-                "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8",
-                "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7",
-                "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533",
-                "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957",
-                "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650",
-                "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e",
-                "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7",
-                "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"
-            ],
-            "version": "==0.19.0"
-        },
-        "watchfiles": {
-            "hashes": [
-                "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc",
-                "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365",
-                "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0",
-                "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e",
-                "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124",
-                "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c",
-                "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317",
-                "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094",
-                "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7",
-                "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235",
-                "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c",
-                "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c",
-                "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c",
-                "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235",
-                "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293",
-                "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa",
-                "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef",
-                "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19",
-                "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8",
-                "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d",
-                "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915",
-                "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429",
-                "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097",
-                "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe",
-                "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0",
-                "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d",
-                "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99",
-                "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1",
-                "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a",
-                "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895",
-                "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94",
-                "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562",
-                "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab",
-                "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360",
-                "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1",
-                "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7",
-                "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f",
-                "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03",
-                "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01",
-                "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58",
-                "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052",
-                "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e",
-                "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765",
-                "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6",
-                "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137",
-                "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85",
-                "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca",
-                "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f",
-                "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214",
-                "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7",
-                "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7",
-                "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3",
-                "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b",
-                "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7",
-                "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6",
-                "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994",
-                "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9",
-                "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec",
-                "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128",
-                "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c",
-                "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2",
-                "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078",
-                "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3",
-                "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e",
-                "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a",
-                "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6",
-                "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49",
-                "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b",
-                "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28",
-                "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9",
-                "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586",
-                "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400",
-                "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165",
-                "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303",
-                "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"
-            ],
-            "version": "==0.21.0"
-        },
-        "websockets": {
-            "hashes": [
-                "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b",
-                "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6",
-                "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df",
-                "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b",
-                "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205",
-                "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892",
-                "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53",
-                "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2",
-                "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed",
-                "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c",
-                "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd",
-                "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b",
-                "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931",
-                "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30",
-                "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370",
-                "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be",
-                "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec",
-                "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf",
-                "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62",
-                "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b",
-                "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402",
-                "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f",
-                "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123",
-                "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9",
-                "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603",
-                "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45",
-                "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558",
-                "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4",
-                "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438",
-                "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137",
-                "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480",
-                "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447",
-                "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8",
-                "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04",
-                "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c",
-                "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb",
-                "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967",
-                "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b",
-                "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d",
-                "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def",
-                "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c",
-                "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92",
-                "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2",
-                "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113",
-                "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b",
-                "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28",
-                "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7",
-                "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d",
-                "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f",
-                "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468",
-                "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8",
-                "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae",
-                "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611",
-                "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d",
-                "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9",
-                "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca",
-                "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f",
-                "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2",
-                "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077",
-                "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2",
-                "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6",
-                "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374",
-                "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc",
-                "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e",
-                "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53",
-                "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399",
-                "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547",
-                "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3",
-                "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870",
-                "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5",
-                "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8",
-                "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"
-            ],
-            "version": "==12.0"
-        }
-    },
-    "develop": {}
-}
+{
+    "_meta": {
+        "hash": {
+            "sha256": "15cc819367b908ccf7d61e3f2b45574c248556600dfeaa795e19045d6781fe90"
+        },
+        "pipfile-spec": 6,
+        "requires": {},
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "annotated-types": {
+            "hashes": [
+                "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
+                "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.7.0"
+        },
+        "anyio": {
+            "hashes": [
+                "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94",
+                "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.4.0"
+        },
+        "beautifulsoup4": {
+            "hashes": [
+                "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051",
+                "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"
+            ],
+            "markers": "python_full_version >= '3.6.0'",
+            "version": "==4.12.3"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
+                "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2024.2.2"
+        },
+        "click": {
+            "hashes": [
+                "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+                "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.7"
+        },
+        "contourpy": {
+            "hashes": [
+                "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2",
+                "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9",
+                "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9",
+                "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4",
+                "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce",
+                "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7",
+                "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f",
+                "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922",
+                "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4",
+                "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e",
+                "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b",
+                "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619",
+                "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205",
+                "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480",
+                "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965",
+                "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c",
+                "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd",
+                "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5",
+                "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f",
+                "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc",
+                "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec",
+                "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd",
+                "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b",
+                "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9",
+                "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe",
+                "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce",
+                "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609",
+                "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8",
+                "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0",
+                "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f",
+                "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8",
+                "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b",
+                "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364",
+                "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040",
+                "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f",
+                "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083",
+                "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df",
+                "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba",
+                "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445",
+                "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da",
+                "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3",
+                "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72",
+                "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02",
+                "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==1.2.1"
+        },
+        "cycler": {
+            "hashes": [
+                "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30",
+                "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.12.1"
+        },
+        "dnspython": {
+            "hashes": [
+                "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50",
+                "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.6.1"
+        },
+        "email-validator": {
+            "hashes": [
+                "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84",
+                "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.1.1"
+        },
+        "exceptiongroup": {
+            "hashes": [
+                "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad",
+                "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==1.2.1"
+        },
+        "fastapi": {
+            "hashes": [
+                "sha256:97ecbf994be0bcbdadedf88c3150252bed7b2087075ac99735403b1b76cc8fc0",
+                "sha256:b9db9dd147c91cb8b769f7183535773d8741dd46f9dc6676cd82eab510228cd7"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.8'",
+            "version": "==0.111.0"
+        },
+        "fastapi-cli": {
+            "hashes": [
+                "sha256:a2552f3a7ae64058cdbb530be6fa6dbfc975dc165e4fa66d224c3d396e25e809",
+                "sha256:e2e9ffaffc1f7767f488d6da34b6f5a377751c996f397902eb6abb99a67bde32"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.0.4"
+        },
+        "fonttools": {
+            "hashes": [
+                "sha256:00d9abf4b400f98fb895566eb298f60432b4b29048e3dc02807427b09a06604e",
+                "sha256:05e4291db6af66f466a203d9922e4c1d3e18ef16868f76f10b00e2c3b9814df2",
+                "sha256:15efb2ba4b8c2d012ee0bb7a850c2e4780c530cc83ec8e843b2a97f8b3a5fd4b",
+                "sha256:1dc626de4b204d025d029e646bae8fdbf5acd9217158283a567f4b523fda3bae",
+                "sha256:21921e5855c399d10ddfc373538b425cabcf8b3258720b51450909e108896450",
+                "sha256:309b617942041073ffa96090d320b99d75648ed16e0c67fb1aa7788e06c834de",
+                "sha256:346d08ff92e577b2dc5a0c228487667d23fe2da35a8b9a8bba22c2b6ba8be21c",
+                "sha256:35af630404223273f1d7acd4761f399131c62820366f53eac029337069f5826a",
+                "sha256:46cc5d06ee05fd239c45d7935aaffd060ee773a88b97e901df50478247472643",
+                "sha256:4b0b9eb0f55dce9c7278ad4175f1cbaed23b799dce5ecc20e3213da241584140",
+                "sha256:4b419207e53db1599b3d385afd4bca6692c219d53732890d0814a2593104d0e2",
+                "sha256:4c3ad89204c2d7f419436f1d6fde681b070c5e20b888beb57ccf92f640628cc9",
+                "sha256:52f6001814ec5e0c961cabe89642f7e8d7e07892b565057aa526569b9ebb711c",
+                "sha256:5ecb88318ff249bd2a715e7aec36774ce7ae3441128007ef72a39a60601f4a8f",
+                "sha256:70d87f2099006304d33438bdaa5101953b7e22e23a93b1c7b7ed0f32ff44b423",
+                "sha256:73ba38b98c012957940a04d9eb5439b42565ac892bba8cfc32e10d88e73921fe",
+                "sha256:7467161f1eed557dbcec152d5ee95540200b1935709fa73307da16bc0b7ca361",
+                "sha256:7dccf4666f716e5e0753f0fa28dad2f4431154c87747bc781c838b8a5dca990e",
+                "sha256:859399b7adc8ac067be8e5c80ef4bb2faddff97e9b40896a9de75606a43d0469",
+                "sha256:8873d6edd1dae5c088dd3d61c9fd4dd80c827c486fa224d368233e7f33dc98af",
+                "sha256:890e7a657574610330e42dd1e38d3b9e0a8cb0eff3da080f80995460a256d3dd",
+                "sha256:89b53386214197bd5b3e3c753895bad691de84726ced3c222a59cde1dd12d57b",
+                "sha256:8b186cd6b8844f6cf04a7e0a174bc3649d3deddbfc10dc59846a4381f796d348",
+                "sha256:9180775c9535389a665cae7c5282f8e07754beabf59b66aeba7f6bfeb32a3652",
+                "sha256:95e8a5975d08d0b624a14eec0f987e204ad81b480e24c5436af99170054434b8",
+                "sha256:9725687db3c1cef13c0f40b380c3c15bea0113f4d0231b204d58edd5f2a53d90",
+                "sha256:9a5d1b0475050056d2e3bc378014f2ea2230e8ae434eeac8dfb182aa8efaf642",
+                "sha256:9ed23a03b7d9f0e29ca0679eafe5152aeccb0580312a3fc36f0662e178b4791b",
+                "sha256:a4daf2751a98c69d9620717826ed6c5743b662ef0ae7bb33dc6c205425e48eba",
+                "sha256:a64e72d2c144630e017ac9c1c416ddf8ac43bef9a083bf81fe08c0695f0baa95",
+                "sha256:a791f002d1b717268235cfae7e4957b7fd132e92e2c5400e521bf191f1b3a9a5",
+                "sha256:b4cba644e2515d685d4ee3ca2fbb5d53930a0e9ec2cf332ed704dc341b145878",
+                "sha256:b9a22cf1adaae7b2ba2ed7d8651a4193a4f348744925b4b740e6b38a94599c5b",
+                "sha256:bb7d206fa5ba6e082ba5d5e1b7107731029fc3a55c71c48de65121710d817986",
+                "sha256:cf694159528022daa71b1777cb6ec9e0ebbdd29859f3e9c845826cafaef4ca29",
+                "sha256:d0184aa88865339d96f7f452e8c5b621186ef7638744d78bf9b775d67e206819",
+                "sha256:d272c7e173c3085308345ccc7fb2ad6ce7f415d777791dd6ce4e8140e354d09c",
+                "sha256:d2cc7906bc0afdd2689aaf88b910307333b1f936262d1d98f25dbf8a5eb2e829",
+                "sha256:e03dae26084bb3632b4a77b1cd0419159d2226911aff6dc4c7e3058df68648c6",
+                "sha256:e176249292eccd89f81d39f514f2b5e8c75dfc9cef8653bdc3021d06697e9eff",
+                "sha256:ebb183ed8b789cece0bd6363121913fb6da4034af89a2fa5408e42a1592889a8",
+                "sha256:fb8cd6559f0ae3a8f5e146f80ab2a90ad0325a759be8d48ee82758a0b89fa0aa"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.52.4"
+        },
+        "geojson": {
+            "hashes": [
+                "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac",
+                "sha256:68a9771827237adb8c0c71f8527509c8f5bef61733aa434cefc9c9d4f0ebe8f3"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.0"
+        },
+        "h11": {
+            "hashes": [
+                "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
+                "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.14.0"
+        },
+        "httpcore": {
+            "hashes": [
+                "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61",
+                "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==1.0.5"
+        },
+        "httptools": {
+            "hashes": [
+                "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563",
+                "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142",
+                "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d",
+                "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b",
+                "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4",
+                "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb",
+                "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658",
+                "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084",
+                "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2",
+                "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97",
+                "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837",
+                "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3",
+                "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58",
+                "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da",
+                "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d",
+                "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90",
+                "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0",
+                "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1",
+                "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2",
+                "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e",
+                "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0",
+                "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf",
+                "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc",
+                "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3",
+                "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503",
+                "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a",
+                "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3",
+                "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949",
+                "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84",
+                "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb",
+                "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a",
+                "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f",
+                "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e",
+                "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81",
+                "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185",
+                "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"
+            ],
+            "version": "==0.6.1"
+        },
+        "httpx": {
+            "hashes": [
+                "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
+                "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.27.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
+                "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.7"
+        },
+        "jinja2": {
+            "hashes": [
+                "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
+                "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.4"
+        },
+        "kiwisolver": {
+            "hashes": [
+                "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf",
+                "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e",
+                "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af",
+                "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f",
+                "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046",
+                "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3",
+                "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5",
+                "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71",
+                "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee",
+                "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3",
+                "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9",
+                "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b",
+                "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985",
+                "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea",
+                "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16",
+                "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89",
+                "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c",
+                "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9",
+                "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712",
+                "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342",
+                "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a",
+                "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958",
+                "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d",
+                "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a",
+                "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130",
+                "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff",
+                "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898",
+                "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b",
+                "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f",
+                "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265",
+                "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93",
+                "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929",
+                "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635",
+                "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709",
+                "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b",
+                "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb",
+                "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a",
+                "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920",
+                "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e",
+                "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544",
+                "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45",
+                "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390",
+                "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77",
+                "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355",
+                "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff",
+                "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4",
+                "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7",
+                "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20",
+                "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c",
+                "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162",
+                "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228",
+                "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437",
+                "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc",
+                "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a",
+                "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901",
+                "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4",
+                "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770",
+                "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525",
+                "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad",
+                "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a",
+                "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29",
+                "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90",
+                "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250",
+                "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d",
+                "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3",
+                "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54",
+                "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f",
+                "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1",
+                "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da",
+                "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238",
+                "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa",
+                "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523",
+                "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0",
+                "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205",
+                "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3",
+                "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4",
+                "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac",
+                "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9",
+                "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb",
+                "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced",
+                "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd",
+                "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0",
+                "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da",
+                "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18",
+                "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9",
+                "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276",
+                "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333",
+                "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b",
+                "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db",
+                "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126",
+                "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9",
+                "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09",
+                "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0",
+                "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec",
+                "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7",
+                "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff",
+                "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9",
+                "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192",
+                "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8",
+                "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d",
+                "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6",
+                "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797",
+                "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892",
+                "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.4.5"
+        },
+        "lxml": {
+            "hashes": [
+                "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3",
+                "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a",
+                "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0",
+                "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b",
+                "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f",
+                "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6",
+                "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73",
+                "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d",
+                "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad",
+                "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b",
+                "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a",
+                "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5",
+                "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab",
+                "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316",
+                "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6",
+                "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df",
+                "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca",
+                "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264",
+                "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8",
+                "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f",
+                "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b",
+                "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3",
+                "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5",
+                "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed",
+                "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab",
+                "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5",
+                "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726",
+                "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d",
+                "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632",
+                "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706",
+                "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8",
+                "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472",
+                "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835",
+                "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf",
+                "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db",
+                "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d",
+                "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545",
+                "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9",
+                "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be",
+                "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe",
+                "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905",
+                "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438",
+                "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db",
+                "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776",
+                "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c",
+                "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed",
+                "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd",
+                "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484",
+                "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d",
+                "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6",
+                "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30",
+                "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182",
+                "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61",
+                "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425",
+                "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb",
+                "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1",
+                "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511",
+                "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e",
+                "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207",
+                "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b",
+                "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585",
+                "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56",
+                "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391",
+                "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85",
+                "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147",
+                "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18",
+                "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1",
+                "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa",
+                "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48",
+                "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3",
+                "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184",
+                "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67",
+                "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7",
+                "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34",
+                "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706",
+                "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8",
+                "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c",
+                "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115",
+                "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009",
+                "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466",
+                "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526",
+                "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d",
+                "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525",
+                "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14",
+                "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3",
+                "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0",
+                "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b",
+                "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1",
+                "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f",
+                "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf",
+                "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf",
+                "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0",
+                "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b",
+                "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff",
+                "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88",
+                "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2",
+                "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40",
+                "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716",
+                "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2",
+                "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2",
+                "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a",
+                "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734",
+                "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87",
+                "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48",
+                "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36",
+                "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b",
+                "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07",
+                "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c",
+                "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573",
+                "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001",
+                "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9",
+                "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3",
+                "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce",
+                "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3",
+                "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04",
+                "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927",
+                "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083",
+                "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d",
+                "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32",
+                "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9",
+                "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f",
+                "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2",
+                "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c",
+                "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d",
+                "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393",
+                "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8",
+                "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6",
+                "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66",
+                "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5",
+                "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97",
+                "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196",
+                "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836",
+                "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae",
+                "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297",
+                "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421",
+                "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6",
+                "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981",
+                "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30",
+                "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30",
+                "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f",
+                "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324",
+                "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==5.2.2"
+        },
+        "markdown-it-py": {
+            "hashes": [
+                "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1",
+                "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.0.0"
+        },
+        "markupsafe": {
+            "hashes": [
+                "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
+                "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
+                "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
+                "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
+                "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
+                "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
+                "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
+                "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
+                "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
+                "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
+                "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
+                "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
+                "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
+                "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
+                "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
+                "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
+                "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
+                "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
+                "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
+                "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
+                "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
+                "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
+                "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
+                "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
+                "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
+                "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
+                "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
+                "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
+                "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
+                "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
+                "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
+                "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
+                "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
+                "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
+                "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
+                "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
+                "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
+                "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
+                "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
+                "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
+                "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
+                "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
+                "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
+                "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
+                "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
+                "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
+                "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
+                "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
+                "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
+                "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
+                "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
+                "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
+                "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
+                "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
+                "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
+                "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
+                "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
+                "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
+                "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
+                "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.1.5"
+        },
+        "matplotlib": {
+            "hashes": [
+                "sha256:063af8587fceeac13b0936c42a2b6c732c2ab1c98d38abc3337e430e1ff75e38",
+                "sha256:06a478f0d67636554fa78558cfbcd7b9dba85b51f5c3b5a0c9be49010cf5f321",
+                "sha256:0a490715b3b9984fa609116481b22178348c1a220a4499cda79132000a79b4db",
+                "sha256:0fc51eaa5262553868461c083d9adadb11a6017315f3a757fc45ec6ec5f02888",
+                "sha256:13beb4840317d45ffd4183a778685e215939be7b08616f431c7795276e067463",
+                "sha256:290d304e59be2b33ef5c2d768d0237f5bd132986bdcc66f80bc9bcc300066a03",
+                "sha256:2bcee1dffaf60fe7656183ac2190bd630842ff87b3153afb3e384d966b57fe56",
+                "sha256:2e7f03e5cbbfacdd48c8ea394d365d91ee8f3cae7e6ec611409927b5ed997ee4",
+                "sha256:3f988bafb0fa39d1074ddd5bacd958c853e11def40800c5824556eb630f94d3b",
+                "sha256:52146fc3bd7813cc784562cb93a15788be0b2875c4655e2cc6ea646bfa30344b",
+                "sha256:550cdda3adbd596078cca7d13ed50b77879104e2e46392dcd7c75259d8f00e85",
+                "sha256:616fabf4981a3b3c5a15cd95eba359c8489c4e20e03717aea42866d8d0465956",
+                "sha256:76cce0f31b351e3551d1f3779420cf8f6ec0d4a8cf9c0237a3b549fd28eb4abb",
+                "sha256:7ff2e239c26be4f24bfa45860c20ffccd118d270c5b5d081fa4ea409b5469fcd",
+                "sha256:8146ce83cbc5dc71c223a74a1996d446cd35cfb6a04b683e1446b7e6c73603b7",
+                "sha256:81c40af649d19c85f8073e25e5806926986806fa6d54be506fbf02aef47d5a89",
+                "sha256:9a2fa6d899e17ddca6d6526cf6e7ba677738bf2a6a9590d702c277204a7c6152",
+                "sha256:a5be985db2596d761cdf0c2eaf52396f26e6a64ab46bd8cd810c48972349d1be",
+                "sha256:af4001b7cae70f7eaacfb063db605280058246de590fa7874f00f62259f2df7e",
+                "sha256:bd4f2831168afac55b881db82a7730992aa41c4f007f1913465fb182d6fb20c0",
+                "sha256:bdd1ecbe268eb3e7653e04f451635f0fb0f77f07fd070242b44c076c9106da84",
+                "sha256:c53aeb514ccbbcbab55a27f912d79ea30ab21ee0531ee2c09f13800efb272674",
+                "sha256:c79f3a585f1368da6049318bdf1f85568d8d04b2e89fc24b7e02cc9b62017382",
+                "sha256:cd53c79fd02f1c1808d2cfc87dd3cf4dbc63c5244a58ee7944497107469c8d8a",
+                "sha256:d38e85a1a6d732f645f1403ce5e6727fd9418cd4574521d5803d3d94911038e5",
+                "sha256:d91a4ffc587bacf5c4ce4ecfe4bcd23a4b675e76315f2866e588686cc97fccdf",
+                "sha256:e6d29ea6c19e34b30fb7d88b7081f869a03014f66fe06d62cc77d5a6ea88ed7a",
+                "sha256:eaf3978060a106fab40c328778b148f590e27f6fa3cd15a19d6892575bce387d",
+                "sha256:fe428e191ea016bb278758c8ee82a8129c51d81d8c4bc0846c09e7e8e9057241"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==3.9.0"
+        },
+        "mdurl": {
+            "hashes": [
+                "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
+                "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.1.2"
+        },
+        "numpy": {
+            "hashes": [
+                "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b",
+                "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818",
+                "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20",
+                "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0",
+                "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010",
+                "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a",
+                "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea",
+                "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c",
+                "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71",
+                "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110",
+                "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be",
+                "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a",
+                "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a",
+                "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5",
+                "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed",
+                "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd",
+                "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c",
+                "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e",
+                "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0",
+                "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c",
+                "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a",
+                "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b",
+                "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0",
+                "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6",
+                "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2",
+                "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a",
+                "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30",
+                "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218",
+                "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5",
+                "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07",
+                "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2",
+                "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4",
+                "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764",
+                "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef",
+                "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3",
+                "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.9'",
+            "version": "==1.26.4"
+        },
+        "orjson": {
+            "hashes": [
+                "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2",
+                "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b",
+                "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5",
+                "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b",
+                "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0",
+                "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0",
+                "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7",
+                "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42",
+                "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5",
+                "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa",
+                "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818",
+                "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25",
+                "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08",
+                "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7",
+                "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b",
+                "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754",
+                "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0",
+                "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912",
+                "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b",
+                "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3",
+                "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700",
+                "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78",
+                "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290",
+                "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134",
+                "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294",
+                "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0",
+                "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5",
+                "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d",
+                "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d",
+                "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16",
+                "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195",
+                "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da",
+                "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8",
+                "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098",
+                "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8",
+                "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063",
+                "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534",
+                "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d",
+                "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109",
+                "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d",
+                "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3",
+                "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2",
+                "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf",
+                "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069",
+                "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7",
+                "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==3.10.3"
+        },
+        "osmpythontools": {
+            "hashes": [
+                "sha256:13ff721f760fdad5dd78b4d1461d286b78bba96ee151a7301ee8c11a0c258be9"
+            ],
+            "index": "pypi",
+            "version": "==0.3.5"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
+                "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==24.0"
+        },
+        "pandas": {
+            "hashes": [
+                "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863",
+                "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2",
+                "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1",
+                "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad",
+                "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db",
+                "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76",
+                "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51",
+                "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32",
+                "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08",
+                "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b",
+                "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4",
+                "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921",
+                "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288",
+                "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee",
+                "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0",
+                "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24",
+                "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99",
+                "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151",
+                "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd",
+                "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce",
+                "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57",
+                "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef",
+                "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54",
+                "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a",
+                "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238",
+                "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23",
+                "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772",
+                "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce",
+                "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==2.2.2"
+        },
+        "pillow": {
+            "hashes": [
+                "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c",
+                "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2",
+                "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb",
+                "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d",
+                "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa",
+                "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3",
+                "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1",
+                "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a",
+                "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd",
+                "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8",
+                "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999",
+                "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599",
+                "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936",
+                "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375",
+                "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d",
+                "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b",
+                "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60",
+                "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572",
+                "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3",
+                "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced",
+                "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f",
+                "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b",
+                "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19",
+                "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f",
+                "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d",
+                "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383",
+                "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795",
+                "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355",
+                "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57",
+                "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09",
+                "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b",
+                "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462",
+                "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf",
+                "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f",
+                "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a",
+                "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad",
+                "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9",
+                "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d",
+                "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45",
+                "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994",
+                "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d",
+                "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338",
+                "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463",
+                "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451",
+                "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591",
+                "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c",
+                "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd",
+                "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32",
+                "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9",
+                "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf",
+                "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5",
+                "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828",
+                "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3",
+                "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5",
+                "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2",
+                "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b",
+                "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2",
+                "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475",
+                "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3",
+                "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb",
+                "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef",
+                "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015",
+                "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002",
+                "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170",
+                "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84",
+                "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57",
+                "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f",
+                "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27",
+                "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==10.3.0"
+        },
+        "pydantic": {
+            "hashes": [
+                "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7",
+                "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.7.2"
+        },
+        "pydantic-core": {
+            "hashes": [
+                "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9",
+                "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b",
+                "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426",
+                "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc",
+                "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9",
+                "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc",
+                "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4",
+                "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702",
+                "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6",
+                "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231",
+                "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083",
+                "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60",
+                "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7",
+                "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a",
+                "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4",
+                "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6",
+                "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39",
+                "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0",
+                "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026",
+                "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c",
+                "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442",
+                "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7",
+                "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044",
+                "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd",
+                "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94",
+                "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361",
+                "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af",
+                "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4",
+                "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022",
+                "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7",
+                "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd",
+                "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8",
+                "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2",
+                "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5",
+                "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be",
+                "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c",
+                "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9",
+                "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417",
+                "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1",
+                "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326",
+                "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4",
+                "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb",
+                "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34",
+                "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d",
+                "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9",
+                "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812",
+                "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558",
+                "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779",
+                "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be",
+                "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c",
+                "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe",
+                "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0",
+                "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c",
+                "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2",
+                "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a",
+                "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab",
+                "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048",
+                "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3",
+                "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106",
+                "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d",
+                "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb",
+                "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6",
+                "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b",
+                "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef",
+                "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5",
+                "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb",
+                "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c",
+                "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06",
+                "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059",
+                "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943",
+                "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1",
+                "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2",
+                "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c",
+                "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522",
+                "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f",
+                "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78",
+                "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7",
+                "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a",
+                "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.18.3"
+        },
+        "pygments": {
+            "hashes": [
+                "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
+                "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.18.0"
+        },
+        "pyparsing": {
+            "hashes": [
+                "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad",
+                "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"
+            ],
+            "markers": "python_full_version >= '3.6.8'",
+            "version": "==3.1.2"
+        },
+        "python-dateutil": {
+            "hashes": [
+                "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+                "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.9.0.post0"
+        },
+        "python-dotenv": {
+            "hashes": [
+                "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
+                "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
+            ],
+            "version": "==1.0.1"
+        },
+        "python-multipart": {
+            "hashes": [
+                "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026",
+                "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.0.9"
+        },
+        "pytz": {
+            "hashes": [
+                "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812",
+                "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"
+            ],
+            "version": "==2024.1"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
+                "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
+                "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "version": "==6.0.1"
+        },
+        "rich": {
+            "hashes": [
+                "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222",
+                "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==13.7.1"
+        },
+        "scipy": {
+            "hashes": [
+                "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d",
+                "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c",
+                "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca",
+                "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9",
+                "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54",
+                "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16",
+                "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2",
+                "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5",
+                "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59",
+                "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326",
+                "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b",
+                "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1",
+                "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d",
+                "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24",
+                "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627",
+                "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c",
+                "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa",
+                "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949",
+                "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989",
+                "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004",
+                "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f",
+                "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884",
+                "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299",
+                "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94",
+                "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.9'",
+            "version": "==1.13.1"
+        },
+        "shellingham": {
+            "hashes": [
+                "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686",
+                "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.5.4"
+        },
+        "six": {
+            "hashes": [
+                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.16.0"
+        },
+        "sniffio": {
+            "hashes": [
+                "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
+                "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.3.1"
+        },
+        "soupsieve": {
+            "hashes": [
+                "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690",
+                "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==2.5"
+        },
+        "starlette": {
+            "hashes": [
+                "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee",
+                "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.37.2"
+        },
+        "typer": {
+            "hashes": [
+                "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914",
+                "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.12.3"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8",
+                "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.12.0"
+        },
+        "tzdata": {
+            "hashes": [
+                "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd",
+                "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"
+            ],
+            "markers": "python_version >= '2'",
+            "version": "==2024.1"
+        },
+        "ujson": {
+            "hashes": [
+                "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e",
+                "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b",
+                "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6",
+                "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7",
+                "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9",
+                "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd",
+                "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569",
+                "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f",
+                "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51",
+                "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20",
+                "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1",
+                "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf",
+                "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc",
+                "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e",
+                "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a",
+                "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539",
+                "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27",
+                "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165",
+                "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126",
+                "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1",
+                "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816",
+                "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64",
+                "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8",
+                "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e",
+                "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287",
+                "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3",
+                "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb",
+                "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0",
+                "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043",
+                "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557",
+                "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e",
+                "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21",
+                "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d",
+                "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd",
+                "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0",
+                "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337",
+                "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753",
+                "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804",
+                "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f",
+                "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f",
+                "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5",
+                "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5",
+                "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1",
+                "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00",
+                "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2",
+                "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050",
+                "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e",
+                "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4",
+                "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8",
+                "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996",
+                "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6",
+                "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1",
+                "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f",
+                "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1",
+                "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4",
+                "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b",
+                "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88",
+                "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518",
+                "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5",
+                "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770",
+                "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4",
+                "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a",
+                "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76",
+                "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe",
+                "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988",
+                "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1",
+                "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5",
+                "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b",
+                "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7",
+                "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8",
+                "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc",
+                "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a",
+                "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720",
+                "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3",
+                "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b",
+                "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9",
+                "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1",
+                "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==5.10.0"
+        },
+        "uvicorn": {
+            "extras": [
+                "standard"
+            ],
+            "hashes": [
+                "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab",
+                "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.30.0"
+        },
+        "uvloop": {
+            "hashes": [
+                "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd",
+                "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec",
+                "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b",
+                "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc",
+                "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797",
+                "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5",
+                "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2",
+                "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d",
+                "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be",
+                "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd",
+                "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12",
+                "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17",
+                "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef",
+                "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24",
+                "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428",
+                "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1",
+                "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849",
+                "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593",
+                "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd",
+                "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67",
+                "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6",
+                "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3",
+                "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd",
+                "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8",
+                "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7",
+                "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533",
+                "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957",
+                "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650",
+                "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e",
+                "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7",
+                "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"
+            ],
+            "version": "==0.19.0"
+        },
+        "watchfiles": {
+            "hashes": [
+                "sha256:00095dd368f73f8f1c3a7982a9801190cc88a2f3582dd395b289294f8975172b",
+                "sha256:00ad0bcd399503a84cc688590cdffbe7a991691314dde5b57b3ed50a41319a31",
+                "sha256:00f39592cdd124b4ec5ed0b1edfae091567c72c7da1487ae645426d1b0ffcad1",
+                "sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab",
+                "sha256:052d668a167e9fc345c24203b104c313c86654dd6c0feb4b8a6dfc2462239249",
+                "sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd",
+                "sha256:0b04a2cbc30e110303baa6d3ddce8ca3664bc3403be0f0ad513d1843a41c97d1",
+                "sha256:0bc3b2f93a140df6806c8467c7f51ed5e55a931b031b5c2d7ff6132292e803d6",
+                "sha256:0c8e0aa0e8cc2a43561e0184c0513e291ca891db13a269d8d47cb9841ced7c71",
+                "sha256:103622865599f8082f03af4214eaff90e2426edff5e8522c8f9e93dc17caee13",
+                "sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171",
+                "sha256:1cc0cba54f47c660d9fa3218158b8963c517ed23bd9f45fe463f08262a4adae1",
+                "sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1",
+                "sha256:213792c2cd3150b903e6e7884d40660e0bcec4465e00563a5fc03f30ea9c166c",
+                "sha256:25c817ff2a86bc3de3ed2df1703e3d24ce03479b27bb4527c57e722f8554d971",
+                "sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb",
+                "sha256:280a4afbc607cdfc9571b9904b03a478fc9f08bbeec382d648181c695648202f",
+                "sha256:28324d6b28bcb8d7c1041648d7b63be07a16db5510bea923fc80b91a2a6cbed6",
+                "sha256:28585744c931576e535860eaf3f2c0ec7deb68e3b9c5a85ca566d69d36d8dd27",
+                "sha256:28f393c1194b6eaadcdd8f941307fc9bbd7eb567995232c830f6aef38e8a6e88",
+                "sha256:2abeb79209630da981f8ebca30a2c84b4c3516a214451bfc5f106723c5f45843",
+                "sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a",
+                "sha256:2f350cbaa4bb812314af5dab0eb8d538481e2e2279472890864547f3fe2281ed",
+                "sha256:3218a6f908f6a276941422b035b511b6d0d8328edd89a53ae8c65be139073f84",
+                "sha256:3973145235a38f73c61474d56ad6199124e7488822f3a4fc97c72009751ae3b0",
+                "sha256:3a0d883351a34c01bd53cfa75cd0292e3f7e268bacf2f9e33af4ecede7e21d1d",
+                "sha256:425440e55cd735386ec7925f64d5dde392e69979d4c8459f6bb4e920210407f2",
+                "sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797",
+                "sha256:4cc382083afba7918e32d5ef12321421ef43d685b9a67cc452a6e6e18920890e",
+                "sha256:52fc9b0dbf54d43301a19b236b4a4614e610605f95e8c3f0f65c3a456ffd7d35",
+                "sha256:55b7cc10261c2786c41d9207193a85c1db1b725cf87936df40972aab466179b6",
+                "sha256:581f0a051ba7bafd03e17127735d92f4d286af941dacf94bcf823b101366249e",
+                "sha256:5834e1f8b71476a26df97d121c0c0ed3549d869124ed2433e02491553cb468c2",
+                "sha256:5e45fb0d70dda1623a7045bd00c9e036e6f1f6a85e4ef2c8ae602b1dfadf7550",
+                "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e",
+                "sha256:68fe0c4d22332d7ce53ad094622b27e67440dacefbaedd29e0794d26e247280c",
+                "sha256:72a44e9481afc7a5ee3291b09c419abab93b7e9c306c9ef9108cb76728ca58d2",
+                "sha256:7a74436c415843af2a769b36bf043b6ccbc0f8d784814ba3d42fc961cdb0a9dc",
+                "sha256:8597b6f9dc410bdafc8bb362dac1cbc9b4684a8310e16b1ff5eee8725d13dcd6",
+                "sha256:8c39987a1397a877217be1ac0fb1d8b9f662c6077b90ff3de2c05f235e6a8f96",
+                "sha256:8c3e3675e6e39dc59b8fe5c914a19d30029e36e9f99468dddffd432d8a7b1c93",
+                "sha256:8dc1fc25a1dedf2dd952909c8e5cb210791e5f2d9bc5e0e8ebc28dd42fed7562",
+                "sha256:8fdebb655bb1ba0122402352b0a4254812717a017d2dc49372a1d47e24073795",
+                "sha256:9165bcab15f2b6d90eedc5c20a7f8a03156b3773e5fb06a790b54ccecdb73385",
+                "sha256:94ebe84a035993bb7668f58a0ebf998174fb723a39e4ef9fce95baabb42b787f",
+                "sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848",
+                "sha256:96eec15e5ea7c0b6eb5bfffe990fc7c6bd833acf7e26704eb18387fb2f5fd087",
+                "sha256:97b94e14b88409c58cdf4a8eaf0e67dfd3ece7e9ce7140ea6ff48b0407a593ec",
+                "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb",
+                "sha256:a8a31bfd98f846c3c284ba694c6365620b637debdd36e46e1859c897123aa232",
+                "sha256:a927b3034d0672f62fb2ef7ea3c9fc76d063c4b15ea852d1db2dc75fe2c09696",
+                "sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2",
+                "sha256:aec83c3ba24c723eac14225194b862af176d52292d271c98820199110e31141e",
+                "sha256:b44b70850f0073b5fcc0b31ede8b4e736860d70e2dbf55701e05d3227a154a67",
+                "sha256:b610fb5e27825b570554d01cec427b6620ce9bd21ff8ab775fc3a32f28bba63e",
+                "sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68",
+                "sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb",
+                "sha256:bd4c06100bce70a20c4b81e599e5886cf504c9532951df65ad1133e508bf20be",
+                "sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71",
+                "sha256:c49b76a78c156979759d759339fb62eb0549515acfe4fd18bb151cc07366629c",
+                "sha256:c4a65474fd2b4c63e2c18ac67a0c6c66b82f4e73e2e4d940f837ed3d2fd9d4da",
+                "sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39",
+                "sha256:c668228833c5619f6618699a2c12be057711b0ea6396aeaece4ded94184304ea",
+                "sha256:c7b978c384e29d6c7372209cbf421d82286a807bbcdeb315427687f8371c340a",
+                "sha256:d048ad5d25b363ba1d19f92dcf29023988524bee6f9d952130b316c5802069cb",
+                "sha256:d3e1f3cf81f1f823e7874ae563457828e940d75573c8fbf0ee66818c8b6a9099",
+                "sha256:d47e9ef1a94cc7a536039e46738e17cce058ac1593b2eccdede8bf72e45f372a",
+                "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538",
+                "sha256:dc1b9b56f051209be458b87edb6856a449ad3f803315d87b2da4c93b43a6fe72",
+                "sha256:dc2e8fe41f3cac0660197d95216c42910c2b7e9c70d48e6d84e22f577d106fc1",
+                "sha256:dc92d2d2706d2b862ce0568b24987eba51e17e14b79a1abcd2edc39e48e743c8",
+                "sha256:dd64f3a4db121bc161644c9e10a9acdb836853155a108c2446db2f5ae1778c3d",
+                "sha256:e0f0a874231e2839abbf473256efffe577d6ee2e3bfa5b540479e892e47c172d",
+                "sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c",
+                "sha256:fe82d13461418ca5e5a808a9e40f79c1879351fcaeddbede094028e74d836e86"
+            ],
+            "version": "==0.22.0"
+        },
+        "websockets": {
+            "hashes": [
+                "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b",
+                "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6",
+                "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df",
+                "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b",
+                "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205",
+                "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892",
+                "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53",
+                "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2",
+                "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed",
+                "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c",
+                "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd",
+                "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b",
+                "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931",
+                "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30",
+                "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370",
+                "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be",
+                "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec",
+                "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf",
+                "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62",
+                "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b",
+                "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402",
+                "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f",
+                "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123",
+                "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9",
+                "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603",
+                "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45",
+                "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558",
+                "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4",
+                "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438",
+                "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137",
+                "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480",
+                "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447",
+                "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8",
+                "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04",
+                "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c",
+                "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb",
+                "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967",
+                "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b",
+                "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d",
+                "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def",
+                "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c",
+                "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92",
+                "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2",
+                "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113",
+                "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b",
+                "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28",
+                "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7",
+                "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d",
+                "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f",
+                "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468",
+                "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8",
+                "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae",
+                "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611",
+                "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d",
+                "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9",
+                "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca",
+                "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f",
+                "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2",
+                "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077",
+                "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2",
+                "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6",
+                "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374",
+                "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc",
+                "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e",
+                "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53",
+                "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399",
+                "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547",
+                "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3",
+                "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870",
+                "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5",
+                "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8",
+                "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"
+            ],
+            "version": "==12.0"
+        },
+        "xarray": {
+            "hashes": [
+                "sha256:7ddedfe2294a0ab00f02d0fbdcb9c6300ec589f3cf436a9c7b7b577a12cd9bcf",
+                "sha256:e0eb1cb265f265126795f388ed9591f3c752f2aca491f6c0576711fd15b708f2"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==2024.5.0"
+        }
+    },
+    "develop": {}
+}
diff --git a/backend/app/main.py b/backend/app/main.py
index 38863e2..257d572 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,34 +1,34 @@
-from src.optimizer import solve_optimization
-from src.landmarks_manager import Landmark
-from fastapi import FastAPI
-
-app = FastAPI()
-
-
-@app.get("/optimize/{max_steps}/{print_details}")
-def main(max_steps: int, print_details: bool):
-    
-    # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
-    #max_steps = 16
-
-
-    # Initialize all landmarks (+ start and goal). Order matters here
-    landmarks = []
-    landmarks.append(Landmark("départ", -1, (0, 0)))
-    landmarks.append(Landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
-    landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
-    landmarks.append(Landmark("louvre", 99, (0,6)))
-    landmarks.append(Landmark("montmartre", 99, (0,10)))
-    landmarks.append(Landmark("concorde", 99, (0,8)))
-    landmarks.append(Landmark("arrivée", -1, (0, 0)))
-
-
-    visiting_order = solve_optimization(landmarks, max_steps, print_details)
-
-    #return visiting_order
-
-    return("max steps :", max_steps, "\n", visiting_order)
-
-
-if __name__ == "__main__":
-    main()
\ No newline at end of file
+from .src.optimizer import solve_optimization
+from .src.landmarks_manager import Landmark
+from fastapi import FastAPI
+
+app = FastAPI()
+
+
+@app.get("/optimize/{max_steps}/{print_details}")
+def main(max_steps: int, print_details: bool):
+    
+    # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS
+    #max_steps = 16
+
+
+    # Initialize all landmarks (+ start and goal). Order matters here
+    landmarks = []
+    landmarks.append(Landmark("départ", -1, (0, 0)))
+    landmarks.append(Landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON
+    landmarks.append(Landmark("arc de triomphe", 99, (0,4)))
+    landmarks.append(Landmark("louvre", 99, (0,6)))
+    landmarks.append(Landmark("montmartre", 99, (0,10)))
+    landmarks.append(Landmark("concorde", 99, (0,8)))
+    landmarks.append(Landmark("arrivée", -1, (0, 0)))
+
+
+    visiting_order = solve_optimization(landmarks, max_steps, print_details)
+
+    #return visiting_order
+
+    return("max steps :", max_steps, "\n", visiting_order)
+
+
+"""if __name__ == "__main__":
+    main()"""
\ No newline at end of file
diff --git a/backend/app/src/landmarks_manager.py b/backend/app/src/landmarks_manager.py
index df2bb9f..0c3a0e2 100644
--- a/backend/app/src/landmarks_manager.py
+++ b/backend/app/src/landmarks_manager.py
@@ -1,52 +1,57 @@
-from OSMPythonTools.api import Api
-from OSMPythonTools.overpass import Overpass
-from dataclasses import dataclass
-
-
-# Defines the landmark class (aka some place there is to visit)
-@dataclass
-class Landmark :
-    name : str
-    attractiveness : int
-    id : int
-
-
-# Converts a OSM id to a landmark
-def add_from_id(id: int, score: int) :
-
-    try :
-        s = 'way/' + str(id)           # prepare string for query
-        obj =  api.query(s)                             # object to add
-    except :
-        s = 'relation/' + str(id)           # prepare string for query
-        obj =  api.query(s)                             # object to add
-
-    return Landmark(obj.tag('name:fr'), score, id)      # create Landmark out of it
-
-
-# take a lsit of tuples (id, score) to generate a list of landmarks
-def generate_landmarks(ids_and_scores: list) :
-
-    L = []
-    for tup in ids_and_scores :
-        L.append(add_from_id(tup[0], tup[1]))
-
-    return L
-
-api = Api()
-
-
-l = (7515426, 70)
-t = (5013364, 100)
-n = (201611261, 99)
-a = (226413508, 50)
-m = (23762981, 30)
-
-
-ids_and_scores = [t, l, n, a, m]
-
-landmarks = generate_landmarks(ids_and_scores)
-
-
-for obj in landmarks :
+from OSMPythonTools.api import Api
+from OSMPythonTools.overpass import Overpass
+from dataclasses import dataclass
+
+
+# Defines the landmark class (aka some place there is to visit)
+@dataclass
+class Landmarkkkk :
+    name : str
+    attractiveness : int
+    id : int
+    
+@dataclass
+class Landmark :
+    name : str
+    attractiveness : int
+    loc : tuple
+
+# Converts a OSM id to a landmark
+def add_from_id(id: int, score: int) :
+
+    try :
+        s = 'way/' + str(id)           # prepare string for query
+        obj =  api.query(s)                             # object to add
+    except :
+        s = 'relation/' + str(id)           # prepare string for query
+        obj =  api.query(s)                             # object to add
+
+    return Landmarkkkk(obj.tag('name:fr'), score, id)      # create Landmark out of it
+
+
+# take a lsit of tuples (id, score) to generate a list of landmarks
+def generate_landmarks(ids_and_scores: list) :
+
+    L = []
+    for tup in ids_and_scores :
+        L.append(add_from_id(tup[0], tup[1]))
+
+    return L
+
+api = Api()
+
+
+l = (7515426, 70)
+t = (5013364, 100)
+n = (201611261, 99)
+a = (226413508, 50)
+m = (23762981, 30)
+
+
+ids_and_scores = [t, l, n, a, m]
+
+landmarks = generate_landmarks(ids_and_scores)
+
+
+for obj in landmarks :
     print(obj)
\ No newline at end of file
diff --git a/backend/app/src/main_example.py b/backend/app/src/main_example.py
index e0072b0..1a49305 100644
--- a/backend/app/src/main_example.py
+++ b/backend/app/src/main_example.py
@@ -1,23 +1,23 @@
-import fastapi
-from dataclasses import dataclass
-
-
-@dataclass
-class Destination:
-    name: str
-    location: tuple
-    attractiveness: int
-
-
-
-d = Destination()
-
-
-
-def get_route() -> list[Destination]:
-    return {"route": "Hello World"}
-
-endpoint = ("/get_route", get_route)
-end
-if __name__ == "__main__":
-    fastapi.run()
+import fastapi
+from dataclasses import dataclass
+
+
+@dataclass
+class Destination:
+    name: str
+    location: tuple
+    attractiveness: int
+
+
+
+d = Destination()
+
+
+
+def get_route() -> list[Destination]:
+    return {"route": "Hello World"}
+
+endpoint = ("/get_route", get_route)
+end
+if __name__ == "__main__":
+    fastapi.run()
-- 
2.47.2


From 8bc7da0b3efc9d79225101039276b1ab04e96e4e Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Fri, 31 May 2024 21:33:04 +0200
Subject: [PATCH 25/33] first ui elements using the new structs

---
 frontend/lib/layout.dart                      |   4 +-
 frontend/lib/modules/destination_card.dart    |  40 -------
 frontend/lib/modules/greeter.dart             |  44 ++++++++
 frontend/lib/modules/landmark_card.dart       |  73 +++++++++++++
 frontend/lib/modules/landmarks_overview.dart  | 101 ++++++++++++++++++
 frontend/lib/modules/map.dart                 |  29 ++++-
 frontend/lib/modules/navigation.dart          |  63 -----------
 frontend/lib/{modules => pages}/overview.dart |  41 ++-----
 frontend/lib/{modules => pages}/profile.dart  |   0
 frontend/lib/structs/destination.dart         |  62 -----------
 frontend/lib/structs/landmark.dart            |  56 ++++++++++
 frontend/lib/structs/route.dart               |   6 +-
 frontend/lib/utils/get_landmarks.dart         |  25 +++++
 frontend/lib/utils/get_route.dart             |  18 ----
 14 files changed, 336 insertions(+), 226 deletions(-)
 delete mode 100644 frontend/lib/modules/destination_card.dart
 create mode 100644 frontend/lib/modules/greeter.dart
 create mode 100644 frontend/lib/modules/landmark_card.dart
 create mode 100644 frontend/lib/modules/landmarks_overview.dart
 delete mode 100644 frontend/lib/modules/navigation.dart
 rename frontend/lib/{modules => pages}/overview.dart (64%)
 rename frontend/lib/{modules => pages}/profile.dart (100%)
 delete mode 100644 frontend/lib/structs/destination.dart
 create mode 100644 frontend/lib/structs/landmark.dart
 create mode 100644 frontend/lib/utils/get_landmarks.dart
 delete mode 100644 frontend/lib/utils/get_route.dart

diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index 875ccbc..1c7a1be 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 
-import 'package:fast_network_navigation/modules/overview.dart';
-import 'package:fast_network_navigation/modules/profile.dart';
+import 'package:fast_network_navigation/pages/overview.dart';
+import 'package:fast_network_navigation/pages/profile.dart';
 
 
 // BasePage is the scaffold that holds all other pages
diff --git a/frontend/lib/modules/destination_card.dart b/frontend/lib/modules/destination_card.dart
deleted file mode 100644
index 5829c5e..0000000
--- a/frontend/lib/modules/destination_card.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
-
-
-class DestinationCard extends StatefulWidget {
-  final String title;
-  final String description;
-  final String image;
-  bool visited;
-
-  @override
-  _DestinationCardState createState() => _DestinationCardState();
-
-
-  DestinationCard(this.title, this.description, this.image, this.visited);
-
-  Widget build() {
-    return Card(
-      child: ListTile(
-        leading: Icon(Icons.location_on),
-        title: Text(title),
-        subtitle: Text(description),
-        onTap: () {
-          // Navigator.pushNamed(context, '/destination');
-        },
-      ),
-    );
-  }
-  
-}
-
-class _DestinationCardState extends State<DestinationCard> {
-  @override
-  Widget build(BuildContext context) {
-    return Card();
-  }
-
-}
\ No newline at end of file
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
new file mode 100644
index 0000000..99c1be2
--- /dev/null
+++ b/frontend/lib/modules/greeter.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+
+Widget Greeter (ThemeData theme, {bool full = false}) {
+  String greeterText = "";
+  try {
+    String cityName = getCityName();
+    greeterText = "Welcome to $cityName!";
+  } catch (e) {
+    greeterText = "Welcome ...";
+  }
+
+  Widget topGreeter = Text(
+    greeterText,
+    style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
+    maxLines: 1,
+  );
+
+  Widget bottomGreeter = Container();
+  if (full) {
+    bottomGreeter = Text(
+      "Busy day ahead? Here is how to make the most of it!",
+      style: TextStyle(color: Colors.black, fontSize: 18.0),
+      maxLines: 1,
+      );
+  }
+  Widget greeter = Center(
+    child: Column(
+      children: [
+        if (!full) Padding(padding: EdgeInsets.only(top: 24.0)),
+        topGreeter,
+        if (full) bottomGreeter,
+        Padding(padding: EdgeInsets.only(bottom: 24.0)),
+      ],
+    ),
+  );
+
+  return greeter;
+}
+
+
+
+String getCityName() {
+  return "Paris";
+}
\ No newline at end of file
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
new file mode 100644
index 0000000..78bf280
--- /dev/null
+++ b/frontend/lib/modules/landmark_card.dart
@@ -0,0 +1,73 @@
+import 'package:fast_network_navigation/structs/landmark.dart';
+import 'package:flutter/material.dart';
+
+
+class LandmarkCard extends StatefulWidget {
+  final Landmark landmark;
+  @override
+  _LandmarkCardState createState() => _LandmarkCardState();
+
+  LandmarkCard(this.landmark);
+
+}
+
+class _LandmarkCardState extends State<LandmarkCard> {
+  @override
+  Widget build(BuildContext context) {
+    ThemeData theme = Theme.of(context);
+    return Card(
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(15.0),
+      ),
+      child: Row(
+        children: [
+          Container(
+            width: 160,
+            height: 160,
+            decoration: BoxDecoration(
+              borderRadius: BorderRadius.only(
+                topLeft: Radius.circular(15.0),
+                bottomLeft: Radius.circular(15.0),
+              ),
+              image: DecorationImage(
+                image: NetworkImage('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg'),
+                fit: BoxFit.cover,
+              ),
+            ),
+          ),
+          Padding(
+            padding: EdgeInsets.all(10),
+            child: Expanded(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text(
+                        widget.landmark.name,
+                        style: TextStyle(
+                          fontSize: 18,
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                    ],
+                  ),
+                  SizedBox(height: 5),
+                  Text(
+                    "${widget.landmark.name} (${widget.landmark.type.name})",
+                    style: TextStyle(fontSize: 14),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          // Align(
+          //   alignment: Alignment.topRight,
+          //   child: Icon(Icons.push_pin, color: theme.primaryColor),
+          // ),
+          ],
+      ),
+    );
+  }
+}
\ No newline at end of file
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
new file mode 100644
index 0000000..120fe0e
--- /dev/null
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -0,0 +1,101 @@
+import 'package:fast_network_navigation/modules/landmark_card.dart';
+import 'package:fast_network_navigation/structs/landmark.dart';
+import 'package:fast_network_navigation/utils/get_landmarks.dart';
+import 'package:flutter/material.dart';
+
+
+
+
+class loadLandmarksOverview extends StatefulWidget {
+  const loadLandmarksOverview({super.key});
+
+  @override
+  State<loadLandmarksOverview> createState() => _loadLandmarksOverviewState();
+}
+
+class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
+  final Future<List<Landmark>> _landmarks = fetchLandmarks();
+
+  @override
+  Widget build(BuildContext context) {
+    return DefaultTextStyle(
+      style: Theme.of(context).textTheme.displayMedium!,
+      textAlign: TextAlign.center,
+      child: FutureBuilder<List<Landmark>>(
+        future: _landmarks,
+        builder: (BuildContext context, AsyncSnapshot<List<Landmark>> snapshot) {
+          List<Widget> children;
+          if (snapshot.hasData) {
+            children = [landmarksWithSteps(snapshot.data!)];
+          } else if (snapshot.hasError) {
+            children = <Widget>[
+              const Icon(
+                Icons.error_outline,
+                color: Colors.red,
+                size: 60,
+              ),
+              Padding(
+                padding: const EdgeInsets.only(top: 16),
+                child: Text('Error: ${snapshot.error}'),
+              ),
+            ];
+          } else {
+            children = [LandmarkCard(Landmark(name: "loading", location: [0,0], type: LandmarkType(name: "loading")))];
+          }
+          return Center(
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: children,
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+Widget landmarksWithSteps(List<Landmark> landmarks) {
+  List<Widget> children = [];
+  for (Landmark landmark in landmarks) {
+    children.add(LandmarkCard(landmark));
+    children.add(stepBetweenLandmarks());
+  }
+
+  return Column(
+    children: children.sublist(0, children.length - 1)
+  );
+}
+
+
+Widget stepBetweenLandmarks() {
+  // This is a simple widget that draws a line between landmark-cards
+  // It's a vertical dotted line
+  // Next to the line is the icon for the mode of transport (walking for now) and the estimated time
+  // There is also a button to open the navigation instructions as a new intent
+  return Row(
+    children: [
+      Container(
+        width: 50,
+        height: 50,
+        decoration: BoxDecoration(
+          border: Border(
+            left: BorderSide(width: 1.0, color: Colors.black),
+          ),
+        ),
+        child: Column(
+          children: [
+            Icon(Icons.directions_walk),
+            Text("5 min", style: TextStyle(fontSize: 10)),
+          ],
+        ),
+      ),
+      Spacer(),
+      ElevatedButton(
+        onPressed: () {
+          // Open navigation instructions
+        },
+        child: Text("Navigate"),
+      ),
+    ],
+  );
+}
\ No newline at end of file
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 9f7a22d..8f88489 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -1,5 +1,5 @@
+import 'package:fast_network_navigation/structs/landmark.dart';
 import 'package:flutter/material.dart';
-
 import 'package:google_maps_flutter/google_maps_flutter.dart';
 
 class MapWidget extends StatefulWidget {
@@ -9,16 +9,37 @@ class MapWidget extends StatefulWidget {
 
 class _MapWidgetState extends State<MapWidget> {
   late GoogleMapController mapController;
-
-  final LatLng _center = const LatLng(45.521563, -122.677433);
+  // coordinates of Paris
+  final LatLng _center = const LatLng(48.8566, 2.3522);
 
   void _onMapCreated(GoogleMapController controller) {
     mapController = controller;
+    addLandmarks();
   }
   void _onCameraIdle() {
     // print(mapController.getLatLng());
   }
 
+  void addLandmarks() {
+    // // adds a marker for each landmark
+    // List<Landmark> landmarks = [
+    //   Landmark(name: "Eiffel Tower", location: [48.8584, 2.2945], type: LandmarkType(name: "Type 1")),
+    //   Landmark(name: "Louvre Museum", location: [48.8606, 2.3376], type: LandmarkType(name: "Type 1")),
+    //   Landmark(name: "Notre-Dame Cathedral", location: [48.8529, 2.3499], type: LandmarkType(name: "Type 1")),
+    //   Landmark(name: "Arc de Triomphe", location: [48.8738, 2.2950], type: LandmarkType(name: "Type 1")),
+    //   Landmark(name: "Palace of Versailles", location: [48.8014, 2.1301], type: LandmarkType(name: "Type 1")),
+    // ];
+
+    // for (Landmark landmark in landmarks) {
+    //   mapController.
+    //   mapController.addMarker(MarkerOptions(
+    //     position: LatLng(landmark.location[0], landmark.location[1]),
+    //     infoWindowText: InfoWindowText(landmark.name, landmark.type.name),
+    //   ));
+    // }
+  }
+
+
   @override
   Widget build(BuildContext context) {
     return GoogleMap(
@@ -28,6 +49,8 @@ class _MapWidgetState extends State<MapWidget> {
         zoom: 11.0,
       ),
       onCameraIdle: _onCameraIdle,
+      // onLongPress: ,
+      // markers: #,
     );
   }
 }
diff --git a/frontend/lib/modules/navigation.dart b/frontend/lib/modules/navigation.dart
deleted file mode 100644
index 7bd62ac..0000000
--- a/frontend/lib/modules/navigation.dart
+++ /dev/null
@@ -1,63 +0,0 @@
-import 'package:fast_network_navigation/modules/destination_card.dart';
-import 'package:flutter/material.dart';
-
-
-List<Widget> loadDestinations() {
-  List<Widget> cities =  [
-    singleDestination(
-      "New York",
-      "The Big Apple",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/3/34/View_of_New_York_City.jpg/800px-View_of_New_York_City.jpg"
-    ),
-    singleDestination(
-      "Los Angeles",
-      "City of Angels",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Los_Angeles_City_Hall_2013.jpg/800px-Los_Angeles_City_Hall_2013.jpg"
-    ),
-    singleDestination(
-      "Chicago",
-      "The Windy City",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Chicago_skyline%2C_viewed_from_John_Hancock_Center.jpg/800px-Chicago_skyline%2C_viewed_from_John_Hancock_Center.jpg"
-    ),
-    singleDestination(
-      "San Francisco",
-      "The Golden City",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/San_Francisco_City_Hall_2013.jpg/800px-San_Francisco_City_Hall_2013.jpg"
-    ),
-    singleDestination(
-      "Miami",
-      "The Magic City",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Miami_collage.jpg/800px-Miami_collage.jpg"
-    ),
-    singleDestination(
-      "Las Vegas",
-      "Sin City",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Las_Vegas_Strip.jpg/800px-Las_Vegas_Strip.jpg"
-    ),
-    singleDestination(
-      "Seattle",
-      "Emerald City",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Seattle_Kerry_Park_Skyline.jpg/800px-Seattle_Kerry_Park_Skyline.jpg"
-    ),
-    singleDestination(
-      "Boston",
-      "Beantown",
-      "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Boston_skyline_from_Longfellow_Bridge_September_2017_panorama_2.jpg/800px-Boston"
-    )
-  ];
-  cities.shuffle();
-  return cities;
-}
-
-Widget singleDestination(String title, String description, String image) {
-  return Card(
-    child: ListTile(
-      leading: Icon(Icons.location_on),
-      title: Text(title),
-      subtitle: Text(description),
-      onTap: () {
-        // Navigator.pushNamed(context, '/destination');
-      },
-    ),
-  );
-}
\ No newline at end of file
diff --git a/frontend/lib/modules/overview.dart b/frontend/lib/pages/overview.dart
similarity index 64%
rename from frontend/lib/modules/overview.dart
rename to frontend/lib/pages/overview.dart
index bfcf18f..ab6ffb8 100644
--- a/frontend/lib/modules/overview.dart
+++ b/frontend/lib/pages/overview.dart
@@ -1,10 +1,8 @@
+import 'package:fast_network_navigation/modules/greeter.dart';
 import 'package:flutter/material.dart';
 import 'package:sliding_up_panel/sliding_up_panel.dart';
-import 'package:geocode/geocode.dart';
-import 'dart:async';
-import 'package:google_maps_flutter/google_maps_flutter.dart';
 
-import 'package:fast_network_navigation/modules/navigation.dart';
+import 'package:fast_network_navigation/modules/landmarks_overview.dart';
 import 'package:fast_network_navigation/modules/map.dart';
 
 
@@ -16,25 +14,6 @@ class NavigationOverview extends StatefulWidget {
 
 
 
-class Debounce {
-  Duration delay;
-  Timer? _timer;
-
-  Debounce(
-    this.delay,
-  );
-
-  call(void Function() callback) {
-    _timer?.cancel();
-    _timer = Timer(delay, callback);
-  }
-
-  dispose() {
-    _timer?.cancel();
-  }
-}
-
-
 class _NavigationOverviewState extends State<NavigationOverview> {
 
   @override
@@ -53,9 +32,10 @@ class _NavigationOverviewState extends State<NavigationOverview> {
       decoration: BoxDecoration(
         color: theme.canvasColor,
         borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
+        boxShadow: []
       ),
       
-      child: Greeting(theme)
+      child: Greeter(theme)
     );
   }
 
@@ -77,9 +57,8 @@ class _NavigationOverviewState extends State<NavigationOverview> {
         child: SingleChildScrollView(
           child: Column(
             children: <Widget>[
-              Greeting(theme),
-              Text("Got a lot to do today! Here is a rundown:"),
-              ...loadDestinations(),
+              Greeter(theme, full: true),
+              loadLandmarksOverview(),
             ],
           ),
         ),
@@ -88,12 +67,4 @@ class _NavigationOverviewState extends State<NavigationOverview> {
     );
   }
 
-  Widget Greeting (ThemeData theme) {
-    return Center(
-        child: Text(
-            "Explore #todo",
-          style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
-        ),
-      );
-  }
 }
diff --git a/frontend/lib/modules/profile.dart b/frontend/lib/pages/profile.dart
similarity index 100%
rename from frontend/lib/modules/profile.dart
rename to frontend/lib/pages/profile.dart
diff --git a/frontend/lib/structs/destination.dart b/frontend/lib/structs/destination.dart
deleted file mode 100644
index c44fbf9..0000000
--- a/frontend/lib/structs/destination.dart
+++ /dev/null
@@ -1,62 +0,0 @@
-import "package:flutter/material.dart";
-
-class Destination {
-  final double latitude;
-  final double longitude;
-  final String name;
-  final String description;
-  // final DestinationType type;
-  final Duration duration;
-  final bool visited;
-
-  const Destination({
-    required this.latitude,
-    required this.longitude,
-    required this.name,
-    required this.description,
-    // required this.type,
-    required this.duration,
-    required this.visited,
-  });
-
-  factory Destination.fromJson(Map<String, dynamic> json) {
-  return switch (json) {
-    {
-      'lat': double latitude,
-      'lon': double longitude,
-      'name': String name,
-      'description': String description,
-      // 'type': String type,
-      'duration': int duration,
-      'visited': bool visited
-
-    } =>
-      Destination(
-        latitude: latitude,
-        longitude: longitude,
-        name: name,
-        description: description,
-        // type: "DestinationType.values.firstWhere((element) => element.name == type)",
-        duration: Duration(minutes: duration),
-        visited: visited
-      ),
-    _ => throw const FormatException('Failed to load destination.'),
-  };
-}
-
-}
-
-
-class DestinationType {
-  final String name;
-  final String description;
-  final Icon icon;
-  
-  const DestinationType({
-    required this.name,
-    required this.description,
-    required this.icon,
-  });
-}
-
-
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
new file mode 100644
index 0000000..a65dbe8
--- /dev/null
+++ b/frontend/lib/structs/landmark.dart
@@ -0,0 +1,56 @@
+class Landmark {
+  final String name;
+  final List location;
+  final LandmarkType type;
+  // final String description;
+  // final Duration duration;
+  // final bool visited;
+
+  const Landmark({
+    required this.name,
+    required this.location,
+    required this.type,
+    // required this.description,
+    // required this.duration,
+    // required this.visited,
+  });
+
+  factory Landmark.fromJson(Map<String, dynamic> json) {
+    return switch (json) {
+      {
+        'loc': List location,
+        'name': String name,
+        'type': String type,
+        // 'description': String description,
+        // 'duration': int duration,
+        // 'visited': bool visited
+
+      } =>
+        Landmark(
+          name: name,
+          location: location,
+          type: LandmarkType(name: type)
+          // description: description,
+          // duration: Duration(minutes: duration),
+          // visited: visited
+        ),
+      _ => throw const FormatException('Failed to load destination.'),
+    };
+  }
+
+}
+
+
+class LandmarkType {
+  final String name;
+  // final String description;
+  // final Icon icon;
+  
+  const LandmarkType({
+    required this.name,
+    // required this.description,
+    // required this.icon,
+  });
+}
+
+
diff --git a/frontend/lib/structs/route.dart b/frontend/lib/structs/route.dart
index 8b46787..62e0baa 100644
--- a/frontend/lib/structs/route.dart
+++ b/frontend/lib/structs/route.dart
@@ -1,14 +1,14 @@
-import "package:fast_network_navigation/structs/destination.dart";
+import "package:fast_network_navigation/structs/landmark.dart";
 
 
 class Route {
   final String name;
   final Duration duration;
-  final List<Destination> destinations;
+  final List<Landmark> landmarks;
   
   Route({
     required this.name,
     required this.duration,
-    required this.destinations
+    required this.landmarks
   });
 }
\ No newline at end of file
diff --git a/frontend/lib/utils/get_landmarks.dart b/frontend/lib/utils/get_landmarks.dart
new file mode 100644
index 0000000..c538b0f
--- /dev/null
+++ b/frontend/lib/utils/get_landmarks.dart
@@ -0,0 +1,25 @@
+import "package:fast_network_navigation/structs/landmark.dart";
+import 'package:http/http.dart' as http;
+
+
+Future<List<Landmark>> fetchLandmarks() async {
+  // final response = await http
+  //     .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
+
+  // if (response.statusCode == 200) {
+    // If the server did return a 200 OK response,
+    // then parse the JSON.
+    List<Landmark> landmarks = [
+      Landmark(name: "Landmark 1", location: [0, 0], type: LandmarkType(name: "Type 1")),
+      Landmark(name: "Landmark 2", location: [0, 0], type: LandmarkType(name: "Type 2")),
+      Landmark(name: "Landmark 3", location: [0, 0], type: LandmarkType(name: "Type 3")),
+      Landmark(name: "Landmark 4", location: [0, 0], type: LandmarkType(name: "Type 4")),
+      Landmark(name: "Landmark 5", location: [0, 0], type: LandmarkType(name: "Type 5")),
+    ];
+    return landmarks;
+  // } else {
+  //   // If the server did not return a 200 OK response,
+  //   // then throw an exception.
+  //   throw Exception('Failed to load destination');
+  // }
+}
\ No newline at end of file
diff --git a/frontend/lib/utils/get_route.dart b/frontend/lib/utils/get_route.dart
deleted file mode 100644
index fccf6ac..0000000
--- a/frontend/lib/utils/get_route.dart
+++ /dev/null
@@ -1,18 +0,0 @@
-import "package:fast_network_navigation/structs/destination.dart";
-import 'package:http/http.dart' as http;
-import 'dart:convert';
-
-Future<Destination> fetchDestination() async {
-  final response = await http
-      .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
-
-  if (response.statusCode == 200) {
-    // If the server did return a 200 OK response,
-    // then parse the JSON.
-    return Destination.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
-  } else {
-    // If the server did not return a 200 OK response,
-    // then throw an exception.
-    throw Exception('Failed to load destination');
-  }
-}
\ No newline at end of file
-- 
2.47.2


From c1d74ab227f5a1996f9b68a5687acdea5ba04a17 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 1 Jun 2024 19:58:25 +0200
Subject: [PATCH 26/33] use structs to draw custom map pointers

---
 .vscode/launch.json                     | 28 ++++++++
 frontend/lib/layout.dart                |  8 +--
 frontend/lib/modules/landmark_card.dart |  2 +-
 frontend/lib/modules/map.dart           | 96 +++++++------------------
 frontend/lib/pages/profile.dart         | 11 +--
 frontend/lib/utils/get_landmarks.dart   | 10 +--
 frontend/pubspec.lock                   | 40 -----------
 frontend/pubspec.yaml                   |  2 -
 frontend/web/index.html                 |  2 +-
 9 files changed, 71 insertions(+), 128 deletions(-)
 create mode 100644 .vscode/launch.json

diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..71f1c8d
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,28 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "frontend",
+            "cwd": "frontend",
+            "request": "launch",
+            "type": "dart"
+        },
+        {
+            "name": "frontend (profile mode)",
+            "cwd": "frontend",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "profile"
+        },
+        {
+            "name": "frontend (release mode)",
+            "cwd": "frontend",
+            "request": "launch",
+            "type": "dart",
+            "flutterMode": "release"
+        },
+    ]
+}
\ No newline at end of file
diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index 1c7a1be..4d2d85f 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -46,7 +46,8 @@ class _BasePageState extends State<BasePage> {
               child: const Text('The fanciest navigation!'),
             ),
             ListTile(
-              title: const Text('Home'),
+              title: const Text('Start'),
+              leading: const Icon(Icons.map),
               selected: _selectedIndex == 0,
               onTap: () {
                 // Update the state of the app
@@ -57,7 +58,8 @@ class _BasePageState extends State<BasePage> {
               },
             ),
             ListTile(
-              title: const Text('Business'),
+              title: const Text('How to use'),
+              leading: Icon(Icons.help),
               selected: _selectedIndex == 1,
               onTap: () {
                 // Update the state of the app
@@ -68,8 +70,6 @@ class _BasePageState extends State<BasePage> {
                 Navigator.pop(context);
               },
             ),
-            // add a whitespace so that the settings are at the bottom
-            
             const Divider(),
             ListTile(
               title: const Text('Settings'),
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index 78bf280..8b42b7a 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -66,7 +66,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
           //   alignment: Alignment.topRight,
           //   child: Icon(Icons.push_pin, color: theme.primaryColor),
           // ),
-          ],
+        ],
       ),
     );
   }
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 8f88489..341d6cd 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -1,4 +1,5 @@
 import 'package:fast_network_navigation/structs/landmark.dart';
+import 'package:fast_network_navigation/utils/get_landmarks.dart';
 import 'package:flutter/material.dart';
 import 'package:google_maps_flutter/google_maps_flutter.dart';
 
@@ -10,33 +11,36 @@ class MapWidget extends StatefulWidget {
 class _MapWidgetState extends State<MapWidget> {
   late GoogleMapController mapController;
   // coordinates of Paris
-  final LatLng _center = const LatLng(48.8566, 2.3522);
+  CameraPosition _cameraPosition = CameraPosition(
+    target: LatLng(48.8566, 2.3522),
+    zoom: 11.0,
+  );
+  Set<Marker> markers = <Marker>{};
+  
 
   void _onMapCreated(GoogleMapController controller) {
     mapController = controller;
-    addLandmarks();
+    drawLandmarks();
   }
+
+  
   void _onCameraIdle() {
-    // print(mapController.getLatLng());
+    // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
   }
 
-  void addLandmarks() {
-    // // adds a marker for each landmark
-    // List<Landmark> landmarks = [
-    //   Landmark(name: "Eiffel Tower", location: [48.8584, 2.2945], type: LandmarkType(name: "Type 1")),
-    //   Landmark(name: "Louvre Museum", location: [48.8606, 2.3376], type: LandmarkType(name: "Type 1")),
-    //   Landmark(name: "Notre-Dame Cathedral", location: [48.8529, 2.3499], type: LandmarkType(name: "Type 1")),
-    //   Landmark(name: "Arc de Triomphe", location: [48.8738, 2.2950], type: LandmarkType(name: "Type 1")),
-    //   Landmark(name: "Palace of Versailles", location: [48.8014, 2.1301], type: LandmarkType(name: "Type 1")),
-    // ];
 
-    // for (Landmark landmark in landmarks) {
-    //   mapController.
-    //   mapController.addMarker(MarkerOptions(
-    //     position: LatLng(landmark.location[0], landmark.location[1]),
-    //     infoWindowText: InfoWindowText(landmark.name, landmark.type.name),
-    //   ));
-    // }
+  void drawLandmarks() async {
+    // (re)draws landmarks on the map
+    List<Landmark> landmarks = await fetchLandmarks();
+    setState(() {
+      for (Landmark landmark in landmarks) {
+        markers.add(Marker(
+          markerId: MarkerId(landmark.name),
+          position: LatLng(landmark.location[0], landmark.location[1]),
+          infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
+        ));
+      }
+    });
   }
 
 
@@ -44,61 +48,11 @@ class _MapWidgetState extends State<MapWidget> {
   Widget build(BuildContext context) {
     return GoogleMap(
       onMapCreated: _onMapCreated,
-      initialCameraPosition: CameraPosition(
-        target: _center,
-        zoom: 11.0,
-      ),
+      initialCameraPosition: _cameraPosition,
       onCameraIdle: _onCameraIdle,
       // onLongPress: ,
-      // markers: #,
+      markers: markers,
     );
   }
 }
 
-
-
-  // GeoCode geoCode = GeoCode();
-  // String _currentCityName = "...";
-  // final Debounce _debounce = Debounce(Duration(seconds: 3));
-
-  // final LatLng _center = const LatLng(45.521563, -122.677433);
-  // late GoogleMapController mapController;
-
-  // void _onMapCreated(GoogleMapController controller) {
-  //   mapController = controller;
-  // }
-
-  // // void _setCurrentCityName() async {
-  //   if (mapController.camera.zoom < 9) {
-  //     return; // Don't bother if the view is too wide
-  //   }
-  //   var currentCoordinates = mapController.camera.center;
-  //   String? city;
-    
-  //   try{
-  //     List<Placemark> placemarks = await placemarkFromCoordinates(currentCoordinates.latitude, currentCoordinates.longitude);
-  //     city = placemarks[0].locality.toString();
-  //   } catch (e) {
-  //     debugPrint("Error: $e");
-  //     try {
-  //       Address address = await geoCode.reverseGeocoding(latitude: currentCoordinates.latitude, longitude: currentCoordinates.longitude);
-        
-  //       if (address.city == null || address.city.toString().contains("Throttled!")){
-  //         throw Exception("Probably rate limited");
-  //       }
-  //       city = address.city.toString();
-  //     } catch (e) {
-  //       debugPrint("Error: $e");
-
-  //     }
-  //   }
-  //   if (city != null) {
-  //     setState(() {
-  //       _currentCityName = city!;
-  //     });
-  //   } else {
-  //     _debounce(() async {_setCurrentCityName();});
-  //   }
-  // }
-
-  
\ No newline at end of file
diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index e8beb08..1133940 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -24,11 +24,14 @@ class _ProfilePageState extends State<ProfilePage> {
           child: Text('Curious traveler', style: TextStyle(fontSize: 24))
         ),
 
-        Padding(
-          padding: EdgeInsets.all(10),
-        ),
+        Padding(padding: EdgeInsets.all(10)),
+        Divider(indent: 25, endIndent: 25),
+        Padding(padding: EdgeInsets.all(10)),
 
-        Text('Please rate your preferences for the following activities:'),
+        Padding(
+          padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
+          child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 20), )
+        ),
 
         // Now the sliders
         ImportanceSliders()
diff --git a/frontend/lib/utils/get_landmarks.dart b/frontend/lib/utils/get_landmarks.dart
index c538b0f..3c47728 100644
--- a/frontend/lib/utils/get_landmarks.dart
+++ b/frontend/lib/utils/get_landmarks.dart
@@ -10,11 +10,11 @@ Future<List<Landmark>> fetchLandmarks() async {
     // If the server did return a 200 OK response,
     // then parse the JSON.
     List<Landmark> landmarks = [
-      Landmark(name: "Landmark 1", location: [0, 0], type: LandmarkType(name: "Type 1")),
-      Landmark(name: "Landmark 2", location: [0, 0], type: LandmarkType(name: "Type 2")),
-      Landmark(name: "Landmark 3", location: [0, 0], type: LandmarkType(name: "Type 3")),
-      Landmark(name: "Landmark 4", location: [0, 0], type: LandmarkType(name: "Type 4")),
-      Landmark(name: "Landmark 5", location: [0, 0], type: LandmarkType(name: "Type 5")),
+      Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
+      Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
+      Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
+      Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
+      Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
     ];
     return landmarks;
   // } else {
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index 7c2f396..e959889 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -112,46 +112,6 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
-  geocode:
-    dependency: "direct main"
-    description:
-      name: geocode
-      sha256: cf9727c369bb3703b97d6e440225962dc27b7f3c686662fe3cdcc91cbfb7074d
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.3"
-  geocoding:
-    dependency: "direct main"
-    description:
-      name: geocoding
-      sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.0.0"
-  geocoding_android:
-    dependency: transitive
-    description:
-      name: geocoding_android
-      sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.3.1"
-  geocoding_ios:
-    dependency: transitive
-    description:
-      name: geocoding_ios
-      sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24"
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.0.1"
-  geocoding_platform_interface:
-    dependency: transitive
-    description:
-      name: geocoding_platform_interface
-      sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.2.0"
   google_maps:
     dependency: transitive
     description:
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 9f1732c..e278579 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -36,8 +36,6 @@ dependencies:
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.6
   sliding_up_panel: ^2.0.0+1
-  geocoding: ^3.0.0
-  geocode: ^1.0.3
   google_maps_flutter: ^2.6.1
   http: ^1.2.1
   shared_preferences: ^2.2.3
diff --git a/frontend/web/index.html b/frontend/web/index.html
index 5cbcc91..8d38cac 100644
--- a/frontend/web/index.html
+++ b/frontend/web/index.html
@@ -14,7 +14,7 @@
     This is a placeholder for base href that will be replaced by the value of
     the `--base-href` argument provided to `flutter build`.
   -->
-  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"></script>
+  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw&libraries=drawing"></script>
   <base href="$FLUTTER_BASE_HREF">
 
   <meta charset="UTF-8">
-- 
2.47.2


From ebaec40d6bbc43928a039e6305dd7c802047c2da Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sun, 2 Jun 2024 19:38:46 +0000
Subject: [PATCH 27/33] Update frontend/lib/pages/profile.dart

---
 frontend/lib/pages/profile.dart | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 1133940..42f08d7 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -55,8 +55,8 @@ class _ImportanceSlidersState extends State<ImportanceSliders> {
 
   @override
   void initState() {
-    super.initState();
     _prefs.load();
+    super.initState();
   }
 
   List<Card> _createSliders() {
-- 
2.47.2


From d5e0b7d51a9fa8d2b3e4df110188437f44f8bd3f Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 3 Jun 2024 13:51:01 +0200
Subject: [PATCH 28/33] Beginning to use different contexts

---
 .../android/app/src/main/AndroidManifest.xml  |   2 +-
 frontend/lib/layout.dart                      | 102 ++++++++++++------
 frontend/lib/main.dart                        |   2 +-
 frontend/lib/modules/greeter.dart             |   4 +-
 frontend/lib/modules/landmarks_overview.dart  |   2 +-
 frontend/lib/modules/trips_overview.dart      |  65 +++++++++++
 frontend/lib/pages/new_trip.dart              |  30 ++++++
 frontend/lib/pages/profile.dart               |   2 +-
 frontend/lib/pages/tutorial.dart              |  27 +++++
 frontend/lib/structs/landmark.dart            |  11 ++
 frontend/lib/structs/trip.dart                |  25 +++++
 frontend/lib/utils/get_landmarks.dart         |   2 +
 frontend/lib/utils/get_trips.dart             |  37 +++++++
 frontend/test/widget_test.dart                |   2 +-
 14 files changed, 271 insertions(+), 42 deletions(-)
 create mode 100644 frontend/lib/modules/trips_overview.dart
 create mode 100644 frontend/lib/pages/new_trip.dart
 create mode 100644 frontend/lib/pages/tutorial.dart
 create mode 100644 frontend/lib/structs/trip.dart
 create mode 100644 frontend/lib/utils/get_trips.dart

diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
index f144d58..b38e92b 100644
--- a/frontend/android/app/src/main/AndroidManifest.xml
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -50,5 +50,5 @@
     </queries>
 
     <!-- Required to fetch data from the internet. -->
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.INTERNET"/>
 </manifest>
diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index 4d2d85f..6612e48 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -1,85 +1,117 @@
+import 'package:fast_network_navigation/modules/trips_overview.dart';
+import 'package:fast_network_navigation/pages/new_trip.dart';
+import 'package:fast_network_navigation/pages/tutorial.dart';
+import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:flutter/material.dart';
 
 import 'package:fast_network_navigation/pages/overview.dart';
 import 'package:fast_network_navigation/pages/profile.dart';
 
-
 // BasePage is the scaffold that holds all other pages
 // A side drawer is used to switch between pages
 class BasePage extends StatefulWidget {
-  const BasePage({super.key, required this.title});
-  final String title;
+  final String mainScreen;
+  final String currentMap;
+  final List<Trip> trips;
+  
+  const BasePage({super.key, required this.mainScreen, this.currentMap = "map", this.trips = const []});
 
   @override
   State<BasePage> createState() => _BasePageState();
 }
 
 class _BasePageState extends State<BasePage> {
-  int _selectedIndex = 0;
-  
-  void _onItemTapped(int index) {
-    setState(() {
-      _selectedIndex = index;
-    });
-  }
 
-  Widget currentView = NavigationOverview();
+
   @override
   Widget build(BuildContext context) {
+    Widget currentView = const Text("loading...");
+    if (widget.mainScreen == "map") {
+      currentView = NavigationOverview();
+    } else if (widget.mainScreen == "tutorial") {
+      currentView = TutorialPage();
+    } else if (widget.mainScreen == "profile") {
+      currentView = ProfilePage();
+    }
+
     final ThemeData theme = Theme.of(context);
     
     return Scaffold(
-      appBar: AppBar(title: Text(widget.title)),
+      appBar: AppBar(title: Text("City Nav")),
       body: Center(child: currentView),
       drawer: Drawer(
         // Add a ListView to the drawer. This ensures the user can scroll
         // through the options in the drawer if there isn't enough vertical
         // space to fit everything.
-        child: ListView(
-          // Important: Remove any padding from the ListView.
-          padding: EdgeInsets.zero,
+        child: Column(
           children: [
             DrawerHeader(
               decoration: BoxDecoration(
                 gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
               ),
-              child: const Text('The fanciest navigation!'),
+              child: Center(
+                child: Text(
+                  'City Nav',
+                  style: TextStyle(
+                    color: Colors.white,
+                    fontSize: 24,
+                    fontWeight: FontWeight.bold,
+                  ),
+                ),
+              ),
             ),
             ListTile(
               title: const Text('Start'),
               leading: const Icon(Icons.map),
-              selected: _selectedIndex == 0,
+              selected: widget.mainScreen == "map",
               onTap: () {
-                // Update the state of the app
-                _onItemTapped(0);
-                // Then close the drawer
-                currentView = NavigationOverview();
-                Navigator.pop(context);
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => BasePage(mainScreen: "map")
+                  )
+                );
               },
             ),
+            ListTile(
+              title: const Text('Trip Overview'),
+              leading: const Icon(Icons.list),
+              trailing: ElevatedButton(
+                onPressed: () {
+                  Navigator.of(context).push(
+                    MaterialPageRoute(
+                      builder: (context) => const NewTripPage()
+                    )
+                  );
+                },
+                child: const Text('New'),
+              ),
+            ),
+            Expanded(child: TripsOverview()),
+            const Divider(),
             ListTile(
               title: const Text('How to use'),
               leading: Icon(Icons.help),
-              selected: _selectedIndex == 1,
+              selected: widget.mainScreen == "tutorial",
               onTap: () {
-                // Update the state of the app
-                _onItemTapped(1);
-                currentView = const Text("ghfhggfhgf");
-
-                // Then close the drawer
-                Navigator.pop(context);
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => BasePage(mainScreen: "tutorial")
+                  )
+                );
               },
             ),
-            const Divider(),
+
             ListTile(
               title: const Text('Settings'),
               leading: const Icon(Icons.settings),
-              selected: _selectedIndex == 2,
+              selected: widget.mainScreen == "profile",
               onTap: () {
-                _onItemTapped(2);
-                currentView = ProfilePage();
-                Navigator.pop(context);
-                },
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => BasePage(mainScreen: "profile")
+                  )
+                );
+              },
             ),
             // settings in the bottom of the drawer
           ],
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index d818628..1fd9fc2 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -12,7 +12,7 @@ class App extends StatelessWidget {
   Widget build(BuildContext context) {
     return MaterialApp(
       title: appTitle,
-      home: BasePage(title: appTitle),
+      home: BasePage(mainScreen: "map"),
       theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
     );
   }
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index 99c1be2..e3066ac 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -19,8 +19,8 @@ Widget Greeter (ThemeData theme, {bool full = false}) {
   if (full) {
     bottomGreeter = Text(
       "Busy day ahead? Here is how to make the most of it!",
-      style: TextStyle(color: Colors.black, fontSize: 18.0),
-      maxLines: 1,
+      style: TextStyle(color: Colors.black, fontSize: 18),
+      textAlign: TextAlign.center,
       );
   }
   Widget greeter = Center(
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index 120fe0e..c967a65 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -40,7 +40,7 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
               ),
             ];
           } else {
-            children = [LandmarkCard(Landmark(name: "loading", location: [0,0], type: LandmarkType(name: "loading")))];
+            children = [Center(child: CircularProgressIndicator())];
           }
           return Center(
             child: Column(
diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart
new file mode 100644
index 0000000..955a109
--- /dev/null
+++ b/frontend/lib/modules/trips_overview.dart
@@ -0,0 +1,65 @@
+import 'package:flutter/material.dart';
+
+import 'package:fast_network_navigation/layout.dart';
+import 'package:fast_network_navigation/structs/trip.dart';
+import 'package:fast_network_navigation/utils/get_trips.dart';
+
+
+class TripsOverview extends StatefulWidget {
+  
+  const TripsOverview({super.key});
+
+  @override
+  State<TripsOverview> createState() => _TripsOverviewState();
+}
+
+class _TripsOverviewState extends State<TripsOverview> {
+  final Future<List<Trip>> _trips = loadTrips();
+
+
+  Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
+    List<Widget> children;
+    if (snapshot.hasData) {
+      children = List<Widget>.generate(snapshot.data!.length, (index) {
+        Trip trip = snapshot.data![index];
+        return ListTile(
+          title: Text("Trip to ${trip.cityName} (${trip.landmarks.length} stops)"),
+          leading: Icon(Icons.pin_drop),
+          onTap: () {
+            Navigator.of(context).push(
+              MaterialPageRoute(
+                builder: (context) => BasePage(mainScreen: "map") //, trip: trip)
+              )
+            );
+          },
+        );
+      });
+    } else if (snapshot.hasError) {
+      children = [
+        const Icon(
+          Icons.error_outline,
+          color: Colors.red,
+          size: 60,
+        ),
+        Padding(
+          padding: const EdgeInsets.only(top: 16),
+          child: Text('Error: ${snapshot.error}'),
+        ),
+      ];
+    } else {
+      children = [Center(child: CircularProgressIndicator())];
+    }
+
+    return ListView(
+      children: children,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder(
+      future: _trips,
+      builder: listBuild,
+    );
+  }
+}
\ No newline at end of file
diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip.dart
new file mode 100644
index 0000000..47e2c5b
--- /dev/null
+++ b/frontend/lib/pages/new_trip.dart
@@ -0,0 +1,30 @@
+
+import 'package:flutter/material.dart';
+
+class NewTripPage extends StatefulWidget {
+  const NewTripPage({Key? key}) : super(key: key);
+
+  @override
+  _NewTripPageState createState() => _NewTripPageState();
+}
+
+class _NewTripPageState extends State<NewTripPage> {
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('New Trip'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            const Text(
+              'Create a new trip',
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
\ No newline at end of file
diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 42f08d7..10fefe8 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -30,7 +30,7 @@ class _ProfilePageState extends State<ProfilePage> {
 
         Padding(
           padding: EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10),
-          child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 20), )
+          child: Text('Please rate your personal preferences so that we can taylor your experience.', style: TextStyle(fontSize: 18))
         ),
 
         // Now the sliders
diff --git a/frontend/lib/pages/tutorial.dart b/frontend/lib/pages/tutorial.dart
new file mode 100644
index 0000000..3b19421
--- /dev/null
+++ b/frontend/lib/pages/tutorial.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+
+
+
+class TutorialPage extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("Tutorial"),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            Text(
+              'Welcome to the tutorial page!',
+            ),
+            Text(
+              'This is where you will learn how to use the app.',
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
index a65dbe8..14d1b5b 100644
--- a/frontend/lib/structs/landmark.dart
+++ b/frontend/lib/structs/landmark.dart
@@ -38,6 +38,17 @@ class Landmark {
     };
   }
 
+
+    Map<String, dynamic> toJson() {
+      return {
+        'name': name,
+        'location': location,
+        'type': type.name,
+        // 'description': description,
+        // 'duration': duration.inMinutes,
+        // 'visited': visited
+      };
+    }
 }
 
 
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
new file mode 100644
index 0000000..dc02332
--- /dev/null
+++ b/frontend/lib/structs/trip.dart
@@ -0,0 +1,25 @@
+// Represents a collection of landmarks that represent a journey
+// Different instances of a Trip can be saved and loaded by the user
+
+import 'package:fast_network_navigation/structs/landmark.dart';
+
+class Trip {
+  final String uuid;
+  final String cityName;
+  final List<Landmark> landmarks;
+
+
+  Trip({required this.uuid, required this.cityName, required this.landmarks});
+
+  factory Trip.fromJson(Map<String, dynamic> json) {
+    List<Landmark> landmarks = [];
+    for (var landmark in json['landmarks']) {
+      landmarks.add(Landmark.fromJson(landmark));
+    }
+    return Trip(
+      uuid: json['uuid'],
+      cityName: json['cityName'],
+      landmarks: landmarks,
+    );
+  }
+}
\ No newline at end of file
diff --git a/frontend/lib/utils/get_landmarks.dart b/frontend/lib/utils/get_landmarks.dart
index 3c47728..5390849 100644
--- a/frontend/lib/utils/get_landmarks.dart
+++ b/frontend/lib/utils/get_landmarks.dart
@@ -16,6 +16,8 @@ Future<List<Landmark>> fetchLandmarks() async {
       Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
       Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
     ];
+    // sleep 10 seconds
+    await Future.delayed(Duration(seconds: 10));
     return landmarks;
   // } else {
   //   // If the server did not return a 200 OK response,
diff --git a/frontend/lib/utils/get_trips.dart b/frontend/lib/utils/get_trips.dart
new file mode 100644
index 0000000..0a64080
--- /dev/null
+++ b/frontend/lib/utils/get_trips.dart
@@ -0,0 +1,37 @@
+import 'dart:convert';
+
+import 'package:fast_network_navigation/structs/trip.dart';
+import 'package:fast_network_navigation/structs/landmark.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+Future<List<Trip>> loadTrips() async {
+  SharedPreferences prefs = await SharedPreferences.getInstance();
+
+  Set<String> keys = prefs.getKeys();
+  List<Trip> itineraries = [];
+  for (String key in keys) {
+    if (key.startsWith("itinerary_")) {
+      String json = prefs.getString(key)!;
+      itineraries.add(Trip.fromJson(jsonDecode(json)));
+    }
+  }
+  itineraries.add(Trip(uuid: "1", cityName: "Paris", landmarks: [
+      Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
+      Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
+      Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
+      Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
+      Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
+    ]));
+  itineraries.add(Trip(uuid: "2", cityName: "Vienna", landmarks: []));
+  itineraries.add(Trip(uuid: "3", cityName: "London", landmarks: []));
+  itineraries.add(Trip(uuid: "4", cityName: "Madrid", landmarks: []));
+  itineraries.add(Trip(uuid: "5", cityName: "Tokyo", landmarks: []));
+  itineraries.add(Trip(uuid: "6", cityName: "New York", landmarks: []));
+  itineraries.add(Trip(uuid: "7", cityName: "Los Angeles", landmarks: []));
+  itineraries.add(Trip(uuid: "8", cityName: "Zurich", landmarks: []));
+  itineraries.add(Trip(uuid: "9", cityName: "Orschwiller", landmarks: []));
+
+  await Future.delayed(Duration(seconds: 3));
+
+  return itineraries;
+}
diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart
index 143b24f..f17a55a 100644
--- a/frontend/test/widget_test.dart
+++ b/frontend/test/widget_test.dart
@@ -14,7 +14,7 @@ import 'package:fast_network_navigation/layout.dart';
 void main() {
   testWidgets('Counter increments smoke test', (WidgetTester tester) async {
     // Build our app and trigger a frame.
-    await tester.pumpWidget(BasePage(title: "City Nav"));
+    await tester.pumpWidget(BasePage(mainScreen: "map",));
 
     // Verfiy that the title is displayed
     expect(find.text('City Nav'), findsOneWidget);
-- 
2.47.2


From 040e5c9f83921c3be6d6de1942d3741f7ed671ff Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Mon, 3 Jun 2024 17:36:13 +0200
Subject: [PATCH 29/33] cleaner ci

---
 .gitea/workflows/frontend_build-android.yaml  | 32 +++++++------
 frontend/android/.gitignore                   |  1 +
 frontend/android/README.md                    | 48 +++++++++++++++++++
 frontend/android/app/build.gradle             |  9 +++-
 .../android/app/src/main/AndroidManifest.xml  | 11 ++---
 frontend/android/build.gradle                 | 11 +++++
 frontend/android/fallback.properties          |  1 +
 frontend/lib/modules/landmark_card.dart       | 46 +++++++++---------
 frontend/lib/structs/landmark.dart            |  2 +
 frontend/lib/utils/get_landmarks.dart         | 37 +++++++++++---
 10 files changed, 148 insertions(+), 50 deletions(-)
 create mode 100644 frontend/android/README.md
 create mode 100644 frontend/android/fallback.properties

diff --git a/.gitea/workflows/frontend_build-android.yaml b/.gitea/workflows/frontend_build-android.yaml
index a1e2d7e..5856576 100644
--- a/.gitea/workflows/frontend_build-android.yaml
+++ b/.gitea/workflows/frontend_build-android.yaml
@@ -42,20 +42,24 @@ jobs:
     - run: flutter pub get
       working-directory: ./frontend
 
-    - run: flutter build apk --release --split-per-abi
+    - name: Add required secrets
+      run: |
+        echo ${{ secrets.ANDROID_SECRETS_BASE64 }} | base64 -d > ./android/secrets.properties
       working-directory: ./frontend
 
-    - name: Release APK
-      uses: https://gitea.com/akkuman/gitea-release-action@v1
+    - name: Sanity check
+      run: |
+        ls
+        ls -lah android
+      working-directory: ./frontend
+
+    - run: flutter build apk --release --split-per-abi --build-number=${{ gitea.run_number }}
+      working-directory: ./frontend
+
+    - name: Upload APKs to artifacts
+      uses: https://gitea.com/actions/upload-artifact@v3
       with:
-        files: ./frontend/build/app/outputs/flutter-apk/*.apk
-        name: Testing release
-        release_name: testing
-        tag: testing
-        tag_name: testing
-        release_body: "This is a testing release."
-        prerelease: true
-        token: ${{ secrets.GITEA_TOKEN }}
-      env:
-        NODE_OPTIONS: '--experimental-fetch'
-      
+        name: app-release
+        path: frontend/build/app/outputs/flutter-apk/
+        if-no-files-found: error
+        retention-days: 15
diff --git a/frontend/android/.gitignore b/frontend/android/.gitignore
index 6f56801..3592eab 100644
--- a/frontend/android/.gitignore
+++ b/frontend/android/.gitignore
@@ -4,6 +4,7 @@ gradle-wrapper.jar
 /gradlew
 /gradlew.bat
 /local.properties
+/secrets.properties
 GeneratedPluginRegistrant.java
 
 # Remember to never publicly share your keystore.
diff --git a/frontend/android/README.md b/frontend/android/README.md
new file mode 100644
index 0000000..1e3354e
--- /dev/null
+++ b/frontend/android/README.md
@@ -0,0 +1,48 @@
+## Android Setup
+
+### Keystore setup
+```bash
+keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias release
+```
+- This is required to store local credentials securely (not used for now).
+- But necesseary in order to restrict the particular api key to a particular app (through the sha1 of the associated keystore).
+
+
+### Building and secret credentials
+Following the guide under [https://developers.google.com/maps/flutter-package/config#android_1](https://developers.google.com/maps/flutter-package/config#android_1).
+- Add the following to `android/build.gradle`:
+    ```gradle
+    buildscript {
+        dependencies {
+            classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
+        }
+    }
+    ```
+- Add the following to `android/app/build.gradle`:
+    ```gradle
+    plugins {
+        // ...
+        id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
+    }
+    ```
+- Add the credentials to `android/secrets.properties`:
+    ```properties
+    MAPS_API_KEY=YOUR_API_KEY
+    ```
+- Reference the credentials in `android/app/src/main/AndroidManifest.xml`:
+    ```xml
+    <meta-data
+        android:name="com.google.android.geo.API_KEY"
+        android:value="${MAPS_API_KEY}" />
+    ```
+
+
+### Using the credentials in CI
+- Add the base64 encoded credentials to the repository secrets (e.g. `ANDROID_SECRETS`).
+    ```bash
+    base64 -i android/secrets.properties
+    ```
+- Use the following in the CI script:
+    ```bash
+    echo {{ secrets.ANDROID_SECRETS }} | base64 -d > android/secrets.properties
+    ```
\ No newline at end of file
diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle
index f6b499e..a62f13a 100644
--- a/frontend/android/app/build.gradle
+++ b/frontend/android/app/build.gradle
@@ -2,14 +2,18 @@ plugins {
     id "com.android.application"
     id "kotlin-android"
     id "dev.flutter.flutter-gradle-plugin"
+    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
+
 }
 
 def localProperties = new Properties()
-def localPropertiesFile = rootProject.file('local.properties')
+def localPropertiesFile = rootProject.file('secrets.properties')
 if (localPropertiesFile.exists()) {
     localPropertiesFile.withReader('UTF-8') { reader ->
         localProperties.load(reader)
     }
+} else {
+    throw new GradleException("Secrets file secrets.properties not found")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -52,6 +56,9 @@ android {
         targetSdkVersion flutter.targetSdkVersion
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
+        // Placeholders of keys that are replaced by the build system.
+        manifestPlaceholders += [MAPS_API_KEY: "some value"]
+
     }
 
     buildTypes {
diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
index b38e92b..d00332c 100644
--- a/frontend/android/app/src/main/AndroidManifest.xml
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Required to fetch data from the internet. -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+
     <application
         android:label="fast_network_navigation"
         android:name="${applicationName}"
@@ -32,11 +35,10 @@
         />
         <meta-data
             android:name="com.google.android.geo.API_KEY"
-            android:value="AIzaSyCeWk_D2xvfOHLidvV56EZeQCUybypEntw"
+            android:value="${MAPS_API_KEY}"
         />
-
-
     </application>
+    
     <!-- Required to query activities that can process text, see:
          https://developer.android.com/training/package-visibility?hl=en and
          https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
@@ -48,7 +50,4 @@
             <data android:mimeType="text/plain"/>
         </intent>
     </queries>
-
-    <!-- Required to fetch data from the internet. -->
-    <uses-permission android:name="android.permission.INTERNET"/>
 </manifest>
diff --git a/frontend/android/build.gradle b/frontend/android/build.gradle
index bc157bd..98e49ef 100644
--- a/frontend/android/build.gradle
+++ b/frontend/android/build.gradle
@@ -16,3 +16,14 @@ subprojects {
 tasks.register("clean", Delete) {
     delete rootProject.buildDir
 }
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
+    }
+}
\ No newline at end of file
diff --git a/frontend/android/fallback.properties b/frontend/android/fallback.properties
new file mode 100644
index 0000000..f7ee9e9
--- /dev/null
+++ b/frontend/android/fallback.properties
@@ -0,0 +1 @@
+MAPS_API_KEY=Key
\ No newline at end of file
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index 8b42b7a..70e8de5 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -30,38 +30,38 @@ class _LandmarkCardState extends State<LandmarkCard> {
                 bottomLeft: Radius.circular(15.0),
               ),
               image: DecorationImage(
-                image: NetworkImage('https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg'),
+                image: NetworkImage(widget.landmark.imageURL),
                 fit: BoxFit.cover,
               ),
             ),
           ),
           Padding(
             padding: EdgeInsets.all(10),
-            child: Expanded(
-              child: Column(
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  Row(
-                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    children: [
-                      Text(
-                        widget.landmark.name,
-                        style: TextStyle(
-                          fontSize: 18,
-                          fontWeight: FontWeight.bold,
-                        ),
+            
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Text(
+                      widget.landmark.name,
+                      style: TextStyle(
+                        fontSize: 18,
+                        fontWeight: FontWeight.bold,
                       ),
-                    ],
-                  ),
-                  SizedBox(height: 5),
-                  Text(
-                    "${widget.landmark.name} (${widget.landmark.type.name})",
-                    style: TextStyle(fontSize: 14),
-                  ),
-                ],
-              ),
+                    ),
+                  ],
+                ),
+                SizedBox(height: 5),
+                Text(
+                  "${widget.landmark.name} (${widget.landmark.type.name})",
+                  style: TextStyle(fontSize: 14),
+                ),
+              ],
             ),
           ),
+
           // Align(
           //   alignment: Alignment.topRight,
           //   child: Icon(Icons.push_pin, color: theme.primaryColor),
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
index 14d1b5b..1e9bf26 100644
--- a/frontend/lib/structs/landmark.dart
+++ b/frontend/lib/structs/landmark.dart
@@ -2,6 +2,7 @@ class Landmark {
   final String name;
   final List location;
   final LandmarkType type;
+  final String imageURL;
   // final String description;
   // final Duration duration;
   // final bool visited;
@@ -10,6 +11,7 @@ class Landmark {
     required this.name,
     required this.location,
     required this.type,
+    this.imageURL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg',
     // required this.description,
     // required this.duration,
     // required this.visited,
diff --git a/frontend/lib/utils/get_landmarks.dart b/frontend/lib/utils/get_landmarks.dart
index 5390849..53e7314 100644
--- a/frontend/lib/utils/get_landmarks.dart
+++ b/frontend/lib/utils/get_landmarks.dart
@@ -10,14 +10,39 @@ Future<List<Landmark>> fetchLandmarks() async {
     // If the server did return a 200 OK response,
     // then parse the JSON.
     List<Landmark> landmarks = [
-      Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
-      Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
-      Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
-      Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
-      Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
+      // 48°51′29.6″N 2°17′40.2″E
+      Landmark(
+        name: "Eiffel Tower",
+        location: [48.51296, 2.17402],
+        type: LandmarkType(name: "Tower"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
+        ),
+      Landmark(
+        name: "Notre Dame Cathedral",
+        location: [48.8530, 2.3498],
+        type: LandmarkType(name: "Monument"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
+        ),
+      Landmark(
+        name: "Louvre palace",
+        location: [48.8606, 2.3376],
+        type: LandmarkType(name: "Museum"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
+        ),
+      Landmark(
+        name: "Pont-des-arts",
+        location: [48.5130, 2.2015],
+        type: LandmarkType(name: "Bridge"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"),
+      Landmark(
+        name: "Panthéon",
+        location: [48.5046, 2.2046],
+        type: LandmarkType(name: "Monument"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
+        ),
     ];
     // sleep 10 seconds
-    await Future.delayed(Duration(seconds: 10));
+    await Future.delayed(Duration(seconds: 5));
     return landmarks;
   // } else {
   //   // If the server did not return a 200 OK response,
-- 
2.47.2


From 40943c5c5bf090e20d967edc56e9107f55ea6642 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Fri, 7 Jun 2024 15:06:33 +0200
Subject: [PATCH 30/33] finally use correct api key

---
 frontend/android/app/build.gradle | 25 ++++++++++++++++++++-----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle
index a62f13a..e1707f4 100644
--- a/frontend/android/app/build.gradle
+++ b/frontend/android/app/build.gradle
@@ -3,17 +3,18 @@ plugins {
     id "kotlin-android"
     id "dev.flutter.flutter-gradle-plugin"
     id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
-
+    // last is probably not needed
 }
 
+def localPropertiesFile = rootProject.file('local.properties')
 def localProperties = new Properties()
-def localPropertiesFile = rootProject.file('secrets.properties')
+
 if (localPropertiesFile.exists()) {
     localPropertiesFile.withReader('UTF-8') { reader ->
         localProperties.load(reader)
     }
 } else {
-    throw new GradleException("Secrets file secrets.properties not found")
+    throw new GradleException("local.properties not found")
 }
 
 def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -26,6 +27,20 @@ if (flutterVersionName == null) {
     flutterVersionName = '1.0'
 }
 
+
+
+def secretPropertiesFile = rootProject.file('secrets.properties')
+def secretProperties = new Properties()
+
+if (secretPropertiesFile.exists()) {
+    secretPropertiesFile.withReader('UTF-8') { reader ->
+        secretProperties.load(reader)
+    }
+} else {
+    throw new GradleException("Secrets file secrets.properties not found")
+}
+
+
 android {
     namespace "com.example.fast_network_navigation"
     compileSdk flutter.compileSdkVersion
@@ -56,8 +71,8 @@ android {
         targetSdkVersion flutter.targetSdkVersion
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
-        // Placeholders of keys that are replaced by the build system.
-        manifestPlaceholders += [MAPS_API_KEY: "some value"]
+        // // Placeholders of keys that are replaced by the build system.
+        manifestPlaceholders += ['MAPS_API_KEY': secretProperties.getProperty('MAPS_API_KEY')]
 
     }
 
-- 
2.47.2


From 9a5ae95d97f8c7671be851d6e0718fcef512c19d Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Fri, 7 Jun 2024 15:09:18 +0200
Subject: [PATCH 31/33] landmark styling

---
 frontend/lib/modules/landmark_card.dart      | 95 ++++++++++----------
 frontend/lib/modules/landmarks_overview.dart | 69 ++++++++------
 frontend/lib/pages/profile.dart              |  4 +-
 3 files changed, 92 insertions(+), 76 deletions(-)

diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index 70e8de5..909a7f3 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -15,59 +15,64 @@ class _LandmarkCardState extends State<LandmarkCard> {
   @override
   Widget build(BuildContext context) {
     ThemeData theme = Theme.of(context);
-    return Card(
-      shape: RoundedRectangleBorder(
-        borderRadius: BorderRadius.circular(15.0),
-      ),
-      child: Row(
-        children: [
-          Container(
-            width: 160,
-            height: 160,
-            decoration: BoxDecoration(
-              borderRadius: BorderRadius.only(
-                topLeft: Radius.circular(15.0),
-                bottomLeft: Radius.circular(15.0),
-              ),
-              image: DecorationImage(
-                image: NetworkImage(widget.landmark.imageURL),
+    return Container(
+      height: 160,
+      child: Card(
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(15.0),
+        ),
+        elevation: 5,
+        clipBehavior: Clip.antiAliasWithSaveLayer,
+        child: Row(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Container( // the image on the left
+              // inherit the height of the parent container
+              height: double.infinity,
+              // force a fixed width
+              width: 160,
+              child: Image.network(
+                widget.landmark.imageURL,
+                // cover the whole container meaning the image will be cropped
                 fit: BoxFit.cover,
               ),
             ),
-          ),
-          Padding(
-            padding: EdgeInsets.all(10),
-            
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                Row(
-                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            Flexible(
+              child: Padding(
+                padding: EdgeInsets.all(10),
+                child: Column(
                   children: [
-                    Text(
-                      widget.landmark.name,
-                      style: TextStyle(
-                        fontSize: 18,
-                        fontWeight: FontWeight.bold,
-                      ),
+                    Row(
+                      children: [
+                        Flexible(
+                          child: Text(
+                            widget.landmark.name,
+                            style: const TextStyle(
+                              fontSize: 18,
+                              fontWeight: FontWeight.bold,
+                            ),
+                            maxLines: 2,
+                          ),
+                        )
+                      ],
+                    ),
+                    Row(
+                      children: [
+                        Flexible(
+                          child: Text(
+                            "${widget.landmark.name} (${widget.landmark.type.name})",
+                            style: const TextStyle(fontSize: 14),
+                          ),
+                        )
+                      ]
                     ),
                   ],
                 ),
-                SizedBox(height: 5),
-                Text(
-                  "${widget.landmark.name} (${widget.landmark.type.name})",
-                  style: TextStyle(fontSize: 14),
-                ),
-              ],
+              ),
             ),
-          ),
-
-          // Align(
-          //   alignment: Alignment.topRight,
-          //   child: Icon(Icons.push_pin, color: theme.primaryColor),
-          // ),
-        ],
+          ],
+        ),
       ),
     );
   }
-}
\ No newline at end of file
+}
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index c967a65..722bfca 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -42,10 +42,13 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
           } else {
             children = [Center(child: CircularProgressIndicator())];
           }
-          return Center(
-            child: Column(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: children,
+          return Padding(
+            padding: const EdgeInsets.all(10),
+            child: Center(
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: children,
+              ),
             ),
           );
         },
@@ -56,46 +59,54 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
 
 Widget landmarksWithSteps(List<Landmark> landmarks) {
   List<Widget> children = [];
-  for (Landmark landmark in landmarks) {
-    children.add(LandmarkCard(landmark));
-    children.add(stepBetweenLandmarks());
+  for (int i = 0; i < landmarks.length; i++) {
+    children.add(LandmarkCard(landmarks[i]));
+    if (i < landmarks.length - 1) {
+      Widget step = stepBetweenLandmarks(landmarks[i], landmarks[i + 1]);
+      children.add(step);
+    }
   }
 
   return Column(
-    children: children.sublist(0, children.length - 1)
+    children: children
   );
 }
 
 
-Widget stepBetweenLandmarks() {
+Widget stepBetweenLandmarks(Landmark before, Landmark after) {
   // This is a simple widget that draws a line between landmark-cards
   // It's a vertical dotted line
   // Next to the line is the icon for the mode of transport (walking for now) and the estimated time
   // There is also a button to open the navigation instructions as a new intent
-  return Row(
-    children: [
-      Container(
-        width: 50,
-        height: 50,
-        decoration: BoxDecoration(
-          border: Border(
-            left: BorderSide(width: 1.0, color: Colors.black),
-          ),
-        ),
-        child: Column(
+  return Container(
+    margin: EdgeInsets.all(10),
+    padding: EdgeInsets.all(10),
+    decoration: BoxDecoration(
+      border: Border(
+        left: BorderSide(width: 3.0, color: Colors.black),
+      ),
+      // gradient: LinearGradient(
+      //   begin: Alignment.topLeft,
+      //   end: Alignment.bottomRight,
+      //   colors: [Colors.grey, Colors.white, Colors.white],
+      // ),
+    ),
+    child: Row( 
+      children: [
+        Column(
           children: [
             Icon(Icons.directions_walk),
             Text("5 min", style: TextStyle(fontSize: 10)),
           ],
         ),
-      ),
-      Spacer(),
-      ElevatedButton(
-        onPressed: () {
-          // Open navigation instructions
-        },
-        child: Text("Navigate"),
-      ),
-    ],
+        Spacer(),
+        ElevatedButton(
+          onPressed: () {
+            // Open navigation instructions
+          },
+          child: Text("Navigate"),
+        ),
+      ],
+    ),
   );
 }
\ No newline at end of file
diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 10fefe8..68d8348 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -51,7 +51,7 @@ class ImportanceSliders extends StatefulWidget {
 
 class _ImportanceSlidersState extends State<ImportanceSliders> {
 
-  UserPreferences _prefs = UserPreferences();
+  final UserPreferences _prefs = UserPreferences();
 
   @override
   void initState() {
@@ -80,7 +80,7 @@ class _ImportanceSlidersState extends State<ImportanceSliders> {
             },
           )
         ),
-        margin: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
+        margin: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 0),
         shadowColor: Colors.grey,
       ));
     }
-- 
2.47.2


From db41528702c93785a04e9cae5f44cbf3ede19069 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Fri, 21 Jun 2024 19:30:40 +0200
Subject: [PATCH 32/33] functional datastructure. Needs to be able to write to
 storage as well

---
 frontend/lib/layout.dart                     |  60 +++++++----
 frontend/lib/main.dart                       |   1 -
 frontend/lib/modules/greeter.dart            |   2 +-
 frontend/lib/modules/landmark_card.dart      |   4 +-
 frontend/lib/modules/landmarks_overview.dart |  49 +++++----
 frontend/lib/modules/map.dart                |  15 ++-
 frontend/lib/modules/trips_overview.dart     |  16 +--
 frontend/lib/pages/overview.dart             |  11 +-
 frontend/lib/pages/profile.dart              |   9 +-
 frontend/lib/structs/landmark.dart           | 106 +++++++++++--------
 frontend/lib/structs/linked_landmarks.dart   |  46 ++++++++
 frontend/lib/structs/preferences.dart        |   8 +-
 frontend/lib/structs/route.dart              |   2 +-
 frontend/lib/structs/trip.dart               |  42 ++++++--
 frontend/lib/utils/fetch_landmarks.dart      |  52 +++++++++
 frontend/lib/utils/get_landmarks.dart        |  52 ---------
 frontend/lib/utils/get_trips.dart            |  37 -------
 frontend/lib/utils/load_trips.dart           |  44 ++++++++
 18 files changed, 346 insertions(+), 210 deletions(-)
 create mode 100644 frontend/lib/structs/linked_landmarks.dart
 create mode 100644 frontend/lib/utils/fetch_landmarks.dart
 delete mode 100644 frontend/lib/utils/get_landmarks.dart
 delete mode 100644 frontend/lib/utils/get_trips.dart
 create mode 100644 frontend/lib/utils/load_trips.dart

diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index 6612e48..63f6c82 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -2,6 +2,7 @@ import 'package:fast_network_navigation/modules/trips_overview.dart';
 import 'package:fast_network_navigation/pages/new_trip.dart';
 import 'package:fast_network_navigation/pages/tutorial.dart';
 import 'package:fast_network_navigation/structs/trip.dart';
+import 'package:fast_network_navigation/utils/load_trips.dart';
 import 'package:flutter/material.dart';
 
 import 'package:fast_network_navigation/pages/overview.dart';
@@ -11,10 +12,13 @@ import 'package:fast_network_navigation/pages/profile.dart';
 // A side drawer is used to switch between pages
 class BasePage extends StatefulWidget {
   final String mainScreen;
-  final String currentMap;
-  final List<Trip> trips;
+  final Trip? trip;
   
-  const BasePage({super.key, required this.mainScreen, this.currentMap = "map", this.trips = const []});
+  const BasePage({
+    super.key,
+    required this.mainScreen,
+    this.trip
+  });
 
   @override
   State<BasePage> createState() => _BasePageState();
@@ -22,12 +26,15 @@ class BasePage extends StatefulWidget {
 
 class _BasePageState extends State<BasePage> {
 
-
   @override
   Widget build(BuildContext context) {
     Widget currentView = const Text("loading...");
+    Future<List<Trip>> trips = loadTrips();
+    Future<Trip> firstTrip = getFirstTrip(trips);
+    // Future<Trip> trip = Future(trips[0]);
+    
     if (widget.mainScreen == "map") {
-      currentView = NavigationOverview();
+      currentView = NavigationOverview(trip: firstTrip);
     } else if (widget.mainScreen == "tutorial") {
       currentView = TutorialPage();
     } else if (widget.mainScreen == "profile") {
@@ -40,9 +47,6 @@ class _BasePageState extends State<BasePage> {
       appBar: AppBar(title: Text("City Nav")),
       body: Center(child: currentView),
       drawer: Drawer(
-        // Add a ListView to the drawer. This ensures the user can scroll
-        // through the options in the drawer if there isn't enough vertical
-        // space to fit everything.
         child: Column(
           children: [
             DrawerHeader(
@@ -61,20 +65,10 @@ class _BasePageState extends State<BasePage> {
               ),
             ),
             ListTile(
-              title: const Text('Start'),
+              title: const Text('Your Trips'),
               leading: const Icon(Icons.map),
               selected: widget.mainScreen == "map",
-              onTap: () {
-                Navigator.of(context).push(
-                  MaterialPageRoute(
-                    builder: (context) => BasePage(mainScreen: "map")
-                  )
-                );
-              },
-            ),
-            ListTile(
-              title: const Text('Trip Overview'),
-              leading: const Icon(Icons.list),
+              onTap: () {},
               trailing: ElevatedButton(
                 onPressed: () {
                   Navigator.of(context).push(
@@ -86,7 +80,23 @@ class _BasePageState extends State<BasePage> {
                 child: const Text('New'),
               ),
             ),
-            Expanded(child: TripsOverview()),
+
+            // Adds a ListView to the drawer. This ensures the user can scroll
+            // through the options in the drawer if there isn't enough vertical
+            // space to fit everything.
+            Expanded(
+              child: TripsOverview(trips: trips),
+            ),
+            ElevatedButton(
+              onPressed: () {
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => const NewTripPage()
+                  )
+                );
+              },
+              child: const Text('Clear trips'),
+            ),
             const Divider(),
             ListTile(
               title: const Text('How to use'),
@@ -101,6 +111,7 @@ class _BasePageState extends State<BasePage> {
               },
             ),
 
+            // settings in the bottom of the drawer
             ListTile(
               title: const Text('Settings'),
               leading: const Icon(Icons.settings),
@@ -113,7 +124,6 @@ class _BasePageState extends State<BasePage> {
                 );
               },
             ),
-            // settings in the bottom of the drawer
           ],
         ),
       ),
@@ -121,3 +131,9 @@ class _BasePageState extends State<BasePage> {
   }
 }
 
+
+
+Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
+  List<Trip> tripsf = await trips;
+  return tripsf[0];
+}
\ No newline at end of file
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index 1fd9fc2..27bbb3f 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -17,4 +17,3 @@ class App extends StatelessWidget {
     );
   }
 }
-
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index e3066ac..767b43e 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -1,6 +1,6 @@
 import 'package:flutter/material.dart';
 
-Widget Greeter (ThemeData theme, {bool full = false}) {
+Widget Greeter(ThemeData theme, {bool full = false}) {
   String greeterText = "";
   try {
     String cityName = getCityName();
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index 909a7f3..d0bfb83 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -32,7 +32,9 @@ class _LandmarkCardState extends State<LandmarkCard> {
               // force a fixed width
               width: 160,
               child: Image.network(
-                widget.landmark.imageURL,
+                widget.landmark.imageURL!,
+                errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
+                // TODO: make this a switch statement to load a placeholder if null
                 // cover the whole container meaning the image will be cropped
                 fit: BoxFit.cover,
               ),
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index 722bfca..0d05c56 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -1,29 +1,34 @@
+import 'dart:collection';
+
 import 'package:fast_network_navigation/modules/landmark_card.dart';
 import 'package:fast_network_navigation/structs/landmark.dart';
-import 'package:fast_network_navigation/utils/get_landmarks.dart';
+
+import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:flutter/material.dart';
 
 
 
 
-class loadLandmarksOverview extends StatefulWidget {
-  const loadLandmarksOverview({super.key});
+class LandmarksOverview extends StatefulWidget {
+  final Future<Trip>? trip;
+  const LandmarksOverview({super.key, this.trip});
 
   @override
-  State<loadLandmarksOverview> createState() => _loadLandmarksOverviewState();
+  State<LandmarksOverview> createState() => _LandmarksOverviewState();
 }
 
-class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
-  final Future<List<Landmark>> _landmarks = fetchLandmarks();
+class _LandmarksOverviewState extends State<LandmarksOverview> {
+  // final Future<List<Landmark>> _landmarks = fetchLandmarks();
 
   @override
   Widget build(BuildContext context) {
+    final Future<LinkedList<Landmark>> _landmarks = getLandmarks(widget.trip);
     return DefaultTextStyle(
       style: Theme.of(context).textTheme.displayMedium!,
       textAlign: TextAlign.center,
-      child: FutureBuilder<List<Landmark>>(
+      child: FutureBuilder<LinkedList<Landmark>>(
         future: _landmarks,
-        builder: (BuildContext context, AsyncSnapshot<List<Landmark>> snapshot) {
+        builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
           List<Widget> children;
           if (snapshot.hasData) {
             children = [landmarksWithSteps(snapshot.data!)];
@@ -42,13 +47,10 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
           } else {
             children = [Center(child: CircularProgressIndicator())];
           }
-          return Padding(
-            padding: const EdgeInsets.all(10),
-            child: Center(
-              child: Column(
-                mainAxisAlignment: MainAxisAlignment.center,
-                children: children,
-              ),
+          return Center(
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: children,
             ),
           );
         },
@@ -57,12 +59,12 @@ class _loadLandmarksOverviewState extends State<loadLandmarksOverview> {
   }
 }
 
-Widget landmarksWithSteps(List<Landmark> landmarks) {
+Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
   List<Widget> children = [];
-  for (int i = 0; i < landmarks.length; i++) {
-    children.add(LandmarkCard(landmarks[i]));
-    if (i < landmarks.length - 1) {
-      Widget step = stepBetweenLandmarks(landmarks[i], landmarks[i + 1]);
+  for (Landmark landmark in landmarks) {
+    children.add(LandmarkCard(landmark));
+    if (landmark.next != null) {
+      Widget step = stepBetweenLandmarks(landmark, landmark.next!);
       children.add(step);
     }
   }
@@ -109,4 +111,9 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
       ],
     ),
   );
-}
\ No newline at end of file
+}
+
+Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async {
+  Trip tripf = await trip!;
+  return tripf.landmarks;
+}
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 341d6cd..9c8fe3f 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -1,9 +1,18 @@
+import 'dart:collection';
+
 import 'package:fast_network_navigation/structs/landmark.dart';
-import 'package:fast_network_navigation/utils/get_landmarks.dart';
+import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:flutter/material.dart';
 import 'package:google_maps_flutter/google_maps_flutter.dart';
 
 class MapWidget extends StatefulWidget {
+
+  final Future<Trip>? trip;
+
+  MapWidget({
+    this.trip
+  });
+
   @override
   State<MapWidget> createState() => _MapWidgetState();
 }
@@ -31,7 +40,8 @@ class _MapWidgetState extends State<MapWidget> {
 
   void drawLandmarks() async {
     // (re)draws landmarks on the map
-    List<Landmark> landmarks = await fetchLandmarks();
+    Trip? trip = await widget.trip;
+    LinkedList<Landmark> landmarks = trip!.landmarks;
     setState(() {
       for (Landmark landmark in landmarks) {
         markers.add(Marker(
@@ -55,4 +65,3 @@ class _MapWidgetState extends State<MapWidget> {
     );
   }
 }
-
diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart
index 955a109..bbc6a6e 100644
--- a/frontend/lib/modules/trips_overview.dart
+++ b/frontend/lib/modules/trips_overview.dart
@@ -2,19 +2,21 @@ import 'package:flutter/material.dart';
 
 import 'package:fast_network_navigation/layout.dart';
 import 'package:fast_network_navigation/structs/trip.dart';
-import 'package:fast_network_navigation/utils/get_trips.dart';
 
 
 class TripsOverview extends StatefulWidget {
-  
-  const TripsOverview({super.key});
+  final Future<List<Trip>> trips;
+  const TripsOverview({
+    super.key,
+    required this.trips,
+    });
 
   @override
   State<TripsOverview> createState() => _TripsOverviewState();
 }
 
 class _TripsOverviewState extends State<TripsOverview> {
-  final Future<List<Trip>> _trips = loadTrips();
+  // final Future<List<Trip>> _trips = loadTrips();
 
 
   Widget listBuild (BuildContext context, AsyncSnapshot<List<Trip>> snapshot) {
@@ -23,12 +25,12 @@ class _TripsOverviewState extends State<TripsOverview> {
       children = List<Widget>.generate(snapshot.data!.length, (index) {
         Trip trip = snapshot.data![index];
         return ListTile(
-          title: Text("Trip to ${trip.cityName} (${trip.landmarks.length} stops)"),
+          title: Text("Trip to ${trip.cityName}"),
           leading: Icon(Icons.pin_drop),
           onTap: () {
             Navigator.of(context).push(
               MaterialPageRoute(
-                builder: (context) => BasePage(mainScreen: "map") //, trip: trip)
+                builder: (context) => BasePage(mainScreen: "map", trip: trip)
               )
             );
           },
@@ -58,7 +60,7 @@ class _TripsOverviewState extends State<TripsOverview> {
   @override
   Widget build(BuildContext context) {
     return FutureBuilder(
-      future: _trips,
+      future: widget.trips,
       builder: listBuild,
     );
   }
diff --git a/frontend/lib/pages/overview.dart b/frontend/lib/pages/overview.dart
index ab6ffb8..22565f5 100644
--- a/frontend/lib/pages/overview.dart
+++ b/frontend/lib/pages/overview.dart
@@ -1,4 +1,5 @@
 import 'package:fast_network_navigation/modules/greeter.dart';
+import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:flutter/material.dart';
 import 'package:sliding_up_panel/sliding_up_panel.dart';
 
@@ -8,6 +9,12 @@ import 'package:fast_network_navigation/modules/map.dart';
 
 
 class NavigationOverview extends StatefulWidget {
+  final Future<Trip> trip;
+
+  NavigationOverview({
+    required this.trip
+  });
+
   @override
   State<NavigationOverview> createState() => _NavigationOverviewState();
 }
@@ -23,7 +30,7 @@ class _NavigationOverviewState extends State<NavigationOverview> {
         renderPanelSheet: false,
         panel: _floatingPanel(theme),
         collapsed: _floatingCollapsed(theme),
-        body: MapWidget()
+        body: MapWidget(trip: widget.trip)
     );
   }
 
@@ -58,7 +65,7 @@ class _NavigationOverviewState extends State<NavigationOverview> {
           child: Column(
             children: <Widget>[
               Greeter(theme, full: true),
-              loadLandmarksOverview(),
+              LandmarksOverview(trip: widget.trip),
             ],
           ),
         ),
diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart
index 68d8348..faa450c 100644
--- a/frontend/lib/pages/profile.dart
+++ b/frontend/lib/pages/profile.dart
@@ -51,13 +51,7 @@ class ImportanceSliders extends StatefulWidget {
 
 class _ImportanceSlidersState extends State<ImportanceSliders> {
 
-  final UserPreferences _prefs = UserPreferences();
-
-  @override
-  void initState() {
-    _prefs.load();
-    super.initState();
-  }
+  UserPreferences _prefs = UserPreferences();
 
   List<Card> _createSliders() {
     List<Card> sliders = [];
@@ -89,6 +83,7 @@ class _ImportanceSlidersState extends State<ImportanceSliders> {
 
   @override
   Widget build(BuildContext context) {
+
     return Column(children: _createSliders());
   }
 }
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
index 1e9bf26..3780cd4 100644
--- a/frontend/lib/structs/landmark.dart
+++ b/frontend/lib/structs/landmark.dart
@@ -1,56 +1,69 @@
-class Landmark {
-  final String name;
-  final List location;
-  final LandmarkType type;
-  final String imageURL;
-  // final String description;
-  // final Duration duration;
-  // final bool visited;
+import 'dart:collection';
+import 'dart:convert';
 
-  const Landmark({
+import 'package:shared_preferences/shared_preferences.dart';
+
+final class Landmark extends LinkedListEntry<Landmark>{
+  // A linked node of a list of Landmarks
+  final String uuid;
+  final String name;
+  final List<double> location;
+  final LandmarkType type;
+  final bool? isSecondary;
+
+  // description to be shown in the overview
+  final String? imageURL;
+  final String? description;
+  final Duration? duration;
+  final bool? visited;
+
+  // Next node
+  // final Landmark? next;
+  final Duration? tripTime;
+
+  Landmark({
+    required this.uuid,
     required this.name,
     required this.location,
     required this.type,
-    this.imageURL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg',
-    // required this.description,
-    // required this.duration,
-    // required this.visited,
+    this.isSecondary,
+
+    this.imageURL,
+    this.description,
+    this.duration,
+    this.visited,
+
+    // this.next,
+    this.tripTime,
   });
 
   factory Landmark.fromJson(Map<String, dynamic> json) {
-    return switch (json) {
-      {
-        'loc': List location,
+    if (json
+      case { // automatically match all the non-optionals and cast them to the right type
+        'uuid': String uuid,
         'name': String name,
-        'type': String type,
-        // 'description': String description,
-        // 'duration': int duration,
-        // 'visited': bool visited
-
-      } =>
-        Landmark(
-          name: name,
-          location: location,
-          type: LandmarkType(name: type)
-          // description: description,
-          // duration: Duration(minutes: duration),
-          // visited: visited
-        ),
-      _ => throw const FormatException('Failed to load destination.'),
-    };
+        'location': List<double> location,
+        'type': LandmarkType type,
+      }) {
+        // parse the rest separately, they could be missing
+        final isSecondary = json['is_secondary'] as bool?;
+        final imageURL = json['image_url'] as String?;
+        final description = json['description'] as String?;
+        final duration = json['duration'] as Duration?;
+        final visited = json['visited'] as bool?;
+        
+        return Landmark(
+          uuid: uuid, name: name, location: location, type: type, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
+    } else {
+      throw FormatException('Invalid JSON: $json');
+    }
   }
 
 
-    Map<String, dynamic> toJson() {
-      return {
-        'name': name,
-        'location': location,
-        'type': type.name,
-        // 'description': description,
-        // 'duration': duration.inMinutes,
-        // 'visited': visited
-      };
-    }
+  @override
+  bool operator ==(Object other) {
+    return other is Landmark && uuid == other.uuid;
+  }
 }
 
 
@@ -67,3 +80,12 @@ class LandmarkType {
 }
 
 
+
+// Helper
+(Landmark, String?) getLandmarkFromPrefs(SharedPreferences prefs, String uuid) {
+  String? content = prefs.getString('landmark_$uuid');
+  Map<String, dynamic> json = jsonDecode(content!);
+  String? nextUUID = json['next_uuid'];
+  return (Landmark.fromJson(json), nextUUID);
+}
+
diff --git a/frontend/lib/structs/linked_landmarks.dart b/frontend/lib/structs/linked_landmarks.dart
new file mode 100644
index 0000000..b27b3a6
--- /dev/null
+++ b/frontend/lib/structs/linked_landmarks.dart
@@ -0,0 +1,46 @@
+// import "package:fast_network_navigation/structs/landmark.dart";
+
+// class Linked<Landmark> {
+//   Landmark? head;
+  
+//   Linked();
+
+//   // class methods
+//   bool get isEmpty => head == null;
+
+//   // Add a new node to the end of the list
+//   void add(Landmark value) {
+//     if (isEmpty) {
+//       // If the list is empty, set the new node as the head
+//       head = value;
+//     } else {
+//       Landmark? current = head;
+//       while (current!.next != null) {
+//         // Traverse the list to find the last node
+//         current = current.next;
+//       }
+//       current.next = value; // Set the new node as the next node of the last node
+//     }
+//   }
+
+//   // Remove the first node with the given value
+//   void remove(Landmark value) {
+//     if (isEmpty) return;
+
+//     // If the value is in the head node, update the head to the next node
+//     if (head! == value) {
+//       head = head.next;
+//       return;
+//     }
+
+//     var current = head;
+//     while (current!.next != null) {
+//       if (current.next! == value) {
+//         // If the value is found in the next node, skip the next node
+//         current.next = current.next.next;
+//         return;
+//       }
+//       current = current.next;
+//     }
+//   }
+// }
\ No newline at end of file
diff --git a/frontend/lib/structs/preferences.dart b/frontend/lib/structs/preferences.dart
index 7746393..9a31fc7 100644
--- a/frontend/lib/structs/preferences.dart
+++ b/frontend/lib/structs/preferences.dart
@@ -67,16 +67,16 @@ class UserPreferences {
 
 
   void save() async {
-    SharedPreferences prefs = await SharedPreferences.getInstance();
+    SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
     for (SinglePreference pref in preferences) {
-      prefs.setInt(pref.key, pref.value);
+      sharedPrefs.setInt(pref.key, pref.value);
     }
   }
 
   void load() async {
-    SharedPreferences prefs = await SharedPreferences.getInstance();
+    SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
     for (SinglePreference pref in preferences) {
-      pref.value = prefs.getInt(pref.key) ?? 0;
+      pref.value = sharedPrefs.getInt(pref.key) ?? 0;
     }
   }
 }
\ No newline at end of file
diff --git a/frontend/lib/structs/route.dart b/frontend/lib/structs/route.dart
index 62e0baa..e88dab6 100644
--- a/frontend/lib/structs/route.dart
+++ b/frontend/lib/structs/route.dart
@@ -5,7 +5,7 @@ class Route {
   final String name;
   final Duration duration;
   final List<Landmark> landmarks;
-  
+
   Route({
     required this.name,
     required this.duration,
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index dc02332..739717f 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -1,25 +1,49 @@
 // Represents a collection of landmarks that represent a journey
 // Different instances of a Trip can be saved and loaded by the user
 
+import 'dart:collection';
+import 'dart:convert';
+
 import 'package:fast_network_navigation/structs/landmark.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 class Trip {
   final String uuid;
   final String cityName;
-  final List<Landmark> landmarks;
+  // TODO: cityName should be inferred from coordinates of the Landmarks
+  final LinkedList<Landmark> landmarks;
+  // could be empty as well
 
-
-  Trip({required this.uuid, required this.cityName, required this.landmarks});
+  Trip({
+    required this.uuid,
+    required this.cityName,
+    required this.landmarks,
+    });
 
   factory Trip.fromJson(Map<String, dynamic> json) {
-    List<Landmark> landmarks = [];
-    for (var landmark in json['landmarks']) {
-      landmarks.add(Landmark.fromJson(landmark));
-    }
     return Trip(
       uuid: json['uuid'],
       cityName: json['cityName'],
-      landmarks: landmarks,
+      landmarks: LinkedList()
     );
   }
-}
\ No newline at end of file
+
+  factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
+    String? content = prefs.getString('trip_$uuid');
+    Map<String, dynamic> json = jsonDecode(content!);
+    Trip trip = Trip.fromJson(json);
+    String? firstUUID = json['entry_uuid'];
+    appendLandmarks(trip.landmarks, prefs, firstUUID);
+    return trip;
+  }
+}
+
+// Helper
+
+appendLandmarks(LinkedList<Landmark> landmarks, SharedPreferences prefs, String? firstUUID) {
+  while (firstUUID != null) {
+    var (head, nextUUID) = getLandmarkFromPrefs(prefs, firstUUID);
+    landmarks.add(head);
+    firstUUID = nextUUID;
+  }
+}
diff --git a/frontend/lib/utils/fetch_landmarks.dart b/frontend/lib/utils/fetch_landmarks.dart
new file mode 100644
index 0000000..d9fa2b6
--- /dev/null
+++ b/frontend/lib/utils/fetch_landmarks.dart
@@ -0,0 +1,52 @@
+// import "package:fast_network_navigation/structs/landmark.dart";
+// import 'package:http/http.dart' as http;
+
+
+// Future<List<Landmark>> fetchLandmarks() async {
+//   // final response = await http
+//   //     .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
+
+//   // if (response.statusCode == 200) {
+//     // If the server did return a 200 OK response,
+//     // then parse the JSON.
+//     List<Landmark> landmarks = [
+//       // 48°51′29.6″N 2°17′40.2″E
+//       Landmark(
+//         name: "Eiffel Tower",
+//         location: [48.51296, 2.17402],
+//         type: LandmarkType(name: "Tower"),
+//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
+//         ),
+//       Landmark(
+//         name: "Notre Dame Cathedral",
+//         location: [48.8530, 2.3498],
+//         type: LandmarkType(name: "Monument"),
+//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
+//         ),
+//       Landmark(
+//         name: "Louvre palace",
+//         location: [48.8606, 2.3376],
+//         type: LandmarkType(name: "Museum"),
+//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
+//         ),
+//       Landmark(
+//         name: "Pont-des-arts",
+//         location: [48.5130, 2.2015],
+//         type: LandmarkType(name: "Bridge"),
+//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"),
+//       Landmark(
+//         name: "Panthéon",
+//         location: [48.5046, 2.2046],
+//         type: LandmarkType(name: "Monument"),
+//         imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
+//         ),
+//     ];
+//     // sleep 10 seconds
+//     await Future.delayed(Duration(seconds: 5));
+//     return landmarks;
+//   // } else {
+//   //   // If the server did not return a 200 OK response,
+//   //   // then throw an exception.
+//   //   throw Exception('Failed to load destination');
+//   // }
+// }
\ No newline at end of file
diff --git a/frontend/lib/utils/get_landmarks.dart b/frontend/lib/utils/get_landmarks.dart
deleted file mode 100644
index 53e7314..0000000
--- a/frontend/lib/utils/get_landmarks.dart
+++ /dev/null
@@ -1,52 +0,0 @@
-import "package:fast_network_navigation/structs/landmark.dart";
-import 'package:http/http.dart' as http;
-
-
-Future<List<Landmark>> fetchLandmarks() async {
-  // final response = await http
-  //     .get(Uri.parse('https://nav.kluster.moll.re/v1/destination/1'));
-
-  // if (response.statusCode == 200) {
-    // If the server did return a 200 OK response,
-    // then parse the JSON.
-    List<Landmark> landmarks = [
-      // 48°51′29.6″N 2°17′40.2″E
-      Landmark(
-        name: "Eiffel Tower",
-        location: [48.51296, 2.17402],
-        type: LandmarkType(name: "Tower"),
-        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
-        ),
-      Landmark(
-        name: "Notre Dame Cathedral",
-        location: [48.8530, 2.3498],
-        type: LandmarkType(name: "Monument"),
-        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
-        ),
-      Landmark(
-        name: "Louvre palace",
-        location: [48.8606, 2.3376],
-        type: LandmarkType(name: "Museum"),
-        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
-        ),
-      Landmark(
-        name: "Pont-des-arts",
-        location: [48.5130, 2.2015],
-        type: LandmarkType(name: "Bridge"),
-        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"),
-      Landmark(
-        name: "Panthéon",
-        location: [48.5046, 2.2046],
-        type: LandmarkType(name: "Monument"),
-        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
-        ),
-    ];
-    // sleep 10 seconds
-    await Future.delayed(Duration(seconds: 5));
-    return landmarks;
-  // } else {
-  //   // If the server did not return a 200 OK response,
-  //   // then throw an exception.
-  //   throw Exception('Failed to load destination');
-  // }
-}
\ No newline at end of file
diff --git a/frontend/lib/utils/get_trips.dart b/frontend/lib/utils/get_trips.dart
deleted file mode 100644
index 0a64080..0000000
--- a/frontend/lib/utils/get_trips.dart
+++ /dev/null
@@ -1,37 +0,0 @@
-import 'dart:convert';
-
-import 'package:fast_network_navigation/structs/trip.dart';
-import 'package:fast_network_navigation/structs/landmark.dart';
-import 'package:shared_preferences/shared_preferences.dart';
-
-Future<List<Trip>> loadTrips() async {
-  SharedPreferences prefs = await SharedPreferences.getInstance();
-
-  Set<String> keys = prefs.getKeys();
-  List<Trip> itineraries = [];
-  for (String key in keys) {
-    if (key.startsWith("itinerary_")) {
-      String json = prefs.getString(key)!;
-      itineraries.add(Trip.fromJson(jsonDecode(json)));
-    }
-  }
-  itineraries.add(Trip(uuid: "1", cityName: "Paris", landmarks: [
-      Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
-      Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
-      Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
-      Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
-      Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
-    ]));
-  itineraries.add(Trip(uuid: "2", cityName: "Vienna", landmarks: []));
-  itineraries.add(Trip(uuid: "3", cityName: "London", landmarks: []));
-  itineraries.add(Trip(uuid: "4", cityName: "Madrid", landmarks: []));
-  itineraries.add(Trip(uuid: "5", cityName: "Tokyo", landmarks: []));
-  itineraries.add(Trip(uuid: "6", cityName: "New York", landmarks: []));
-  itineraries.add(Trip(uuid: "7", cityName: "Los Angeles", landmarks: []));
-  itineraries.add(Trip(uuid: "8", cityName: "Zurich", landmarks: []));
-  itineraries.add(Trip(uuid: "9", cityName: "Orschwiller", landmarks: []));
-
-  await Future.delayed(Duration(seconds: 3));
-
-  return itineraries;
-}
diff --git a/frontend/lib/utils/load_trips.dart b/frontend/lib/utils/load_trips.dart
new file mode 100644
index 0000000..2facd3d
--- /dev/null
+++ b/frontend/lib/utils/load_trips.dart
@@ -0,0 +1,44 @@
+import 'dart:collection';
+
+import 'package:fast_network_navigation/structs/linked_landmarks.dart';
+import 'package:fast_network_navigation/structs/trip.dart';
+import 'package:fast_network_navigation/structs/landmark.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+Future<List<Trip>> loadTrips() async {
+  SharedPreferences prefs = await SharedPreferences.getInstance();
+
+  List<Trip> trips = [];
+  Set<String> keys = prefs.getKeys();
+  for (String key in keys) {
+    if (key.startsWith('trip_')) {
+      String uuid = key.replaceFirst('trip_', '');
+      trips.add(Trip.fromPrefs(prefs, uuid));
+    }
+  }
+
+  if (trips.isEmpty) {
+    String now = DateTime.now().toString();
+    trips.add(
+      Trip(uuid: '1', cityName: 'Paris (generated $now)', landmarks: LinkedList<Landmark>())
+    );
+    //   Trip(uuid: "1", cityName: "Paris", landmarks: [
+    //     Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
+    //     Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
+    //     Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
+    //     Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
+    //     Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
+    //   ]));
+    // trips.add(Trip(uuid: "2", cityName: "Vienna", landmarks: []));
+    // trips.add(Trip(uuid: "3", cityName: "London", landmarks: []));
+    // trips.add(Trip(uuid: "4", cityName: "Madrid", landmarks: []));
+    // trips.add(Trip(uuid: "5", cityName: "Tokyo", landmarks: []));
+    // trips.add(Trip(uuid: "6", cityName: "New York", landmarks: []));
+    // trips.add(Trip(uuid: "7", cityName: "Los Angeles", landmarks: []));
+    // trips.add(Trip(uuid: "8", cityName: "Zurich", landmarks: []));
+    // trips.add(Trip(uuid: "9", cityName: "Orschwiller", landmarks: []));
+
+  }
+
+  return trips;
+}
-- 
2.47.2


From eede94add407cc99bf2040a5b713d703bd34b9db Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sun, 23 Jun 2024 21:19:06 +0200
Subject: [PATCH 33/33] working save and load functionality with custom
 datastructures

---
 frontend/lib/layout.dart                     |  15 +--
 frontend/lib/modules/greeter.dart            |  99 ++++++++++------
 frontend/lib/modules/landmark_card.dart      |   2 +-
 frontend/lib/modules/landmarks_overview.dart |  15 ++-
 frontend/lib/modules/map.dart                |  32 ++++--
 frontend/lib/modules/trips_overview.dart     |   2 +-
 frontend/lib/pages/overview.dart             |  20 ++--
 frontend/lib/structs/landmark.dart           |  51 +++++---
 frontend/lib/structs/trip.dart               |  36 +++++-
 frontend/lib/utils/load_trips.dart           | 115 +++++++++++++++----
 10 files changed, 279 insertions(+), 108 deletions(-)

diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index 63f6c82..d1b80a8 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -12,7 +12,7 @@ import 'package:fast_network_navigation/pages/profile.dart';
 // A side drawer is used to switch between pages
 class BasePage extends StatefulWidget {
   final String mainScreen;
-  final Trip? trip;
+  final Future<Trip>? trip;
   
   const BasePage({
     super.key,
@@ -30,11 +30,10 @@ class _BasePageState extends State<BasePage> {
   Widget build(BuildContext context) {
     Widget currentView = const Text("loading...");
     Future<List<Trip>> trips = loadTrips();
-    Future<Trip> firstTrip = getFirstTrip(trips);
-    // Future<Trip> trip = Future(trips[0]);
+    
     
     if (widget.mainScreen == "map") {
-      currentView = NavigationOverview(trip: firstTrip);
+      currentView = NavigationOverview(trip: widget.trip ?? getFirstTrip(trips));
     } else if (widget.mainScreen == "tutorial") {
       currentView = TutorialPage();
     } else if (widget.mainScreen == "profile") {
@@ -88,12 +87,8 @@ class _BasePageState extends State<BasePage> {
               child: TripsOverview(trips: trips),
             ),
             ElevatedButton(
-              onPressed: () {
-                Navigator.of(context).push(
-                  MaterialPageRoute(
-                    builder: (context) => const NewTripPage()
-                  )
-                );
+              onPressed: () async {
+                removeAllTripsFromPrefs();
               },
               child: const Text('Clear trips'),
             ),
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index 767b43e..8ed404f 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -1,44 +1,73 @@
+import 'package:fast_network_navigation/structs/trip.dart';
+
 import 'package:flutter/material.dart';
 
-Widget Greeter(ThemeData theme, {bool full = false}) {
-  String greeterText = "";
-  try {
-    String cityName = getCityName();
-    greeterText = "Welcome to $cityName!";
-  } catch (e) {
-    greeterText = "Welcome ...";
-  }
+class Greeter extends StatefulWidget {
+  final Future<Trip> trip;
+  final bool standalone;
 
-  Widget topGreeter = Text(
-    greeterText,
-    style: TextStyle(color: theme.primaryColor, fontSize: 24.0, fontWeight: FontWeight.bold),
-    maxLines: 1,
-  );
+  Greeter({
+    required this.standalone,
+    required this.trip
+  });
 
-  Widget bottomGreeter = Container();
-  if (full) {
-    bottomGreeter = Text(
-      "Busy day ahead? Here is how to make the most of it!",
-      style: TextStyle(color: Colors.black, fontSize: 18),
-      textAlign: TextAlign.center,
-      );
-  }
-  Widget greeter = Center(
-    child: Column(
-      children: [
-        if (!full) Padding(padding: EdgeInsets.only(top: 24.0)),
-        topGreeter,
-        if (full) bottomGreeter,
-        Padding(padding: EdgeInsets.only(bottom: 24.0)),
-      ],
-    ),
-  );
-
-  return greeter;
+  @override
+  State<Greeter> createState() => _GreeterState();
 }
 
 
 
-String getCityName() {
-  return "Paris";
+class _GreeterState extends State<Greeter> {
+  Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
+    ThemeData theme = Theme.of(context);
+    String cityName = "";
+    if (snapshot.hasData) {
+      cityName = snapshot.data?.cityName ?? '...';
+    } else if (snapshot.hasError) {
+      cityName = "error";
+    } else { // still awaiting the cityname
+      cityName = "...";
+    }
+
+    Widget topGreeter = Text(
+      'Welcome to $cityName!',
+      style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+    );
+
+    if (widget.standalone) {
+      return Center(
+        child: Padding(
+          padding: EdgeInsets.only(top: 24.0),
+          child: topGreeter,
+        ),
+      );
+    } else {
+      return Center(
+        child: Column(
+          children: [
+            Padding(padding: EdgeInsets.only(top: 24.0)),
+            topGreeter,
+            bottomGreeter,
+            Padding(padding: EdgeInsets.only(bottom: 24.0)),
+          ],
+        )
+      );
+    }
+  }
+
+  Widget bottomGreeter = const Text(
+    "Busy day ahead? Here is how to make the most of it!",
+    style: TextStyle(color: Colors.black, fontSize: 18),
+    textAlign: TextAlign.center,
+  );
+
+
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder(
+      future: widget.trip,
+      builder: greeterBuild,
+    );
+  }
 }
\ No newline at end of file
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index d0bfb83..070ff54 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -32,7 +32,7 @@ class _LandmarkCardState extends State<LandmarkCard> {
               // force a fixed width
               width: 160,
               child: Image.network(
-                widget.landmark.imageURL!,
+                widget.landmark.imageURL ?? '',
                 errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
                 // TODO: make this a switch statement to load a placeholder if null
                 // cover the whole container meaning the image will be cropped
diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart
index 0d05c56..a29cd2a 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -5,6 +5,7 @@ import 'package:fast_network_navigation/structs/landmark.dart';
 
 import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 
 
@@ -31,7 +32,7 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
         builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
           List<Widget> children;
           if (snapshot.hasData) {
-            children = [landmarksWithSteps(snapshot.data!)];
+            children = [landmarksWithSteps(snapshot.data!), saveButton()];
           } else if (snapshot.hasError) {
             children = <Widget>[
               const Icon(
@@ -41,7 +42,7 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
               ),
               Padding(
                 padding: const EdgeInsets.only(top: 16),
-                child: Text('Error: ${snapshot.error}'),
+                child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
               ),
             ];
           } else {
@@ -57,6 +58,15 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
       ),
     );
   }
+  Widget saveButton() => ElevatedButton(
+    onPressed: () async {
+      Trip? trip = await widget.trip;
+      SharedPreferences prefs = await SharedPreferences.getInstance();
+      trip?.toPrefs(prefs);
+    },
+    child: const Text('Save'),
+  );
+
 }
 
 Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
@@ -117,3 +127,4 @@ Future<LinkedList<Landmark>> getLandmarks (Future<Trip>? trip) async {
   Trip tripf = await trip!;
   return tripf.landmarks;
 }
+
diff --git a/frontend/lib/modules/map.dart b/frontend/lib/modules/map.dart
index 9c8fe3f..916d231 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -27,12 +27,18 @@ class _MapWidgetState extends State<MapWidget> {
   Set<Marker> markers = <Marker>{};
   
 
-  void _onMapCreated(GoogleMapController controller) {
+  void _onMapCreated(GoogleMapController controller) async {
     mapController = controller;
+    Trip? trip = await widget.trip;
+    List<double>? newLocation = trip?.landmarks.first.location;
+    if (newLocation != null) {
+      CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
+      controller.moveCamera(update);
+    }
     drawLandmarks();
   }
 
-  
+
   void _onCameraIdle() {
     // print(mapController.getLatLng(ScreenCoordinate(x: 0, y: 0)));
   }
@@ -41,16 +47,18 @@ class _MapWidgetState extends State<MapWidget> {
   void drawLandmarks() async {
     // (re)draws landmarks on the map
     Trip? trip = await widget.trip;
-    LinkedList<Landmark> landmarks = trip!.landmarks;
-    setState(() {
-      for (Landmark landmark in landmarks) {
-        markers.add(Marker(
-          markerId: MarkerId(landmark.name),
-          position: LatLng(landmark.location[0], landmark.location[1]),
-          infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
-        ));
-      }
-    });
+    LinkedList<Landmark>? landmarks = trip?.landmarks;
+    if (landmarks != null){
+      setState(() {
+        for (Landmark landmark in landmarks) {
+          markers.add(Marker(
+            markerId: MarkerId(landmark.name),
+            position: LatLng(landmark.location[0], landmark.location[1]),
+            infoWindow: InfoWindow(title: landmark.name, snippet: landmark.type.name),
+          ));
+        }
+      });
+    }
   }
 
 
diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart
index bbc6a6e..bb803bb 100644
--- a/frontend/lib/modules/trips_overview.dart
+++ b/frontend/lib/modules/trips_overview.dart
@@ -30,7 +30,7 @@ class _TripsOverviewState extends State<TripsOverview> {
           onTap: () {
             Navigator.of(context).push(
               MaterialPageRoute(
-                builder: (context) => BasePage(mainScreen: "map", trip: trip)
+                builder: (context) => BasePage(mainScreen: "map", trip: Future.value(trip))
               )
             );
           },
diff --git a/frontend/lib/pages/overview.dart b/frontend/lib/pages/overview.dart
index 22565f5..5354641 100644
--- a/frontend/lib/pages/overview.dart
+++ b/frontend/lib/pages/overview.dart
@@ -1,10 +1,11 @@
-import 'package:fast_network_navigation/modules/greeter.dart';
-import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:flutter/material.dart';
 import 'package:sliding_up_panel/sliding_up_panel.dart';
 
+import 'package:fast_network_navigation/structs/trip.dart';
+
 import 'package:fast_network_navigation/modules/landmarks_overview.dart';
 import 'package:fast_network_navigation/modules/map.dart';
+import 'package:fast_network_navigation/modules/greeter.dart';
 
 
 
@@ -25,16 +26,16 @@ class _NavigationOverviewState extends State<NavigationOverview> {
 
   @override
   Widget build(BuildContext context) {
-    final ThemeData theme = Theme.of(context);
     return SlidingUpPanel(
         renderPanelSheet: false,
-        panel: _floatingPanel(theme),
-        collapsed: _floatingCollapsed(theme),
+        panel: _floatingPanel(),
+        collapsed: _floatingCollapsed(),
         body: MapWidget(trip: widget.trip)
     );
   }
 
-  Widget _floatingCollapsed(ThemeData theme){
+  Widget _floatingCollapsed(){
+    final ThemeData theme = Theme.of(context);
     return Container(
       decoration: BoxDecoration(
         color: theme.canvasColor,
@@ -42,11 +43,12 @@ class _NavigationOverviewState extends State<NavigationOverview> {
         boxShadow: []
       ),
       
-      child: Greeter(theme)
+      child: Greeter(standalone: true, trip: widget.trip)
     );
   }
 
-  Widget _floatingPanel(ThemeData theme){
+  Widget _floatingPanel(){
+    final ThemeData theme = Theme.of(context);
     return Container(
       decoration: BoxDecoration(
         color: Colors.white,
@@ -64,7 +66,7 @@ class _NavigationOverviewState extends State<NavigationOverview> {
         child: SingleChildScrollView(
           child: Column(
             children: <Widget>[
-              Greeter(theme, full: true),
+              Greeter(standalone: false, trip: widget.trip),
               LandmarksOverview(trip: widget.trip),
             ],
           ),
diff --git a/frontend/lib/structs/landmark.dart b/frontend/lib/structs/landmark.dart
index 3780cd4..9e5e1ef 100644
--- a/frontend/lib/structs/landmark.dart
+++ b/frontend/lib/structs/landmark.dart
@@ -21,6 +21,7 @@ final class Landmark extends LinkedListEntry<Landmark>{
   // final Landmark? next;
   final Duration? tripTime;
 
+
   Landmark({
     required this.uuid,
     required this.name,
@@ -37,23 +38,28 @@ final class Landmark extends LinkedListEntry<Landmark>{
     this.tripTime,
   });
 
+
   factory Landmark.fromJson(Map<String, dynamic> json) {
     if (json
       case { // automatically match all the non-optionals and cast them to the right type
         'uuid': String uuid,
         'name': String name,
-        'location': List<double> location,
-        'type': LandmarkType type,
+        'location': List<dynamic> location,
+        'type': String type,
       }) {
-        // parse the rest separately, they could be missing
-        final isSecondary = json['is_secondary'] as bool?;
-        final imageURL = json['image_url'] as String?;
-        final description = json['description'] as String?;
-        final duration = json['duration'] as Duration?;
-        final visited = json['visited'] as bool?;
-        
-        return Landmark(
-          uuid: uuid, name: name, location: location, type: type, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
+      // refine the parsing on a few
+      List<double> locationFixed = List<double>.from(location);
+      // parse the rest separately, they could be missing
+      LandmarkType typeFixed = LandmarkType(name: type);
+      final isSecondary = json['is_secondary'] as bool?;
+      final imageURL = json['image_url'] as String?;
+      final description = json['description'] as String?;
+      var duration = Duration(minutes: json['duration'] ?? 0) as Duration?;
+      if (duration == const Duration()) {duration = null;};
+      final visited = json['visited'] as bool?;
+      
+      return Landmark(
+        uuid: uuid, name: name, location: locationFixed, type: typeFixed, isSecondary: isSecondary, imageURL: imageURL, description: description, duration: duration, visited: visited);
     } else {
       throw FormatException('Invalid JSON: $json');
     }
@@ -64,6 +70,19 @@ final class Landmark extends LinkedListEntry<Landmark>{
   bool operator ==(Object other) {
     return other is Landmark && uuid == other.uuid;
   }
+
+
+  Map<String, dynamic> toJson() => {
+    'uuid': uuid,
+    'name': name,
+    'location': location,
+    'type': type.name,
+    'is_secondary': isSecondary,
+    'image_url': imageURL,
+    'description': description,
+    'duration': duration?.inMinutes,
+    'visited': visited
+  };
 }
 
 
@@ -80,8 +99,8 @@ class LandmarkType {
 }
 
 
-
-// Helper
+// Helpers
+// Handling the landmarks requires a little bit of special care because the linked list is not directly representable in json
 (Landmark, String?) getLandmarkFromPrefs(SharedPreferences prefs, String uuid) {
   String? content = prefs.getString('landmark_$uuid');
   Map<String, dynamic> json = jsonDecode(content!);
@@ -89,3 +108,9 @@ class LandmarkType {
   return (Landmark.fromJson(json), nextUUID);
 }
 
+
+void landmarkToPrefs(SharedPreferences prefs, Landmark current, Landmark? next) {
+  Map<String, dynamic> json = current.toJson();
+  json['next_uuid'] = next?.uuid;
+  prefs.setString('landmark_${current.uuid}', jsonEncode(json));
+}
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index 739717f..7ac40e5 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -14,36 +14,62 @@ class Trip {
   final LinkedList<Landmark> landmarks;
   // could be empty as well
 
+
   Trip({
     required this.uuid,
     required this.cityName,
     required this.landmarks,
-    });
+  });
+
 
   factory Trip.fromJson(Map<String, dynamic> json) {
     return Trip(
       uuid: json['uuid'],
-      cityName: json['cityName'],
+      cityName: json['city_name'],
       landmarks: LinkedList()
     );
   }
 
+
   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
     String? content = prefs.getString('trip_$uuid');
     Map<String, dynamic> json = jsonDecode(content!);
     Trip trip = Trip.fromJson(json);
     String? firstUUID = json['entry_uuid'];
-    appendLandmarks(trip.landmarks, prefs, firstUUID);
+    readLandmarks(trip.landmarks, prefs, firstUUID);
     return trip;
   }
+
+
+  Map<String, dynamic> toJson() => {
+    'uuid': uuid,
+    'city_name': cityName,
+    'entry_uuid': landmarks.first?.uuid ?? ''
+  };
+
+
+  void toPrefs(SharedPreferences prefs){
+    Map<String, dynamic> json = toJson();
+    prefs.setString('trip_$uuid', jsonEncode(json));
+    for (Landmark landmark in landmarks) {
+      landmarkToPrefs(prefs, landmark, landmark.next);
+    }
+  }
 }
 
-// Helper
 
-appendLandmarks(LinkedList<Landmark> landmarks, SharedPreferences prefs, String? firstUUID) {
+// Helper
+readLandmarks(LinkedList<Landmark> landmarks, SharedPreferences prefs, String? firstUUID) {
   while (firstUUID != null) {
     var (head, nextUUID) = getLandmarkFromPrefs(prefs, firstUUID);
     landmarks.add(head);
     firstUUID = nextUUID;
   }
 }
+
+
+
+void removeAllTripsFromPrefs () async {
+  SharedPreferences prefs = await SharedPreferences.getInstance();
+  prefs.clear();
+}
diff --git a/frontend/lib/utils/load_trips.dart b/frontend/lib/utils/load_trips.dart
index 2facd3d..3c90c4a 100644
--- a/frontend/lib/utils/load_trips.dart
+++ b/frontend/lib/utils/load_trips.dart
@@ -1,6 +1,5 @@
 import 'dart:collection';
 
-import 'package:fast_network_navigation/structs/linked_landmarks.dart';
 import 'package:fast_network_navigation/structs/trip.dart';
 import 'package:fast_network_navigation/structs/landmark.dart';
 import 'package:shared_preferences/shared_preferences.dart';
@@ -18,27 +17,103 @@ Future<List<Trip>> loadTrips() async {
   }
 
   if (trips.isEmpty) {
-    String now = DateTime.now().toString();
-    trips.add(
-      Trip(uuid: '1', cityName: 'Paris (generated $now)', landmarks: LinkedList<Landmark>())
+    Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>());
+    t1.landmarks.add(
+      Landmark(
+        uuid: '1',
+        name: "Eiffel Tower",
+        location: [48.859, 2.295],
+        type: LandmarkType(name: "Tower"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Tour_Eiffel_Wikimedia_Commons.jpg/1037px-Tour_Eiffel_Wikimedia_Commons.jpg"
+      ),
     );
-    //   Trip(uuid: "1", cityName: "Paris", landmarks: [
-    //     Landmark(name: "Landmark 1", location: [48.85, 2.35], type: LandmarkType(name: "Type 1")),
-    //     Landmark(name: "Landmark 2", location: [48.86, 2.36], type: LandmarkType(name: "Type 2")),
-    //     Landmark(name: "Landmark 3", location: [48.75, 2.3], type: LandmarkType(name: "Type 3")),
-    //     Landmark(name: "Landmark 4", location: [48.9, 2.4], type: LandmarkType(name: "Type 4")),
-    //     Landmark(name: "Landmark 5", location: [48.91, 2.45], type: LandmarkType(name: "Type 5")),
-    //   ]));
-    // trips.add(Trip(uuid: "2", cityName: "Vienna", landmarks: []));
-    // trips.add(Trip(uuid: "3", cityName: "London", landmarks: []));
-    // trips.add(Trip(uuid: "4", cityName: "Madrid", landmarks: []));
-    // trips.add(Trip(uuid: "5", cityName: "Tokyo", landmarks: []));
-    // trips.add(Trip(uuid: "6", cityName: "New York", landmarks: []));
-    // trips.add(Trip(uuid: "7", cityName: "Los Angeles", landmarks: []));
-    // trips.add(Trip(uuid: "8", cityName: "Zurich", landmarks: []));
-    // trips.add(Trip(uuid: "9", cityName: "Orschwiller", landmarks: []));
+    t1.landmarks.add(
+      Landmark(
+        uuid: "2",
+        name: "Notre Dame Cathedral",
+        location: [48.8530, 2.3498],
+        type: LandmarkType(name: "Monument"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Notre-Dame_de_Paris%2C_4_October_2017.jpg/440px-Notre-Dame_de_Paris%2C_4_October_2017.jpg"
+      ),
+    );
+    t1.landmarks.add(
+      Landmark(
+        uuid: "3",
+        name: "Louvre palace",
+        location: [48.8606, 2.3376],
+        type: LandmarkType(name: "Museum"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/66/Louvre_Museum_Wikimedia_Commons.jpg/540px-Louvre_Museum_Wikimedia_Commons.jpg"
+      ),
+    );
+    t1.landmarks.add(
+      Landmark(
+        uuid: "4",
+        name: "Pont-des-arts",
+        location: [48.8585, 2.3376],
+        type: LandmarkType(name: "Bridge"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg/560px-Pont_des_Arts%2C_6e_Arrondissement%2C_Paris_%28HDR%29_20140320_1.jpg"
+      ),
+    );
+    t1.landmarks.add(
+      Landmark(
+        uuid: "5",
+        name: "Panthéon",
+        location: [48.847, 2.347],
+        type: LandmarkType(name: "Monument"),
+        imageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Pantheon_of_Paris_007.JPG/1280px-Pantheon_of_Paris_007.JPG"
+      ),
+    );
+    trips.add(t1);
+
+
+    Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>());
+
+    t2.landmarks.add(
+      Landmark(
+        uuid: '21',
+        name: "St. Charles's Church",
+        location: [48.1924563,16.3334399],
+        type: LandmarkType(name: "Monument"),
+        imageURL: "https://lh5.googleusercontent.com/p/AF1QipNNmA76Ps71NCL9rOOFoyheCEOyXWdHcUgQx9jd=w408-h305-k-no"
+      ),
+    );
+    t2.landmarks.add(
+      Landmark(
+        uuid: "22",
+        name: "Vienna State Opera",
+        location: [48.1949124,16.3483292],
+        type: LandmarkType(name: "Culture"),
+        imageURL: "https://lh5.googleusercontent.com/p/AF1QipMOx398kcoeDXFruSHNsb4lmZtdT8vibtK0cLi-=w408-h306-k-no"
+      ),
+    );
+    t2.landmarks.add(
+      Landmark(
+        uuid: "23",
+        name: "Belvedere-Schlossgarten",
+        location: [48.1956427,16.3711521],
+        type: LandmarkType(name: "Nature"),
+        imageURL: "https://lh5.googleusercontent.com/p/AF1QipNcI5LImH2Qdzx0GmF-5CY1wRKINFZ7HkahPEy1=w408-h306-k-no"
+      ),
+    );
+    t2.landmarks.add(
+      Landmark(
+        uuid: "24",
+        name: "Kunsthistorisches Museum Wien",
+        location: [48.2047501,16.3581904],
+        type: LandmarkType(name: "Museum"),
+        imageURL: "https://lh5.googleusercontent.com/p/AF1QipPuDu-kCCowO4TcawjziE8AhDVAANagVtRYBjlv=w408-h450-k-no"
+      ),
+    );
+    t2.landmarks.add(
+      Landmark(
+        uuid: "25",
+        name: "Salztorbrücke",
+        location: [48.2132382,16.369051],
+        type: LandmarkType(name: "Bridge"),
+      ),
+    );
+    trips.add(t2);
 
   }
-
   return trips;
 }
-- 
2.47.2