finished tracking. But not tested enough yet

time
This commit is contained in:
Nico
2025-05-19 22:11:52 +02:00
parent fccc810b8c
commit b97f703a47
18 changed files with 263 additions and 62 deletions

View File

@@ -22,7 +22,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.fforte" applicationId = "de.lupus.apps"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.INTERNET"/>
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> --> <!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> -->
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> --> <!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> -->
<application <application

Binary file not shown.

View File

@@ -159,6 +159,7 @@
"zurueckgelegteStrecke": "Zurückgelegte Strecke", "zurueckgelegteStrecke": "Zurückgelegte Strecke",
"tracking": "Tracking", "tracking": "Tracking",
"couldntDeterminePosition": "Position konnte nicht ermittelt werden", "couldntDeterminePosition": "Position konnte nicht ermittelt werden",
"trackingRunningInBackground": "Die Tracking funktion läuft im Hintergrund" "trackingRunningInBackground": "Die Tracking funktion läuft im Hintergrund",
"needsAlwaysLocation": "Diese app braucht die Standort berechtigung auf immer gesetzt",
} "deleteWholeRouteBody": "Sind Sie sicher, dass die gesamte bisher gegangene Route gelöscht werden soll?"
}

View File

@@ -667,5 +667,9 @@
"couldntDeterminePosition": "couldn't determine position", "couldntDeterminePosition": "couldn't determine position",
"trackingRunningInBackground": "The tracking service is running in the background" "trackingRunningInBackground": "The tracking service is running in the background",
"needsAlwaysLocation": "This app needs the location permission set to always",
"deleteWholeRouteBody": "Do you really want to delete the existing route?"
} }

View File

@@ -1066,6 +1066,18 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'The tracking service is running in the background'** /// **'The tracking service is running in the background'**
String get trackingRunningInBackground; String get trackingRunningInBackground;
/// No description provided for @needsAlwaysLocation.
///
/// In en, this message translates to:
/// **'This app needs the location permission set to always'**
String get needsAlwaysLocation;
/// No description provided for @deleteWholeRouteBody.
///
/// In en, this message translates to:
/// **'Do you really want to delete the existing route?'**
String get deleteWholeRouteBody;
} }
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -493,4 +493,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get trackingRunningInBackground => 'Die Tracking funktion läuft im Hintergrund'; String get trackingRunningInBackground => 'Die Tracking funktion läuft im Hintergrund';
@override
String get needsAlwaysLocation => 'Diese app braucht die Standort berechtigung auf immer gesetzt';
@override
String get deleteWholeRouteBody => 'Sind Sie sicher, dass die gesamte bisher gegangene Route gelöscht werden soll?';
} }

View File

@@ -493,4 +493,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get trackingRunningInBackground => 'The tracking service is running in the background'; String get trackingRunningInBackground => 'The tracking service is running in the background';
@override
String get needsAlwaysLocation => 'This app needs the location permission set to always';
@override
String get deleteWholeRouteBody => 'Do you really want to delete the existing route?';
} }

View File

@@ -1,10 +1,11 @@
import 'package:fforte/screens/addCam/exceptions/location_disabled_exception.dart'; import 'package:fforte/screens/addCam/exceptions/location_disabled_exception.dart';
import 'package:fforte/screens/addCam/exceptions/location_forbidden_exception.dart'; import 'package:fforte/screens/addCam/exceptions/location_forbidden_exception.dart';
import 'package:fforte/screens/excursion/exceptions/need_always_location_exception.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
class GeolocatorService { class GeolocatorService {
// determine live position with checks for denied permission and turned off location service // determine live position with checks for denied permission and turned off location service
static Future<Position> deteterminePosition() async { static Future<Position> deteterminePosition({bool alwaysOnNeeded = false}) async {
bool locationEnabled; bool locationEnabled;
LocationPermission permissionGiven; LocationPermission permissionGiven;
@@ -20,6 +21,10 @@ class GeolocatorService {
throw LocationForbiddenException(); throw LocationForbiddenException();
} }
} }
if (alwaysOnNeeded && permissionGiven != LocationPermission.always) {
throw NeedAlwaysLocation();
}
return await Geolocator.getCurrentPosition(); return await Geolocator.getCurrentPosition();
} }

