From c87a01b2e8a813b227e7f55764692bb694bf4112 Mon Sep 17 00:00:00 2001
From: Remy Moll <me@moll.re>
Date: Sat, 3 Aug 2024 16:52:29 +0200
Subject: [PATCH] overhaul using a trip struct that notifies its ui
 dependencies

---
 backend/src/main.py                          |  2 +-
 frontend/lib/layout.dart                     | 68 +++++++++++---
 frontend/lib/main.dart                       |  2 +-
 frontend/lib/modules/greeter.dart            | 95 +++++++++++---------
 frontend/lib/modules/landmark_card.dart      |  8 +-
 frontend/lib/modules/landmarks_overview.dart | 67 +++++++-------
 frontend/lib/modules/map.dart                |  8 +-
 frontend/lib/modules/trips_overview.dart     |  2 +-
 frontend/lib/pages/new_trip.dart             | 17 +++-
 frontend/lib/pages/overview.dart             | 81 +++++++++--------
 frontend/lib/structs/trip.dart               | 51 +++++++----
 frontend/lib/utils/fetch_trip.dart           | 17 ++--
 frontend/lib/utils/load_trips.dart           |  4 +-
 frontend/pubspec.lock                        | 64 +++++++++++++
 frontend/pubspec.yaml                        |  2 +
 15 files changed, 324 insertions(+), 164 deletions(-)

