diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f3ea6d1..1dc8b96 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -103,7 +103,8 @@ class _MaizeBusCoreState extends State { static const LatLng _defaultCenter = LatLng(42.276463, -83.7374598); Set _displayedPolylines = {}; - Set _displayedStopMarkers = {}; + Map _displayedStopMarkers = {}; // maps from stopID to marker + Map _displayedFavoriteStopMarkers = {}; Set _displayedBusMarkers = {}; // Journey overlays for search results Set _displayedJourneyPolylines = {}; @@ -119,6 +120,7 @@ class _MaizeBusCoreState extends State { Marker? _searchLocationMarker; final Set _selectedRoutes = {}; List> _availableRoutes = []; + Map _stopIsRide = {}; // Custom marker icons BitmapDescriptor? _busIcon; @@ -134,7 +136,7 @@ class _MaizeBusCoreState extends State { // Memoization caches final Map _routePolylines = {}; - final Map> _routeStopMarkers = {}; + final Map> _routeStopMarkers = {}; // maps from route to a map of stopID to marker // Whether a journey search overlay is currently active (shows only journey path) bool _journeyOverlayActive = false; // maximum allowed distance (meters) from a stop to a candidate polyline point @@ -770,51 +772,59 @@ class _MaizeBusCoreState extends State { ); } if (!_routeStopMarkers.containsKey(routeKey)) { - _routeStopMarkers[routeKey] = r.stops - .map( - (stop) => Marker( - markerId: MarkerId( - 'stop_${stop.id}_${Object.hashAll(r.points)}', - ), - position: stop.location, - flat: true, - icon: _favoriteStops.contains(stop.id) - ? (stop.isRide - ? _favRideStopIcon ?? - BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - ) - : _favStopIcon ?? - BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - )) - : (stop.isRide - ? _rideStopIcon ?? - BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - ) - : _stopIcon ?? - BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - )), - consumeTapEvents: true, - onTap: () { - try { - Haptics.vibrate(HapticsType.light); - } catch (e) {} - - _showStopSheet( - stop.id, - stop.name, - stop.location.latitude, - stop.location.longitude, - ); - }, - rotation: stop.rotation, - anchor: Offset(0.5, 0.5), - ), - ) - .toSet(); + _routeStopMarkers[routeKey] = {}; + for (final stop in r.stops) { // iterate through all stops in this route + final isFavorite = _favoriteStops.contains(stop.id); + + final marker = Marker( + markerId: MarkerId( + 'stop_${stop.id}_${Object.hashAll(r.points)}', + ), + position: stop.location, + flat: true, + icon: isFavorite + ? (stop.isRide + ? _favRideStopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + ) + : _favStopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + )) + : (stop.isRide + ? _rideStopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + ) + : _stopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + )), + consumeTapEvents: true, + onTap: () { + try { + Haptics.vibrate(HapticsType.light); + } catch (e) {} + + _showStopSheet( + stop.id, + stop.name, + stop.location.latitude, + stop.location.longitude, + ); + }, + rotation: stop.rotation, + anchor: Offset(0.5, 0.5), + ); + _routeStopMarkers[routeKey]?[stop.id] = marker; + + // gets first marker of this stop and adds it to the favorited stop markers + if (isFavorite && !_displayedFavoriteStopMarkers.containsKey(stop.id)) { + _displayedFavoriteStopMarkers[stop.id] = marker; + } + _stopIsRide[stop.id] = stop.isRide; + } } } } @@ -850,45 +860,73 @@ class _MaizeBusCoreState extends State { // Update cached markers for a specific stop id to reflect favorite/unfavorite void _setStopFavorited(String stpid, bool favored) { // Update all routeStopMarkers entries that match this stop id + final isRide = _stopIsRide[stpid] ?? false; _routeStopMarkers.forEach((routeKey, markers) { - final updated = markers.map((m) { - if (m.markerId.value.startsWith('stop_${stpid}_')) { - return Marker( - flat: true, - markerId: m.markerId, - position: m.position, - icon: favored - ? (_favStopIcon ?? - _stopIcon ?? - BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - )) - : (_stopIcon ?? - BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - )), - consumeTapEvents: m.consumeTapEvents, - onTap: m.onTap, - rotation: m.rotation, - anchor: m.anchor, - ); - } - return m; - }).toSet(); - _routeStopMarkers[routeKey] = updated; + // if marker does not exist in this route, return + if (!markers.containsKey(stpid)) return; + + final m = markers[stpid]!; // get old marker + final newMarker = Marker( + flat: true, + markerId: m.markerId, + position: m.position, + icon: favored + ? (isRide + ? _favRideStopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + ) + : _favStopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + )) + : (isRide + ? _rideStopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + ) + : _stopIcon ?? + BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure, + )), + consumeTapEvents: m.consumeTapEvents, + onTap: m.onTap, + rotation: m.rotation, + anchor: m.anchor, + ); + + // gets first marker of this stop id and adds it to the favorited stop markers + if (favored && !_displayedFavoriteStopMarkers.containsKey(stpid)) { + _displayedFavoriteStopMarkers[stpid] = newMarker; + } + + markers[stpid] = newMarker; // set as new marker }); + // remove favorite stop marker if not favored + if (!favored) { + _displayedFavoriteStopMarkers.remove(stpid); + } + // If displayed, update displayed markers as well setState(() { // Rebuild displayed stop markers based on current selected routes - final selectedStopMarkers = {}; + final selectedStopMarkers = {}; for (final routeId in _selectedRoutes) { final routeVariants = _routePolylines.keys.where( (key) => key.startsWith('${routeId}_'), ); for (final routeKey in routeVariants) { final stops = _routeStopMarkers[routeKey]; - if (stops != null) selectedStopMarkers.addAll(stops); + if (stops == null) continue; + + // iterate through and add the stop markers + // if they are not already in the selected stop markesr + stops.forEach((key, value) { + if (!selectedStopMarkers.containsKey(key)) { + selectedStopMarkers[key] = value; + } + }); } } _displayedStopMarkers = selectedStopMarkers; @@ -898,7 +936,7 @@ class _MaizeBusCoreState extends State { void _updateDisplayedRoutes() { final selectedPolylines = {}; - final selectedStopMarkers = {}; + final selectedStopMarkers = {}; for (final routeId in _selectedRoutes) { // Find all variants of this route @@ -910,9 +948,13 @@ class _MaizeBusCoreState extends State { final polyline = _routePolylines[routeKey]; if (polyline != null) selectedPolylines.add(polyline); final stops = _routeStopMarkers[routeKey]; - if (stops != null) { - selectedStopMarkers.addAll(stops); - } + if (stops == null) continue; + + stops.forEach((key, value) { + if (!selectedStopMarkers.containsKey(key)) { + selectedStopMarkers[key] = value; + } + }); } } @@ -1009,7 +1051,8 @@ class _MaizeBusCoreState extends State { } void _updateAllDisplayedMarkers() { - _allDisplayedStopMarkers = _displayedStopMarkers + _allDisplayedStopMarkers = _displayedStopMarkers.values.toSet() + .union(_displayedFavoriteStopMarkers.values.toSet()) .union(_displayedBusMarkers) .union(_displayedJourneyMarkers) .union(_searchLocationMarker != null ? {_searchLocationMarker!} : {}); @@ -1078,6 +1121,8 @@ class _MaizeBusCoreState extends State { void _refreshCachedStopMarkers() { // Clear cached stop markers so they'll be recreated with the new icons _routeStopMarkers.clear(); + // also clear persistent favorited stop markers to be refreshed in _cacheRouteOverlays(..) + _displayedFavoriteStopMarkers.clear(); // Re-cache all route overlays with the new icons _cacheRouteOverlays( Provider.of(context, listen: false).routes, @@ -2128,7 +2173,8 @@ class _MaizeBusCoreState extends State { ? {_searchLocationMarker!} : {}, ) - : _displayedStopMarkers + : _displayedStopMarkers.values.toSet() + .union(_displayedFavoriteStopMarkers.values.toSet()) .union(_displayedJourneyMarkers) .union( _searchLocationMarker != null