View File

@@ -106,7 +106,7 @@ class KarteState extends State<Karte> {
children: [ children: [
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.app', userAgentPackageName: 'de.lupus.apps',
), ),
MarkerLayer(markers: [currentMarker!]), MarkerLayer(markers: [currentMarker!]),
]), ]),

View File

@@ -0,0 +1 @@
class NeedAlwaysLocation implements Exception {}

View File

@@ -3,6 +3,7 @@ import 'package:fforte/enums/databases.dart';
import 'package:fforte/screens/addCam/exceptions/location_disabled_exception.dart'; import 'package:fforte/screens/addCam/exceptions/location_disabled_exception.dart';
import 'package:fforte/screens/addCam/exceptions/location_forbidden_exception.dart'; import 'package:fforte/screens/addCam/exceptions/location_forbidden_exception.dart';
import 'package:fforte/screens/addCam/services/geolocator_service.dart'; import 'package:fforte/screens/addCam/services/geolocator_service.dart';
import 'package:fforte/screens/excursion/exceptions/need_always_location_exception.dart';
import 'package:fforte/screens/excursion/widgets/anzahlen.dart'; import 'package:fforte/screens/excursion/widgets/anzahlen.dart';
import 'package:fforte/screens/excursion/widgets/bima_nutzer.dart'; import 'package:fforte/screens/excursion/widgets/bima_nutzer.dart';
import 'package:fforte/screens/excursion/widgets/hinweise.dart'; import 'package:fforte/screens/excursion/widgets/hinweise.dart';
@@ -13,6 +14,7 @@ import 'package:fforte/screens/excursion/widgets/strecke_u_spurbedingungen.dart'
import 'package:fforte/screens/excursion/widgets/tracking.dart'; import 'package:fforte/screens/excursion/widgets/tracking.dart';
import 'package:fforte/screens/helper/add_entries_dialog_helper.dart'; import 'package:fforte/screens/helper/add_entries_dialog_helper.dart';
import 'package:fforte/screens/helper/snack_bar_helper.dart'; import 'package:fforte/screens/helper/snack_bar_helper.dart';
import 'package:fforte/screens/helper/view_entries_dialog_helper.dart';
import 'package:fforte/screens/sharedMethods/check_required.dart'; import 'package:fforte/screens/sharedMethods/check_required.dart';
import 'package:fforte/screens/sharedMethods/save_template.dart'; import 'package:fforte/screens/sharedMethods/save_template.dart';
import 'package:fforte/screens/sharedWidgets/datum.dart'; import 'package:fforte/screens/sharedWidgets/datum.dart';
@@ -20,7 +22,6 @@ import 'package:fforte/screens/sharedWidgets/var_text_field.dart';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
class ExcursionMain extends StatefulWidget { class ExcursionMain extends StatefulWidget {
final bool isTemplate; final bool isTemplate;
@@ -120,7 +121,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
@override @override
void initState() { void initState() {
GeolocatorService.deteterminePosition() GeolocatorService.deteterminePosition(alwaysOnNeeded: true)
.then((result) => currentPosition = result) .then((result) => currentPosition = result)
.catchError((error) { .catchError((error) {
if (error is LocationDisabledException) { if (error is LocationDisabledException) {
@@ -137,6 +138,10 @@ class _ExcursionMainState extends State<ExcursionMain> {
AppLocalizations.of(context)!.locationForbidden, AppLocalizations.of(context)!.locationForbidden,
); );
} }
} else if (error is NeedAlwaysLocation) {
if (mounted) {
AddEntriesDialogHelper.locationSettingsDialog(context);
}
} }
return currentPosition; return currentPosition;
}); });
@@ -302,10 +307,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
builder: (context) { builder: (context) {
return Tracking( return Tracking(
weg: rmap["Weg"]!["controller"]!, weg: rmap["Weg"]!["controller"]!,
startPosition: LatLng( startPosition: currentPosition,
currentPosition.latitude,
currentPosition.longitude,
),
); );
}, },
), ),

