mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-11-15 22:56:37 +01:00
1076 lines
34 KiB
C#
1076 lines
34 KiB
C#
using Xamarin.Forms;
|
|
using TINK.View;
|
|
using TINK.Model.Stations;
|
|
using System;
|
|
using System.Linq;
|
|
using TINK.Model.Bikes;
|
|
using TINK.Repository.Exception;
|
|
using TINK.Model;
|
|
using Serilog;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using System.ComponentModel;
|
|
using Xamarin.Forms.GoogleMaps;
|
|
using System.Collections.ObjectModel;
|
|
using TINK.Settings;
|
|
using TINK.Model.Connector;
|
|
using TINK.Model.Services.CopriApi;
|
|
using TINK.Services.Permissions;
|
|
using Xamarin.Essentials;
|
|
using System.Threading;
|
|
using TINK.MultilingualResources;
|
|
using TINK.Repository;
|
|
using TINK.Services.Geolocation;
|
|
using TINK.Model.State;
|
|
using TINK.Model.Bikes.BikeInfoNS.BC;
|
|
using TINK.Model.Stations.StationNS;
|
|
|
|
namespace TINK.ViewModel.Map
|
|
{
|
|
public class MapPageViewModel : INotifyPropertyChanged
|
|
{
|
|
/// <summary> True if message was already shown to user. </summary>
|
|
private static bool WasMerchantMessageAlreadyShown { get; set; } = false;
|
|
|
|
|
|
/// <summary> Holds the count of custom icons centered.</summary>
|
|
private const int CUSTOM_ICONS_COUNT = 30;
|
|
|
|
/// <summary> Reference on view service to show modal notifications and to perform navigation. </summary>
|
|
private IViewService ViewService { get; }
|
|
|
|
/// <summary>
|
|
/// Holds the exception which occurred getting bikes occupied information.
|
|
/// </summary>
|
|
private Exception m_oException;
|
|
|
|
/// <summary>
|
|
/// Service to query/ manage permissions (location) of the app.
|
|
/// </summary>
|
|
private ILocationPermission PermissionsService { get; }
|
|
|
|
/// <summary>
|
|
/// Service to manage bluetooth stack.
|
|
/// </summary>
|
|
private Plugin.BLE.Abstractions.Contracts.IBluetoothLE BluetoothService { get; set; }
|
|
|
|
/// <summary> Notifies view about changes. </summary>
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
|
|
/// <summary> Object to manage update of view model objects from Copri.</summary>
|
|
private IPollingUpdateTaskManager m_oViewUpdateManager;
|
|
|
|
/// <summary>Holds whether to poll or not and the period length is polling is on.</summary>
|
|
private PollingParameters Polling { get; set; }
|
|
|
|
/// <summary> Reference on the tink app instance. </summary>
|
|
private ITinkApp TinkApp { get; }
|
|
|
|
/// <summary>Delegate to perform navigation.</summary>
|
|
private INavigation m_oNavigation;
|
|
|
|
private ObservableCollection<Pin> pins;
|
|
|
|
public ObservableCollection<Pin> Pins
|
|
{
|
|
get
|
|
{
|
|
if (pins == null)
|
|
pins = new ObservableCollection<Pin>(); // If view model is not binding context pins collection must be set programmatically.
|
|
|
|
return pins;
|
|
}
|
|
|
|
set => pins = value;
|
|
}
|
|
|
|
/// <summary>Delegate to move map to region.</summary>
|
|
private Action<MapSpan> m_oMoveToRegionDelegate;
|
|
|
|
/// <summary> False if user tabed on station marker to show bikes at a given station.</summary>
|
|
private bool isMapPageEnabled = false;
|
|
|
|
IGeolocationService GeolocationService { get; }
|
|
|
|
/// <summary> False if user tabed on station marker to show bikes at a given station.</summary>
|
|
public bool IsMapPageEnabled
|
|
{
|
|
get => isMapPageEnabled;
|
|
private set
|
|
{
|
|
if (isMapPageEnabled == value)
|
|
return;
|
|
|
|
isMapPageEnabled = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMapPageEnabled)));
|
|
}
|
|
}
|
|
|
|
/// <summary> Prevents an invalid instane to be created. </summary>
|
|
/// <param name="tinkApp"> Reference to tink app model.</param>
|
|
/// <param name="moveToRegionDelegate">Delegate to center map and set zoom level.</param>
|
|
/// <param name="viewService">View service to notify user.</param>
|
|
/// <param name="navigation">Interface to navigate.</param>
|
|
public MapPageViewModel(
|
|
ITinkApp tinkApp,
|
|
ILocationPermission permissionsService,
|
|
Plugin.BLE.Abstractions.Contracts.IBluetoothLE bluetoothService,
|
|
IGeolocationService geolocationService,
|
|
Action<MapSpan> moveToRegionDelegate,
|
|
IViewService viewService,
|
|
INavigation navigation)
|
|
{
|
|
TinkApp = tinkApp
|
|
?? throw new ArgumentException("Can not instantiate map page view model- object. No tink 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;
|
|
|
|
tinkKonradToggleViewModel = new EmptyToggleViewModel();
|
|
|
|
IsConnected = TinkApp.GetIsConnected();
|
|
}
|
|
|
|
/// <summary>Sets the stations filter to to apply (Konrad or TINK). </summary>
|
|
public IGroupFilterMapPage ActiveFilterMap
|
|
{
|
|
get => tinkKonradToggleViewModel.FilterDictionary ?? new GroupFilterMapPage();
|
|
set
|
|
{
|
|
tinkKonradToggleViewModel = new TinkKonradToggleViewModel(value);
|
|
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TinkColor)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(KonradColor)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NoTinkColor)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NoKonradColor)));
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsToggleVisible)));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Counts the number of reserved or occupied bikes -> visualized in MyBikes-Icon
|
|
/// </summary>
|
|
public void GetMyBikesCount(BikeCollection bikesAll)
|
|
{
|
|
int MyBikesCount = 0;
|
|
Log.ForContext<MapPageViewModel>().Debug($"Number of reserved or rented bikes is extracted.");
|
|
if (bikesAll != null)
|
|
{
|
|
foreach (var bike in bikesAll)
|
|
{
|
|
if (bike.State.Value.IsOccupied())
|
|
{
|
|
MyBikesCount = MyBikesCount + 1;
|
|
continue;
|
|
}
|
|
|
|
}
|
|
}
|
|
MyBikesCountText = MyBikesCount > 0 ? string.Format(MyBikesCount.ToString()) : string.Empty;
|
|
}
|
|
|
|
public Command<PinClickedEventArgs> PinClickedCommand => new Command<PinClickedEventArgs>(
|
|
args =>
|
|
{
|
|
OnStationClicked(args.Pin.Tag.ToString());
|
|
args.Handled = true; // Prevents map to be centered to selected pin.
|
|
});
|
|
|
|
/// <summary>
|
|
/// One time setup: Sets pins into map and connects to events.
|
|
/// </summary>
|
|
public void InitializePins(StationDictionary stations)
|
|
{
|
|
// Add pins to stations.
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().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);
|
|
}
|
|
}
|
|
|
|
/// <summary> Update all stations from TINK. </summary>
|
|
/// <param name="stationsColorList">List of colors to apply.</param>
|
|
private void UpdatePinsColor(IList<Color> stationsColorList)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().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<MapPageViewModel>().Error($"Unexpected count of pins detected. Expected {stationsColorList.Count} but is {pinsCount}.");
|
|
Pins[pinIndex].IsVisible = false;
|
|
}
|
|
|
|
Log.ForContext<MapPageViewModel>().Debug("Update of stations pins color done.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// label for number of reserved/rented bikes;
|
|
/// </summary>
|
|
private string _myBikesCountText = string.Empty;
|
|
public string MyBikesCountText
|
|
{
|
|
get { return _myBikesCountText; }
|
|
set
|
|
{
|
|
_myBikesCountText = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyBikesCountText)));
|
|
}
|
|
}
|
|
|
|
/// <summary> Gets the color related part of the ressrouce name.</summary>
|
|
/// <param name="color">Color to get name for.</param>
|
|
/// <returns>Resource name.</returns>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when page is shown.
|
|
/// Starts update process.
|
|
/// </summary>
|
|
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 = TinkApp.Polling;
|
|
|
|
Log.ForContext<MapPageViewModel>().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 = TinkApp.GroupFilterMapPage;
|
|
|
|
ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes;
|
|
IsConnected = TinkApp.GetIsConnected();
|
|
var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync();
|
|
|
|
TinkApp.Stations = resultStationsAndBikes.Response.StationsAll;
|
|
TinkApp.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 xf-issue requires call (see issue #594).
|
|
TinkApp.PostAction(async (x) =>
|
|
{
|
|
// Show COPRI message once.
|
|
await ViewService.DisplayAlert(
|
|
AppResources.MessageInformationTitle,
|
|
resultStationsAndBikes.GeneralData.MerchantMessage,
|
|
AppResources.MessageAnswerOk);
|
|
}, null);
|
|
|
|
WasMerchantMessageAlreadyShown = true;
|
|
}
|
|
|
|
await SetStationsOnMap(resultStationsAndBikes.Response.StationsAll);
|
|
await HandleAuthCookieNotDefinedException(resultStationsAndBikes.Exception);
|
|
|
|
// Update pin colors.
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().Verbose("Update pins color done.");
|
|
|
|
// Load MyBikes Count -> MyBikes Icon/Button
|
|
GetMyBikesCount(resultStationsAndBikes.Response.BikesOccupied);
|
|
|
|
// Move and scale before getting stations and bikes which takes some time.
|
|
ActionText = AppResources.ActivityTextCenterMap;
|
|
|
|
// Get map display area
|
|
Model.Map.IMapSpan mapSpan = null;
|
|
if (TinkApp.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)
|
|
{
|
|
TinkApp.UserMapSpan = MapSpan.FromCenterAndRadius(
|
|
new Xamarin.Forms.GoogleMaps.Position(mapSpan.Center.Latitude, mapSpan.Center.Longitude),
|
|
new Distance(mapSpan.Radius * 1000));
|
|
TinkApp.Save();
|
|
|
|
MoveAndScale(m_oMoveToRegionDelegate, TinkApp.ActiveMapSpan);
|
|
}
|
|
|
|
m_oViewUpdateManager = CreateUpdateTask();
|
|
|
|
Log.ForContext<MapPageViewModel>().Verbose("Update pins color done.");
|
|
|
|
try
|
|
{
|
|
// Update bikes at station or my bikes depending on context.
|
|
await m_oViewUpdateManager.StartAsync(Polling);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Excpetions are handled insde update task;
|
|
}
|
|
|
|
Exception = resultStationsAndBikes.Exception;
|
|
ActionText = string.Empty;
|
|
IsProcessWithRunningProcessView = false;
|
|
IsNavBarVisible = true;
|
|
IsMapPageEnabled = true;
|
|
}
|
|
catch (Exception l_oException)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// IsLocationPermissionGranted = true, if Location Permissions granted.
|
|
/// </summary>
|
|
private async void GetLocationPermissionStatus()
|
|
{
|
|
Log.ForContext<MapPageViewModel>().Verbose("Check Location permissions.");
|
|
var status = await PermissionsService.CheckStatusAsync();
|
|
IsLocationPermissionGranted = status == Status.Granted ? true : false;
|
|
Log.ForContext<MapPageViewModel>().Verbose("Location permissions: {0}.", status);
|
|
}
|
|
|
|
private bool isLocationPermissionGranted = false;
|
|
/// <summary>
|
|
/// Exposes IsLocationPermissionGranted.
|
|
/// </summary>
|
|
public bool IsLocationPermissionGranted
|
|
{
|
|
get => isLocationPermissionGranted;
|
|
set
|
|
{
|
|
if (value == isLocationPermissionGranted)
|
|
return;
|
|
|
|
Log.ForContext<MapPageViewModel>().Debug($"Switch value of {nameof(isLocationPermissionGranted)} to {value}.");
|
|
isLocationPermissionGranted = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLocationPermissionGranted)));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the auth cookie is not defined.
|
|
/// </summary>
|
|
private async Task HandleAuthCookieNotDefinedException(Exception exception)
|
|
{
|
|
if (exception?.GetType() == typeof(AuthcookieNotDefinedException))
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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 = TinkApp.GetIsConnected();
|
|
await TinkApp.GetConnector(IsConnected).Command.DoLogout();
|
|
TinkApp.ActiveUser.Logout();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the available stations on the map.
|
|
/// </summary>
|
|
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<MapPageViewModel>().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<MapPageViewModel>().Verbose("No pins detected on page.");
|
|
if (stations.CopriVersion < CopriCallsStatic.UnsupportedVersionLower)
|
|
{
|
|
await ViewService.DisplayAlert(
|
|
AppResources.MessageWaring,
|
|
string.Format(AppResources.MessageCopriVersionIsOutdated, TinkApp.Flavor.GetDisplayName()),
|
|
AppResources.MessageAnswerOk);
|
|
|
|
Log.ForContext<MapPageViewModel>().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, TinkApp.Flavor.GetDisplayName()),
|
|
AppResources.MessageAnswerOk);
|
|
|
|
Log.ForContext<MapPageViewModel>().Error($"Outdated version of app detected. Version expected is {stations.CopriVersion}.");
|
|
}
|
|
|
|
// Set pins to their positions on map.
|
|
InitializePins(stations);
|
|
|
|
Log.ForContext<MapPageViewModel>().Verbose("Update of pins done.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the map to the current position of the user.
|
|
/// If location permission hasn't been granted, the position is not adjusted.
|
|
/// </summary>
|
|
private async Task<Model.Map.IMapSpan> GetFromLocationService(Status status)
|
|
{
|
|
IGeolocation currentLocation = null;
|
|
try
|
|
{
|
|
currentLocation = await GeolocationService.GetAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().Error("Getting location failed. {Exception}", ex);
|
|
}
|
|
if (currentLocation == null)
|
|
return null;
|
|
|
|
return Model.Map.MapSpanFactory.Create(
|
|
PositionFactory.Create(currentLocation.Latitude, currentLocation.Longitude),
|
|
TinkApp.ActiveMapSpan.Radius.Kilometers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Requests the location permission from the user.
|
|
/// If the user declines, a dialog prompot is shown, telling the user to toggle the permission in the device settings.
|
|
/// </summary>
|
|
/// <returns>The permission status.</returns>
|
|
private async Task<Status> 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;
|
|
}
|
|
|
|
/// <summary> Moves map and scales visible region depending on active filter. </summary>
|
|
public static void MoveAndScale(
|
|
Action<MapSpan> moveToRegionDelegate,
|
|
MapSpan currentMapSpan = null)
|
|
{
|
|
if (currentMapSpan != null)
|
|
{
|
|
// Move to current location.
|
|
moveToRegionDelegate(currentMapSpan);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary> Creates a update task object. </summary>
|
|
/// <param name="p_oSynchronizationContext">Object to use for synchronization.</param>
|
|
private PollingUpdateTaskManager CreateUpdateTask()
|
|
{
|
|
// Start task which periodically updates pins.
|
|
return new PollingUpdateTaskManager(
|
|
() =>
|
|
{
|
|
try
|
|
{
|
|
Log.ForContext<MapPageViewModel>().Verbose("Entering update cycle.");
|
|
Result<StationsAndBikesContainer> resultStationsAndBikes;
|
|
|
|
TinkApp.PostAction(
|
|
unused =>
|
|
{
|
|
ActionText = AppResources.ActivityTextUpdating;
|
|
IsConnected = TinkApp.GetIsConnected();
|
|
},
|
|
null);
|
|
|
|
resultStationsAndBikes = TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync().Result;
|
|
|
|
var exception = resultStationsAndBikes.Exception;
|
|
if (exception != null)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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.
|
|
TinkApp.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.
|
|
TinkApp.PostAction(
|
|
unused =>
|
|
{
|
|
UpdatePinsColor(l_oColors);
|
|
ActionText = string.Empty;
|
|
Exception = resultStationsAndBikes.Exception;
|
|
},
|
|
null);
|
|
|
|
Log.ForContext<MapPageViewModel>().Verbose("Leaving update cycle.");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().Error("Getting stations and bikes from update task failed. {Exception}", exception);
|
|
TinkApp.PostAction(
|
|
unused =>
|
|
{
|
|
Exception = exception;
|
|
ActionText = string.Empty;
|
|
},
|
|
null);
|
|
|
|
Log.ForContext<MapPageViewModel>().Verbose("Leaving update cycle.");
|
|
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when pages is closed/ hidden.
|
|
/// Stops update process.
|
|
/// </summary>
|
|
public async Task OnDisappearing()
|
|
{
|
|
Log.Information("Map page is disappearing...");
|
|
|
|
await m_oViewUpdateManager.StopAsync();
|
|
}
|
|
|
|
/// <summary> User clicked on a bike. </summary>
|
|
/// <param name="selectedStationId">Id of station user clicked on.</param>
|
|
public async void OnStationClicked(string selectedStationId)
|
|
{
|
|
try
|
|
{
|
|
Log.ForContext<MapPageViewModel>().Information($"User taped station {selectedStationId}.");
|
|
|
|
// Lock action to prevent multiple instances of "BikeAtStation" being opened.
|
|
IsMapPageEnabled = false;
|
|
|
|
TinkApp.SelectedStation = TinkApp.Stations.FirstOrDefault(x => x.Id == selectedStationId)
|
|
?? new Station(selectedStationId, new List<string>(), 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<MapPageViewModel>().Error("Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {Exception}", exception);
|
|
await ViewService.DisplayAlert(
|
|
AppResources.ErrorPageNotLoadedTitle,
|
|
$"{AppResources.ErrorPageNotLoaded}\r\n {exception.Message}",
|
|
AppResources.MessageAnswerOk);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the list of station color for all stations.
|
|
/// </summary>
|
|
/// <param name="stationsId">Station id list to get color for.</param>
|
|
/// <param name="stations">Station object dictionary to get count of available bike from for each station.</param>
|
|
/// <param name="bikesReserved">Bike collection to get count of reserved/ rented bikes from for each station.</param>
|
|
/// <returns></returns>
|
|
internal static IList<Color> GetStationColors(
|
|
IEnumerable<string> stationsId,
|
|
IEnumerable<IStation> stations,
|
|
IEnumerable<BikeInfo> bikesReserved)
|
|
{
|
|
if (stationsId == null)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().Debug("No stations available to update color for.");
|
|
return new List<Color>();
|
|
}
|
|
|
|
if (stations == null)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().Error("No bikes info available to determine whether a pins is light blue or not.");
|
|
}
|
|
|
|
// Get state for each station.
|
|
var colors = new List<Color>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exception which occurred getting bike information.
|
|
/// </summary>
|
|
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)));
|
|
}
|
|
}
|
|
|
|
/// <summary> Holds info about current action. </summary>
|
|
private string actionText;
|
|
|
|
/// <summary> Holds info about current action. </summary>
|
|
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)));
|
|
}
|
|
}
|
|
|
|
/// <summary> Used to block more than on copri requests at a given time.</summary>
|
|
private bool isProcessWithRunningProcessView = false;
|
|
|
|
/// <summary>
|
|
/// True if any action can be performed (request and cancel request)
|
|
/// </summary>
|
|
public bool IsProcessWithRunningProcessView
|
|
{
|
|
get => isProcessWithRunningProcessView;
|
|
set
|
|
{
|
|
if (value == isProcessWithRunningProcessView)
|
|
return;
|
|
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().Debug($"Switch value of {nameof(isNavBarVisible)} to {value}.");
|
|
isNavBarVisible = value;
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNavBarVisible)));
|
|
}
|
|
}
|
|
|
|
/// <summary> Holds information whether app is connected to web or not. </summary>
|
|
private bool? isConnected = null;
|
|
|
|
/// <summary>Exposes the is connected state. </summary>
|
|
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)));
|
|
}
|
|
}
|
|
|
|
/// <summary> Holds the status information text. </summary>
|
|
public string StatusInfoText
|
|
{
|
|
get
|
|
{
|
|
//if (Exception != null)
|
|
//{
|
|
// // An error occurred getting data from copri.
|
|
// return Exception.GetShortErrorInfoText(TinkApp.IsReportLevelVerbose);
|
|
//}
|
|
|
|
return ActionText ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary> Processes request to view my bikes.</summary>
|
|
public System.Windows.Input.ICommand OnMyBikesButtonClicked => new Xamarin.Forms.Command(async () =>
|
|
{
|
|
try
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().Error("Fehler beim Öffnen der Ansicht \"Meine Räder\" aufgetreten. {Exception}", exception);
|
|
await ViewService.DisplayAlert(
|
|
AppResources.ErrorPageNotLoadedTitle,
|
|
$"{AppResources.ErrorPageNotLoaded}\r\n{exception.Message}",
|
|
AppResources.MessageAnswerOk);
|
|
}
|
|
});
|
|
|
|
/// <summary> Command object to bind login button to view model.</summary>
|
|
public System.Windows.Input.ICommand OnToggleTinkToKonrad => new Xamarin.Forms.Command(async () => await ToggleTinkToKonrad());
|
|
|
|
/// <summary> Command object to bind login button to view model.</summary>
|
|
public System.Windows.Input.ICommand OnToggleKonradToTink => new Xamarin.Forms.Command(async () => await ToggleKonradToTink());
|
|
|
|
/// <summary> Manages toggle functionality. </summary>
|
|
private ITinkKonradToggleViewModel tinkKonradToggleViewModel;
|
|
|
|
/// <summary> User request to toggle from TINK to Konrad. </summary>
|
|
public async Task ToggleTinkToKonrad()
|
|
{
|
|
if (tinkKonradToggleViewModel.CurrentFilter == FilterHelper.CITYBIKE)
|
|
{
|
|
// Konrad is already activated, nothing to do.
|
|
return;
|
|
}
|
|
|
|
Log.ForContext<MapPageViewModel>().Information("User toggles to Konrad.");
|
|
await ActivateFilter(FilterHelper.CARGOBIKE);
|
|
}
|
|
|
|
/// <summary> User request to toggle from TINK to Konrad. </summary>
|
|
public async Task ToggleKonradToTink()
|
|
{
|
|
if (tinkKonradToggleViewModel.CurrentFilter == FilterHelper.CARGOBIKE)
|
|
{
|
|
// Konrad is already activated, nothing to do.
|
|
return;
|
|
}
|
|
|
|
Log.ForContext<MapPageViewModel>().Information("User toggles to TINK.");
|
|
|
|
await ActivateFilter(FilterHelper.CITYBIKE);
|
|
}
|
|
|
|
/// <summary> User request to toggle from TINK to Konrad. </summary>
|
|
private async Task ActivateFilter(string selectedFilter)
|
|
{
|
|
try
|
|
{
|
|
IsMapPageEnabled = false;
|
|
IsProcessWithRunningProcessView = true;
|
|
IsNavBarVisible = false;
|
|
|
|
Log.ForContext<MapPageViewModel>().Information($"Request to toggle to \"{selectedFilter}\".");
|
|
|
|
// Stop polling.
|
|
ActionText = AppResources.ActivityTextOneMomentPlease;
|
|
await m_oViewUpdateManager.StopAsync();
|
|
|
|
// Clear error info.
|
|
Exception = null;
|
|
|
|
// Toggle view
|
|
tinkKonradToggleViewModel = new TinkKonradToggleViewModel(ActiveFilterMap).DoToggle();
|
|
|
|
ActiveFilterMap = tinkKonradToggleViewModel.FilterDictionary;
|
|
TinkApp.GroupFilterMapPage = ActiveFilterMap;
|
|
TinkApp.Save();
|
|
|
|
TinkApp.UpdateConnector();
|
|
|
|
Pins.Clear();
|
|
|
|
// Update stations
|
|
ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes;
|
|
IsConnected = TinkApp.GetIsConnected();
|
|
var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync();
|
|
|
|
// Set pins to their positions on map.
|
|
InitializePins(resultStationsAndBikes.Response.StationsAll);
|
|
Log.ForContext<MapPageViewModel>().Verbose("Update of pins on toggle done...");
|
|
|
|
// Update pin colors.
|
|
Log.ForContext<MapPageViewModel>().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<MapPageViewModel>().Verbose("Update pins color done.");
|
|
|
|
try
|
|
{
|
|
// Update bikes at station or my bikes depending on context.
|
|
await m_oViewUpdateManager.StartAsync(Polling);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Excpetions are handled insde update task;
|
|
}
|
|
|
|
ActionText = string.Empty;
|
|
IsProcessWithRunningProcessView = false;
|
|
IsNavBarVisible = true;
|
|
IsMapPageEnabled = true;
|
|
Log.ForContext<MapPageViewModel>().Information($"Toggle to \"{selectedFilter}\" done.");
|
|
}
|
|
catch (Exception l_oException)
|
|
{
|
|
Log.ForContext<MapPageViewModel>().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 TinkColor => tinkKonradToggleViewModel.TinkColor;
|
|
|
|
public Color KonradColor => tinkKonradToggleViewModel.KonradColor;
|
|
|
|
public Color NoTinkColor => tinkKonradToggleViewModel.NoTinkColor;
|
|
|
|
public Color NoKonradColor => tinkKonradToggleViewModel.NoKonradColor;
|
|
|
|
public bool IsToggleVisible => tinkKonradToggleViewModel.IsToggleVisible;
|
|
}
|
|
}
|