2022-10-17 18:45:38 +02:00
using Xamarin.Forms ;
2021-05-13 20:03:07 +02:00
using TINK.View ;
2023-04-19 12:14:14 +02:00
using TINK.Model.Stations ;
2021-05-13 20:03:07 +02:00
using System ;
using System.Linq ;
2022-08-30 15:42:25 +02:00
using TINK.Model.Bikes ;
2021-06-26 20:57:55 +02:00
using TINK.Repository.Exception ;
2021-05-13 20:03:07 +02:00
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 ;
2021-11-07 19:42:59 +01:00
using TINK.Services.Permissions ;
2021-05-13 20:03:07 +02:00
using Xamarin.Essentials ;
using System.Threading ;
using TINK.MultilingualResources ;
2021-06-26 20:57:55 +02:00
using TINK.Repository ;
2022-04-10 17:38:34 +02:00
using TINK.Services.Geolocation ;
2022-08-30 15:42:25 +02:00
using TINK.Model.State ;
2023-05-09 08:47:52 +02:00
using TINK.Model.Bikes.BikeInfoNS.BC ;
using TINK.Model.Stations.StationNS ;
2022-12-07 16:54:52 +01:00
2021-05-13 20:03:07 +02:00
namespace TINK.ViewModel.Map
{
2022-09-06 16:08:19 +02:00
public class MapPageViewModel : INotifyPropertyChanged
{
/// <summary> True if message was already shown to user. </summary>
private static bool WasMerchantMessageAlreadyShown { get ; set ; } = false ;
2022-01-04 18:59:16 +01:00
2023-04-19 12:14:14 +02:00
/// <summary> Holds the count of custom icons centered.</summary>
2022-09-06 16:08:19 +02:00
private const int CUSTOM_ICONS_COUNT = 30 ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Reference on view service to show modal notifications and to perform navigation. </summary>
private IViewService ViewService { get ; }
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary>
/// Holds the exception which occurred getting bikes occupied information.
/// </summary>
private Exception m_oException ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary>
/// Service to query/ manage permissions (location) of the app.
/// </summary>
private ILocationPermission PermissionsService { get ; }
2021-06-26 20:57:55 +02:00
2022-09-06 16:08:19 +02:00
/// <summary>
/// Service to manage bluetooth stack.
/// </summary>
private Plugin . BLE . Abstractions . Contracts . IBluetoothLE BluetoothService { get ; set ; }
2021-06-26 20:57:55 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Notifies view about changes. </summary>
public event PropertyChangedEventHandler PropertyChanged ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Object to manage update of view model objects from Copri.</summary>
private IPollingUpdateTaskManager m_oViewUpdateManager ;
2021-05-13 20:03:07 +02:00
2023-04-19 12:14:14 +02:00
/// <summary>Holds whether to poll or not and the period length is polling is on.</summary>
2022-09-06 16:08:19 +02:00
private PollingParameters Polling { get ; set ; }
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Reference on the tink app instance. </summary>
private ITinkApp TinkApp { get ; }
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary>Delegate to perform navigation.</summary>
private INavigation m_oNavigation ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
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 ;
2023-04-05 15:02:10 +02:00
IGeolocationService GeolocationService { get ; }
2022-09-06 16:08:19 +02:00
/// <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 ,
2023-04-05 15:02:10 +02:00
IGeolocationService geolocationService ,
2022-09-06 16:08:19 +02:00
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 ( ) ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
Polling = PollingParameters . NoPolling ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
tinkKonradToggleViewModel = new EmptyToggleViewModel ( ) ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
IsConnected = TinkApp . GetIsConnected ( ) ;
}
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <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 ) ) ) ;
}
}
2021-05-13 20:03:07 +02:00
2023-04-05 15:02:10 +02:00
/// <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 ;
}
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
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>
2022-12-13 10:53:08 +01:00
public void InitializePins ( StationDictionary stations )
2022-09-06 16:08:19 +02:00
{
// Add pins to stations.
Log . ForContext < MapPageViewModel > ( ) . Debug ( $"Request to draw {stations.Count} pins." ) ;
foreach ( var station in stations )
{
if ( station . Position = = null )
{
2023-04-19 12:14:14 +02:00
// 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 ) ;
2022-09-06 16:08:19 +02:00
continue ;
}
2022-10-12 21:02:34 +02:00
var pin = new Pin
2022-09-06 16:08:19 +02:00
{
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).
} ;
2022-10-12 21:02:34 +02:00
Pins . Add ( pin ) ;
2022-09-06 16:08:19 +02:00
}
}
/// <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 + + )
{
2023-03-08 13:18:54 +01:00
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.
2022-09-06 16:08:19 +02:00
2023-05-09 08:47:52 +02:00
var colorPartPrefix = GetResourceNameColorPart ( stationsColorList [ pinIndex ] ) ;
2023-03-08 13:18:54 +01:00
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 ] ) ;
}
2022-09-06 16:08:19 +02:00
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." ) ;
}
2023-03-08 13:18:54 +01:00
/// <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 ) ) ) ;
}
}
2022-09-06 16:08:19 +02:00
/// <summary> Gets the color related part of the ressrouce name.</summary>
/// <param name="color">Color to get name for.</param>
/// <returns>Resource name.</returns>
2023-05-09 08:47:52 +02:00
private static string GetResourceNameColorPart ( Color color )
2022-09-06 16:08:19 +02:00
{
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
{
2022-12-07 16:54:52 +01:00
//Request Location Permission on iOS
2023-02-22 14:03:35 +01:00
if ( DeviceInfo . Platform = = DevicePlatform . iOS )
2022-12-07 16:54:52 +01:00
{
var status = await PermissionsService . RequestAsync ( ) ;
}
2023-01-18 14:22:51 +01:00
IsProcessWithRunningProcessView = true ;
2022-12-07 16:54:52 +01:00
IsNavBarVisible = false ;
2023-04-05 15:02:10 +02:00
// Get and expose status of location permission
GetLocationPermissionStatus ( ) ;
2022-09-06 16:08:19 +02:00
// Process map page.
Polling = TinkApp . Polling ;
Log . ForContext < MapPageViewModel > ( ) . Information (
2023-04-19 12:14:14 +02:00
$"{(Polling != null && Polling.IsActivated ? $" Map page is appearing . Update period is { Polling . Periode . TotalSeconds } sec . " : " Map page is appearing . Polling is off . ")}" +
2022-09-06 16:08:19 +02:00
$"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}." ) ;
2023-04-05 15:02:10 +02:00
// Update map page filter
2022-09-06 16:08:19 +02:00
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 ;
2023-01-18 14:22:51 +01:00
// Check if there is a message from COPRI ("merchant_message") to be shown to user.
2022-09-06 16:08:19 +02:00
if ( ! string . IsNullOrEmpty ( resultStationsAndBikes ? . GeneralData ? . MerchantMessage )
& & ! WasMerchantMessageAlreadyShown )
{
2023-02-22 14:03:35 +01:00
// Context switch should not be required because code is called from GUI thread
// but a xf-issue requires call (see issue #594).
2023-03-08 13:18:54 +01:00
TinkApp . PostAction ( async ( x ) = >
2023-02-22 14:03:35 +01:00
{
// Show COPRI message once.
await ViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . MessageInformationTitle ,
2023-02-22 14:03:35 +01:00
resultStationsAndBikes . GeneralData . MerchantMessage ,
AppResources . MessageAnswerOk ) ;
} , null ) ;
2022-09-06 16:08:19 +02:00
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 ( ) ,
2023-05-09 08:47:52 +02:00
resultStationsAndBikes . Response . StationsAll ,
resultStationsAndBikes . Response . BikesOccupied ) ;
2022-09-06 16:08:19 +02:00
// Update pins color form count of bikes located at station.
UpdatePinsColor ( colors ) ;
Log . ForContext < MapPageViewModel > ( ) . Verbose ( "Update pins color done." ) ;
2023-04-05 15:02:10 +02:00
// Load MyBikes Count -> MyBikes Icon/Button
2023-05-09 08:47:52 +02:00
GetMyBikesCount ( resultStationsAndBikes . Response . BikesOccupied ) ;
2023-04-05 15:02:10 +02:00
2022-09-06 16:08:19 +02:00
// Move and scale before getting stations and bikes which takes some time.
ActionText = AppResources . ActivityTextCenterMap ;
// Get map display area
Model . Map . IMapSpan mapSpan = null ;
2022-12-07 16:54:52 +01:00
if ( TinkApp . CenterMapToCurrentLocation )
2022-09-06 16:08:19 +02:00
{
2022-12-07 16:54:52 +01:00
var status = await PermissionsService . CheckStatusAsync ( ) ;
if ( status = = Status . Granted )
2023-03-08 13:18:54 +01:00
{
// Get from smart device
mapSpan = await GetFromLocationService ( status ) ;
2022-12-07 16:54:52 +01:00
}
2023-03-08 13:18:54 +01:00
}
2022-09-06 16:08:19 +02:00
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.
2023-08-31 12:20:06 +02:00
await m_oViewUpdateManager . StartAsync ( Polling ) ;
2022-09-06 16:08:19 +02:00
}
catch ( Exception )
{
// Excpetions are handled insde update task;
}
Exception = resultStationsAndBikes . Exception ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2023-01-18 14:22:51 +01:00
IsProcessWithRunningProcessView = false ;
2022-12-07 16:54:52 +01:00
IsNavBarVisible = true ;
2022-09-06 16:08:19 +02:00
IsMapPageEnabled = true ;
}
catch ( Exception l_oException )
{
Log . ForContext < MapPageViewModel > ( ) . Error ( $"An error occurred showing bike stations page.\r\n{l_oException.Message}" ) ;
2023-01-18 14:22:51 +01:00
IsProcessWithRunningProcessView = false ;
2022-12-07 16:54:52 +01:00
IsNavBarVisible = true ;
2022-09-06 16:08:19 +02:00
await ViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . ErrorPageNotLoadedTitle ,
$"{AppResources.ErrorPageNotLoaded}\r\n{l_oException.Message}" ,
AppResources . MessageAnswerOk ) ;
2022-09-06 16:08:19 +02:00
IsMapPageEnabled = true ;
}
}
2023-04-05 15:02:10 +02:00
/// <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 ) ;
}
2023-06-06 12:00:24 +02:00
private bool isLocationPermissionGranted = false ;
2023-04-05 15:02:10 +02:00
/// <summary>
/// Exposes IsLocationPermissionGranted.
/// </summary>
2023-06-06 12:00:24 +02:00
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 ) ) ) ;
}
}
2023-04-05 15:02:10 +02:00
2022-09-06 16:08:19 +02:00
/// <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 ,
2023-08-31 12:20:06 +02:00
AppResources . ErrorMapPageAuthcookieUndefined ,
2022-09-06 16:08:19 +02:00
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 )
{
2023-03-08 13:18:54 +01:00
// Either
2023-04-19 12:14:14 +02:00
// - user logged in/ logged out which might lead to more/ less stations being available
2023-03-08 13:18:54 +01:00
// - new stations were added/ existing ones remove
Pins . Clear ( ) ;
2022-09-06 16:08:19 +02:00
}
2023-04-19 12:14:14 +02:00
// Check if there are already any pins to the map
// i.e detects first call of member OnAppearing after construction
2022-09-06 16:08:19 +02:00
if ( Pins . Count < = 0 )
2023-03-08 13:18:54 +01:00
{
2022-09-06 16:08:19 +02:00
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 )
{
2023-04-05 15:02:10 +02:00
IGeolocation currentLocation = null ;
2022-09-06 16:08:19 +02:00
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 ( ) ;
2022-12-07 16:54:52 +01:00
if ( ! GeolocationService . IsSimulation
// && DeviceInfo.Platform == DevicePlatform.Android
2022-09-06 16:08:19 +02:00
& & status ! = Status . Granted )
{
2022-12-07 16:54:52 +01:00
var dialogResult = await ViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . MessageHintTitle ,
AppResources . ErrorMapCenterNoLocationPermissionOpenDialog ,
2022-12-07 16:54:52 +01:00
AppResources . MessageAnswerYes ,
AppResources . MessageAnswerNo ) ;
2022-09-06 16:08:19 +02:00
2023-03-08 13:18:54 +01:00
if ( dialogResult )
{
// User decided to give access to locations permissions.
PermissionsService . OpenAppSettings ( ) ;
ActionText = string . Empty ;
IsProcessWithRunningProcessView = false ;
IsNavBarVisible = true ;
IsMapPageEnabled = true ;
}
2022-09-06 16:08:19 +02:00
}
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 (
2022-12-07 16:54:52 +01:00
( ) = >
2022-09-06 16:08:19 +02:00
{
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 ) ;
}
2023-06-06 12:00:24 +02:00
// Get and expose status of location permission
GetLocationPermissionStatus ( ) ;
2023-04-05 15:02:10 +02:00
// Load MyBikes Count -> MyBikes Icon/Button
2023-05-09 08:47:52 +02:00
GetMyBikesCount ( resultStationsAndBikes . Response . BikesOccupied ) ;
2023-04-05 15:02:10 +02:00
2023-04-19 12:14:14 +02:00
// Check if there are already any pins to the map.
// If no initialize pins.
2022-09-06 16:08:19 +02:00
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 ( ) ,
2023-05-09 08:47:52 +02:00
resultStationsAndBikes . Response . StationsAll ,
resultStationsAndBikes . Response . BikesOccupied ) ;
2022-09-06 16:08:19 +02:00
// 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." ) ;
2022-12-07 16:54:52 +01:00
2022-09-06 16:08:19 +02:00
return ;
}
} ) ;
}
/// <summary>
/// Invoked when pages is closed/ hidden.
/// Stops update process.
/// </summary>
public async Task OnDisappearing ( )
{
Log . Information ( "Map page is disappearing..." ) ;
2023-08-31 12:20:06 +02:00
await m_oViewUpdateManager . StopAsync ( ) ;
2022-09-06 16:08:19 +02:00
}
/// <summary> User clicked on a bike. </summary>
/// <param name="selectedStationId">Id of station user clicked on.</param>
public async void OnStationClicked ( string selectedStationId )
{
2023-03-08 13:18:54 +01:00
try
{
Log . ForContext < MapPageViewModel > ( ) . Information ( $"User taped station {selectedStationId}." ) ;
2022-09-06 16:08:19 +02:00
2023-03-08 13:18:54 +01:00
// Lock action to prevent multiple instances of "BikeAtStation" being opened.
IsMapPageEnabled = false ;
2022-09-06 16:08:19 +02:00
2023-03-08 13:18:54 +01:00
TinkApp . SelectedStation = TinkApp . Stations . FirstOrDefault ( x = > x . Id = = selectedStationId )
2023-11-06 12:23:09 +01:00
? ? new Station ( selectedStationId , new List < string > ( ) , null ) ; // Station might not be in list StationDictinaly because this list is not updated in background task.
2021-05-13 20:03:07 +02:00
2022-11-17 10:05:05 +01:00
{
2023-03-08 13:18:54 +01:00
// Show page.
await ViewService . PushAsync ( ViewTypes . BikesAtStation ) ;
2022-11-17 10:05:05 +01:00
IsMapPageEnabled = true ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2022-11-17 10:05:05 +01:00
}
2023-03-08 13:18:54 +01:00
}
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 (
2023-08-31 12:20:06 +02:00
AppResources . ErrorPageNotLoadedTitle ,
$"{AppResources.ErrorPageNotLoaded}\r\n {exception.Message}" ,
AppResources . MessageAnswerOk ) ;
2023-03-08 13:18:54 +01:00
}
2022-09-06 16:08:19 +02:00
}
/// <summary>
/// Gets the list of station color for all stations.
/// </summary>
/// <param name="stationsId">Station id list to get color for.</param>
2023-05-09 08:47:52 +02:00
/// <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>
2022-09-06 16:08:19 +02:00
/// <returns></returns>
2023-05-09 08:47:52 +02:00
internal static IList < Color > GetStationColors (
2022-09-06 16:08:19 +02:00
IEnumerable < string > stationsId ,
2023-05-09 08:47:52 +02:00
IEnumerable < IStation > stations ,
IEnumerable < BikeInfo > bikesReserved )
2022-09-06 16:08:19 +02:00
{
if ( stationsId = = null )
{
Log . ForContext < MapPageViewModel > ( ) . Debug ( "No stations available to update color for." ) ;
return new List < Color > ( ) ;
}
2023-05-09 08:47:52 +02:00
if ( stations = = null )
2022-09-06 16:08:19 +02:00
{
2023-05-09 08:47:52 +02:00
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." ) ;
2022-09-06 16:08:19 +02:00
}
// Get state for each station.
var colors = new List < Color > ( ) ;
foreach ( var stationId in stationsId )
{
2022-12-13 10:53:08 +01:00
// Get color of given station.
2023-05-09 08:47:52 +02:00
if ( bikesReserved ? . Where ( x = > x . StationId = = stationId ) . Count ( ) > 0 )
2022-09-06 16:08:19 +02:00
{
2022-12-13 10:53:08 +01:00
// There is at least one requested or booked bike
colors . Add ( Color . LightBlue ) ;
continue ;
2022-11-17 10:05:05 +01:00
}
2022-12-13 10:53:08 +01:00
2023-05-09 08:47:52 +02:00
if ( stations ? . FirstOrDefault ( x = > x . Id = = stationId ) ? . AvailableBikesCount > 0 )
2022-09-06 16:08:19 +02:00
{
2022-12-13 10:53:08 +01:00
// There is at least one bike available
colors . Add ( Color . Green ) ;
continue ;
2022-09-06 16:08:19 +02:00
}
2022-12-13 10:53:08 +01:00
colors . Add ( Color . Red ) ;
2022-09-06 16:08:19 +02:00
}
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>
2023-01-18 14:22:51 +01:00
private bool isProcessWithRunningProcessView = false ;
2022-09-06 16:08:19 +02:00
/// <summary>
/// True if any action can be performed (request and cancel request)
/// </summary>
2023-01-18 14:22:51 +01:00
public bool IsProcessWithRunningProcessView
2022-09-06 16:08:19 +02:00
{
2023-01-18 14:22:51 +01:00
get = > isProcessWithRunningProcessView ;
2022-09-06 16:08:19 +02:00
set
{
2023-01-18 14:22:51 +01:00
if ( value = = isProcessWithRunningProcessView )
2022-09-06 16:08:19 +02:00
return ;
2023-01-18 14:22:51 +01:00
Log . ForContext < MapPageViewModel > ( ) . Debug ( $"Switch value of {nameof(isProcessWithRunningProcessView)} to {value}." ) ;
isProcessWithRunningProcessView = value ;
PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( IsProcessWithRunningProcessView ) ) ) ;
2022-09-06 16:08:19 +02:00
}
}
2022-12-07 16:54:52 +01:00
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 ) ) ) ;
}
}
2022-09-06 16:08:19 +02:00
/// <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
{
2023-08-31 12:20:06 +02:00
//if (Exception != null)
//{
// // An error occurred getting data from copri.
// return Exception.GetShortErrorInfoText(TinkApp.IsReportLevelVerbose);
//}
2022-09-06 16:08:19 +02:00
return ActionText ? ? string . Empty ;
}
}
2023-03-08 13:18:54 +01:00
/// <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 (
2023-08-31 12:20:06 +02:00
AppResources . ErrorPageNotLoadedTitle ,
$"{AppResources.ErrorPageNotLoaded}\r\n{exception.Message}" ,
AppResources . MessageAnswerOk ) ;
2023-03-08 13:18:54 +01:00
}
} ) ;
2022-09-06 16:08:19 +02:00
/// <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 ;
2023-01-18 14:22:51 +01:00
IsProcessWithRunningProcessView = true ;
2022-12-07 16:54:52 +01:00
IsNavBarVisible = false ;
2022-09-06 16:08:19 +02:00
Log . ForContext < MapPageViewModel > ( ) . Information ( $"Request to toggle to \" { selectedFilter } \ "." ) ;
// Stop polling.
ActionText = AppResources . ActivityTextOneMomentPlease ;
2023-08-31 12:20:06 +02:00
await m_oViewUpdateManager . StopAsync ( ) ;
2022-09-06 16:08:19 +02:00
// 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 ( ) ,
2023-05-09 08:47:52 +02:00
resultStationsAndBikes . Response . StationsAll ,
resultStationsAndBikes . Response . BikesOccupied ) ;
2022-09-06 16:08:19 +02:00
// 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.
2023-08-31 12:20:06 +02:00
await m_oViewUpdateManager . StartAsync ( Polling ) ;
2022-09-06 16:08:19 +02:00
}
catch ( Exception )
{
// Excpetions are handled insde update task;
}
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2023-01-18 14:22:51 +01:00
IsProcessWithRunningProcessView = false ;
2022-12-07 16:54:52 +01:00
IsNavBarVisible = true ;
2022-09-06 16:08:19 +02:00
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.{}" ) ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2023-01-18 14:22:51 +01:00
IsProcessWithRunningProcessView = false ;
2022-12-07 16:54:52 +01:00
IsNavBarVisible = true ;
2022-09-06 16:08:19 +02:00
await ViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . ErrorPageNotLoadedTitle ,
AppResources . ErrorMapPageSwitchBikeType ,
String . Format ( AppResources . ErrorMapPageSwitchBikeType , l_oException . Message ) ,
2022-09-06 16:08:19 +02:00
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 ;
}
2022-10-17 18:45:38 +02:00
}