View File

@@ -2,15 +2,17 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/screens/helper/add_entries_dialog_helper.dart';
import 'package:fforte/screens/helper/snack_bar_helper.dart'; import 'package:fforte/screens/helper/snack_bar_helper.dart';
import 'package:fforte/services/notification_service.dart'; import 'package:fforte/services/notification_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class Tracking extends StatefulWidget { class Tracking extends StatefulWidget {
final LatLng startPosition; final Position startPosition;
final TextEditingController weg; final TextEditingController weg;
const Tracking({super.key, required this.startPosition, required this.weg}); const Tracking({super.key, required this.startPosition, required this.weg});
@@ -21,6 +23,7 @@ class Tracking extends StatefulWidget {
class _TrackingState extends State<Tracking> { class _TrackingState extends State<Tracking> {
List<LatLng> pathList = []; List<LatLng> pathList = [];
StreamSubscription<Position>? positionStream; StreamSubscription<Position>? positionStream;
LocationMarkerPosition? locationMarkerPosition;
bool positionStreamRunning = false; bool positionStreamRunning = false;
MapController mapController = MapController(); MapController mapController = MapController();
@@ -48,6 +51,13 @@ class _TrackingState extends State<Tracking> {
); );
} }
} }
locationMarkerPosition = LocationMarkerPosition(
latitude: widget.startPosition.latitude,
longitude: widget.startPosition.longitude,
accuracy: widget.startPosition.accuracy,
);
super.initState(); super.initState();
} }
@@ -66,6 +76,8 @@ class _TrackingState extends State<Tracking> {
widget.weg.text += "${pos.latitude},${pos.longitude}"; widget.weg.text += "${pos.latitude},${pos.longitude}";
} }
} }
NotificationService().deleteNotification();
super.dispose(); super.dispose();
} }
@@ -88,20 +100,26 @@ class _TrackingState extends State<Tracking> {
locationSettings: AndroidSettings( locationSettings: AndroidSettings(
accuracy: LocationAccuracy.high, accuracy: LocationAccuracy.high,
distanceFilter: 0, distanceFilter: 0,
// foregroundNotificationConfig: foregroundNotificationConfig:
// mounted mounted
// ? ForegroundNotificationConfig( ? ForegroundNotificationConfig(
// notificationTitle: notificationTitle:
// AppLocalizations.of(context)!.trackingRunningInBackground, AppLocalizations.of(context)!.trackingRunningInBackground,
// notificationText: "", notificationText: "",
// ) )
// : null, : null,
), ),
).listen((Position? position) { ).listen((Position? position) {
if (position != null) { if (position != null) {
setState(() { setState(() {
pathList.add(LatLng(position.latitude, position.longitude)); pathList.add(LatLng(position.latitude, position.longitude));
// pathList.add(LatLng(rand.nextInt(5) + 40, position.longitude)); // pathList.add(LatLng(rand.nextInt(5) + 40, position.longitude));
locationMarkerPosition = LocationMarkerPosition(
latitude: position.latitude,
longitude: position.longitude,
accuracy: position.accuracy,
);
}); });
} else { } else {
if (mounted) { if (mounted) {
@@ -112,6 +130,10 @@ class _TrackingState extends State<Tracking> {
} }
} }
}); });
positionStream!.onError((e) {
NotificationService().deleteNotification();
NotificationService().showNotification(title: "ERROR: $e");
});
} }
@override @override
@@ -121,6 +143,25 @@ class _TrackingState extends State<Tracking> {
title: Text(AppLocalizations.of(context)!.tracking), title: Text(AppLocalizations.of(context)!.tracking),
// leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_rounded)), // leading: IconButton(onPressed: () {}, icon: Icon(Icons.arrow_back_rounded)),
actions: [ actions: [
if (!positionStreamRunning)
IconButton(
onPressed: () async {
bool delete =
await AddEntriesDialogHelper.deleteCompleteRouteDialog(
context,
);
if (delete) {
setState(() {
pathList = [];
});
}
},
icon: Icon(
Icons.delete,
color: Theme.of(context).colorScheme.errorContainer,
),
),
if (positionStreamRunning) if (positionStreamRunning)
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -130,7 +171,10 @@ class _TrackingState extends State<Tracking> {
NotificationService().deleteNotification(); NotificationService().deleteNotification();
}); });
}, },
icon: Icon(Icons.stop_rounded), icon: Icon(
Icons.stop_rounded,
color: Theme.of(context).colorScheme.errorContainer,
),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
@@ -152,7 +196,13 @@ class _TrackingState extends State<Tracking> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
mapController.move(pathList.last, 16); mapController.move(
LatLng(
locationMarkerPosition!.latitude,
locationMarkerPosition!.longitude,
),
16,
);
}, },
child: Icon(Icons.my_location), child: Icon(Icons.my_location),
), ),
@@ -165,13 +215,16 @@ class _TrackingState extends State<Tracking> {
InteractiveFlag.drag | InteractiveFlag.drag |
InteractiveFlag.pinchMove, InteractiveFlag.pinchMove,
), ),
initialCenter: widget.startPosition, initialCenter: LatLng(
widget.startPosition.latitude,
widget.startPosition.longitude,
),
initialZoom: 16.0, initialZoom: 16.0,
), ),
children: [ children: [
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.app', userAgentPackageName: 'de.lupus.apps',
), ),
if (pathList.isNotEmpty) if (pathList.isNotEmpty)
PolylineLayer( PolylineLayer(
@@ -179,16 +232,17 @@ class _TrackingState extends State<Tracking> {
Polyline(strokeWidth: 2.0, points: pathList, color: Colors.red), Polyline(strokeWidth: 2.0, points: pathList, color: Colors.red),
], ],
), ),
CircleLayer( // CircleLayer(
circles: [ // circles: [
CircleMarker( // CircleMarker(
color: Colors.blue, // color: Colors.blue,
point: pathList.isEmpty ? widget.startPosition : pathList.last, // point: pathList.isEmpty ? widget.startPosition : pathList.last,
radius: 5, // radius: 5,
useRadiusInMeter: true, // useRadiusInMeter: true,
), // ),
], // ],
), // ),
CurrentLocationLayer(),
], ],
), ),
); );

