using Xamarin.Forms; using ShareeBike.View; using ShareeBike.Model.Stations; using System; using System.Linq; using ShareeBike.Model.Bikes; using ShareeBike.Repository.Exception; using ShareeBike.Model; using Serilog; using System.Collections.Generic; using System.Threading.Tasks; using System.ComponentModel; using Xamarin.Forms.GoogleMaps; using System.Collections.ObjectModel; using ShareeBike.Settings; using ShareeBike.Model.Connector; using ShareeBike.Model.Services.CopriApi; using ShareeBike.Services.Permissions; using Xamarin.Essentials; using System.Threading; using ShareeBike.MultilingualResources; using ShareeBike.Repository; using ShareeBike.Services.Geolocation; using ShareeBike.Model.State; using ShareeBike.Model.Bikes.BikeInfoNS.BC; using ShareeBike.Model.Stations.StationNS; namespace ShareeBike.ViewModel.Map { public class MapPageViewModel : INotifyPropertyChanged { /// True if message was already shown to user. private static bool WasMerchantMessageAlreadyShown { get; set; } = false; /// Holds the count of custom icons centered. private const int CUSTOM_ICONS_COUNT = 30; /// Reference on view service to show modal notifications and to perform navigation. private IViewService ViewService { get; } /// /// Holds the exception which occurred getting bikes occupied information. /// private Exception m_oException; /// /// Service to query/ manage permissions (location) of the app. /// private ILocationPermission PermissionsService { get; } /// /// Service to manage bluetooth stack. /// private Plugin.BLE.Abstractions.Contracts.IBluetoothLE BluetoothService { get; set; } /// Notifies view about changes. public event PropertyChangedEventHandler PropertyChanged; /// Object to manage update of view model objects from Copri. private IPollingUpdateTaskManager m_oViewUpdateManager; /// Holds whether to poll or not and the period length is polling is on. private PollingParameters Polling { get; set; } /// Reference on the shareeBike app instance. private IShareeBikeApp ShareeBikeApp { get; } /// Delegate to perform navigation. private INavigation m_oNavigation; private ObservableCollection pins; public ObservableCollection Pins { get { if (pins == null) pins = new ObservableCollection(); // If view model is not binding context pins collection must be set from code. return pins; } set => pins = value; } /// Delegate to move map to region. private Action m_oMoveToRegionDelegate; /// False if user taped on station marker to show bikes at a given station. private bool isMapPageEnabled = false; IGeolocationService GeolocationService { get; } /// False if user taped on station marker to show bikes at a given station. public bool IsMapPageEnabled { get => isMapPageEnabled; private set { if (isMapPageEnabled == value) return; isMapPageEnabled = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMapPageEnabled))); } } /// Prevents an invalid instance to be created. /// Reference to shareeBike app model. /// Delegate to center map and set zoom level. /// View service to notify user. /// Interface to navigate. public MapPageViewModel( IShareeBikeApp shareeBikeApp, ILocationPermission permissionsService, Plugin.BLE.Abstractions.Contracts.IBluetoothLE bluetoothService, IGeolocationService geolocationService, Action moveToRegionDelegate, IViewService viewService, INavigation navigation) { ShareeBikeApp = shareeBikeApp ?? throw new ArgumentException("Can not instantiate map page view model- object. No shareeBike app object available."); PermissionsService = permissionsService ?? throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Permissions service object must never be null."); BluetoothService = bluetoothService ?? throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Bluetooth service object must never be null."); GeolocationService = geolocationService ?? throw new ArgumentException($"Can not instantiate {nameof(MapPageViewModel)}. Geolocation service object must never be null."); m_oMoveToRegionDelegate = moveToRegionDelegate ?? throw new ArgumentException("Can not instantiate map page view model- object. No move delegate available."); ViewService = viewService ?? throw new ArgumentException("Can not instantiate map page view model- object. No view available."); m_oNavigation = navigation ?? throw new ArgumentException("Can not instantiate map page view model- object. No navigation service available."); m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); Polling = PollingParameters.NoPolling; cargoCitybikeToggleViewModel = new EmptyToggleViewModel(); IsConnected = ShareeBikeApp.GetIsConnected(); } /// Sets the stations filter to apply (Citybike or ShareeBike). public IGroupFilterMapPage ActiveFilterMap { get => cargoCitybikeToggleViewModel.FilterDictionary ?? new GroupFilterMapPage(); set { cargoCitybikeToggleViewModel = new CargoCitybikeToggleViewModel(value); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CargoColor))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CitybikeColor))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NoCargoColor))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NoCitybikeColor))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsToggleVisible))); } } /// /// Counts the number of reserved or occupied bikes -> visualized in MyBikes-Icon /// public void GetMyBikesCount(BikeCollection bikes_occupied) { int MyBikesCount = bikes_occupied.Count; MyBikesCountText = MyBikesCount > 0 ? string.Format(MyBikesCount.ToString()) : string.Empty; } public Command PinClickedCommand => new Command( args => { OnStationClicked(args.Pin.Tag.ToString()); args.Handled = true; // Prevents map to be centered to selected pin. }); /// /// One time setup: Sets pins into map and connects to events. /// public void InitializePins(StationDictionary stations) { // Add pins to stations. Log.ForContext().Debug($"Request to draw {stations.Count} pins."); foreach (var station in stations) { if (station.Position == null) { // There should be no reason for a position object to be null but this already occurred in past. Log.ForContext().Error("Position object of station {@l_oStation} is null.", station); continue; } var pin = new Pin { Position = new Xamarin.Forms.GoogleMaps.Position(station.Position.Latitude, station.Position.Longitude), Label = long.TryParse(station.Id, out long stationId) && stationId > CUSTOM_ICONS_COUNT ? station.GetStationName() : string.Empty, // Stations with custom icons have already a id marker. No need for a label. Tag = station.Id, IsVisible = false, // Set to false to prevent showing default icons (flickering). }; Pins.Add(pin); } } /// Update all stations from ShareeBike. /// List of colors to apply. private void UpdatePinsColor(IList stationsColorList) { Log.ForContext().Debug($"Starting update of stations pins color for {stationsColorList.Count} stations..."); // Update colors of pins. for (int pinIndex = 0; pinIndex < stationsColorList.Count; pinIndex++) { var indexPartPrefix = int.TryParse(Pins[pinIndex].Tag.ToString(), out int stationId) && stationId <= CUSTOM_ICONS_COUNT ? $"{stationId}" // there is a station marker with index letter for given station id : "Open"; // there is no station marker. Use open marker. var colorPartPrefix = GetResourceNameColorPart(stationsColorList[pinIndex]); var name = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? ".png" : string.Empty)}"; try { Pins[pinIndex].Icon = BitmapDescriptorFactory.FromBundle(name); } catch (Exception excption) { Log.ForContext().Error("Station icon {name} can not be loaded. {@excption}.", name, excption); Pins[pinIndex].Label = stationId.ToString(); Pins[pinIndex].Icon = BitmapDescriptorFactory.DefaultMarker(stationsColorList[pinIndex]); } Pins[pinIndex].IsVisible = true; } var pinsCount = Pins.Count; for (int pinIndex = stationsColorList.Count; pinIndex < pinsCount; pinIndex++) { Log.ForContext().Error($"Unexpected count of pins detected. Expected {stationsColorList.Count} but is {pinsCount}."); Pins[pinIndex].IsVisible = false; } Log.ForContext().Debug("Update of stations pins color done."); } /// /// label for number of reserved/rented bikes; /// private string _myBikesCountText = string.Empty; public string MyBikesCountText { get { return _myBikesCountText; } set { _myBikesCountText = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyBikesCountText))); } } /// Gets the color related part of the resource name. /// Color to get name for. /// Resource name. private static string GetResourceNameColorPart(Color color) { if (color == Color.Blue) { return "Blue"; } if (color == Color.Green) { return "Green"; } if (color == Color.LightBlue) { return "LightBlue"; } if (color == Color.Red) { return "Red"; } return color.ToString(); } /// /// Invoked when page is shown. /// Starts update process. /// public async Task OnAppearing() { try { //Request Location Permission on iOS if (DeviceInfo.Platform == DevicePlatform.iOS) { var status = await PermissionsService.RequestAsync(); } IsProcessWithRunningProcessView = true; IsNavBarVisible = false; // Get and expose status of location permission GetLocationPermissionStatus(); // Process map page. Polling = ShareeBikeApp.Polling; Log.ForContext().Information( $"{(Polling != null && Polling.IsActivated ? $"Map page is appearing. Update period is {Polling.Periode.TotalSeconds} sec." : "Map page is appearing. Polling is off.")}" + $"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}."); // Update map page filter ActiveFilterMap = ShareeBikeApp.GroupFilterMapPage; // get stations from COPRI ActionText = AppResources.ActivityTextOneMomentPlease; IsConnected = ShareeBikeApp.GetIsConnected(); var resultStationsAndBikes = await ShareeBikeApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); ShareeBikeApp.Stations = resultStationsAndBikes.Response.StationsAll; ShareeBikeApp.ResourceUrls = resultStationsAndBikes.GeneralData.ResourceUrls; // Check if there is a message from COPRI ("merchant_message") to be shown to user. if (!string.IsNullOrEmpty(resultStationsAndBikes?.GeneralData?.MerchantMessage) && !WasMerchantMessageAlreadyShown) { // Context switch should not be required because code is called from GUI thread // but a xamarin forms-issue requires call (see issue #594). ShareeBikeApp.PostAction(async (x) => { // Show COPRI message once. await ViewService.DisplayAlert( AppResources.MessageInformationTitle, resultStationsAndBikes.GeneralData.MerchantMessage, AppResources.MessageAnswerOk); }, null); WasMerchantMessageAlreadyShown = true; } // Move and scale before setting stations and bikes which takes some time. ActionText = AppResources.ActivityTextCenterMap; // Get map display area Model.Map.IMapSpan mapSpan = null; if (ShareeBikeApp.CenterMapToCurrentLocation) { var status = await PermissionsService.CheckStatusAsync(); if (status == Status.Granted) { // Get from smart device mapSpan = await GetFromLocationService(status); } } if (mapSpan == null) { // Use map display are from COPRI mapSpan = resultStationsAndBikes.GeneralData.InitialMapSpan; } if (mapSpan.IsValid) { ShareeBikeApp.UserMapSpan = MapSpan.FromCenterAndRadius( new Xamarin.Forms.GoogleMaps.Position(mapSpan.Center.Latitude, mapSpan.Center.Longitude), new Distance(mapSpan.Radius * 1000)); ShareeBikeApp.Save(); MoveAndScale(m_oMoveToRegionDelegate, ShareeBikeApp.ActiveMapSpan); } ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes; await SetStationsOnMap(resultStationsAndBikes.Response.StationsAll); await HandleAuthCookieNotDefinedException(resultStationsAndBikes.Exception); // Update pin colors. Log.ForContext().Verbose("Starting update pins color..."); var colors = GetStationColors( Pins.Select(x => x.Tag.ToString()).ToList(), resultStationsAndBikes.Response.StationsAll, resultStationsAndBikes.Response.BikesOccupied); // Update pins color form count of bikes located at station. UpdatePinsColor(colors); Log.ForContext().Verbose("Update pins color done."); // Load MyBikes Count -> MyBikes Icon/Button GetMyBikesCount(resultStationsAndBikes.Response.BikesOccupied); m_oViewUpdateManager = CreateUpdateTask(); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartAsync(Polling); } catch (Exception) { // Exceptions are handled inside update task; } Exception = resultStationsAndBikes.Exception; ActionText = string.Empty; IsProcessWithRunningProcessView = false; IsNavBarVisible = true; IsMapPageEnabled = true; } catch (Exception l_oException) { Log.ForContext().Error($"An error occurred showing bike stations page.\r\n{l_oException.Message}"); IsProcessWithRunningProcessView = false; IsNavBarVisible = true; await ViewService.DisplayAlert( AppResources.ErrorPageNotLoadedTitle, $"{AppResources.ErrorPageNotLoaded}\r\n{l_oException.Message}", AppResources.MessageAnswerOk); IsMapPageEnabled = true; } } /// /// IsLocationPermissionGranted = true, if Location Permissions granted. /// private async void GetLocationPermissionStatus() { Log.ForContext().Verbose("Check Location permissions."); var status = await PermissionsService.CheckStatusAsync(); IsLocationPermissionGranted = status == Status.Granted ? true : false; Log.ForContext().Verbose("Location permissions: {0}.", status); } private bool isLocationPermissionGranted; /// /// Exposes IsLocationPermissionGranted. /// public bool IsLocationPermissionGranted { get => isLocationPermissionGranted; set { if (value == isLocationPermissionGranted) return; Log.ForContext().Debug($"Switch value of {nameof(isLocationPermissionGranted)} to {value}."); isLocationPermissionGranted = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLocationPermissionGranted))); } } /// /// Invoked when the auth cookie is not defined. /// private async Task HandleAuthCookieNotDefinedException(Exception exception) { if (exception?.GetType() == typeof(AuthcookieNotDefinedException)) { Log.ForContext().Error("Map page is shown (probable for the first time after startup of app) and COPRI auth cookie is not defined. {@l_oException}", exception); // COPRI reports an auth cookie error. await ViewService.DisplayAlert( AppResources.MessageWaring, AppResources.ErrorMapPageAuthcookieUndefined, AppResources.MessageAnswerOk); IsConnected = ShareeBikeApp.GetIsConnected(); await ShareeBikeApp.GetConnector(IsConnected).Command.DoLogout(); ShareeBikeApp.ActiveUser.Logout(); } } /// /// Sets the available stations on the map. /// private async Task SetStationsOnMap(StationDictionary stations) { if (Pins.Count > 0 && Pins.Count != stations.Count) { // Either // - user logged in/ logged out which might lead to more/ less stations being available // - new stations were added/ existing ones remove Pins.Clear(); } // Check if there are already any pins to the map // i.e detects first call of member OnAppearing after construction if (Pins.Count <= 0) { Log.ForContext().Debug($"{(ActiveFilterMap.GetGroup().Any() ? $"Active map filter is {string.Join(",", ActiveFilterMap.GetGroup())}." : "Map filter is off.")}"); // Map was not yet initialized. // Get stations from Copri Log.ForContext().Verbose("No pins detected on page."); if (stations.CopriVersion < CopriCallsStatic.UnsupportedVersionLower) { await ViewService.DisplayAlert( AppResources.MessageWaring, string.Format(AppResources.MessageCopriVersionIsOutdated, ShareeBikeApp.Flavor.GetDisplayName()), AppResources.MessageAnswerOk); Log.ForContext().Error($"Outdated version of app detected. Version expected is {stations.CopriVersion}."); } if (stations.CopriVersion >= CopriCallsStatic.UnsupportedVersionUpper) { await ViewService.DisplayAlert( AppResources.MessageWaring, string.Format(AppResources.MessageAppVersionIsOutdated, ShareeBikeApp.Flavor.GetDisplayName()), AppResources.MessageAnswerOk); Log.ForContext().Error($"Outdated version of app detected. Version expected is {stations.CopriVersion}."); } // Set pins to their positions on map. InitializePins(stations); Log.ForContext().Verbose("Update of pins done."); } } /// /// Moves the map to the current position of the user. /// If location permission hasn't been granted, the position is not adjusted. /// private async Task GetFromLocationService(Status status) { IGeolocation currentLocation = null; try { currentLocation = await GeolocationService.GetAsync(); } catch (Exception ex) { Log.ForContext().Error("Getting location failed. {Exception}", ex); } if (currentLocation == null) return null; return Model.Map.MapSpanFactory.Create( PositionFactory.Create(currentLocation.Latitude, currentLocation.Longitude), ShareeBikeApp.ActiveMapSpan.Radius.Kilometers); } /// /// Requests the location permission from the user. /// If the user declines, a dialog prompt is shown, telling the user to toggle the permission in the device settings. /// /// The permission status. private async Task RequestLocationPermission() { // Check location permission var status = await PermissionsService.CheckStatusAsync(); if (!GeolocationService.IsSimulation // && DeviceInfo.Platform == DevicePlatform.Android && status != Status.Granted) { var dialogResult = await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.ErrorMapCenterNoLocationPermissionOpenDialog, AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (dialogResult) { // User decided to give access to locations permissions. PermissionsService.OpenAppSettings(); ActionText = string.Empty; IsProcessWithRunningProcessView = false; IsNavBarVisible = true; IsMapPageEnabled = true; } } return status; } /// Moves map and scales visible region depending on active filter. public static void MoveAndScale( Action moveToRegionDelegate, MapSpan currentMapSpan = null) { if (currentMapSpan != null) { // Move to current location. moveToRegionDelegate(currentMapSpan); return; } } /// Creates a update task object. /// Object to use for synchronization. private PollingUpdateTaskManager CreateUpdateTask() { // Start task which periodically updates pins. return new PollingUpdateTaskManager( () => { try { Log.ForContext().Verbose("Entering update cycle."); Result resultStationsAndBikes; ShareeBikeApp.PostAction( unused => { ActionText = AppResources.ActivityTextUpdating; IsConnected = ShareeBikeApp.GetIsConnected(); }, null); resultStationsAndBikes = ShareeBikeApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync().Result; var exception = resultStationsAndBikes.Exception; if (exception != null) { Log.ForContext().Error("Getting bikes and stations in polling context failed with exception {Exception}.", exception); } // Get and expose status of location permission GetLocationPermissionStatus(); // Load MyBikes Count -> MyBikes Icon/Button GetMyBikesCount(resultStationsAndBikes.Response.BikesOccupied); // Check if there are already any pins to the map. // If no initialize pins. if (Pins.Count <= 0) { // Set pins to their positions on map. ShareeBikeApp.PostAction( unused => { InitializePins(resultStationsAndBikes.Response.StationsAll); }, null); } // Set/ update pins colors. var l_oColors = GetStationColors( Pins.Select(x => x.Tag.ToString()).ToList(), resultStationsAndBikes.Response.StationsAll, resultStationsAndBikes.Response.BikesOccupied); // Update pins color form count of bikes located at station. ShareeBikeApp.PostAction( unused => { UpdatePinsColor(l_oColors); ActionText = string.Empty; Exception = resultStationsAndBikes.Exception; }, null); Log.ForContext().Verbose("Leaving update cycle."); } catch (Exception exception) { Log.ForContext().Error("Getting stations and bikes from update task failed. {Exception}", exception); ShareeBikeApp.PostAction( unused => { Exception = exception; ActionText = string.Empty; }, null); Log.ForContext().Verbose("Leaving update cycle."); return; } }); } /// /// Invoked when pages is closed/ hidden. /// Stops update process. /// public async Task OnDisappearing() { Log.Information("Map page is disappearing..."); await m_oViewUpdateManager.StopAsync(); } /// User clicked on a bike. /// Id of station user clicked on. public async void OnStationClicked(string selectedStationId) { try { Log.ForContext().Information($"User taped station {selectedStationId}."); // Lock action to prevent multiple instances of "BikeAtStation" being opened. IsMapPageEnabled = false; ShareeBikeApp.SelectedStation = ShareeBikeApp.Stations.FirstOrDefault(x => x.Id == selectedStationId) ?? new Station(selectedStationId, new List(), null); // Station might not be in list StationDictinaly because this list is not updated in background task. { // Show page. await ViewService.PushAsync(ViewTypes.BikesAtStation); IsMapPageEnabled = true; ActionText = string.Empty; } } catch (Exception exception) { IsMapPageEnabled = true; ActionText = string.Empty; Log.ForContext().Error("Error occurred opening view \"Map Page\". {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorPageNotLoadedTitle, $"{AppResources.ErrorPageNotLoaded}\r\n {exception.Message}", AppResources.MessageAnswerOk); } } /// /// Gets the list of station color for all stations. /// /// Station id list to get color for. /// Station object dictionary to get count of available bike from for each station. /// Bike collection to get count of reserved/ rented bikes from for each station. /// internal static IList GetStationColors( IEnumerable stationsId, IEnumerable stations, IEnumerable bikesReserved) { if (stationsId == null) { Log.ForContext().Debug("No stations available to update color for."); return new List(); } if (stations == null) { Log.ForContext().Error("No stations info available to get count of bikes available to determine whether a pin is green or not."); } if (bikesReserved == null) { Log.ForContext().Error("No bikes info available to determine whether a pins is light blue or not."); } // Get state for each station. var colors = new List(); foreach (var stationId in stationsId) { // Get color of given station. if (bikesReserved?.Where(x => x.StationId == stationId).Count() > 0) { // There is at least one requested or booked bike colors.Add(Color.LightBlue); continue; } if (stations?.FirstOrDefault(x => x.Id == stationId)?.AvailableBikesCount > 0) { // There is at least one bike available colors.Add(Color.Green); continue; } colors.Add(Color.Red); } return colors; } /// /// Exception which occurred getting bike information. /// public Exception Exception { get { return m_oException; } private set { var statusInfoText = StatusInfoText; m_oException = value; if (statusInfoText == StatusInfoText) { // Nothing to do because value did not change. return; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Holds info about current action. private string actionText; /// Holds info about current action. private string ActionText { get => actionText; set { var statusInfoText = StatusInfoText; actionText = value; if (statusInfoText == StatusInfoText) { // Nothing to do because value did not change. return; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Used to block more than on copri requests at a given time. private bool isProcessWithRunningProcessView = false; /// /// True if any action can be performed (request and cancel request) /// public bool IsProcessWithRunningProcessView { get => isProcessWithRunningProcessView; set { if (value == isProcessWithRunningProcessView) return; Log.ForContext().Debug($"Switch value of {nameof(isProcessWithRunningProcessView)} to {value}."); isProcessWithRunningProcessView = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsProcessWithRunningProcessView))); } } private bool isNavBarVisible = true; public bool IsNavBarVisible { get => isNavBarVisible; set { if (value == isNavBarVisible) return; Log.ForContext().Debug($"Switch value of {nameof(isNavBarVisible)} to {value}."); isNavBarVisible = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNavBarVisible))); } } /// Holds information whether app is connected to web or not. private bool? isConnected = null; /// Exposes the is connected state. private bool IsConnected { get => isConnected ?? false; set { var statusInfoText = StatusInfoText; isConnected = value; if (statusInfoText == StatusInfoText) { // Nothing to do. return; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Holds the status information text. public string StatusInfoText { get { //if (Exception != null) //{ // // An error occurred getting data from copri. // return Exception.GetShortErrorInfoText(ShareeBikeApp.IsReportLevelVerbose); //} return ActionText ?? string.Empty; } } /// Processes request to view my bikes. public System.Windows.Input.ICommand OnMyBikesButtonClicked => new Xamarin.Forms.Command(async () => { try { Log.ForContext().Information($"User clicked on MyBikesButton."); // Lock action to prevent multiple instances of "BikeAtStation" being opened. IsMapPageEnabled = false; // Show page. await ViewService.PushAsync(ViewTypes.MyBikesPage); IsMapPageEnabled = true; ActionText = string.Empty; } catch (Exception exception) { IsMapPageEnabled = true; ActionText = string.Empty; Log.ForContext().Error("Error opening page \"Map Page\". {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorPageNotLoadedTitle, $"{AppResources.ErrorPageNotLoaded}\r\n{exception.Message}", AppResources.MessageAnswerOk); } }); /// Command object to bind login button to view model. public System.Windows.Input.ICommand OnToggleCargoToCitybike => new Xamarin.Forms.Command(async () => await ToggleCargoToCitybike()); /// Command object to bind login button to view model. public System.Windows.Input.ICommand OnToggleCitybikeToCargo => new Xamarin.Forms.Command(async () => await ToggleCitybikeToCargo()); /// Manages toggle functionality. private ICargoCitybikeToggleViewModel cargoCitybikeToggleViewModel; /// User request to toggle from Cargo to Citybike. public async Task ToggleCargoToCitybike() { if (cargoCitybikeToggleViewModel.CurrentFilter == FilterHelper.CITYBIKE) { // Citybike is already activated, nothing to do. return; } Log.ForContext().Information("User toggles to Citybike."); await ActivateFilter(FilterHelper.CARGOBIKE); } /// User request to toggle from Citybike to Cargo. public async Task ToggleCitybikeToCargo() { if (cargoCitybikeToggleViewModel.CurrentFilter == FilterHelper.CARGOBIKE) { // Citybike is already activated, nothing to do. return; } Log.ForContext().Information("User toggles to ShareeBike."); await ActivateFilter(FilterHelper.CITYBIKE); } /// User request to toggle from Cargo to Citybike. private async Task ActivateFilter(string selectedFilter) { try { IsMapPageEnabled = false; IsProcessWithRunningProcessView = true; IsNavBarVisible = false; Log.ForContext().Information($"Request to toggle to \"{selectedFilter}\"."); // Stop polling. ActionText = AppResources.ActivityTextOneMomentPlease; await m_oViewUpdateManager.StopAsync(); // Clear error info. Exception = null; // Toggle view cargoCitybikeToggleViewModel = new CargoCitybikeToggleViewModel(ActiveFilterMap).DoToggle(); ActiveFilterMap = cargoCitybikeToggleViewModel.FilterDictionary; ShareeBikeApp.GroupFilterMapPage = ActiveFilterMap; ShareeBikeApp.Save(); ShareeBikeApp.UpdateConnector(); Pins.Clear(); // Update stations ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes; IsConnected = ShareeBikeApp.GetIsConnected(); var resultStationsAndBikes = await ShareeBikeApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); // Set pins to their positions on map. InitializePins(resultStationsAndBikes.Response.StationsAll); Log.ForContext().Verbose("Update of pins on toggle done..."); // Update pin colors. Log.ForContext().Verbose("Starting update pins color on toggle..."); var l_oColors = GetStationColors( Pins.Select(x => x.Tag.ToString()).ToList(), resultStationsAndBikes.Response.StationsAll, resultStationsAndBikes.Response.BikesOccupied); // Update pins color form count of bikes located at station. UpdatePinsColor(l_oColors); Log.ForContext().Verbose("Update pins color done."); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartAsync(Polling); } catch (Exception) { // Exceptions are handled inside update task; } ActionText = string.Empty; IsProcessWithRunningProcessView = false; IsNavBarVisible = true; IsMapPageEnabled = true; Log.ForContext().Information($"Toggle to \"{selectedFilter}\" done."); } catch (Exception l_oException) { Log.ForContext().Error("An error occurred switching view Cargobike/ Citybike.{}"); ActionText = string.Empty; IsProcessWithRunningProcessView = false; IsNavBarVisible = true; await ViewService.DisplayAlert( AppResources.ErrorPageNotLoadedTitle, AppResources.ErrorMapPageSwitchBikeType, String.Format(AppResources.ErrorMapPageSwitchBikeType, l_oException.Message), AppResources.MessageAnswerOk); IsMapPageEnabled = true; } } public Color CargoColor => cargoCitybikeToggleViewModel.CargoColor; public Color CitybikeColor => cargoCitybikeToggleViewModel.CitybikeColor; public Color NoCargoColor => cargoCitybikeToggleViewModel.NoCargoColor; public Color NoCitybikeColor => cargoCitybikeToggleViewModel.NoCitybikeColor; public bool IsToggleVisible => cargoCitybikeToggleViewModel.IsToggleVisible; } }