2021-05-13 20:03:07 +02:00
using Xamarin.Forms ;
using TINK.View ;
using TINK.Model.Station ;
using System ;
using System.Linq ;
using TINK.Model.Bike ;
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 ;
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-05-13 20:03:07 +02:00
using TINK.View.MasterDetail ;
2021-06-26 20:57:55 +02:00
#endif
2021-05-13 20:03:07 +02:00
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 ;
using TINK.Services.BluetoothLock ;
using TINK.ViewModel.Info ;
2021-06-26 20:57:55 +02:00
using TINK.Repository ;
using TINK.Model.Services.Geolocation ;
2021-05-13 20:03:07 +02:00
#if ! TRYNOTBACKSTYLE
#endif
namespace TINK.ViewModel.Map
{
public class MapPageViewModel : INotifyPropertyChanged
{
2021-07-22 22:41:35 +02:00
/// <summary> Holds the count of custom icons availalbe.</summary>
2021-05-13 20:03:07 +02:00
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 ; }
2021-05-13 20:03:07 +02:00
/// <summary>
/// Holds the exception which occurred getting bikes occupied information.
/// </summary>
private Exception m_oException ;
2021-06-26 20:57:55 +02:00
/// <summary>
/// Service to query/ manage permissions (location) of the app.
/// </summary>
2021-11-07 19:42:59 +01:00
private ILocationPermission PermissionsService { get ; }
2021-06-26 20:57:55 +02:00
/// <summary>
/// Service to manage bluetooth stack.
/// </summary>
private Plugin . BLE . Abstractions . Contracts . IBluetoothLE BluetoothService { get ; set ; }
2021-05-13 20:03:07 +02:00
/// <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 periode leght 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 ;
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-05-13 20:03:07 +02:00
/// <summary>Delegate to perform navigation.</summary>
private INavigationMasterDetail m_oNavigationMasterDetail ;
2021-06-26 20:57:55 +02:00
#endif
2021-05-13 20:03:07 +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 ;
2021-06-26 20:57:55 +02:00
Model . Services . Geolocation . IGeolocation GeolocationService { get ; }
2021-05-13 20:03:07 +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>
2021-06-26 20:57:55 +02:00
/// <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>
2021-05-13 20:03:07 +02:00
public MapPageViewModel (
ITinkApp tinkApp ,
2021-11-07 19:42:59 +01:00
ILocationPermission permissionsService ,
2021-06-26 20:57:55 +02:00
Plugin . BLE . Abstractions . Contracts . IBluetoothLE bluetoothService ,
IGeolocation geolocationService ,
Action < MapSpan > moveToRegionDelegate ,
IViewService viewService ,
INavigation navigation )
2021-05-13 20:03:07 +02:00
{
TinkApp = tinkApp
? ? throw new ArgumentException ( "Can not instantiate map page view model- object. No tink app object available." ) ;
2021-06-26 20:57:55 +02:00
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
2021-05-13 20:03:07 +02:00
? ? throw new ArgumentException ( "Can not instantiate map page view model- object. No move delegate available." ) ;
2021-07-22 22:41:35 +02:00
ViewService = viewService
2021-05-13 20:03:07 +02:00
? ? throw new ArgumentException ( "Can not instantiate map page view model- object. No view available." ) ;
2021-06-26 20:57:55 +02:00
m_oNavigation = navigation
2021-05-13 20:03:07 +02:00
? ? throw new ArgumentException ( "Can not instantiate map page view model- object. No navigation service available." ) ;
m_oViewUpdateManager = new IdlePollingUpdateTaskManager ( ) ;
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-05-13 20:03:07 +02:00
m_oNavigationMasterDetail = new EmptyNavigationMasterDetail ( ) ;
2021-06-26 20:57:55 +02:00
#endif
2021-05-13 20:03:07 +02:00
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 ( IsToggleVisible ) ) ) ;
}
}
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-05-13 20:03:07 +02:00
/// <summary> Delegate to perform navigation.</summary>
public INavigationMasterDetail NavigationMasterDetail
{
set { m_oNavigationMasterDetail = value ; }
}
2021-06-26 20:57:55 +02:00
#endif
2021-05-13 20:03:07 +02:00
public Command < PinClickedEventArgs > PinClickedCommand = > new Command < PinClickedEventArgs > (
args = >
{
2021-06-26 20:57:55 +02:00
OnStationClicked ( args . Pin . Tag . ToString ( ) ) ;
2021-05-13 20:03:07 +02:00
args . Handled = true ; // Prevents map to be centered to selected pin.
} ) ;
/// <summary>
/// One time setup: Sets pins into map and connects to events.
/// </summary>
2021-06-26 20:57:55 +02:00
private void InitializePins ( StationDictionary stations )
2021-05-13 20:03:07 +02:00
{
// Add pins to stations.
2021-06-26 20:57:55 +02:00
Log . ForContext < MapPageViewModel > ( ) . Debug ( $"Request to draw {stations.Count} pins." ) ;
foreach ( var station in stations )
2021-05-13 20:03:07 +02:00
{
2021-06-26 20:57:55 +02:00
if ( station . Position = = null )
2021-05-13 20:03:07 +02:00
{
// There should be no reason for a position object to be null but this alreay occurred in past.
2021-06-26 20:57:55 +02:00
Log . ForContext < MapPageViewModel > ( ) . Error ( "Postion object of station {@l_oStation} is null." , station ) ;
2021-05-13 20:03:07 +02:00
continue ;
}
var l_oPin = new Pin
{
2021-11-15 10:06:55 +01:00
2021-06-26 20:57:55 +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 ( )
2021-05-13 20:03:07 +02:00
: string . Empty , // Stations with custom icons have already a id marker. No need for a label.
2021-06-26 20:57:55 +02:00
Tag = station . Id ,
2021-05-13 20:03:07 +02:00
IsVisible = false , // Set to false to prevent showing default icons (flickering).
} ;
Pins . Add ( l_oPin ) ;
}
}
/// <summary> Update all stations from TINK. </summary>
2021-06-26 20:57:55 +02:00
/// <param name="stationsColorList">List of colors to apply.</param>
private void UpdatePinsColor ( IList < Color > stationsColorList )
2021-05-13 20:03:07 +02:00
{
2021-06-26 20:57:55 +02:00
Log . ForContext < MapPageViewModel > ( ) . Debug ( $"Starting update of stations pins color for {stationsColorList.Count} stations..." ) ;
2021-05-13 20:03:07 +02:00
// Update colors of pins.
2021-06-26 20:57:55 +02:00
for ( int pinIndex = 0 ; pinIndex < stationsColorList . Count ; pinIndex + + )
2021-05-13 20:03:07 +02:00
{
2021-11-15 10:06:55 +01:00
var indexPartPrefix = int . TryParse ( Pins [ pinIndex ] . Tag . ToString ( ) , out int stationId )
2021-06-26 20:57:55 +02:00
& & stationId < = CUSTOM_ICONS_COUNT
? $"{stationId}" // there is a station marker with index letter for given station id
2021-05-13 20:03:07 +02:00
: "Open" ; // there is no station marker. Use open marker.
2021-06-26 20:57:55 +02:00
var colorPartPrefix = GetRessourceNameColorPart ( stationsColorList [ pinIndex ] ) ;
2021-11-15 10:06:55 +01:00
2021-05-13 20:03:07 +02:00
var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? " . png " : string.Empty)}" ;
try
{
2021-06-26 20:57:55 +02:00
Pins [ pinIndex ] . Icon = BitmapDescriptorFactory . FromBundle ( l_iName ) ;
2021-05-13 20:03:07 +02:00
}
catch ( Exception l_oException )
{
Log . ForContext < MapPageViewModel > ( ) . Error ( "Station icon {l_strName} can not be loaded. {@l_oException}." , l_oException ) ;
2021-06-26 20:57:55 +02:00
Pins [ pinIndex ] . Label = stationId . ToString ( ) ;
Pins [ pinIndex ] . Icon = BitmapDescriptorFactory . DefaultMarker ( stationsColorList [ pinIndex ] ) ;
2021-05-13 20:03:07 +02:00
}
2021-06-26 20:57:55 +02:00
Pins [ pinIndex ] . IsVisible = true ;
2021-05-13 20:03:07 +02:00
}
var pinsCount = Pins . Count ;
2021-06-26 20:57:55 +02:00
for ( int pinIndex = stationsColorList . Count ; pinIndex < pinsCount ; pinIndex + + )
2021-05-13 20:03:07 +02:00
{
2021-06-26 20:57:55 +02:00
Log . ForContext < MapPageViewModel > ( ) . Error ( $"Unexpected count of pins detected. Expected {stationsColorList.Count} but is {pinsCount}." ) ;
2021-05-13 20:03:07 +02:00
Pins [ pinIndex ] . IsVisible = false ;
}
Log . ForContext < MapPageViewModel > ( ) . Debug ( "Update of stations pins color done." ) ;
}
/// <summary> Gets the color related part of the ressrouce name.</summary>
2021-06-26 20:57:55 +02:00
/// <param name="color">Color to get name for.</param>
2021-05-13 20:03:07 +02:00
/// <returns>Resource name.</returns>
2021-06-26 20:57:55 +02:00
private static string GetRessourceNameColorPart ( Color color )
2021-05-13 20:03:07 +02:00
{
2021-06-26 20:57:55 +02:00
if ( color = = Color . Blue )
2021-05-13 20:03:07 +02:00
{
return "Blue" ;
}
2021-06-26 20:57:55 +02:00
if ( color = = Color . Green )
2021-05-13 20:03:07 +02:00
{
return "Green" ;
}
2021-06-26 20:57:55 +02:00
if ( color = = Color . LightBlue )
2021-05-13 20:03:07 +02:00
{
return "LightBlue" ;
}
2021-06-26 20:57:55 +02:00
if ( color = = Color . Red )
2021-05-13 20:03:07 +02:00
{
return "Red" ;
}
2021-06-26 20:57:55 +02:00
return color . ToString ( ) ;
2021-05-13 20:03:07 +02:00
}
/// <summary>
2021-11-15 10:06:55 +01:00
/// Invoked when page is shown.
2021-05-13 20:03:07 +02:00
/// 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.
Polling = TinkApp . Polling ;
Log . ForContext < MapPageViewModel > ( ) . Information (
2021-11-15 10:06:55 +01:00
$"{(Polling != null && Polling.IsActivated ? $" Map page is appearing . Update periode is { Polling . Periode . TotalSeconds } sec . " : " Map page is appearing . Polling is off . ")}" +
2021-05-13 20:03:07 +02:00
$"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}." ) ;
2021-11-15 10:06:55 +01:00
// Update map page filter
2021-05-13 20:03:07 +02:00
ActiveFilterMap = TinkApp . GroupFilterMapPage ;
2021-08-28 10:04:10 +02:00
ActionText = AppResources . ActivityTextRequestingLocationPermissions ;
2021-11-15 11:55:26 +01:00
Status status = await RequestLocationPermission ( ) ;
2021-05-13 20:03:07 +02:00
ActionText = AppResources . ActivityTextMapLoadingStationsAndBikes ;
IsConnected = TinkApp . GetIsConnected ( ) ;
2021-11-09 16:08:00 +01:00
Result < StationsAndBikesContainer > resultStationsAndBikes = await TinkApp . GetConnector ( IsConnected ) . Query . GetBikesAndStationsAsync ( ) ;
2021-05-13 20:03:07 +02:00
2021-11-09 16:08:00 +01:00
TinkApp . Stations = resultStationsAndBikes . Response . StationsAll ;
2021-05-13 20:03:07 +02:00
2021-11-09 16:08:00 +01:00
await SetStationsOnMap ( resultStationsAndBikes . Response . StationsAll ) ;
await HandleAuthCookieNotDefinedException ( resultStationsAndBikes . Exception ) ;
2021-11-15 10:06:55 +01:00
2021-05-13 20:03:07 +02:00
// Update pin colors.
Log . ForContext < MapPageViewModel > ( ) . Verbose ( "Starting update pins color..." ) ;
2021-06-26 20:57:55 +02:00
var colors = GetStationColors (
2021-05-13 20:03:07 +02:00
Pins . Select ( x = > x . Tag . ToString ( ) ) . ToList ( ) ,
resultStationsAndBikes . Response . Bikes ) ;
// Update pins color form count of bikes located at station.
2021-06-26 20:57:55 +02:00
UpdatePinsColor ( colors ) ;
2021-05-13 20:03:07 +02:00
2021-11-09 16:08:00 +01:00
Log . ForContext < MapPageViewModel > ( ) . Verbose ( "Update pins color done." ) ;
2021-11-07 19:42:59 +01:00
2021-11-09 16:08:00 +01:00
// Move and scale before getting stations and bikes which takes some time.
ActionText = AppResources . ActivityTextCenterMap ;
2021-11-15 11:55:26 +01:00
await MoveMapToCurrentPositionOfUser ( status ) ;
2021-05-13 20:03:07 +02:00
m_oViewUpdateManager = CreateUpdateTask ( ) ;
try
{
// Update bikes at station or my bikes depending on context.
await m_oViewUpdateManager . StartUpdateAyncPeridically ( Polling ) ;
}
catch ( Exception )
{
// Excpetions are handled insde update task;
}
Exception = resultStationsAndBikes . Exception ;
ActionText = "" ;
IsRunning = false ;
IsMapPageEnabled = true ;
}
catch ( Exception l_oException )
{
Log . ForContext < MapPageViewModel > ( ) . Error ( $"An error occurred switching view TINK/ Konrad.\r\n{l_oException.Message}" ) ;
IsRunning = false ;
2021-07-22 22:41:35 +02:00
await ViewService . DisplayAlert (
2021-05-13 20:03:07 +02:00
"Fehler" ,
$"Beim Anzeigen der Fahrradstandorte- Seite ist ein Fehler aufgetreten.\r\n{l_oException.Message}" ,
"OK" ) ;
IsMapPageEnabled = true ;
}
}
2021-11-09 16:08:00 +01:00
2021-11-09 14:22:53 +01:00
/// <summary>
2021-11-15 10:06:55 +01:00
/// Invoked when the auth cookie is not defined.
2021-11-09 14:22:53 +01:00
/// </summary>
2021-11-09 16:08:00 +01:00
private async Task HandleAuthCookieNotDefinedException ( Exception exception )
2021-11-09 14:22:53 +01:00
{
2021-11-09 16:08:00 +01:00
if ( exception ? . GetType ( ) = = typeof ( AuthcookieNotDefinedException ) )
2021-11-09 14:22:53 +01:00
{
2021-11-09 16:08:00 +01:00
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 . MessageMapPageErrorAuthcookieUndefined ,
AppResources . MessageAnswerOk ) ;
await TinkApp . GetConnector ( IsConnected ) . Command . DoLogout ( ) ;
TinkApp . ActiveUser . Logout ( ) ;
2021-11-09 14:22:53 +01:00
}
2021-11-09 16:08:00 +01:00
}
/// <summary>
/// Sets the available stations on the map.
/// </summary>
private async Task SetStationsOnMap ( StationDictionary stations )
{
if ( Pins . Count > 0 & & Pins . Count ! = stations . Count )
2021-11-09 14:22:53 +01:00
{
2021-11-09 16:08:00 +01:00
// Either
// - user logged in/ logged out which might lead to more/ less stations beeing available
// - new stations were added/ existing ones remove
Pins . Clear ( ) ;
2021-11-09 14:22:53 +01:00
}
2021-11-15 10:06:55 +01:00
// Check if there are alreay any pins to the map
2021-11-09 16:08:00 +01:00
// i.e detecte 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 . ")}" ) ;
2021-11-09 14:22:53 +01:00
2021-11-09 16:08:00 +01:00
// 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 , ContactPageViewModel . GetAppName ( TinkApp . Uris . ActiveUri ) ) ,
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 , ContactPageViewModel . GetAppName ( TinkApp . Uris . ActiveUri ) ) ,
AppResources . MessageAnswerOk ) ;
Log . ForContext < MapPageViewModel > ( ) . Error ( $"Outdated version of app detected. Version expected is {stations.CopriVersion}." ) ;
}
2021-11-15 10:06:55 +01:00
// Set pins to their positions on map.
2021-11-15 11:15:43 +01:00
InitializePins ( stations ) ;
2021-11-09 16:08:00 +01:00
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>
2021-11-15 11:55:26 +01:00
private async Task MoveMapToCurrentPositionOfUser ( Status status )
2021-11-09 16:08:00 +01:00
{
if ( status = = Status . Granted )
{
2021-11-07 19:42:59 +01:00
ActionText = AppResources . ActivityTextCenterMap ;
2021-11-14 23:27:29 +01:00
if ( TinkApp . CenterMapToCurrentLocation )
2021-11-09 16:08:00 +01:00
{
2021-11-14 23:27:29 +01:00
Location currentLocation = null ;
try
{
currentLocation = await GeolocationService . GetAsync ( ) ;
}
catch ( Exception ex )
{
Log . ForContext < MapPageViewModel > ( ) . Error ( "Getting location failed. {Exception}" , ex ) ;
}
TinkApp . MapSpan = MapSpan . FromCenterAndRadius (
new Xamarin . Forms . GoogleMaps . Position ( currentLocation . Latitude , currentLocation . Longitude ) ,
TinkApp . MapSpan . Radius ) ;
2021-11-09 16:08:00 +01:00
2021-11-14 23:27:29 +01:00
TinkApp . Save ( ) ;
2021-11-07 19:42:59 +01:00
}
2021-11-14 23:27:29 +01:00
MoveAndScale ( m_oMoveToRegionDelegate , TinkApp . MapSpan ) ;
2021-05-13 20:03:07 +02:00
}
2021-11-09 16:08:00 +01:00
}
/// <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 ( TinkApp . CenterMapToCurrentLocation
& & ! GeolocationService . IsSimulation
& & status ! = Status . Granted )
{
status = await PermissionsService . RequestAsync ( ) ;
if ( status ! = Status . Granted )
{
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 status ;
2021-11-09 14:22:53 +01:00
}
2021-05-13 20:03:07 +02:00
/// <summary> Moves map and scales visible region depending on active filter. </summary>
public static void MoveAndScale (
Action < MapSpan > moveToRegionDelegate ,
2021-11-14 23:27:29 +01:00
MapSpan currentMapSpan = null )
2021-05-13 20:03:07 +02:00
{
2021-11-14 23:27:29 +01:00
if ( currentMapSpan ! = null )
2021-05-13 20:03:07 +02:00
{
// Move to current location.
2021-11-14 23:27:29 +01:00
moveToRegionDelegate ( currentMapSpan ) ;
2021-05-13 20:03:07 +02:00
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 (
( ) = > GetType ( ) . Name ,
( ) = >
{
try
{
Log . ForContext < MapPageViewModel > ( ) . Verbose ( "Entering update cycle." ) ;
Result < StationsAndBikesContainer > resultStationsAndBikes ;
TinkApp . PostAction (
unused = >
{
2021-06-26 20:57:55 +02:00
ActionText = AppResources . ActivityTextUpdating ;
2021-05-13 20:03:07 +02:00
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 ) ;
}
2021-11-15 10:06:55 +01:00
// Check if there are alreay any pins to the map.
2021-05-13 20:03:07 +02:00
// If no initialze 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 . Bikes ) ;
// 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>
2021-11-15 10:06:55 +01:00
/// Invoked when pages is closed/ hidden.
2021-05-13 20:03:07 +02:00
/// Stops update process.
/// </summary>
public async Task OnDisappearing ( )
{
Log . Information ( "Map page is disappearing..." ) ;
await m_oViewUpdateManager . StopUpdatePeridically ( ) ;
}
/// <summary> User clicked on a bike. </summary>
/// <param name="selectedStationId">Id of station user clicked on.</param>
2021-06-26 20:57:55 +02:00
public async void OnStationClicked ( string selectedStationId )
2021-05-13 20:03:07 +02:00
{
try
{
Log . ForContext < MapPageViewModel > ( ) . Information ( $"User taped station {selectedStationId}." ) ;
// Lock action to prevent multiple instances of "BikeAtStation" being opened.
IsMapPageEnabled = false ;
2021-11-15 10:06:55 +01:00
TinkApp . SelectedStation = TinkApp . Stations . FirstOrDefault ( x = > x . Id = = selectedStationId )
2021-06-26 20:57:55 +02:00
? ? new Station ( selectedStationId , new List < string > ( ) , null ) ; // Station might not be in list StationDictinaly because this list is not updatd in background task.
2021-05-13 20:03:07 +02:00
#if TRYNOTBACKSTYLE
m_oNavigation . ShowPage (
typeof ( BikesAtStationPage ) ,
p_strStationName ) ;
#else
// Show page.
2021-07-22 22:41:35 +02:00
await ViewService . PushAsync ( ViewTypes . BikesAtStation ) ;
2021-05-13 20:03:07 +02:00
IsMapPageEnabled = true ;
ActionText = "" ;
}
catch ( Exception exception )
{
IsMapPageEnabled = true ;
ActionText = "" ;
Log . ForContext < MapPageViewModel > ( ) . Error ( "Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {Exception}" , exception ) ;
2021-07-22 22:41:35 +02:00
await ViewService . DisplayAlert (
2021-05-13 20:03:07 +02:00
"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>
2021-06-26 20:57:55 +02:00
/// <param name="stationsId">Station id list to get color for.</param>
2021-05-13 20:03:07 +02:00
/// <returns></returns>
private static IList < Color > GetStationColors (
2021-06-26 20:57:55 +02:00
IEnumerable < string > stationsId ,
2021-05-13 20:03:07 +02:00
BikeCollection bikesAll )
{
2021-06-26 20:57:55 +02:00
if ( stationsId = = null )
2021-05-13 20:03:07 +02:00
{
Log . ForContext < MapPageViewModel > ( ) . 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 < MapPageViewModel > ( ) . Error ( "No bikes available to determine pins color." ) ;
2021-06-26 20:57:55 +02:00
return new List < Color > ( stationsId . Select ( x = > Color . Blue ) ) ;
2021-05-13 20:03:07 +02:00
}
// Get state for each station.
2021-06-26 20:57:55 +02:00
var colors = new List < Color > ( ) ;
foreach ( var stationId in stationsId )
2021-05-13 20:03:07 +02:00
{
// Get color of given station.
2021-06-26 20:57:55 +02:00
var bikesAtStation = bikesAll . Where ( x = > x . CurrentStation = = stationId ) . ToList ( ) ;
if ( bikesAtStation . FirstOrDefault ( x = > x . State . Value ! = Model . State . InUseStateEnum . Disposable ) ! = null )
2021-05-13 20:03:07 +02:00
{
// There is at least one requested or booked bike
2021-06-26 20:57:55 +02:00
colors . Add ( Color . LightBlue ) ;
2021-05-13 20:03:07 +02:00
continue ;
}
2021-06-26 20:57:55 +02:00
if ( bikesAtStation . ToList ( ) . Count > 0 )
2021-05-13 20:03:07 +02:00
{
// There is at least one bike available
2021-06-26 20:57:55 +02:00
colors . Add ( Color . Green ) ;
2021-05-13 20:03:07 +02:00
continue ;
}
2021-06-26 20:57:55 +02:00
colors . Add ( Color . Red ) ;
2021-05-13 20:03:07 +02:00
}
2021-06-26 20:57:55 +02:00
return colors ;
2021-05-13 20:03:07 +02:00
}
/// <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 < MapPageViewModel > ( ) . 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.
2021-06-26 20:57:55 +02:00
return TinkApp . IsReportLevelVerbose
? Exception . GetShortErrorInfoText ( )
: AppResources . ActivityTextException ;
2021-05-13 20:03:07 +02:00
}
if ( ! IsConnected )
{
2021-06-26 20:57:55 +02:00
return AppResources . ActivityTextConnectionStateOffline ;
2021-05-13 20:03:07 +02:00
}
return ActionText ? ? string . Empty ;
}
}
/// <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 . FILTERKONRAD )
{
// Konrad is already activated, nothing to do.
return ;
}
Log . ForContext < MapPageViewModel > ( ) . Information ( "User toggles to Konrad." ) ;
await ActivateFilter ( FilterHelper . FILTERTINKGENERAL ) ;
}
/// <summary> User request to toggle from TINK to Konrad. </summary>
public async Task ToggleKonradToTink ( )
{
if ( tinkKonradToggleViewModel . CurrentFilter = = FilterHelper . FILTERTINKGENERAL )
{
// Konrad is already activated, nothing to do.
return ;
}
Log . ForContext < MapPageViewModel > ( ) . Information ( "User toggles to TINK." ) ;
await ActivateFilter ( FilterHelper . FILTERKONRAD ) ;
}
/// <summary> User request to toggle from TINK to Konrad. </summary>
private async Task ActivateFilter ( string p_strSelectedFilter )
{
try
{
Log . ForContext < MapPageViewModel > ( ) . Information ( $"Request to toggle to \" { p_strSelectedFilter } \ "." ) ;
// Stop polling.
await m_oViewUpdateManager . StopUpdatePeridically ( ) ;
// Clear error info.
Exception = null ;
// Toggle view
tinkKonradToggleViewModel = new TinkKonradToggleViewModel ( ActiveFilterMap ) . DoToggle ( ) ;
ActiveFilterMap = tinkKonradToggleViewModel . FilterDictionary ;
TinkApp . GroupFilterMapPage = ActiveFilterMap ;
TinkApp . Save ( ) ;
TinkApp . UpdateConnector ( ) ;
Pins . Clear ( ) ;
// Check location permission
2021-11-07 19:42:59 +01:00
var status = await PermissionsService . CheckStatusAsync ( ) ;
2021-05-13 20:03:07 +02:00
if ( TinkApp . CenterMapToCurrentLocation
2021-06-26 20:57:55 +02:00
& & ! GeolocationService . IsSimulation
2021-11-07 19:42:59 +01:00
& & status ! = Status . Granted )
2021-05-13 20:03:07 +02:00
{
2021-11-07 19:42:59 +01:00
var permissionResult = await PermissionsService . RequestAsync ( ) ;
2021-05-13 20:03:07 +02:00
2021-11-07 19:42:59 +01:00
if ( permissionResult ! = Status . Granted )
2021-05-13 20:03:07 +02:00
{
2021-07-22 22:41:35 +02:00
var dialogResult = await ViewService . DisplayAlert (
2021-11-15 10:06:55 +01:00
AppResources . MessageTitleHint ,
2021-05-13 20:03:07 +02:00
AppResources . MessageBikesManagementLocationPermission ,
"Ja" ,
"Nein" ) ;
if ( dialogResult )
{
// User decided to give access to locations permissions.
2021-06-26 20:57:55 +02:00
PermissionsService . OpenAppSettings ( ) ;
2021-05-13 20:03:07 +02:00
IsMapPageEnabled = true ;
ActionText = "" ;
return ;
}
}
2021-11-15 10:06:55 +01:00
// Do not use property .State to get bluetooth state due
// to issue https://hausource.visualstudio.com/TINK/_workitems/edit/116 /
2021-05-13 20:03:07 +02:00
// see https://github.com/xabre/xamarin-bluetooth-le/issues/112#issuecomment-380994887
2021-06-26 20:57:55 +02:00
if ( await BluetoothService . GetBluetoothState ( ) ! = Plugin . BLE . Abstractions . Contracts . BluetoothState . On )
2021-05-13 20:03:07 +02:00
{
2021-07-22 22:41:35 +02:00
await ViewService . DisplayAlert (
2021-05-13 20:03:07 +02:00
AppResources . MessageTitleHint ,
2021-11-15 10:06:55 +01:00
AppResources . MessageBikesManagementBluetoothActivation ,
2021-05-13 20:03:07 +02:00
AppResources . MessageAnswerOk ) ;
IsMapPageEnabled = true ;
ActionText = "" ;
return ;
}
}
// Move and scale before getting stations and bikes which takes some time.
2021-11-14 23:27:29 +01:00
if ( TinkApp . CenterMapToCurrentLocation )
2021-05-13 20:03:07 +02:00
{
2021-11-14 23:27:29 +01:00
Location currentLocation = null ;
try
{
currentLocation = await GeolocationService . GetAsync ( ) ;
}
catch ( Exception ex )
{
Log . ForContext < MapPageViewModel > ( ) . Error ( "Getting location failed. {Exception}" , ex ) ;
}
TinkApp . MapSpan = MapSpan . FromCenterAndRadius (
new Xamarin . Forms . GoogleMaps . Position ( currentLocation . Latitude , currentLocation . Longitude ) ,
TinkApp . MapSpan . Radius ) ;
TinkApp . Save ( ) ;
2021-05-13 20:03:07 +02:00
}
// Update stations
2021-11-14 23:27:29 +01:00
MoveAndScale ( m_oMoveToRegionDelegate , TinkApp . MapSpan ) ;
2021-05-13 20:03:07 +02:00
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 . Bikes ) ;
// 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 . StartUpdateAyncPeridically ( Polling ) ;
}
catch ( Exception )
{
// Excpetions are handled insde update task;
}
Log . ForContext < MapPageViewModel > ( ) . Information ( $"Toggle to \" { p_strSelectedFilter } \ " done." ) ;
}
catch ( Exception l_oException )
{
Log . ForContext < MapPageViewModel > ( ) . Error ( "An error occurred switching view TINK/ Konrad.{}" ) ;
2021-07-22 22:41:35 +02:00
await ViewService . DisplayAlert (
2021-05-13 20:03:07 +02:00
"Fehler" ,
$"Beim Umschalten TINK/ Konrad ist ein Fehler aufgetreten.\r\n{l_oException.Message}" ,
"OK" ) ;
}
}
public Color TinkColor = > tinkKonradToggleViewModel . TinkColor ;
public Color KonradColor = > tinkKonradToggleViewModel . KonradColor ;
public bool IsToggleVisible = > tinkKonradToggleViewModel . IsToggleVisible ;
}
2021-11-15 10:06:55 +01:00
}