From 89511f39cbdce4ff5080fcc1948bca6fd5fcef75 Mon Sep 17 00:00:00 2001 From: Remy Moll Date: Mon, 5 Aug 2024 15:27:25 +0200 Subject: [PATCH] better errorhandling, slimmed down optimizer --- backend/src/main.py | 13 +++-- .../src/parameters/landmark_parameters.yaml | 2 +- backend/src/utils/landmarks_manager.py | 46 +++++++++++------- frontend/lib/constants.dart | 2 +- frontend/lib/modules/greeter.dart | 40 +++++++++++----- frontend/lib/modules/landmarks_overview.dart | 48 +++++++++---------- frontend/lib/pages/profile.dart | 27 +++++++++++ frontend/lib/structs/trip.dart | 6 +++ frontend/lib/utils/fetch_trip.dart | 24 +++++----- 9 files changed, 138 insertions(+), 70 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index b9c1837..313df93 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -29,9 +29,11 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl :return: the uuid of the first landmark in the optimized route ''' if preferences is None: - raise ValueError("Please provide preferences in the form of a 'Preference' BaseModel class.") + raise HTTPException(status_code=406, detail="Preferences not provided") + if preferences.shopping.score == 0 and preferences.sightseeing.score == 0 and preferences.nature.score == 0: + raise HTTPException(status_code=406, detail="All preferences are 0.") if start is None: - raise ValueError("Please provide the starting coordinates as a tuple of floats.") + raise HTTPException(status_code=406, detail="Start coordinates not provided") if end is None: end = start logger.info("No end coordinates provided. Using start=end.") @@ -50,7 +52,12 @@ def new_trip(preferences: Preferences, start: tuple[float, float], end: tuple[fl landmarks_short.append(end_landmark) # First stage optimization - base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) + try: + base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) + except ArithmeticError: + raise HTTPException(status_code=500, detail="No solution found") + except TimeoutError: + raise HTTPException(status_code=500, detail="Optimzation took too long") # Second stage optimization refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) diff --git a/backend/src/parameters/landmark_parameters.yaml b/backend/src/parameters/landmark_parameters.yaml index 5de9c48..777c18f 100644 --- a/backend/src/parameters/landmark_parameters.yaml +++ b/backend/src/parameters/landmark_parameters.yaml @@ -1,6 +1,6 @@ city_bbox_side: 5000 #m radius_close_to: 50 church_coeff: 0.8 -park_coeff: 1.2 +park_coeff: 1.0 tag_coeff: 10 N_important: 40 diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index 2422c11..6cde21c 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -21,7 +21,6 @@ class LandmarkManager: logger = logging.getLogger(__name__) - city_bbox_side: int # bbox side in meters radius_close_to: int # radius in meters church_coeff: float # coeff to adjsut score of churches park_coeff: float # coeff to adjust score of parks @@ -36,12 +35,17 @@ class LandmarkManager: with constants.LANDMARK_PARAMETERS_PATH.open('r') as f: parameters = yaml.safe_load(f) - self.city_bbox_side = parameters['city_bbox_side'] + self.max_bbox_side = parameters['city_bbox_side'] self.radius_close_to = parameters['radius_close_to'] self.church_coeff = parameters['church_coeff'] self.park_coeff = parameters['park_coeff'] self.tag_coeff = parameters['tag_coeff'] self.N_important = parameters['N_important'] + + with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f: + parameters = yaml.safe_load(f) + self.walking_speed = parameters['average_walking_speed'] + self.detour_factor = parameters['detour_factor'] self.overpass = Overpass() CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR) @@ -65,23 +69,26 @@ class LandmarkManager: - A list of the most important landmarks based on the user's preferences. """ + max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor + reachable_bbox_side = min(max_walk_dist, self.max_bbox_side) + L = [] - bbox = self.create_bbox(center_coordinates) + bbox = self.create_bbox(center_coordinates, reachable_bbox_side) # list for sightseeing if preferences.sightseeing.score != 0: - score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.church_coeff) + score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.church_coeff) # self.count_elements_close_to(loc) + L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function) L += L1 # list for nature if preferences.nature.score != 0: - score_function = lambda loc, n_tags: int((self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff) )*self.park_coeff) + score_function = lambda loc, n_tags: int((((n_tags**1.2)*self.tag_coeff) )*self.park_coeff) # self.count_elements_close_to(loc) + L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function) L += L2 # list for shopping if preferences.shopping.score != 0: - score_function = lambda loc, n_tags: int(self.count_elements_close_to(loc) + ((n_tags**1.2)*self.tag_coeff)) + score_function = lambda loc, n_tags: int(((n_tags**1.2)*self.tag_coeff)) # self.count_elements_close_to(loc) + L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function) L += L3 @@ -183,12 +190,13 @@ class LandmarkManager: return 0 - def create_bbox(self, coordinates: tuple[float, float]) -> tuple[float, float, float, float]: + def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]: """ Create a bounding box around the given coordinates. Args: coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box. + reachable_bbox_side (int): The side length of the bounding box in meters. Returns: tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude @@ -199,7 +207,7 @@ class LandmarkManager: lon = coordinates[1] # Half the side length in km (since it's a square bbox) - half_side_length_km = self.city_bbox_side / 2 / 1000 + half_side_length_km = reachable_bbox_side / 2 / 1000 # Convert distance to degrees lat_diff = half_side_length_km / 111 # 1 degree latitude is approximately 111 km @@ -288,19 +296,24 @@ class LandmarkManager: break if "wikipedia" in tag: - n_tags += 3 # wikipedia entries count more + n_tags += 1 # wikipedia entries count more - if tag == "wikidata": - Q = elem.tag('wikidata') - site = Site("wikidata", "wikidata") - item = ItemPage(site, Q) - item.get() - n_languages = len(item.labels) - n_tags += n_languages/10 + # if tag == "wikidata": + # Q = elem.tag('wikidata') + # site = Site("wikidata", "wikidata") + # item = ItemPage(site, Q) + # item.get() + # n_languages = len(item.labels) + # n_tags += n_languages/10 + if "viewpoint" in tag: + n_tags += 10 if elem_type != "nature": if "leisure" in tag and elem.tag('leisure') == "park": elem_type = "nature" + + if elem_type == "nature": + n_tags += 1 if landmarktype != "shopping": if "shop" in tag: @@ -310,7 +323,6 @@ class LandmarkManager: if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: skip = True break - if skip: continue diff --git a/frontend/lib/constants.dart b/frontend/lib/constants.dart index dbd009a..efa0a1f 100644 --- a/frontend/lib/constants.dart +++ b/frontend/lib/constants.dart @@ -1,4 +1,4 @@ const String APP_NAME = 'AnyWay'; -const String API_URL_BASE = 'https://anyway.kluster.moll.re'; +String API_URL_BASE = 'https://anyway.kluster.moll.re'; diff --git a/frontend/lib/modules/greeter.dart b/frontend/lib/modules/greeter.dart index 034720e..b0771d5 100644 --- a/frontend/lib/modules/greeter.dart +++ b/frontend/lib/modules/greeter.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:anyway/structs/trip.dart'; import 'package:auto_size_text/auto_size_text.dart'; @@ -15,12 +17,15 @@ class Greeter extends StatefulWidget { } - class _GreeterState extends State { + Widget greeterBuilder (BuildContext context, Widget? child) { ThemeData theme = Theme.of(context); + TextStyle greeterStyle = TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24); + Widget topGreeter; - if (widget.trip.landmarks.length > 1) { + + if (widget.trip.uuid != 'pending') { topGreeter = FutureBuilder( future: widget.trip.cityName, builder: (BuildContext context, AsyncSnapshot snapshot) { @@ -28,17 +33,20 @@ class _GreeterState extends State { return AutoSizeText( maxLines: 1, 'Welcome to ${snapshot.data}!', - style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24), + style: greeterStyle ); } else if (snapshot.hasError) { - return const AutoSizeText( + log('Error while fetching city name'); + return AutoSizeText( maxLines: 1, - 'Welcome to your trip!' + 'Welcome to your trip!', + style: greeterStyle ); } else { - return const AutoSizeText( + return AutoSizeText( maxLines: 1, - 'Welcome to ...' + 'Welcome to ...', + style: greeterStyle ); } } @@ -54,14 +62,24 @@ class _GreeterState extends State { future: widget.trip.cityName, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { - return Text( + return AutoSizeText( + maxLines: 1, 'Generating your trip to ${snapshot.data}...', - style: TextStyle(color: theme.primaryColor, fontWeight: FontWeight.bold, fontSize: 24), + style: greeterStyle ); } else if (snapshot.hasError) { - return const Text('Error while fetching city name'); + // the exact error is shown in the central part of the trip overview. No need to show it here + return AutoSizeText( + maxLines: 1, + 'Error while loading trip.', + style: greeterStyle + ); } - return const Text('Generating your trip...'); + return AutoSizeText( + maxLines: 1, + 'Generating your trip...', + style: greeterStyle + ); } ), Padding( diff --git a/frontend/lib/modules/landmarks_overview.dart b/frontend/lib/modules/landmarks_overview.dart index 844673d..a368e3d 100644 --- a/frontend/lib/modules/landmarks_overview.dart +++ b/frontend/lib/modules/landmarks_overview.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -19,32 +18,19 @@ class LandmarksOverview extends StatefulWidget { } class _LandmarksOverviewState extends State { - // final Future> _landmarks = fetchLandmarks(); @override Widget build(BuildContext context) { - return ListenableBuilder(//> + return ListenableBuilder( listenable: widget.trip!, builder: (BuildContext context, Widget? child) { Trip trip = widget.trip!; log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); + List 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, - size: 60, - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Text('Error: ${trip.cityName}'), - ), - ]; - } else { + + if (trip.uuid != 'pending' && trip.uuid != 'error') { + log("Trip ${trip.uuid} ${trip.landmarks.length} landmarks"); if (trip.landmarks.length <= 1) { children = [ const Text("No landmarks in this trip"), @@ -55,7 +41,26 @@ class _LandmarksOverviewState extends State { saveButton(), ]; } + } else if(trip.uuid == 'pending') { + // the trip is still being fetched from the api + children = [Center(child: CircularProgressIndicator())]; + } else { + // trip.uuid == 'error' + // show the error raised by the api + // String error = + children = [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 60, + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text('Error: ${trip.errorDescription}'), + ), + ]; } + return Column( children: children, ); @@ -119,11 +124,6 @@ class _LandmarksOverviewState extends State { Widget stepBetweenLandmarks(Landmark current, Landmark next) { - // 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 - // next landmark is not actually required, but it ensures that the widget is deleted when the next landmark is removed (which makes sense, because then there will be another step) int timeRounded = 5 * (current.tripTime?.inMinutes ?? 0) ~/ 5; // ~/ is integer division (rounding) return Container( diff --git a/frontend/lib/pages/profile.dart b/frontend/lib/pages/profile.dart index 0ee2b8f..f9d3ca7 100644 --- a/frontend/lib/pages/profile.dart +++ b/frontend/lib/pages/profile.dart @@ -1,3 +1,4 @@ +import 'package:anyway/constants.dart'; import 'package:anyway/structs/preferences.dart'; import 'package:flutter/material.dart'; @@ -24,6 +25,32 @@ class _ProfilePageState extends State { onChanged: (bool? newValue) { setState(() { debugMode = newValue!; + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Debug mode - custom API'), + content: TextField( + decoration: InputDecoration( + hintText: 'http://localhost:8000' + ), + onChanged: (value) { + setState(() { + API_URL_BASE = value; + }); + }, + ), + actions: [ + TextButton( + child: Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } + ); }); } ) diff --git a/frontend/lib/structs/trip.dart b/frontend/lib/structs/trip.dart index b8176db..ec0f228 100644 --- a/frontend/lib/structs/trip.dart +++ b/frontend/lib/structs/trip.dart @@ -14,6 +14,7 @@ class Trip with ChangeNotifier { int totalTime; LinkedList landmarks; // could be empty as well + String? errorDescription; Future get cityName async { List? location = landmarks.firstOrNull?.location; @@ -64,6 +65,11 @@ class Trip with ChangeNotifier { landmarks.remove(landmark); notifyListeners(); } + + void updateError(String error) { + errorDescription = error; + notifyListeners(); + } factory Trip.fromPrefs(SharedPreferences prefs, String uuid) { String? content = prefs.getString('trip_$uuid'); diff --git a/frontend/lib/utils/fetch_trip.dart b/frontend/lib/utils/fetch_trip.dart index cf76d8b..d85ab76 100644 --- a/frontend/lib/utils/fetch_trip.dart +++ b/frontend/lib/utils/fetch_trip.dart @@ -11,9 +11,11 @@ import "package:anyway/structs/preferences.dart"; Dio dio = Dio( BaseOptions( baseUrl: API_URL_BASE, - // baseUrl: 'http://localhost:8000', connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 120), + // also accept 500 errors, since we cannot rule out that the server is at fault. We still want to gracefully handle these errors + validateStatus: (status) => status! <= 500, + receiveDataWhenStatusError: true, // api is notoriously slow // headers: { // HttpHeaders.userAgentHeader: 'dio', @@ -45,24 +47,20 @@ 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"]); + if (response.data["detail"] != null) { + trip.updateError(response.data["detail"]); + // throw Exception(response.data["detail"]); + } } + log(response.data.toString()); Map json = response.data; // only fill in the trip "meta" data for now trip.loadFromJson(json); - // now fill the trip with landmarks - // if (trip.landmarks.isNotEmpty) { - // trip.landmarks.clear(); - // } - // we are going to recreate all the landmarks from the information given by the api + // we are going to recreate ALL the landmarks from the information given by the api trip.landmarks.remove(trip.landmarks.first); String? nextUUID = json["first_landmark_uuid"]; while (nextUUID != null) { @@ -83,8 +81,8 @@ Future<(Landmark, String?)> fetchLandmark(String uuid) async { if (response.statusCode != 200) { throw Exception('Failed to load landmark'); } - if (response.data["error"] != null) { - throw Exception(response.data["error"]); + if (response.data["detail"] != null) { + throw Exception(response.data["detail"]); } log(response.data.toString()); Map json = response.data;