View File

@@ -6,6 +6,8 @@ import 'package:fforte/screens/sharedMethods/save_template.dart';
import 'package:fforte/screens/sharedMethods/save_main_entry.dart'; import 'package:fforte/screens/sharedMethods/save_main_entry.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
class AddEntriesDialogHelper { class AddEntriesDialogHelper {
// Function to show the dialog where the user has to choose if he want to safe his values as a template // Function to show the dialog where the user has to choose if he want to safe his values as a template
@@ -258,4 +260,67 @@ class AddEntriesDialogHelper {
}, },
); );
} }
static void locationSettingsDialog(BuildContext context) async {
return showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
content: Text(AppLocalizations.of(context)!.needsAlwaysLocation),
actions: [
TextButton(
onPressed: () {
Geolocator.openAppSettings();
Navigator.pop(context);
},
child: Text("Ok"),
),
TextButton(
onPressed: () {},
child: Text(AppLocalizations.of(context)!.cancel),
),
],
);
},
);
}
static Future<bool> deleteCompleteRouteDialog(BuildContext context) async {
bool confirmed = false;
await showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteEverything),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(AppLocalizations.of(context)!.deleteWholeRouteBody),
],
),
),
actions: [
TextButton(
onPressed: () {
confirmed = true;
Navigator.of(context).pop();
},
child: Text("Ok"),
),
TextButton(
onPressed: () => {
Navigator.of(context).pop()
},
child: Text(AppLocalizations.of(context)!.cancel),
),
],
);
},
);
return confirmed;
}
} }

View File

@@ -3,6 +3,7 @@ import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/screens/sharedMethods/delete_main_entries.dart'; import 'package:fforte/screens/sharedMethods/delete_main_entries.dart';
import 'package:fforte/screens/sharedMethods/delete_templates.dart'; import 'package:fforte/screens/sharedMethods/delete_templates.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
class ViewEntriesDialogHelper { class ViewEntriesDialogHelper {
static void deleteAllMainEntries( static void deleteAllMainEntries(
@@ -43,32 +44,41 @@ class ViewEntriesDialogHelper {
); );
} }
static void deleteAllTemplates(BuildContext context, DatabasesEnum dbType) async{ static void deleteAllTemplates(
BuildContext context,
DatabasesEnum dbType,
) async {
return showDialog( return showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteEverything), title: Text(AppLocalizations.of(context)!.deleteEverything),
content: SingleChildScrollView( content: SingleChildScrollView(
child: ListBody(children: <Widget>[ child: ListBody(
Text(AppLocalizations.of(context)!.deleteEverythingContent) children: <Widget>[
]), Text(AppLocalizations.of(context)!.deleteEverythingContent),
],
), ),
actions: <Widget>[ ),
TextButton( actions: <Widget>[
onPressed: () { TextButton(
onPressed: () {
DeleteTemplates.deleteAll(dbType); DeleteTemplates.deleteAll(dbType);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(AppLocalizations.of(context)!.deleteEverything)), child: Text(AppLocalizations.of(context)!.deleteEverything),
TextButton( ),
onPressed: () { TextButton(
Navigator.of(context).pop(); onPressed: () {
}, Navigator.of(context).pop();
child: Text(AppLocalizations.of(context)!.cancel)) },
], child: Text(AppLocalizations.of(context)!.cancel),
); ),
}); ],
);
},
);
} }
} }

View File

@@ -235,6 +235,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.1" version: "8.1.1"
flutter_map_location_marker:
dependency: "direct main"
description:
name: flutter_map_location_marker
sha256: "474695ec9052c17e307bdef98b66be2c183324f956efad24d86ad34a71942e4d"
url: "https://pub.dev"
source: hosted
version: "10.1.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -251,6 +259,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.6" version: "0.0.6"
flutter_rotation_sensor:
dependency: transitive
description:
name: flutter_rotation_sensor
sha256: "7944dbadf3d05be128a81cb0f37b2ba2b53247fef22b85f8096c60b06c1e50a4"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
flutter_shaders: flutter_shaders:
dependency: transitive dependency: transitive
description: description:
@@ -437,6 +453,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -469,6 +493,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
native_device_orientation:
dependency: transitive
description:
name: native_device_orientation
sha256: "0c330c068575e4be72cce5968ca479a3f8d5d1e5dfce7d89d5c13a1e943b338c"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -34,6 +34,7 @@ dependencies:
geocoding: ^3.0.0 geocoding: ^3.0.0
flutter_slidable: ^4.0.0 flutter_slidable: ^4.0.0
flutter_local_notifications: ^19.2.0 flutter_local_notifications: ^19.2.0
flutter_map_location_marker: ^10.1.0
dev_dependencies: dev_dependencies:
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0

View File

@@ -85,3 +85,4 @@
15.mai 2h 30min 15.mai 2h 30min
17.mai 1h 30min 17.mai 1h 30min
18.mai 35min 18.mai 35min
19.mai 2h 15min