Compare commits

...

1 Commits

Author SHA1 Message Date
89511f39cb better errorhandling, slimmed down optimizer
All checks were successful
Build and push docker image / Build (pull_request) Successful in 1m40s
Build and release APK / Build APK (pull_request) Successful in 5m26s
2024-08-05 16:03:29 +02:00
9 changed files with 138 additions and 70 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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';

View File

@ -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<Greeter> {
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<String> snapshot) {
@ -28,17 +33,20 @@ class _GreeterState extends State<Greeter> {
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<Greeter> {
future: widget.trip.cityName,
builder: (BuildContext context, AsyncSnapshot<String> 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(

View File

@ -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<LandmarksOverview> {
// final Future<List<Landmark>> _landmarks = fetchLandmarks();
@override
Widget build(BuildContext context) {
return ListenableBuilder(//<LinkedList<Landmark>>
return ListenableBuilder(
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,
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<LandmarksOverview> {
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<LandmarksOverview> {
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(

View File

@ -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<ProfilePage> {
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();
},
),
],
);
}
);
});
}
)

View File

@ -14,6 +14,7 @@ class Trip with ChangeNotifier {
int totalTime;
LinkedList<Landmark> landmarks;
// could be empty as well
String? errorDescription;
Future<String> get cityName async {
List<double>? 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');

View File

@ -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<String, dynamic> 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<String, dynamic> json = response.data;