diff --git a/backend/src/main.py b/backend/src/main.py
index c59d864..b9c1837 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -37,7 +37,7 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl
         logger.info("No end coordinates provided. Using start=end.")
 
     start_landmark = Landmark(name='start', type='start', location=(start[0], start[1]), osm_type='start', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
-    end_landmark = Landmark(name='end', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
+    end_landmark = Landmark(name='finish', type='finish', location=(end[0], end[1]), osm_type='end', osm_id=0, attractiveness=0, must_do=True, n_tags = 0)
 
     # Generate the landmarks from the start location
     landmarks, landmarks_short = manager.generate_landmarks_list(
diff --git a/frontend/lib/layout.dart b/frontend/lib/layout.dart
index abc8b24..fbfc6ca 100644
--- a/frontend/lib/layout.dart
+++ b/frontend/lib/layout.dart
@@ -1,3 +1,6 @@
+import 'dart:collection';
+
+import 'package:anyway/structs/landmark.dart';
 import 'package:flutter/material.dart';
 
 import 'package:anyway/constants.dart';
@@ -15,12 +18,12 @@ import 'package:anyway/pages/profile.dart';
 // A side drawer is used to switch between pages
 class BasePage extends StatefulWidget {
   final String mainScreen;
-  final Future<Trip>? trip;
-  
+  final Trip? trip;
+
   const BasePage({
     super.key,
     required this.mainScreen,
-    this.trip
+    this.trip,
   });
 
   @override
@@ -53,13 +56,13 @@ class _BasePageState extends State<BasePage> {
           children: [
             DrawerHeader(
               decoration: BoxDecoration(
-                gradient: LinearGradient(colors: [Colors.cyan, theme.primaryColor])
+                gradient: LinearGradient(colors: [Colors.red, Colors.yellow])
               ),
               child: Center(
                 child: Text(
                   APP_NAME,
                   style: TextStyle(
-                    color: Colors.white,
+                    color: Colors.grey[800],
                     fontSize: 24,
                     fontWeight: FontWeight.bold,
                   ),
@@ -129,9 +132,54 @@ class _BasePageState extends State<BasePage> {
   }
 }
 
-
-
-Future<Trip> getFirstTrip (Future<List<Trip>> trips) async {
-  List<Trip> tripsf = await trips;
-  return tripsf[0];
+// This function is used to get the first trip from a list of trips
+// TODO: Implement this function
+Trip getFirstTrip(Future<List<Trip>> trips) {
+  Trip t1 = Trip(uuid: '1', 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"
+    ),
+  );
+  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"
+    ),
+  );
+  return t1;
 }
\ No newline at end of file
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index 7fbb6f8..44c3922 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -13,7 +13,7 @@ class App extends StatelessWidget {
     return MaterialApp(
       title: APP_NAME,
       home: BasePage(mainScreen: "map"),
-      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green),
+      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.red[600]),
     );
   }
 }
diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart
index 77e55de..6b74c13 100644
--- a/frontend/lib/modules/greeter.dart
+++ b/frontend/lib/modules/greeter.dart
@@ -3,12 +3,10 @@ import 'package:anyway/structs/trip.dart';
 import 'package:flutter/material.dart';
 
 class Greeter extends StatefulWidget {
-  final Future<Trip> trip;
-  final bool standalone;
+  final Trip trip;
 
   Greeter({
-    required this.standalone,
-    required this.trip
+    required this.trip,
   });
 
   @override
@@ -18,55 +16,66 @@ class Greeter extends StatefulWidget {
 
 
 class _GreeterState extends State<Greeter> {
-  Widget greeterBuild (BuildContext context, AsyncSnapshot<Trip> snapshot) {
+  Widget greeterBuilder (BuildContext context, Widget? child) {
     ThemeData theme = Theme.of(context);
     Widget topGreeter;
-    if (snapshot.hasData) {
-      topGreeter = Padding(
-        padding: const EdgeInsets.only(top: 20, bottom: 20),
-        child: Text(
-          'Welcome to ${snapshot.data?.cityName}!',
-          style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
-        )
-      );
-    } else if (snapshot.hasError) {
-      topGreeter = const Padding(
-        padding: EdgeInsets.only(top: 20, bottom: 20),
-        child: Text('Error while fetching trip')
+    if (widget.trip.landmarks.length > 1) {
+      topGreeter = FutureBuilder(
+        future: widget.trip.cityName,
+        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+          if (snapshot.hasData) {
+            return Text(
+              'Welcome to ${snapshot.data}!',
+              style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+            );
+          } else if (snapshot.hasError) {
+            return const Text('Welcome to your trip!');
+          } else {
+            return const Text('Welcome to ...');
+          }
+        }
       );
     } else {
-      // still awaiting the cityname
+      // still awaiting the trip
+      // We can hopefully infer the city name from the cityName future
       // Show a linear loader at the bottom and an info message above
       topGreeter = Column(
         mainAxisAlignment: MainAxisAlignment.end,
         children: [
-          Padding(
-            padding: const EdgeInsets.only(top: 20, bottom: 20),
-            child: const Text('Generating your trip...', style: TextStyle(fontSize: 20),)
+          FutureBuilder(
+            future: widget.trip.cityName,
+            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+              if (snapshot.hasData) {
+                return Text(
+                  'Generating your trip to ${snapshot.data}...',
+                  style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24),
+                );
+              } else if (snapshot.hasError) {
+                return const Text('Error while fetching city name');
+              }
+              return const Text('Generating your trip...');
+            }
           ),
-          const LinearProgressIndicator()
+          Padding(
+            padding: EdgeInsets.all(5),
+            child: const LinearProgressIndicator()
+          )
         ]
       );
     }
 
-    
-
-    if (widget.standalone) {
-      return Center(
-        child: topGreeter,
-      );
-    } else {
-      return Center(
-        child: Column(
-          children: [
-            Padding(padding: EdgeInsets.only(top: 24.0)),
-            topGreeter,
-            bottomGreeter,
-            Padding(padding: EdgeInsets.only(bottom: 24.0)),
-          ],
-        )
-      );
-    }
+    return Center(
+      child: Column(
+        children: [
+          // Padding(padding: EdgeInsets.only(top: 20)),
+          topGreeter,
+          Padding(
+            padding: EdgeInsets.all(20),
+            child: bottomGreeter
+          ),
+        ],
+      )
+    );
   }
 
   Widget bottomGreeter = const Text(
@@ -79,9 +88,9 @@ class _GreeterState extends State<Greeter> {
 
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder(
-      future: widget.trip,
-      builder: greeterBuild,
+    return ListenableBuilder(
+      listenable: widget.trip,
+      builder: greeterBuilder,
     );
   }
 }
\ No newline at end of file
diff --git a/frontend/lib/modules/landmark_card.dart b/frontend/lib/modules/landmark_card.dart
index ca663b6..579ebca 100644
--- a/frontend/lib/modules/landmark_card.dart
+++ b/frontend/lib/modules/landmark_card.dart
@@ -1,4 +1,5 @@
 import 'package:anyway/structs/landmark.dart';
+import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 
 
@@ -31,9 +32,10 @@ class _LandmarkCardState extends State<LandmarkCard> {
               height: double.infinity,
               // force a fixed width
               width: 160,
-              child: Image.network(
-                widget.landmark.imageURL ?? '',
-                errorBuilder: (context, error, stackTrace) => Icon(Icons.question_mark_outlined),
+              child: CachedNetworkImage(
+                imageUrl: widget.landmark.imageURL ?? '',
+                placeholder: (context, url) => CircularProgressIndicator(),
+                errorWidget: (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 720fe49..387bf11 100644
--- a/frontend/lib/modules/landmarks_overview.dart
+++ b/frontend/lib/modules/landmarks_overview.dart
@@ -1,17 +1,17 @@
 import 'dart:collection';
+import 'dart:developer';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 import 'package:anyway/modules/landmark_card.dart';
 import 'package:anyway/structs/landmark.dart';
-
 import 'package:anyway/structs/trip.dart';
-import 'package:flutter/material.dart';
-import 'package:shared_preferences/shared_preferences.dart';
 
 
 
 
 class LandmarksOverview extends StatefulWidget {
-  final Future<Trip>? trip;
+  final Trip? trip;
   const LandmarksOverview({super.key, this.trip});
 
   @override
@@ -23,18 +23,17 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
 
   @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<LinkedList<Landmark>>(
-        future: _landmarks,
-        builder: (BuildContext context, AsyncSnapshot<LinkedList<Landmark>> snapshot) {
-          List<Widget> children;
-          if (snapshot.hasData) {
-            children = [landmarksWithSteps(snapshot.data!), saveButton()];
-          } else if (snapshot.hasError) {
-            children = <Widget>[
+    return ListenableBuilder(//<LinkedList<Landmark>>
+      listenable: widget.trip!,
+      builder: (BuildContext context, Widget? child) {
+        Trip trip = widget.trip!;
+        log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks");
+        List<Widget> children;
+        if (trip.uuid == 'pending') {
+          // the trip is still being fetched from the api
+          children = [Center(child: CircularProgressIndicator())];
+        } else if (trip.uuid == 'error') {
+            children = [
               const Icon(
                 Icons.error_outline,
                 color: Colors.red,
@@ -42,20 +41,25 @@ class _LandmarksOverviewState extends State<LandmarksOverview> {
               ),
               Padding(
                 padding: const EdgeInsets.only(top: 16),
-                child: Text('Error: ${snapshot.error}', style: TextStyle(fontSize: 12)),
+                child: Text('Error: ${trip.cityName}'),
               ),
             ];
+        } else {
+          if (trip.landmarks.length <= 1) {
+            children = [
+              const Text("No landmarks in this trip"),
+            ];
           } else {
-            children = [Center(child: CircularProgressIndicator())];
+            children = [
+              landmarksWithSteps(trip.landmarks),
+              saveButton(),
+            ];
           }
-          return Center(
-            child: Column(
-              mainAxisAlignment: MainAxisAlignment.center,
-              children: children,
-            ),
-          );
-        },
-      ),
+        }
+        return Column(
+          children: children,
+        );
+      },
     );
   }
   Widget saveButton() => ElevatedButton(
@@ -100,7 +104,7 @@ Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
     );
     lkey++;
     if (landmark.next != null) {
-      Widget step = stepBetweenLandmarks(landmark, landmark.next!);
+      Widget step = stepBetweenLandmarks(landmark);
       children.add(step);
     }
   }
@@ -111,7 +115,7 @@ Widget landmarksWithSteps(LinkedList<Landmark> landmarks) {
 }
 
 
-Widget stepBetweenLandmarks(Landmark before, Landmark after) {
+Widget stepBetweenLandmarks(Landmark landmark) {
   // 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
@@ -134,7 +138,7 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
         Column(
           children: [
             Icon(Icons.directions_walk),
-            Text("5 min", style: TextStyle(fontSize: 10)),
+            Text("${landmark.tripTime} min", style: TextStyle(fontSize: 10)),
           ],
         ),
         Spacer(),
@@ -149,8 +153,5 @@ Widget stepBetweenLandmarks(Landmark before, Landmark after) {
   );
 }
 
-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 d0ab511..2e95ccd 100644
--- a/frontend/lib/modules/map.dart
+++ b/frontend/lib/modules/map.dart
@@ -8,7 +8,7 @@ import 'package:the_widget_marker/the_widget_marker.dart';
 
 class MapWidget extends StatefulWidget {
 
-  final Future<Trip>? trip;
+  final Trip? trip;
 
   MapWidget({
     this.trip
@@ -31,8 +31,7 @@ class _MapWidgetState extends State<MapWidget> {
 
   void _onMapCreated(GoogleMapController controller) async {
     mapController = controller;
-    Trip? trip = await widget.trip;
-    List<double>? newLocation = trip?.landmarks.first.location;
+    List<double>? newLocation = widget.trip?.landmarks.first.location;
     if (newLocation != null) {
       CameraUpdate update = CameraUpdate.newLatLng(LatLng(newLocation[0], newLocation[1]));
       controller.moveCamera(update);
@@ -48,8 +47,7 @@ class _MapWidgetState extends State<MapWidget> {
 
   void drawLandmarks() async {
     // (re)draws landmarks on the map
-    Trip? trip = await widget.trip;
-    LinkedList<Landmark>? landmarks = trip?.landmarks;
+    LinkedList<Landmark>? landmarks = widget.trip?.landmarks;
     if (landmarks != null){
       for (Landmark landmark in landmarks) {
         markers.add(Marker(
diff --git a/frontend/lib/modules/trips_overview.dart b/frontend/lib/modules/trips_overview.dart
index 8501013..fec7f2a 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: Future.value(trip))
+                builder: (context) => BasePage(mainScreen: "map", trip: trip)
               )
             );
           },
diff --git a/frontend/lib/pages/new_trip.dart b/frontend/lib/pages/new_trip.dart
index 2464177..79c15ae 100644
--- a/frontend/lib/pages/new_trip.dart
+++ b/frontend/lib/pages/new_trip.dart
@@ -1,8 +1,10 @@
+import 'package:anyway/structs/landmark.dart';
+import 'package:flutter/material.dart';
+import 'package:geocoding/geocoding.dart';
 
 import 'package:anyway/layout.dart';
-import 'package:anyway/structs/preferences.dart';
 import 'package:anyway/utils/fetch_trip.dart';
-import 'package:flutter/material.dart';
+import 'package:anyway/structs/preferences.dart';
 import "package:anyway/structs/trip.dart";
 
 
@@ -64,7 +66,16 @@ class _NewTripPageState extends State<NewTripPage> {
                         double.parse(lonController.text)
                       ];
                       Future<UserPreferences> preferences = loadUserPreferences();
-                      Future<Trip>? trip = fetchTrip(startPoint, preferences);
+                      Trip trip = Trip();
+                      trip.landmarks.add(
+                        Landmark(
+                          location: startPoint,
+                          name: "start",
+                          type: LandmarkType(name: 'start'),
+                          uuid: "pending"
+                        )
+                      );
+                      fetchTrip(trip, preferences);
                         Navigator.of(context).push(
                           MaterialPageRoute(
                             builder: (context) => BasePage(mainScreen: "map", trip: trip)
diff --git a/frontend/lib/pages/overview.dart b/frontend/lib/pages/overview.dart
index ce3646b..e98d91b 100644
--- a/frontend/lib/pages/overview.dart
+++ b/frontend/lib/pages/overview.dart
@@ -10,10 +10,10 @@ import 'package:anyway/modules/greeter.dart';
 
 
 class NavigationOverview extends StatefulWidget {
-  final Future<Trip> trip;
+  final Trip trip;
 
   NavigationOverview({
-    required this.trip
+    required this.trip,
   });
 
   @override
@@ -27,53 +27,56 @@ class _NavigationOverviewState extends State<NavigationOverview> {
   @override
   Widget build(BuildContext context) {
     return SlidingUpPanel(
-        renderPanelSheet: false,
         panel: _floatingPanel(),
-        collapsed: _floatingCollapsed(),
-        body: MapWidget(trip: widget.trip)
+        // collapsed: _floatingCollapsed(),
+        body: MapWidget(trip: widget.trip),
+        // renderPanelSheet: false,
+        // backdropEnabled: true,
+        maxHeight: MediaQuery.of(context).size.height * 0.8,
+        padding: EdgeInsets.all(10),
+        // panelSnapping: false,
+        borderRadius: BorderRadius.only(topLeft: Radius.circular(25), topRight: Radius.circular(25)),
+        boxShadow: [
+          BoxShadow(
+            blurRadius: 20.0,
+            color: Colors.black,
+          )
+        ],
     );
   }
 
   Widget _floatingCollapsed(){
-    final ThemeData theme = Theme.of(context);
-    return Container(
-      decoration: BoxDecoration(
-        color: theme.canvasColor,
-        borderRadius: BorderRadius.only(topLeft: Radius.circular(24.0), topRight: Radius.circular(24.0)),
-        boxShadow: []
-      ),
-      
-      child: Greeter(standalone: true, trip: widget.trip)
+    return Greeter(
+      trip: widget.trip
     );
   }
 
   Widget _floatingPanel(){
-    final ThemeData theme = Theme.of(context);
-    return Container(
-      decoration: BoxDecoration(
-        color: Colors.white,
-        borderRadius: BorderRadius.all(Radius.circular(24.0)),
-        boxShadow: [
-          BoxShadow(
-            blurRadius: 20.0,
-            color: theme.shadowColor,
-          ),
-        ]
-      ),
-      child: Center(
-        child: Padding(
-        padding: EdgeInsets.all(8.0),
-        child: SingleChildScrollView(
-          child: Column(
-            children: <Widget>[
-              Greeter(standalone: false, trip: widget.trip),
-              LandmarksOverview(trip: widget.trip),
-            ],
-          ),
+    return Column(
+      children: [
+        Padding(
+          padding: const EdgeInsets.all(15),
+          child: 
+            Center(
+              child: Container(
+                width: 40,
+                height: 5,
+                decoration: BoxDecoration(
+                  color: Colors.grey[300],
+                  borderRadius: BorderRadius.all(Radius.circular(12.0)),
+                ),
+              ),
+            ),
         ),
-      ),
-    ),
+        Expanded(
+          child: ListView(
+            children: [
+              Greeter(trip: widget.trip),
+              LandmarksOverview(trip: widget.trip)
+            ]
+          )
+        )
+      ],
     );
   }
-
 }
diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart
index f6d94e3..3b8c1ff 100644
--- a/frontend/lib/structs/trip.dart
+++ b/frontend/lib/structs/trip.dart
@@ -5,35 +5,56 @@ import 'dart:collection';
 import 'dart:convert';
 
 import 'package:anyway/structs/landmark.dart';
+import 'package:flutter/foundation.dart';
+import 'package:geocoding/geocoding.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
-class Trip {
-  final String uuid;
-  final String cityName;
-  // TODO: cityName should be inferred from coordinates of the Landmarks
-  final int totalTime;
-  final LinkedList<Landmark> landmarks;
+class Trip with ChangeNotifier {
+  String uuid;
+  int totalTime;
+  LinkedList<Landmark> landmarks;
   // could be empty as well
 
+  Future<String> get cityName async {
+    if (GeocodingPlatform.instance == null) {
+      return '${landmarks.first.location[0]}, ${landmarks.first.location[1]}';
+    }
+    List<Placemark> placemarks = await placemarkFromCoordinates(landmarks.first.location[0], landmarks.first.location[1]);
+    return placemarks.first.locality ?? 'Unknown';
+  }
+
 
   Trip({
-    required this.uuid,
-    required this.cityName,
-    required this.landmarks,
-    this.totalTime = 0
-  });
-
+    this.uuid = 'pending',
+    this.totalTime = 0,
+    LinkedList<Landmark>? landmarks
+    // a trip can be created with no landmarks, but the list should be initialized anyway
+  }) : landmarks = landmarks ?? LinkedList<Landmark>();
+  
 
   factory Trip.fromJson(Map<String, dynamic> json) {
     Trip trip = Trip(
       uuid: json['uuid'],
-      cityName: json['city_name'] ?? 'Not communicated',
-      landmarks: LinkedList()
+      totalTime: json['total_time'],
     );
 
     return trip;
   }
 
+  void loadFromJson(Map<String, dynamic> json) {
+    uuid = json['uuid'];
+    totalTime = json['total_time'];
+    notifyListeners();
+  }
+  void addLandmark(Landmark landmark) {
+    landmarks.add(landmark);
+    notifyListeners();
+  }
+
+  void updateUUID(String newUUID) {
+    uuid = newUUID;
+    notifyListeners();
+  }
 
   factory Trip.fromPrefs(SharedPreferences prefs, String uuid) {
     String? content = prefs.getString('trip_$uuid');
@@ -47,7 +68,7 @@ class Trip {
 
   Map<String, dynamic> toJson() => {
     'uuid': uuid,
-    'city_name': cityName,
+    'total_time': totalTime,
     'first_landmark_uuid': landmarks.first.uuid
   };
 
diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart
index aeba3b8..54d9365 100644
--- a/frontend/lib/utils/fetch_trip.dart
+++ b/frontend/lib/utils/fetch_trip.dart
@@ -1,7 +1,7 @@
 import "dart:convert";
 import "dart:developer";
-
 import 'package:dio/dio.dart';
+
 import 'package:anyway/constants.dart';
 import "package:anyway/structs/landmark.dart";
 import "package:anyway/structs/trip.dart";
@@ -25,14 +25,14 @@ Dio dio = Dio(
   ),
 );
 
-Future<Trip>? fetchTrip(
-  List<double> startPoint,
+fetchTrip(
+  Trip trip,
   Future<UserPreferences> preferences,
 ) async {
   UserPreferences prefs = await preferences;
   Map<String, dynamic> data = {
     "preferences": prefs.toJson(),
-    "start": startPoint
+    "start": trip.landmarks!.first.location,
   };
   String dataString = jsonEncode(data);
   log(dataString);
@@ -44,24 +44,25 @@ Future<Trip>? fetchTrip(
 
   // handle errors
   if (response.statusCode != 200) {
+    trip.updateUUID("error");
     throw Exception('Failed to load trip');
   }
   if (response.data["error"] != null) {
+    trip.updateUUID("error");
     throw Exception(response.data["error"]);
   }
   log(response.data.toString());
   Map<String, dynamic> json = response.data;
 
-  // only fetch the trip "meta" data for now
-  Trip trip = Trip.fromJson(json);
+  // only fill in the trip "meta" data for now
+  trip.loadFromJson(json);
 
   String? nextUUID = json["first_landmark_uuid"];
   while (nextUUID != null) {
     var (landmark, newUUID) = await fetchLandmark(nextUUID);
-    trip.landmarks.add(landmark);
+    trip.addLandmark(landmark);
     nextUUID = newUUID;
   }
-  return trip;
 }
 
 
diff --git a/frontend/lib/utils/load_trips.dart b/frontend/lib/utils/load_trips.dart
index 22b9ced..f608294 100644
--- a/frontend/lib/utils/load_trips.dart
+++ b/frontend/lib/utils/load_trips.dart
@@ -17,7 +17,7 @@ Future<List<Trip>> loadTrips() async {
   }
 
   if (trips.isEmpty) {
-    Trip t1 = Trip(uuid: '1', cityName: 'Paris', landmarks: LinkedList<Landmark>());
+    Trip t1 = Trip(uuid: '1', landmarks: LinkedList<Landmark>());
     t1.landmarks.add(
       Landmark(
         uuid: '1',
@@ -66,7 +66,7 @@ Future<List<Trip>> loadTrips() async {
     trips.add(t1);
 
 
-    Trip t2 = Trip(uuid: '2', cityName: 'Vienna', landmarks: LinkedList<Landmark>());
+    Trip t2 = Trip(uuid: '2', landmarks: LinkedList<Landmark>());
 
     t2.landmarks.add(
       Landmark(
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index 8d3dc88..a891618 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -25,6 +25,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.1"
+  cached_network_image:
+    dependency: "direct main"
+    description:
+      name: cached_network_image
+      sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.4.0"
+  cached_network_image_platform_interface:
+    dependency: transitive
+    description:
+      name: cached_network_image_platform_interface
+      sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.0"
+  cached_network_image_web:
+    dependency: transitive
+    description:
+      name: cached_network_image_web
+      sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
   characters:
     dependency: transitive
     description:
@@ -168,6 +192,38 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  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:
@@ -296,6 +352,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.12.0"
+  octo_image:
+    dependency: transitive
+    description:
+      name: octo_image
+      sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   path:
     dependency: transitive
     description:
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 6f6d8cf..a2f41d4 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -41,6 +41,8 @@ dependencies:
   dio: ^5.5.0+1
   google_maps_flutter: ^2.7.0
   the_widget_marker: ^1.0.0
+  cached_network_image: ^3.4.0
+  geocoding: ^3.0.0
 
 dev_dependencies:
   flutter_test: