From e9a0c3d8577fbc32fc28e2241111b3821e00a5bb Mon Sep 17 00:00:00 2001 From: PhilippMatthes Date: Tue, 9 Jul 2024 12:24:32 +0200 Subject: [PATCH] Free ride enhancements (#636) * WIP * Finish * Remove unnecessary check * Add tracking * Rework free ride button in home view * Add animation to new free ride button * Add freeRide flag to toJson --------- Co-authored-by: adeveloper-wq --- assets/images/explorer-mode-button.png | Bin 0 -> 32472 bytes .../trafficlights/free-ride-green-dark.png | Bin 0 -> 4158 bytes .../trafficlights/free-ride-green-light.png | Bin 0 -> 4109 bytes .../images/trafficlights/free-ride-green.png | Bin 3739 -> 0 bytes .../trafficlights/free-ride-none-dark.png | Bin 2159 -> 1817 bytes .../trafficlights/free-ride-none-light.png | Bin 1817 -> 2159 bytes .../trafficlights/free-ride-red-dark.png | Bin 0 -> 4068 bytes .../trafficlights/free-ride-red-light.png | Bin 0 -> 4132 bytes assets/images/trafficlights/free-ride-red.png | Bin 3866 -> 0 bytes lib/common/map/layers/sg_layers_free.dart | 120 ++++++++++++++-- lib/common/map/symbols.dart | 6 +- lib/feedback/services/feedback.dart | 8 +- lib/home/views/main.dart | 123 ++++++++++++++++- lib/home/views/poi/your_bike.dart | 67 +-------- lib/home/views/shortcuts/selection.dart | 27 ++-- lib/positioning/services/positioning.dart | 16 +++ lib/ride/services/free_ride.dart | 2 +- lib/ride/services/ride.dart | 5 - lib/ride/views/free.dart | 24 +++- lib/ride/views/free_map.dart | 128 ++++++------------ lib/settings/models/positioning.dart | 3 + lib/settings/services/settings.dart | 26 ---- lib/settings/views/internal.dart | 23 ---- lib/tracking/models/track.dart | 6 + lib/tracking/services/tracking.dart | 21 ++- 25 files changed, 355 insertions(+), 250 deletions(-) create mode 100644 assets/images/explorer-mode-button.png create mode 100644 assets/images/trafficlights/free-ride-green-dark.png create mode 100644 assets/images/trafficlights/free-ride-green-light.png delete mode 100644 assets/images/trafficlights/free-ride-green.png create mode 100644 assets/images/trafficlights/free-ride-red-dark.png create mode 100644 assets/images/trafficlights/free-ride-red-light.png delete mode 100644 assets/images/trafficlights/free-ride-red.png diff --git a/assets/images/explorer-mode-button.png b/assets/images/explorer-mode-button.png new file mode 100644 index 0000000000000000000000000000000000000000..c1285dc7404cd7f25c2c1cd075b7d3b5130b7752 GIT binary patch literal 32472 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 $T6m{3fOx_1ve+_u8 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 vh6w6+7P&ZjxT>|)gP9aYTDQ7A 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 ZTv7`dY$sbXc>4cGnHt_TXwY+w`yV1P;y3^R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c3401f6ee93a46a6f63bcf8805127583441e2ea0 GIT binary patch literal 4109 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 literal 0 HcmV?d00001 diff --git a/assets/images/trafficlights/free-ride-green.png b/assets/images/trafficlights/free-ride-green.png deleted file mode 100644 index d737eedbd63ca5b090f70f6694bb166b0f9c83f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3739 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 JPDHLkV1f=RG<^U7 delta 2060 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 JPDHLkV1f=RG<^U7 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 0000000000000000000000000000000000000000..79f03193194cad9b531d02b09aee7737b0825d9b GIT binary patch literal 4068 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 b>pAly5ympgl%kn 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 Udrb!V$5a+T(7J&y*RT!#A4I1Ws{jB1 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,