diff --git a/assets/images/explorer-mode-button.png b/assets/images/explorer-mode-button.png new file mode 100644 index 000000000..c1285dc74 Binary files /dev/null and b/assets/images/explorer-mode-button.png differ diff --git a/assets/images/trafficlights/free-ride-green-dark.png b/assets/images/trafficlights/free-ride-green-dark.png new file mode 100644 index 000000000..625a43514 Binary files /dev/null and b/assets/images/trafficlights/free-ride-green-dark.png differ diff --git a/assets/images/trafficlights/free-ride-green-light.png b/assets/images/trafficlights/free-ride-green-light.png new file mode 100644 index 000000000..c3401f6ee Binary files /dev/null and b/assets/images/trafficlights/free-ride-green-light.png differ diff --git a/assets/images/trafficlights/free-ride-green.png b/assets/images/trafficlights/free-ride-green.png deleted file mode 100644 index d737eedbd..000000000 Binary files a/assets/images/trafficlights/free-ride-green.png and /dev/null differ diff --git a/assets/images/trafficlights/free-ride-none-dark.png b/assets/images/trafficlights/free-ride-none-dark.png index a0bcfa5e4..36709518e 100644 Binary files a/assets/images/trafficlights/free-ride-none-dark.png and b/assets/images/trafficlights/free-ride-none-dark.png differ diff --git a/assets/images/trafficlights/free-ride-none-light.png b/assets/images/trafficlights/free-ride-none-light.png index 36709518e..a0bcfa5e4 100644 Binary files a/assets/images/trafficlights/free-ride-none-light.png and b/assets/images/trafficlights/free-ride-none-light.png differ diff --git a/assets/images/trafficlights/free-ride-red-dark.png b/assets/images/trafficlights/free-ride-red-dark.png new file mode 100644 index 000000000..79f031931 Binary files /dev/null and b/assets/images/trafficlights/free-ride-red-dark.png differ diff --git a/assets/images/trafficlights/free-ride-red-light.png b/assets/images/trafficlights/free-ride-red-light.png new file mode 100644 index 000000000..67bd3be44 Binary files /dev/null and b/assets/images/trafficlights/free-ride-red-light.png differ diff --git a/assets/images/trafficlights/free-ride-red.png b/assets/images/trafficlights/free-ride-red.png deleted file mode 100644 index 5ca848c20..000000000 Binary files a/assets/images/trafficlights/free-ride-red.png and /dev/null differ diff --git a/lib/common/map/layers/sg_layers_free.dart b/lib/common/map/layers/sg_layers_free.dart index 260ee0638..b4642247a 100644 --- a/lib/common/map/layers/sg_layers_free.dart +++ b/lib/common/map/layers/sg_layers_free.dart @@ -18,7 +18,14 @@ class AllTrafficLightsPredictionLayer { /// The features to display. final List features = List.empty(growable: true); - AllTrafficLightsPredictionLayer({Map? propertiesBySgId, double? userBearing}) { + /// If a dark version of the layer should be used. + final bool isDark; + + AllTrafficLightsPredictionLayer( + this.isDark, { + Map? propertiesBySgId, + double? userBearing, + }) { final freeRide = getIt(); if (freeRide.sgs == null || freeRide.sgs!.isEmpty) return; if (freeRide.sgBearings == null || freeRide.sgBearings!.isEmpty) return; @@ -77,11 +84,11 @@ class AllTrafficLightsPredictionLayer { textFont: ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], textSize: 24, textColor: Colors.white.value, - textHaloColor: Colors.black.value, - textHaloWidth: 1, + textHaloColor: const Color.fromARGB(255, 31, 31, 31).value, + textHaloWidth: 0.75, textAnchor: mapbox.TextAnchor.BOTTOM, textOffset: [0, -1], - textOpacity: 0.8, + textOpacity: 1, ), mapbox.LayerPosition(at: at), ); @@ -96,14 +103,14 @@ class AllTrafficLightsPredictionLayer { ["get", "greenNow"], false ], - "free-ride-red", + isDark ? "free-ride-red-dark" : "free-ride-red-light", [ "==", ["get", "greenNow"], true ], - "free-ride-green", - "free-ride-none-light", + isDark ? "free-ride-green-dark" : "free-ride-green-light", + isDark ? "free-ride-none-dark" : "free-ride-none-light", ]), ); @@ -140,6 +147,14 @@ class AllTrafficLightsPredictionLayer { "get", "textSize", ])); + + await mapController.style.setStyleLayerProperty( + layerId, + 'text-opacity', + jsonEncode([ + "get", + "opacity", + ])); } } @@ -157,13 +172,26 @@ class AllTrafficLightsPredictionGeometryLayer { /// The ID of the Mapbox source. static const sourceId = "all-traffic-lights-prediction-geometry-source"; + /// The ID of the chevron layer. + static const layerIdChevrons = "all-traffic-lights-predictions-geometry-layer-chevrons"; + /// The ID of the Mapbox layer. static const layerId = "all-traffic-lights-predictions-geometry-layer"; + /// The ID of the background layer. + static const layerIdBackground = "all-traffic-lights-predictions-geometry-layer-background"; + /// The features to display. final List features = List.empty(growable: true); - AllTrafficLightsPredictionGeometryLayer({Map? propertiesBySgId, double? userBearing}) { + /// If a dark version of the layer should be used. + final bool isDark; + + AllTrafficLightsPredictionGeometryLayer( + this.isDark, { + Map? propertiesBySgId, + double? userBearing, + }) { final freeRide = getIt(); if (freeRide.sgGeometries == null || freeRide.sgGeometries!.isEmpty) return; @@ -196,12 +224,41 @@ class AllTrafficLightsPredictionGeometryLayer { await update(mapController); } + final trafficLightLineChevronLayerExists = await mapController.style.styleLayerExists(layerIdChevrons); + if (!trafficLightLineChevronLayerExists) { + await mapController.style.addLayerAt( + mapbox.SymbolLayer( + sourceId: sourceId, + id: layerIdChevrons, + symbolPlacement: mapbox.SymbolPlacement.LINE, + symbolSpacing: 0, + iconSize: 1, + iconAllowOverlap: true, + iconOpacity: 0.6, + iconIgnorePlacement: true, + iconRotate: 90, + iconImage: isDark ? "routechevronlight" : "routechevrondark", + ), + mapbox.LayerPosition(at: at)); + + await mapController.style.setStyleLayerProperty( + layerIdChevrons, + 'icon-opacity', + jsonEncode([ + "get", + "opacity", + ])); + } + final trafficLightLineLayerExists = await mapController.style.styleLayerExists(layerId); if (!trafficLightLineLayerExists) { await mapController.style.addLayerAt( mapbox.LineLayer( sourceId: sourceId, id: layerId, + lineJoin: mapbox.LineJoin.ROUND, + lineCap: mapbox.LineCap.ROUND, + lineWidth: 20, ), mapbox.LayerPosition(at: at), ); @@ -216,14 +273,14 @@ class AllTrafficLightsPredictionGeometryLayer { ["get", "greenNow"], false ], - "#ff0000", + "#f30034", [ "==", ["get", "greenNow"], true ], - "#00ff00", - "#000000", + "#17F54D", + isDark ? "#000000" : "#FFFFFF", ]), ); @@ -234,13 +291,48 @@ class AllTrafficLightsPredictionGeometryLayer { "get", "opacity", ])); + } + + final trafficLightLineBackgroundLayerExists = await mapController.style.styleLayerExists(layerIdBackground); + if (!trafficLightLineBackgroundLayerExists) { + await mapController.style.addLayerAt( + mapbox.LineLayer( + sourceId: sourceId, + id: layerIdBackground, + lineJoin: mapbox.LineJoin.ROUND, + lineCap: mapbox.LineCap.ROUND, + lineWidth: 26, + ), + mapbox.LayerPosition(at: at), + ); await mapController.style.setStyleLayerProperty( - layerId, - 'line-width', + layerIdBackground, + "line-color", + jsonEncode([ + "case", + [ + "==", + ["get", "greenNow"], + false + ], + isDark ? "#FF7B7B" : "#B50000", + [ + "==", + ["get", "greenNow"], + true + ], + isDark ? "#8EFFB4" : "#00B01C", + isDark ? "#000000" : "#FFFFFF", + ]), + ); + + await mapController.style.setStyleLayerProperty( + layerIdBackground, + 'line-opacity', jsonEncode([ "get", - "lineWidth", + "opacity", ])); } } diff --git a/lib/common/map/symbols.dart b/lib/common/map/symbols.dart index c73c80499..e75d0cf0f 100644 --- a/lib/common/map/symbols.dart +++ b/lib/common/map/symbols.dart @@ -65,8 +65,10 @@ class SymbolLoader { await add("greenwavedark", "assets/images/green-wave-dark.png", 200, 200); await add("greenwavelight", "assets/images/green-wave-light.png", 200, 200); - await add("free-ride-green", "assets/images/trafficlights/free-ride-green.png", 200, 200); - await add("free-ride-red", "assets/images/trafficlights/free-ride-red.png", 200, 200); + await add("free-ride-green-dark", "assets/images/trafficlights/free-ride-green-dark.png", 200, 200); + await add("free-ride-green-light", "assets/images/trafficlights/free-ride-green-light.png", 200, 200); + await add("free-ride-red-dark", "assets/images/trafficlights/free-ride-red-dark.png", 200, 200); + await add("free-ride-red-light", "assets/images/trafficlights/free-ride-red-light.png", 200, 200); await add("free-ride-none-light", "assets/images/trafficlights/free-ride-none-light.png", 200, 200); await add("free-ride-none-dark", "assets/images/trafficlights/free-ride-none-dark.png", 200, 200); } diff --git a/lib/feedback/services/feedback.dart b/lib/feedback/services/feedback.dart index 55edbceda..c81365dc1 100644 --- a/lib/feedback/services/feedback.dart +++ b/lib/feedback/services/feedback.dart @@ -7,9 +7,9 @@ import 'package:priobike/feedback/models/question.dart'; import 'package:priobike/http.dart'; import 'package:priobike/logging/logger.dart'; import 'package:priobike/main.dart'; -import 'package:priobike/ride/services/ride.dart'; import 'package:priobike/settings/models/backend.dart'; import 'package:priobike/settings/services/settings.dart'; +import 'package:priobike/tracking/services/tracking.dart'; import 'package:priobike/user.dart'; class Feedback with ChangeNotifier { @@ -44,7 +44,11 @@ class Feedback with ChangeNotifier { isSendingFeedback = true; notifyListeners(); - final sessionId = getIt().sessionId; + final sessionId = getIt().track?.sessionId; + if (sessionId == null) { + log.e("Error sending feedback: No sessionId available."); + return false; + } final userId = await User.getOrCreateId(); // Send all of the answered questions to the backend. diff --git a/lib/home/views/main.dart b/lib/home/views/main.dart index 604450c3a..ef1481047 100644 --- a/lib/home/views/main.dart +++ b/lib/home/views/main.dart @@ -8,6 +8,7 @@ import 'package:priobike/common/layout/dialog.dart'; import 'package:priobike/common/layout/modal.dart'; import 'package:priobike/common/layout/spacing.dart'; import 'package:priobike/common/layout/text.dart'; +import 'package:priobike/common/layout/tiles.dart'; import 'package:priobike/home/models/shortcut.dart'; import 'package:priobike/home/models/shortcut_location.dart'; import 'package:priobike/home/models/shortcut_route.dart'; @@ -23,6 +24,7 @@ import 'package:priobike/main.dart'; import 'package:priobike/news/services/news.dart'; import 'package:priobike/news/views/main.dart'; import 'package:priobike/ride/services/ride.dart'; +import 'package:priobike/ride/views/free.dart'; import 'package:priobike/routing/models/waypoint.dart'; import 'package:priobike/routing/services/profile.dart'; import 'package:priobike/routing/services/routing.dart'; @@ -226,6 +228,8 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw /// A callback that is fired when free routing was selected. void onStartFreeRouting() { + if (routing.isFetchingRoute) return; + HapticFeedback.mediumImpact(); pushRoutingView(); @@ -245,6 +249,43 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw ); } + /// A callback that is fired when free ride was selected. + void onStartFreeRide() { + HapticFeedback.mediumImpact(); + + if (settings.didViewWarning) { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const FreeRideView())); + return; + } + + // Before we start the free ride view, we always notify the user of its drawbacks. + showDialog( + context: context, + builder: (BuildContext context) { + return DialogLayout( + title: "Wirklich ohne Route fahren?", + text: + "In diesem Modus siehst Du lediglich Countdowns der Ampeln. Diese können ungenau sein. Mit Routenplanung erhältst Du genauere Geschwindigkeitsempfehlungen. Denke an Deine Sicherheit und achte stets auf Deine Umgebung. Beachte die Hinweisschilder und die örtlichen Gesetze.", + actions: [ + BigButtonPrimary( + label: "Fortfahren", + onPressed: () { + getIt().setDidViewWarning(true); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const FreeRideView())); + }, + boxConstraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width, minHeight: 36), + ), + BigButtonTertiary( + label: "Abbrechen", + onPressed: () => Navigator.pop(context), + boxConstraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width, minHeight: 36), + ), + ], + ); + }, + ); + } + /// A callback that is fired when the shortcuts should be edited. void onOpenShortcutEditView() { Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ShortcutsEditView())); @@ -309,7 +350,7 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw crossAxisAlignment: CrossAxisAlignment.start, children: [ BoldSubHeader( - text: "Navigation", + text: "Navigation (empfohlen)", context: context, ), const SizedBox(height: 4), @@ -364,7 +405,10 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw padding: EdgeInsets.fromLTRB(25, 0, 25, 24), ), ), - ShortcutsView(onSelectShortcut: onSelectShortcut, onStartFreeRouting: onStartFreeRouting) + ShortcutsView( + onSelectShortcut: onSelectShortcut, + onStartFreeRouting: onStartFreeRouting, + ) ], ), ), @@ -372,14 +416,79 @@ class HomeViewState extends State with WidgetsBindingObserver, RouteAw delay: Duration(milliseconds: 750), child: YourBikeView(), ), + const SmallVSpace(), BlendIn( - delay: const Duration(milliseconds: 1000), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Divider(color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.1)), + delay: const Duration(milliseconds: 750), + child: HPad( + child: Tile( + onPressed: onStartFreeRide, + shadowIntensity: 0, + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.5, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BoldSmall( + text: "Erkundungsmodus", + overflow: TextOverflow.ellipsis, + context: context, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 8), + Icon( + Icons.explore_rounded, + color: Theme.of(context).colorScheme.onSurface, + size: 16, + ), + ], + ), + ), + const SmallVSpace(), + SizedBox( + width: MediaQuery.of(context).size.width * 0.5, + child: Small( + text: "Ohne Route durch die Stadt bewegen", + overflow: TextOverflow.ellipsis, + context: context, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.only(left: 8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onTertiary.withOpacity(0.5), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + BoldSmall( + text: "Losfahren", + context: context, + color: Theme.of(context).colorScheme.tertiary), + Transform.translate( + offset: const Offset(2, 0), + child: Icon( + Icons.chevron_right_rounded, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ], + ), + ), + ], + ), + ), ), ), - const SmallVSpace(), + const VSpace(), const TrackHistoryView(), BlendIn( delay: const Duration(milliseconds: 1000), diff --git a/lib/home/views/poi/your_bike.dart b/lib/home/views/poi/your_bike.dart index 1edaf9c14..bddbf99ec 100644 --- a/lib/home/views/poi/your_bike.dart +++ b/lib/home/views/poi/your_bike.dart @@ -1,72 +1,9 @@ import 'package:flutter/material.dart'; import 'package:priobike/common/layout/text.dart'; -import 'package:priobike/common/layout/tiles.dart'; import 'package:priobike/home/services/poi.dart'; import 'package:priobike/home/views/poi/nearby_poi_list.dart'; import 'package:priobike/main.dart'; -class YourBikeElementButton extends StatelessWidget { - final Image image; - final String title; - final Color? color; - final Color? backgroundColor; - final Color? touchColor; - final Color? borderColor; - final void Function()? onPressed; - - const YourBikeElementButton({ - super.key, - required this.image, - required this.title, - this.color, - this.backgroundColor, - this.touchColor, - this.borderColor, - this.onPressed, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return LayoutBuilder( - builder: (context, constraints) { - return Tile( - fill: backgroundColor ?? theme.colorScheme.surface, - splash: touchColor ?? theme.colorScheme.surfaceTint, - borderRadius: const BorderRadius.all(Radius.circular(16)), - borderColor: borderColor ?? theme.colorScheme.primary, - padding: const EdgeInsets.all(8), - borderWidth: 4, - shadowIntensity: 0.05, - content: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(right: 2), - // Resize image - child: SizedBox( - width: constraints.maxWidth * 0.15, - height: constraints.maxWidth * 0.2, - child: image, - ), - ), - Small( - text: title, - color: color ?? theme.colorScheme.primary, - textAlign: TextAlign.center, - context: context, - overflow: TextOverflow.ellipsis, - ), - ], - ), - onPressed: onPressed, - ); - }, - ); - } -} - class YourBikeView extends StatefulWidget { const YourBikeView({super.key}); @@ -79,7 +16,9 @@ class YourBikeViewState extends State { late POI poi; /// Called when a listener callback of a ChangeNotifier is fired. - void update() => setState(() {}); + void update() { + if (mounted) setState(() {}); + } @override void initState() { diff --git a/lib/home/views/shortcuts/selection.dart b/lib/home/views/shortcuts/selection.dart index b42f0f8ce..ccb5be2db 100644 --- a/lib/home/views/shortcuts/selection.dart +++ b/lib/home/views/shortcuts/selection.dart @@ -16,6 +16,13 @@ import 'package:priobike/routing/services/routing.dart'; class ShortcutView extends StatelessWidget { final Shortcut? shortcut; + + /// What text to show when no shortcut is available. + final String? alternativeText; + + /// What icon to show when no shortcut is available. + final IconData? alternativeIcon; + final void Function() onPressed; final void Function()? onLongPressed; final double width; @@ -28,6 +35,8 @@ class ShortcutView extends StatelessWidget { const ShortcutView({ super.key, this.shortcut, + this.alternativeText, + this.alternativeIcon, required this.onPressed, required this.width, required this.height, @@ -52,9 +61,9 @@ class ShortcutView extends StatelessWidget { content: Stack( children: [ if (shortcut == null) - const Padding( - padding: EdgeInsets.all(8), - child: Icon(Icons.map_rounded, size: 64, color: Colors.white), + Padding( + padding: const EdgeInsets.all(8), + child: Icon(alternativeIcon ?? Icons.error, size: 64, color: Colors.white), ) else if (shortcut is ShortcutRoute) Container( @@ -104,9 +113,9 @@ class ShortcutView extends StatelessWidget { shortcut == null ? null : Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.75), ), child: shortcut == null - ? const Text( - 'Freie Route', - style: TextStyle( + ? Text( + alternativeText ?? 'Missing alternative text', + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, @@ -227,9 +236,9 @@ class ShortcutsViewState extends State { padding: EdgeInsets.only(left: leftPad), ), ShortcutView( - onPressed: () { - if (!routing.isFetchingRoute) widget.onStartFreeRouting(); - }, + alternativeText: 'Route planen', + alternativeIcon: Icons.map_rounded, + onPressed: widget.onStartFreeRouting, width: shortcutWidth, height: shortcutHeight, rightPad: shortcutRightPad, diff --git a/lib/positioning/services/positioning.dart b/lib/positioning/services/positioning.dart index 86d68f657..31d920239 100644 --- a/lib/positioning/services/positioning.dart +++ b/lib/positioning/services/positioning.dart @@ -155,6 +155,22 @@ class Positioning with ChangeNotifier { [settings.city.center]; positionSource = PathMockPositionSource(idealSpeed: 18 / 3.6, positions: positions, autoSpeed: true); log.i("Using mocked auto speed positioning source."); + } else if (settings.positioningMode == PositioningMode.hamburgInCircles) { + // In circles around Millerntorplatz, an area with many traffic lights. + positionSource = PathMockPositionSource(idealSpeed: 18 / 3.6, positions: const [ + LatLng(53.549912, 9.967677), + LatLng(53.550012, 9.971065), + LatLng(53.549654, 9.972879), + LatLng(53.549981, 9.973134), + LatLng(53.550216, 9.971639), + LatLng(53.551221, 9.969480), + LatLng(53.550957, 9.969092), + LatLng(53.550727, 9.969593), + LatLng(53.550174, 9.969269), + LatLng(53.550115, 9.967710), + LatLng(53.549912, 9.967677), + ]); + log.i("Using mocked position source for Hamburg in circles."); } else if (settings.positioningMode == PositioningMode.sensor) { final routing = getIt(); final positions = routing.selectedRoute?.route // Fallback to center location of city. diff --git a/lib/ride/services/free_ride.dart b/lib/ride/services/free_ride.dart index c2749c7c6..9f7be3916 100644 --- a/lib/ride/services/free_ride.dart +++ b/lib/ride/services/free_ride.dart @@ -44,7 +44,7 @@ class FreeRide with ChangeNotifier { final Set subscriptions = {}; /// The max distance in meters for an SG to be considered on screen. - static const maxDistance = 200; + static const maxDistance = 300; final vincenty = const Distance(roundResult: false); diff --git a/lib/ride/services/ride.dart b/lib/ride/services/ride.dart index b9c5f6dbb..4da5b60a4 100644 --- a/lib/ride/services/ride.dart +++ b/lib/ride/services/ride.dart @@ -61,9 +61,6 @@ class Ride with ChangeNotifier { /// The calculated distance to the next turn. double? calcDistanceToNextTurn; - /// The session id, set randomly by `startNavigation`. - String? sessionId; - /// The prediction provider. PredictionProvider? predictionProvider; @@ -211,8 +208,6 @@ class Ride with ChangeNotifier { ); predictionProvider!.connectMQTTClient(); - // Mark that navigation is now active. - sessionId = UniqueKey().toString(); navigationIsActive = true; } diff --git a/lib/ride/views/free.dart b/lib/ride/views/free.dart index 9e398d4ee..027808545 100644 --- a/lib/ride/views/free.dart +++ b/lib/ride/views/free.dart @@ -16,6 +16,7 @@ import 'package:priobike/ride/services/free_ride.dart'; import 'package:priobike/ride/views/free_map.dart'; import 'package:priobike/ride/views/screen_tracking.dart'; import 'package:priobike/settings/services/settings.dart'; +import 'package:priobike/tracking/services/tracking.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; class FreeRideView extends StatefulWidget { @@ -46,6 +47,10 @@ class FreeRideViewState extends State { SchedulerBinding.instance.addPostFrameCallback( (_) async { + final deviceWidth = MediaQuery.of(context).size.width; + final deviceHeight = MediaQuery.of(context).size.height; + + final tracking = getIt(); final positioning = getIt(); freeRide.prepare(); @@ -56,10 +61,18 @@ class FreeRideViewState extends State { showLocationAccessDeniedDialog(context, positioning.positionSource); }, onNewPosition: () async { - // Note: Only called in routing mode since it depends on the snapped position. (Maybe a FIXME) + await tracking.updatePosition(); }, ); + bool? isDark; + if (mounted) { + isDark = Theme.of(context).brightness == Brightness.dark; + } + + // Start tracking once the `sessionId` is set and the positioning stream is available. + await tracking.start(deviceWidth, deviceHeight, settings.saveBatteryModeEnabled, isDark, freeRide: true); + // Allow user to rotate the screen in ride view. // Landscape-Mode will be removed in FinishRideButton. await SystemChrome.setPreferredOrientations([ @@ -126,10 +139,15 @@ class FreeRideViewState extends State { child: SafeArea( child: Tile( onPressed: () async { + // End the tracking and collect the data. + await getIt().end(); // Performs all needed resets. await freeRide.reset(); final positioning = getIt(); await positioning.stopGeolocation(); + // Disable the wakelock which was set when the ride started. + WakelockPlus.disable(); + if (!context.mounted) return; Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (BuildContext context) => const HomeView()), @@ -164,9 +182,7 @@ class FreeRideViewState extends State { ), ], ) - : const Center( - child: Text('Error.'), - ), + : Container(), ), ), ); diff --git a/lib/ride/views/free_map.dart b/lib/ride/views/free_map.dart index 78a8398e1..e481e3b9a 100644 --- a/lib/ride/views/free_map.dart +++ b/lib/ride/views/free_map.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mapbox; +import 'package:priobike/common/layout/annotated_region.dart'; import 'package:priobike/common/map/layers/sg_layers_free.dart'; import 'package:priobike/common/map/symbols.dart'; import 'package:priobike/common/map/view.dart'; @@ -36,17 +37,12 @@ class FreeRideMapView extends StatefulWidget { class FreeRideMapViewState extends State { static const viewId = "free.ride.views.map"; - static const bearingDiffThreshold = 90; - /// The associated settings service, which is injected by the provider. late Settings settings; /// The associated free ride service, which is injected by the provider. late FreeRide freeRide; - /// If the SG filter is active. - late bool sgFilterActive; - /// A map controller for the map. mapbox.MapboxMap? mapController; @@ -64,7 +60,9 @@ class FreeRideMapViewState extends State { /// The index in the list represents the layer order in z axis. final List layerOrder = [ + AllTrafficLightsPredictionGeometryLayer.layerIdBackground, AllTrafficLightsPredictionGeometryLayer.layerId, + AllTrafficLightsPredictionGeometryLayer.layerIdChevrons, AllTrafficLightsPredictionLayer.layerId, AllTrafficLightsPredictionLayer.countdownLayerId, ]; @@ -93,10 +91,10 @@ class FreeRideMapViewState extends State { freeRide = getIt(); freeRide.addListener(onFreeRideUpdate); settings = getIt(); - sgFilterActive = settings.isFreeRideFilterEnabled; } void onFreeRideUpdate() { + if (!mounted) return; setState(() {}); } @@ -189,6 +187,7 @@ class FreeRideMapViewState extends State { /// A callback which is executed when the map style was loaded. Future onStyleLoaded(mapbox.StyleLoadedEventData styleLoadedEventData) async { if (mapController == null || !mounted) return; + final isDark = Theme.of(context).brightness == Brightness.dark; await getFirstLabelLayer(); @@ -200,10 +199,10 @@ class FreeRideMapViewState extends State { // await AllTrafficLightsLayer().install(mapController!, at: index); var index = await getIndex(AllTrafficLightsPredictionGeometryLayer.layerId); if (!mounted) return; - await AllTrafficLightsPredictionGeometryLayer().install(mapController!, at: index); + await AllTrafficLightsPredictionGeometryLayer(isDark).install(mapController!, at: index); index = await getIndex(AllTrafficLightsPredictionLayer.layerId); if (!mounted) return; - await AllTrafficLightsPredictionLayer().install(mapController!, at: index); + await AllTrafficLightsPredictionLayer(isDark).install(mapController!, at: index); onPositioningUpdate(); @@ -303,7 +302,7 @@ class FreeRideMapViewState extends State { freeRide.updateVisibleSgs(cameraBounds, LatLng(lat, lon), cameraState.zoom); }); - updateSgPredictionsTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + updateSgPredictionsTimer = Timer.periodic(const Duration(milliseconds: 499), (timer) async { if (mapController == null) return; final Map propertiesBySgId = {}; @@ -317,89 +316,37 @@ class FreeRideMapViewState extends State { int? secondsToPhaseChange; (greenNow, secondsToPhaseChange) = await getGreenNowAndSecondsToPhaseChange(entry.value); - if (!sgFilterActive) { - propertiesBySgId[entry.key] = { - "greenNow": greenNow, - "opacity": 1, - "countdown": secondsToPhaseChange?.toInt(), - "lineWidth": 5, - }; - continue; - } - - // Bool that holds the state if a sg is most likely relevant for the user or not. - bool isRelevant = false; - - final sgGeometry = freeRide.sgGeometries![entry.key]; double sgBearing = freeRide.sgBearings![entry.key]!; - // Fix sg bearing to make it comparable with user bearing. if (sgBearing < 0) { sgBearing = 180 + (180 - sgBearing.abs()); } - - // 1. A sg facing towards the user is considered as relevant. - // 360 need to be considered. - final bearingDiff = getBearingDiff(currentCalcBearing!, sgBearing); - if (-45 < bearingDiff && bearingDiff < 45) { - isRelevant = true; - } - - // 2. A sg facing to the left of the user with a lane going left from the user is considered as relevant. - final coordinates = sgGeometry?['coordinates']; - - if (45 < bearingDiff && bearingDiff < 135) { - if (coordinates != null && coordinates.length > 1) { - final secondLast = coordinates[coordinates.length - 2]; - final last = coordinates[coordinates.length - 1]; - double laneEndBearing = vincenty.bearing(LatLng(secondLast[1], secondLast[0]), LatLng(last[1], last[0])); - if (laneEndBearing < 0) { - laneEndBearing = 180 + (180 - laneEndBearing.abs()); - } - - final bearingDiffLastSegment = getBearingDiff(currentCalcBearing!, laneEndBearing); - - // relative left is okay. - // Just not - if (45 < bearingDiffLastSegment && bearingDiffLastSegment < 135) { - // Left sg. - isRelevant = true; - } - } - } - - // 3. A sg facing to the right of the user and being oriented towards the right side of the user is considered as relevant. - if (-180 < bearingDiff && bearingDiff < 0) { - if (coordinates != null && coordinates.length > 1) { - final last = coordinates[coordinates.length - 1]; - double laneEndPositionBearing = vincenty.bearing( - LatLng(positioning.lastPosition!.latitude, positioning.lastPosition!.longitude), - LatLng(last[1], last[0]), - ); - if (laneEndPositionBearing < 0) { - laneEndPositionBearing = 180 + (180 - laneEndPositionBearing.abs()); - } - - final bearingDiffUserSG = getBearingDiff(currentCalcBearing!, laneEndPositionBearing); - - if (170 < bearingDiffUserSG && bearingDiffUserSG < 0) { - isRelevant = true; - } - } + var opacity = 1 - (bearingDiff.abs() / 135); + if (opacity < 0) { + opacity = 0; + } else if (opacity > 1) { + opacity = 1; } propertiesBySgId[entry.key] = { "greenNow": greenNow, "countdown": secondsToPhaseChange?.toInt(), - "opacity": isRelevant ? 1 : 0.25, - "lineWidth": isRelevant ? 5 : 1, + "opacity": greenNow == null ? 0 : opacity, + "lineWidth": 5, }; } - AllTrafficLightsPredictionLayer(propertiesBySgId: propertiesBySgId, userBearing: currentCalcBearing) - .update(mapController!); - AllTrafficLightsPredictionGeometryLayer(propertiesBySgId: propertiesBySgId, userBearing: currentCalcBearing) - .update(mapController!); + final isDark = Theme.of(context).brightness == Brightness.dark; + AllTrafficLightsPredictionLayer( + isDark, + propertiesBySgId: propertiesBySgId, + userBearing: currentCalcBearing, + ).update(mapController!); + AllTrafficLightsPredictionGeometryLayer( + isDark, + propertiesBySgId: propertiesBySgId, + userBearing: currentCalcBearing, + ).update(mapController!); }); } @@ -420,15 +367,20 @@ class FreeRideMapViewState extends State { marginYAttribution = -5; } - return AppMap( - onMapCreated: onMapCreated, - onStyleLoaded: onStyleLoaded, - logoViewMargins: Point(10, marginYLogo), - logoViewOrnamentPosition: mapbox.OrnamentPosition.TOP_LEFT, - attributionButtonMargins: Point(10, marginYAttribution), - attributionButtonOrnamentPosition: mapbox.OrnamentPosition.TOP_RIGHT, - saveBatteryModeEnabled: settings.saveBatteryModeEnabled, - useMapboxPositioning: true, + return AnnotatedRegionWrapper( + colorMode: Theme.of(context).brightness, + bottomBackgroundColor: const Color(0xFF000000), + bottomTextBrightness: Brightness.light, + child: AppMap( + onMapCreated: onMapCreated, + onStyleLoaded: onStyleLoaded, + logoViewMargins: Point(10, marginYLogo), + logoViewOrnamentPosition: mapbox.OrnamentPosition.TOP_LEFT, + attributionButtonMargins: Point(10, marginYAttribution), + attributionButtonOrnamentPosition: mapbox.OrnamentPosition.TOP_RIGHT, + saveBatteryModeEnabled: settings.saveBatteryModeEnabled, + useMapboxPositioning: true, + ), ); } } diff --git a/lib/settings/models/positioning.dart b/lib/settings/models/positioning.dart index 8cf7c7f5e..7f973284a 100644 --- a/lib/settings/models/positioning.dart +++ b/lib/settings/models/positioning.dart @@ -3,6 +3,7 @@ enum PositioningMode { follow18kmh, follow40kmh, autospeed, + hamburgInCircles, sensor, recordedDresden, recordedHamburg, @@ -23,6 +24,8 @@ extension PositioningDescription on PositioningMode { return "Route mit 40 km/h folgen"; case PositioningMode.autospeed: return "Der besten Grünphase folgen"; + case PositioningMode.hamburgInCircles: + return "Hamburg im Kreis um den Millerntorplatz"; case PositioningMode.sensor: return "Speed Sensor Daten"; case PositioningMode.recordedDresden: diff --git a/lib/settings/services/settings.dart b/lib/settings/services/settings.dart index 52eb4660f..5c5c04765 100644 --- a/lib/settings/services/settings.dart +++ b/lib/settings/services/settings.dart @@ -85,9 +85,6 @@ class Settings with ChangeNotifier { /// Enable live tracking mode for app. bool enableLiveTrackingMode; - /// If the filter for the free ride view is enabled. - bool isFreeRideFilterEnabled; - /// If we want to show the speed with increased precision in the speedometer. bool isIncreasedSpeedPrecisionInSpeedometerEnabled = false; @@ -434,23 +431,6 @@ class Settings with ChangeNotifier { return success; } - static const isFreeRideFilterEnabledKey = "priobike.settings.isFreeRideFilterEnabled"; - static const defaultIsFreeRideFilterEnabled = false; - - Future setFreeRideFilterEnabled(bool isFreeRideFilterEnabled, [SharedPreferences? storage]) async { - storage ??= await SharedPreferences.getInstance(); - final prev = this.isFreeRideFilterEnabled; - this.isFreeRideFilterEnabled = isFreeRideFilterEnabled; - final bool success = await storage.setBool(isFreeRideFilterEnabledKey, isFreeRideFilterEnabled); - if (!success) { - log.e("Failed to set isFreeRideFilterEnabled to $isFreeRideFilterEnabled"); - this.isFreeRideFilterEnabled = prev; - } else { - notifyListeners(); - } - return success; - } - static const isIncreasedSpeedPrecisionInSpeedometerEnabledKey = "priobike.settings.isIncreasedSpeedPrecisionInSpeedometerEnabled"; static const defaultIsIncreasedSpeedPrecisionInSpeedometerEnabled = false; @@ -493,7 +473,6 @@ class Settings with ChangeNotifier { this.didMigrateBackgroundImages = defaultDidMigrateBackgroundImages, this.enableSimulatorMode = defaultSimulatorMode, this.enableLiveTrackingMode = defaultLiveTrackingMode, - this.isFreeRideFilterEnabled = defaultIsFreeRideFilterEnabled, this.isIncreasedSpeedPrecisionInSpeedometerEnabled = defaultIsIncreasedSpeedPrecisionInSpeedometerEnabled, }); @@ -529,11 +508,6 @@ class Settings with ChangeNotifier { } catch (e) { /* Do nothing and use the default value given by the constructor. */ } - try { - isFreeRideFilterEnabled = storage.getBool(isFreeRideFilterEnabledKey) ?? defaultIsFreeRideFilterEnabled; - } catch (e) { - /* Do nothing and use the default value given by the constructor. */ - } try { isIncreasedSpeedPrecisionInSpeedometerEnabled = storage.getBool(isIncreasedSpeedPrecisionInSpeedometerEnabledKey) ?? diff --git a/lib/settings/views/internal.dart b/lib/settings/views/internal.dart index e37957cc8..f3b0b9fd0 100644 --- a/lib/settings/views/internal.dart +++ b/lib/settings/views/internal.dart @@ -16,7 +16,6 @@ import 'package:priobike/positioning/services/positioning.dart'; import 'package:priobike/positioning/views/location_access_denied_dialog.dart'; import 'package:priobike/privacy/services.dart'; import 'package:priobike/ride/services/live_tracking.dart'; -import 'package:priobike/ride/views/free.dart'; import 'package:priobike/routing/services/boundary.dart'; import 'package:priobike/routing/services/routing.dart'; import 'package:priobike/settings/models/backend.dart' hide Simulator, LiveTracking; @@ -229,28 +228,6 @@ class InternalSettingsViewState extends State { ], ), const VSpace(), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Freie Fahrt", - icon: Icons.directions_bike_rounded, - callback: () => Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (BuildContext context) => const FreeRideView(), - ), - (route) => false, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: SettingsElement( - title: "Freie Fahrt Filter", - icon: settings.isFreeRideFilterEnabled ? Icons.check_box : Icons.check_box_outline_blank, - callback: () => settings.setFreeRideFilterEnabled(!settings.isFreeRideFilterEnabled), - ), - ), Padding( padding: const EdgeInsets.only(top: 8), child: SettingsElement( diff --git a/lib/tracking/models/track.dart b/lib/tracking/models/track.dart index dcfd87d2c..188e7b474 100644 --- a/lib/tracking/models/track.dart +++ b/lib/tracking/models/track.dart @@ -68,6 +68,9 @@ class Track { /// With this field we can determine tracks in debug mode. bool debug; + /// If the track was recorded using the free ride mode. + bool freeRide; + /// The city of the ride. City city; @@ -171,6 +174,7 @@ class Track { required this.startTime, this.endTime, required this.debug, + required this.freeRide, required this.city, required this.backend, required this.positioningMode, @@ -203,6 +207,7 @@ class Track { 'startTime': startTime, 'endTime': endTime, 'debug': debug, + 'freeRide': freeRide, 'city': city.name, 'backend': backend.name, 'positioningMode': positioningMode.name, @@ -249,6 +254,7 @@ class Track { startTime: json['startTime'], endTime: json['endTime'], debug: json['debug'], + freeRide: json['freeRide'] ?? false, city: City.values.byName(json['city']), backend: Backend.values.byName(json['backend']), positioningMode: PositioningMode.values.byName(json['positioningMode']), diff --git a/lib/tracking/services/tracking.dart b/lib/tracking/services/tracking.dart index 8ad9a2536..89a80d96c 100644 --- a/lib/tracking/services/tracking.dart +++ b/lib/tracking/services/tracking.dart @@ -95,7 +95,13 @@ class Tracking with ChangeNotifier { } /// Start a new track. - Future start(double deviceWidth, double deviceHeight, bool saveBatteryModeEnabled, bool? isDarkMode) async { + Future start( + double deviceWidth, + double deviceHeight, + bool saveBatteryModeEnabled, + bool? isDarkMode, { + bool freeRide = false, + }) async { log.i("Starting a new track."); // Get some session- and device-specific data. @@ -117,9 +123,10 @@ class Tracking with ChangeNotifier { final routing = getIt(); final settings = getIt(); final status = getIt(); - final ride = getIt(); final profile = getIt(); + final sessionId = UniqueKey().toString(); + try { Feature feature = getIt(); track = Track( @@ -128,11 +135,12 @@ class Tracking with ChangeNotifier { startTime: startTime, endTime: null, debug: kDebugMode, + freeRide: freeRide, city: settings.city, backend: settings.city.selectedBackend(true), positioningMode: settings.positioningMode, userId: await User.getOrCreateId(), - sessionId: ride.sessionId!, + sessionId: sessionId, deviceType: deviceType, deviceWidth: deviceWidth, deviceHeight: deviceHeight, @@ -140,11 +148,14 @@ class Tracking with ChangeNotifier { buildNumber: packageInfo.buildNumber, statusSummary: status.current!, taps: [], + // Predictions will be empty using the free ride mode. predictionServicePredictions: [], predictorPredictions: [], - selectedWaypoints: routing.selectedWaypoints!, + // Can be empty if the free ride mode is selected. + selectedWaypoints: routing.selectedWaypoints ?? [], bikeType: profile.bikeType, - routes: {startTime: routing.selectedRoute!}, + // Can be null if the free ride mode is selected. + routes: routing.selectedRoute == null ? {} : {startTime: routing.selectedRoute!}, subVersion: feature.buildTrigger, batteryStates: [], saveBatteryModeEnabled: saveBatteryModeEnabled,