2021-07-22 22:41:35 +02:00
using Xamarin.Forms ;
using TINK.View ;
using TINK.Model.Station ;
using System ;
using System.Linq ;
using TINK.Model.Bike ;
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 ;
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-22 22:41:35 +02:00
using TINK.View.MasterDetail ;
#endif
2021-11-07 19:42:59 +01:00
using TINK.Services.Permissions ;
2021-07-22 22:41:35 +02:00
using Xamarin.Essentials ;
using System.Threading ;
using TINK.MultilingualResources ;
using TINK.ViewModel.Info ;
using TINK.Repository ;
using TINK.Model.Services.Geolocation ;
namespace TINK.ViewModel.Contact
{
public class SelectStationPageViewModel : INotifyPropertyChanged
{
/// <summary> Holds the count of custom icons availalbe.</summary>
private const int CUSTOM_ICONS_COUNT = 30 ;
2021-08-01 17:24:15 +02:00
/// <summary> Reference on view service to show modal notifications and to perform navigation. </summary>
2021-07-22 22:41:35 +02:00
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>
2021-11-07 19:42:59 +01:00
private ILocationPermission PermissionsService { get ; }
2021-07-22 22:41:35 +02:00
/// <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> Reference on the tink app instance. </summary>
private ITinkApp TinkApp { get ; }
/// <summary>Delegate to perform navigation.</summary>
private INavigation m_oNavigation ;
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-22 22:41:35 +02:00
/// <summary>Delegate to perform navigation.</summary>
private INavigationMasterDetail m_oNavigationMasterDetail ;
#endif
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 ;
Model . Services . Geolocation . IGeolocation 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 SelectStationPageViewModel (
ITinkApp tinkApp ,
2021-11-07 19:42:59 +01:00
ILocationPermission permissionsService ,
2021-07-22 22:41:35 +02:00
Plugin . BLE . Abstractions . Contracts . IBluetoothLE bluetoothService ,
IGeolocation 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(SelectStationPageViewModel)}. Permissions service object must never be null." ) ;
BluetoothService = bluetoothService ? ?
throw new ArgumentException ( $"Can not instantiate {nameof(SelectStationPageViewModel)}. Bluetooth service object must never be null." ) ;
GeolocationService = geolocationService ? ?
throw new ArgumentException ( $"Can not instantiate {nameof(SelectStationPageViewModel)}. 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." ) ;
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-22 22:41:35 +02:00
m_oNavigationMasterDetail = new EmptyNavigationMasterDetail ( ) ;
#endif
IsConnected = TinkApp . GetIsConnected ( ) ;
}
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-22 22:41:35 +02:00
/// <summary> Delegate to perform navigation.</summary>
public INavigationMasterDetail NavigationMasterDetail
{
set { m_oNavigationMasterDetail = value ; }
}
#endif
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>
private void InitializePins ( StationDictionary stations )
{
// Add pins to stations.
Log . ForContext < SelectStationPageViewModel > ( ) . 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 alreay occurred in past.
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( "Postion object of station {@l_oStation} is null." , station ) ;
continue ;
}
var l_oPin = 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 ( l_oPin ) ;
}
}
/// <summary> Update all stations from TINK. </summary>
/// <param name="stationsColorList">List of colors to apply.</param>
private void UpdatePinsColor ( IList < Color > stationsColorList )
{
Log . ForContext < SelectStationPageViewModel > ( ) . 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 = GetRessourceNameColorPart ( stationsColorList [ pinIndex ] ) ;
var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? " . png " : string.Empty)}" ;
try
{
Pins [ pinIndex ] . Icon = BitmapDescriptorFactory . FromBundle ( l_iName ) ;
}
catch ( Exception l_oException )
{
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( "Station icon {l_strName} can not be loaded. {@l_oException}." , l_oException ) ;
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 < SelectStationPageViewModel > ( ) . Error ( $"Unexpected count of pins detected. Expected {stationsColorList.Count} but is {pinsCount}." ) ;
Pins [ pinIndex ] . IsVisible = false ;
}
Log . ForContext < SelectStationPageViewModel > ( ) . Debug ( "Update of stations pins color done." ) ;
}
/// <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 GetRessourceNameColorPart ( 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>
/// <param name="p_oFilterDictionaryMapPage">Holds map page filter settings.</param>
/// <param name="p_oPolling">Holds polling management object.</param>
/// <param name="p_bIsShowWhatsNewRequired">If true whats new page will be shown.</param>
public async Task OnAppearing ( )
{
try
{
IsRunning = true ;
// Process map page.
Log . ForContext < SelectStationPageViewModel > ( ) . Information (
$"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}." ) ;
if ( Pins . Count < = 0 )
{
2021-08-28 10:04:10 +02:00
ActionText = AppResources . ActivityTextRequestingLocationPermissions ;
2021-07-22 22:41:35 +02:00
// Check location permission
2021-11-07 19:42:59 +01:00
var status = await PermissionsService . CheckStatusAsync ( ) ;
2021-07-22 22:41:35 +02:00
if ( TinkApp . CenterMapToCurrentLocation
& & ! GeolocationService . IsSimulation
2021-11-07 19:42:59 +01:00
& & status ! = Status . Granted )
2021-07-22 22:41:35 +02:00
{
2021-11-07 19:42:59 +01:00
var permissionResult = await PermissionsService . RequestAsync ( ) ;
2021-07-22 22:41:35 +02:00
2021-11-07 19:42:59 +01:00
if ( permissionResult ! = Status . Granted )
2021-07-22 22:41:35 +02:00
{
var dialogResult = await ViewService . DisplayAlert (
AppResources . MessageTitleHint ,
AppResources . MessageCenterMapLocationPermissionOpenDialog ,
AppResources . MessageAnswerYes ,
AppResources . MessageAnswerNo ) ;
if ( dialogResult )
{
// User decided to give access to locations permissions.
PermissionsService . OpenAppSettings ( ) ;
ActionText = "" ;
IsRunning = false ;
IsMapPageEnabled = true ;
return ;
}
}
}
// Move and scale before getting stations and bikes which takes some time.
ActionText = AppResources . ActivityTextCenterMap ;
Location currentLocation = null ;
try
{
currentLocation = TinkApp . CenterMapToCurrentLocation
? await GeolocationService . GetAsync ( )
: null ;
}
catch ( Exception ex )
{
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( "Getting location failed. {Exception}" , ex ) ;
}
MoveAndScale ( m_oMoveToRegionDelegate , TinkApp . Uris . ActiveUri , currentLocation ) ;
}
ActionText = AppResources . ActivityTextMapLoadingStationsAndBikes ;
IsConnected = TinkApp . GetIsConnected ( ) ;
var resultStationsAndBikes = await TinkApp . GetConnector ( IsConnected ) . Query . GetBikesAndStationsAsync ( ) ;
TinkApp . Stations = resultStationsAndBikes . Response . StationsAll ;
if ( Pins . Count > 0 & & Pins . Count ! = resultStationsAndBikes . Response . StationsAll . Count )
{
// Either
// - user logged in/ logged out which might lead to more/ less stations beeing available
// - new stations were added/ existing ones remove
Pins . Clear ( ) ;
}
// Check if there are alreay any pins to the map
// i.e detecte first call of member OnAppearing after construction
if ( Pins . Count < = 0 )
{
// Map was not yet initialized.
// Get stations from Copri
Log . ForContext < SelectStationPageViewModel > ( ) . Verbose ( "No pins detected on page." ) ;
if ( resultStationsAndBikes . Response . StationsAll . CopriVersion < CopriCallsStatic . UnsupportedVersionLower )
{
await ViewService . DisplayAlert (
AppResources . MessageWaring ,
string . Format ( AppResources . MessageCopriVersionIsOutdated , ContactPageViewModel . GetAppName ( TinkApp . Uris . ActiveUri ) ) ,
AppResources . MessageAnswerOk ) ;
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( $"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}." ) ;
}
if ( resultStationsAndBikes . Response . StationsAll . CopriVersion > = CopriCallsStatic . UnsupportedVersionUpper )
{
await ViewService . DisplayAlert (
AppResources . MessageWaring ,
string . Format ( AppResources . MessageAppVersionIsOutdated , ContactPageViewModel . GetAppName ( TinkApp . Uris . ActiveUri ) ) ,
AppResources . MessageAnswerOk ) ;
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( $"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}." ) ;
}
// Set pins to their positions on map.
InitializePins ( resultStationsAndBikes . Response . StationsAll ) ;
Log . ForContext < SelectStationPageViewModel > ( ) . Verbose ( "Update of pins done." ) ;
}
if ( resultStationsAndBikes . Exception ? . GetType ( ) = = typeof ( AuthcookieNotDefinedException ) )
{
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( "Map page is shown (probable for the first time after startup of app) and COPRI auth cookie is not defined. {@l_oException}" , resultStationsAndBikes . Exception ) ;
// COPRI reports an auth cookie error.
await ViewService . DisplayAlert (
AppResources . MessageWaring ,
AppResources . MessageMapPageErrorAuthcookieUndefined ,
AppResources . MessageAnswerOk ) ;
await TinkApp . GetConnector ( IsConnected ) . Command . DoLogout ( ) ;
TinkApp . ActiveUser . Logout ( ) ;
}
// Update pin colors.
Log . ForContext < SelectStationPageViewModel > ( ) . Verbose ( "Starting update pins color..." ) ;
var colors = GetStationColors (
Pins . Select ( x = > x . Tag . ToString ( ) ) . ToList ( ) ,
resultStationsAndBikes . Response . Bikes ) ;
// Update pins color form count of bikes located at station.
UpdatePinsColor ( colors ) ;
Log . ForContext < SelectStationPageViewModel > ( ) . Verbose ( "Update pins color done." ) ;
Exception = resultStationsAndBikes . Exception ;
ActionText = "" ;
IsRunning = false ;
IsMapPageEnabled = true ;
}
catch ( Exception l_oException )
{
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( $"An error occurred switching view TINK/ Konrad.\r\n{l_oException.Message}" ) ;
IsRunning = false ;
await ViewService . DisplayAlert (
"Fehler" ,
$"Beim Anzeigen der Fahrradstandorte- Seite ist ein Fehler aufgetreten.\r\n{l_oException.Message}" ,
"OK" ) ;
IsMapPageEnabled = true ;
}
}
/// <summary> Moves map and scales visible region depending on active filter. </summary>
public static void MoveAndScale (
Action < MapSpan > moveToRegionDelegate ,
Uri activeUri ,
Location currentLocation = null )
{
if ( currentLocation ! = null )
{
// Move to current location.
moveToRegionDelegate ( MapSpan . FromCenterAndRadius (
new Xamarin . Forms . GoogleMaps . Position ( currentLocation . Latitude , currentLocation . Longitude ) ,
Distance . FromKilometers ( 1.0 ) ) ) ;
return ;
}
// Center map to Freiburg
moveToRegionDelegate ( MapSpan . FromCenterAndRadius (
new Xamarin . Forms . GoogleMaps . Position ( 47.995865 , 7.815086 ) ,
Distance . FromKilometers ( 2.9 ) ) ) ;
}
/// <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 < SelectStationPageViewModel > ( ) . 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 updatd in background task.
#if TRYNOTBACKSTYLE
m_oNavigation . ShowPage (
typeof ( BikesAtStationPage ) ,
p_strStationName ) ;
#else
// Show page.
ViewService . ShowPage ( ViewTypes . ContactPage , AppResources . MarkingContactPageTitle ) ;
IsMapPageEnabled = true ;
ActionText = "" ;
}
catch ( Exception exception )
{
IsMapPageEnabled = true ;
ActionText = "" ;
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( "Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {Exception}" , exception ) ;
await ViewService . DisplayAlert (
"Fehler" ,
$"Fehler beim Öffnen der Ansicht \" Fahrräder an Station \ " aufgetreten. {exception.Message}" ,
"OK" ) ;
}
#endif
}
/// <summary>
/// Gets the list of station color for all stations.
/// </summary>
/// <param name="stationsId">Station id list to get color for.</param>
/// <returns></returns>
private static IList < Color > GetStationColors (
IEnumerable < string > stationsId ,
BikeCollection bikesAll )
{
if ( stationsId = = null )
{
Log . ForContext < SelectStationPageViewModel > ( ) . Debug ( "No stations available to update color for." ) ;
return new List < Color > ( ) ;
}
if ( bikesAll = = null )
{
// If object is null an error occurred querrying bikes availalbe or bikes occpied which results in an unknown state.
Log . ForContext < SelectStationPageViewModel > ( ) . Error ( "No bikes available to determine pins color." ) ;
return new List < Color > ( stationsId . Select ( x = > Color . Blue ) ) ;
}
// Get state for each station.
var colors = new List < Color > ( ) ;
foreach ( var stationId in stationsId )
{
// Get color of given station.
var bikesAtStation = bikesAll . Where ( x = > x . CurrentStation = = stationId ) . ToList ( ) ;
if ( bikesAtStation . FirstOrDefault ( x = > x . State . Value ! = Model . State . InUseStateEnum . Disposable ) ! = null )
{
// There is at least one requested or booked bike
colors . Add ( Color . LightBlue ) ;
continue ;
}
if ( bikesAtStation . ToList ( ) . Count > 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 isRunning = false ;
/// <summary>
/// True if any action can be performed (request and cancel request)
/// </summary>
public bool IsRunning
{
get = > isRunning ;
set
{
if ( value = = isRunning )
return ;
Log . ForContext < SelectStationPageViewModel > ( ) . Debug ( $"Switch value of {nameof(isRunning)} to {value}." ) ;
isRunning = value ;
PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( IsRunning ) ) ) ;
}
}
/// <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 TinkApp . IsReportLevelVerbose
? Exception . GetShortErrorInfoText ( )
: AppResources . ActivityTextException ;
}
if ( ! IsConnected )
{
return AppResources . ActivityTextConnectionStateOffline ;
}
return ActionText ? ? string . Empty ;
}
}
}
}