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;
}
}