mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2025-04-22 04:46:30 +02:00
Version 3.0.381
This commit is contained in:
parent
f963c0a219
commit
3a363acf3a
1525 changed files with 60589 additions and 125098 deletions
394
SharedBusinessLogic/ViewModel/Contact/ContactPageViewModel.cs
Normal file
394
SharedBusinessLogic/ViewModel/Contact/ContactPageViewModel.cs
Normal file
|
@ -0,0 +1,394 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Acr.Collections;
|
||||
using Plugin.Messaging;
|
||||
using Serilog;
|
||||
using ShareeBike.Model;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using ShareeBike.MultilingualResources;
|
||||
using ShareeBike.View;
|
||||
using ShareeBike.ViewModel.Bikes;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
using ICommand = System.Windows.Input.ICommand;
|
||||
|
||||
namespace ShareeBike.ViewModel.Contact
|
||||
{
|
||||
/// <summary> View model for contact page.</summary>
|
||||
public class ContactPageViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public Xamarin.Forms.Command ShowSelectStationInfoText { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mail address for app related support.
|
||||
/// </summary>
|
||||
public const string APPSUPPORTMAILADDRESS = "hotline@sharee.bike";
|
||||
|
||||
/// <summary> Reference on view service to show modal notifications and to perform navigation. </summary>
|
||||
private IViewService ViewService { get; }
|
||||
|
||||
/// <summary> Station selected by user. </summary>
|
||||
private IBikesViewModel SelectedBike { get; set; }
|
||||
|
||||
/// <summary> Station selected by user. </summary>
|
||||
private IStation SelectedStation { get; set; } = new NullStation();
|
||||
|
||||
/// <summary> Holds the name of the app (sharee.bike, ...)</summary>
|
||||
string AppFlavorName { get; }
|
||||
|
||||
/// <summary> Reference on the shareeBike app instance. </summary>
|
||||
private IShareeBikeApp ShareeBikeApp { get; }
|
||||
|
||||
/// <summary> Holds a reference to the external trigger service. </summary>
|
||||
private Action OpenUrlInExternalBrowser { get; }
|
||||
|
||||
Func<string> CreateAttachment { get; }
|
||||
|
||||
/// <summary> Provides a connector object.</summary>
|
||||
protected Func<bool, IConnector> ConnectorFactory { get; }
|
||||
|
||||
/// <summary> Delegate to retrieve connected state. </summary>
|
||||
protected Func<bool> IsConnectedDelegate { get; }
|
||||
|
||||
/// <summary> Notifies view about changes. </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary> Constructs a contact page view model. </summary>
|
||||
/// <param name="openUrlInExternalBrowser">Action to open an external browser.</param>
|
||||
/// <param name="user">Mail address of active user.</param>
|
||||
/// <param name="isReportLevelVerbose">True if report level is verbose, false if not.</param>
|
||||
/// <param name="permissions">Holds object to query location permissions.</param>
|
||||
/// <param name="bluetoothLE">Holds object to query bluetooth state.</param>
|
||||
/// <param name="runtimPlatform">Specifies on which platform code is run.</param>
|
||||
/// <param name="isConnectedDelegate">Returns if mobile is connected to web or not.</param>
|
||||
/// <param name="connectorFactory">Connects system to copri.</param>
|
||||
/// <param name="lockService">Service to control lock retrieve info.</param>
|
||||
/// <param name="stations">Stations to get station name from station id.</param>
|
||||
/// <param name="polling"> Holds whether to poll or not and the period length is polling is on. </param>
|
||||
/// <param name="postAction">Executes actions on GUI thread.</param>
|
||||
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
|
||||
/// <param name="viewService">Interface to actuate methods on GUI.</param>
|
||||
public ContactPageViewModel(
|
||||
string appFlavorName,
|
||||
IShareeBikeApp shareeBikeApp,
|
||||
Func<string> createAttachment,
|
||||
Action openUrlInExternalBrowser,
|
||||
IViewService viewService,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory)
|
||||
{
|
||||
AppFlavorName = !string.IsNullOrEmpty(appFlavorName)
|
||||
? appFlavorName
|
||||
: throw new ArgumentException("Can not instantiate contact page view model- object. No app name centered.");
|
||||
|
||||
ShareeBikeApp = shareeBikeApp
|
||||
?? throw new ArgumentException("Can not instantiate settings page view model- object. No shareeBike app object available.");
|
||||
|
||||
CreateAttachment = createAttachment
|
||||
?? throw new ArgumentException("Can not instantiate contact page view model- object. No create attachment provider available.");
|
||||
|
||||
ViewService = viewService
|
||||
?? throw new ArgumentException("Can not instantiate contact page view model- object. No user view service available.");
|
||||
|
||||
OpenUrlInExternalBrowser = openUrlInExternalBrowser
|
||||
?? throw new ArgumentException("Can not instantiate contact page view model- object. No user external browse service available.");
|
||||
|
||||
ConnectorFactory = connectorFactory
|
||||
?? throw new ArgumentException("Can not instantiate bikes page view model- object. No connector available.");
|
||||
|
||||
IsConnectedDelegate = isConnectedDelegate
|
||||
?? throw new ArgumentException("Can not instantiate bikes page view model- object. No is connected delegate available.");
|
||||
|
||||
ShowSelectStationInfoText = new Xamarin.Forms.Command(async () => {
|
||||
|
||||
await ViewService.DisplayAlert(
|
||||
AppResources.MarkingLastSelectedStation,
|
||||
AppResources.MarkingContactNoStationInfoAvailableNoButton,
|
||||
AppResources.MessageAnswerOk);
|
||||
});
|
||||
}
|
||||
|
||||
public ICommand OnFAQClickedRequest
|
||||
=> new Xamarin.Forms.Command(async () => await OpenFAQAsync());
|
||||
|
||||
/// <summary> Opens Help page, FAQ. </summary>
|
||||
public async Task OpenFAQAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ViewService.PushAsync(ViewTypes.HelpPage);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<ContactPageViewModel>().Error("Fehler beim Öffnen der Seite 'Hilfe' aufgetreten. {Exception}", exception);
|
||||
|
||||
await ViewService.DisplayAlert(
|
||||
AppResources.ErrorPageNotLoadedTitle,
|
||||
$"{AppResources.ErrorPageNotLoaded}\r\n{exception.Message}",
|
||||
AppResources.MessageAnswerOk);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Is invoked when page is shown. </summary>
|
||||
public async Task OnAppearing(
|
||||
IStation selectedStation)
|
||||
{
|
||||
if (SelectedStation?.Id == selectedStation?.Id)
|
||||
{
|
||||
// Nothing to do because either both are null or of same id.
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedStation = selectedStation;
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedStationId)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedStationName)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MailAddressText)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OfficeHoursText)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PhoneNumberText)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ProviderNameText)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsOperatorInfoAvaliable)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDoPhoncallAvailable)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSendMailAvailable)));
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary> Command object to bind mail button to view model. </summary>
|
||||
public ICommand OnMailToOperatorRequest
|
||||
=> new Xamarin.Forms.Command(async () => await DoSendMailToOperator());
|
||||
|
||||
/// <summary> Command object to bind mail app related button to model. </summary>
|
||||
public ICommand OnMailAppRelatedRequest
|
||||
=> new Xamarin.Forms.Command(async () => await DoSendMailAppRelated());
|
||||
|
||||
/// <summary>True if sending mail is possible.</summary>
|
||||
public bool IsSendMailAvailable
|
||||
=> CrossMessaging.Current.EmailMessenger.CanSendEmail;
|
||||
|
||||
|
||||
/// <summary>cTrue if doing a phone call is possible.</summary>
|
||||
public bool IsDoPhoncallAvailable
|
||||
=> CrossMessaging.Current.PhoneDialer.CanMakePhoneCall;
|
||||
|
||||
/// <summary>Holds the mail address to mail to.</summary>
|
||||
public string MailAddressText
|
||||
=> SelectedStation?.OperatorData?.MailAddressText ?? string.Empty;
|
||||
|
||||
/// <summary>Holds the mail address to send mail to.</summary>
|
||||
public string PhoneNumberText
|
||||
=> SelectedStation?.OperatorData?.PhoneNumberText ?? string.Empty;
|
||||
|
||||
/// <summary>Holds the mail address to send mail to.</summary>
|
||||
public string OfficeHoursText
|
||||
=> SelectedStation?.OperatorData?.Hours ?? string.Empty;
|
||||
|
||||
/// <summary> Gets whether any operator support info is available. </summary>
|
||||
public bool IsOperatorInfoAvaliable
|
||||
=> MailAddressText.Length > 0 || PhoneNumberText.Length > 0;
|
||||
|
||||
/// <returns> Returns true if either mail was sent or if no mailer available.</returns>
|
||||
public async Task DoSendMailToOperator()
|
||||
{
|
||||
if (!IsSendMailAvailable)
|
||||
{
|
||||
await ViewService.DisplayAlert(
|
||||
String.Empty,
|
||||
AppResources.ErrorSupportmailMailingFailed,
|
||||
AppResources.MessageAnswerOk);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// Send operator related support mail to operator.
|
||||
await Email.ComposeAsync(new EmailMessage
|
||||
{
|
||||
To = new List<string> { MailAddressText },
|
||||
Cc = APPSUPPORTMAILADDRESS.ToUpper() != MailAddressText.ToUpper() // do not sent copy if same mail address
|
||||
? new List<string> { APPSUPPORTMAILADDRESS } : new List<string>(),
|
||||
Subject = string.Format(AppResources.SupportmailSubjectOperatormail, AppFlavorName, SelectedStation?.Id),
|
||||
Body = ShareeBikeApp.ActiveUser.Mail != null ? $"{AppResources.SupportmailBodyText}\r\n\r\n\r\n\r\n{string.Format(AppResources.MarkingLoggedInStateInfoLoggedIn, ShareeBikeApp.ActiveUser.Mail)}" : $"{AppResources.SupportmailBodyText}\r\n\r\n\r\n\r\n{string.Format(AppResources.SupportmailBodyNotLoggedIn)}"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("An unexpected error occurred sending mail to operator. {@Exception}", exception);
|
||||
await ViewService.DisplayAdvancedAlert(
|
||||
AppResources.MessageWaring,
|
||||
AppResources.ErrorSupportmailMailingFailed,
|
||||
exception.Message,
|
||||
AppResources.MessageAnswerOk);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Request to send a app related mail. </summary>
|
||||
public async Task DoSendMailAppRelated()
|
||||
{
|
||||
if (!IsSendMailAvailable)
|
||||
{
|
||||
await ViewService.DisplayAlert(
|
||||
String.Empty,
|
||||
AppResources.ErrorSupportmailMailingFailed,
|
||||
AppResources.MessageAnswerOk);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ask for permission to append diagnostics.
|
||||
await ViewService.DisplayAlert(
|
||||
AppResources.QuestionSupportmailAttachmentTitle,
|
||||
AppResources.QuestionSupportmailAttachment,
|
||||
AppResources.MessageAnswerOk);
|
||||
|
||||
var message = new EmailMessage
|
||||
{
|
||||
To = new List<string> { APPSUPPORTMAILADDRESS },
|
||||
Subject = SelectedStation?.Id != null ? string.Format(AppResources.SupportmailSubjectAppmailWithStation, AppFlavorName, SelectedStation?.Id) : string.Format(AppResources.SupportmailSubjectAppmail, AppFlavorName),
|
||||
Body = ShareeBikeApp.ActiveUser.Mail != null ? $"{AppResources.SupportmailBodyText}\r\n\r\n\r\n\r\n{string.Format(AppResources.MarkingLoggedInStateInfoLoggedIn, ShareeBikeApp.ActiveUser.Mail)}" : $"{AppResources.SupportmailBodyText}\r\n\r\n\r\n\r\n{string.Format(AppResources.SupportmailBodyNotLoggedIn)}"
|
||||
};
|
||||
|
||||
// Send with attachment.
|
||||
var logFileName = string.Empty;
|
||||
try
|
||||
{
|
||||
logFileName = CreateAttachment();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await ViewService.DisplayAdvancedAlert(
|
||||
AppResources.MessageWaring,
|
||||
AppResources.ErrorSupportmailCreateAttachment,
|
||||
exception.Message,
|
||||
AppResources.MessageAnswerOk);
|
||||
|
||||
Log.ForContext<ContactPageViewModel>().Error("An error occurred creating attachment for app mail. {@Exception)", exception);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(logFileName))
|
||||
{
|
||||
message.Attachments.Add(new Xamarin.Essentials.EmailAttachment(logFileName));
|
||||
}
|
||||
|
||||
// Send a shareeBike app related mail
|
||||
await Email.ComposeAsync(message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<ContactPageViewModel>().Error("An unexpected error occurred sending mail. {@Exception}", exception);
|
||||
await ViewService.DisplayAdvancedAlert(
|
||||
AppResources.MessageWaring,
|
||||
AppResources.ErrorSupportmailMailingFailed,
|
||||
exception.Message,
|
||||
AppResources.MessageAnswerOk);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Command object to bind login button to view model. </summary>
|
||||
public ICommand OnSelectStationRequest
|
||||
=> new Xamarin.Forms.Command(async () => await OpenSelectStationPageAsync());
|
||||
|
||||
/// <summary> Opens login page. </summary>
|
||||
public async Task OpenSelectStationPageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Switch to map page
|
||||
|
||||
await ViewService.PushAsync(ViewTypes.SelectStationPage);
|
||||
}
|
||||
catch (Exception p_oException)
|
||||
{
|
||||
Log.Error("Ein unerwarteter Fehler ist in der Klasse ContactPageViewModel aufgetreten. Kontext: Klick auf Hinweistext auf Station N- seite ohne Anmeldung. {@Exception}", p_oException);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Command object to bind phone call button. </summary>
|
||||
public ICommand OnPhoneRequest
|
||||
=> new Xamarin.Forms.Command(async () => await DoPhoneCall());
|
||||
|
||||
/// <summary> Request to do a phone call. </summary>
|
||||
public async Task DoPhoneCall()
|
||||
{
|
||||
if (!IsDoPhoncallAvailable)
|
||||
{
|
||||
await ViewService.DisplayAlert(
|
||||
String.Empty,
|
||||
AppResources.ErrorSupportmailPhoningFailed,
|
||||
AppResources.MessageAnswerOk);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// Make Phone Call
|
||||
CrossMessaging.Current.PhoneDialer.MakePhoneCall(PhoneNumberText);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("An unexpected error occurred doing a phone call. {@Exception}", exception);
|
||||
await ViewService.DisplayAdvancedAlert(
|
||||
AppResources.MessageWaring,
|
||||
AppResources.ErrorSupportmailPhoningFailed,
|
||||
exception.Message,
|
||||
AppResources.MessageAnswerOk);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Text providing the id of the selected station.</summary>
|
||||
public string SelectedStationId
|
||||
=> SelectedStation?.Id != null ? SelectedStation?.Id : string.Empty;
|
||||
|
||||
public string SelectedStationName
|
||||
=> SelectedStation?.StationName;
|
||||
|
||||
/// <summary> Text providing the name of the operator of the selected station </summary>
|
||||
public string ProviderNameText
|
||||
=> SelectedStation?.OperatorData?.Name;
|
||||
|
||||
/// <summary> Text providing mail address and possible reasons to contact. </summary>
|
||||
public FormattedString MailAddressAndMotivationsText
|
||||
{
|
||||
get
|
||||
{
|
||||
var hint = new FormattedString();
|
||||
hint.Spans.Add(new Span { Text = AppResources.MessageContactMail });
|
||||
return hint;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Invitation to rate app.</summary>
|
||||
public FormattedString PhoneContactText
|
||||
{
|
||||
get
|
||||
{
|
||||
var l_oHint = new FormattedString();
|
||||
l_oHint.Spans.Add(new Span
|
||||
{
|
||||
Text = string.Format(AppResources.MessagePhoneMail, AppFlavorName) + $"{(OfficeHoursText.Length > 0 ? $" {OfficeHoursText}" : string.Empty)}"
|
||||
});
|
||||
return l_oHint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,622 @@
|
|||
using Xamarin.Forms;
|
||||
using ShareeBike.View;
|
||||
using ShareeBike.Model.Stations;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Model;
|
||||
using Serilog;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using Xamarin.Forms.GoogleMaps;
|
||||
using System.Collections.ObjectModel;
|
||||
using ShareeBike.Services.Permissions;
|
||||
using Xamarin.Essentials;
|
||||
using System.Threading;
|
||||
using ShareeBike.MultilingualResources;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
using ShareeBike.Model.State;
|
||||
using ShareeBike.ViewModel.Map;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
|
||||
|
||||
namespace ShareeBike.ViewModel.Contact
|
||||
{
|
||||
public class SelectStationPageViewModel : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary> Holds the count of custom icons centered.</summary>
|
||||
private const int CUSTOM_ICONS_COUNT = 30;
|
||||
|
||||
/// <summary> Reference on view service to show modal notifications and to perform navigation. </summary>
|
||||
private IViewService ViewService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the exception which occurred getting bikes occupied information.
|
||||
/// </summary>
|
||||
private Exception m_oException;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Service to query/ manage permissions (location) of the app.
|
||||
/// </summary>
|
||||
private ILocationPermission PermissionsService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service to manage bluetooth stack.
|
||||
/// </summary>
|
||||
private Plugin.BLE.Abstractions.Contracts.IBluetoothLE BluetoothService { get; set; }
|
||||
|
||||
|
||||
/// <summary> Notifies view about changes. </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
|
||||
/// <summary> Reference on the shareeBike app instance. </summary>
|
||||
private IShareeBikeApp ShareeBikeApp { get; }
|
||||
|
||||
/// <summary>Delegate to perform navigation.</summary>
|
||||
private INavigation m_oNavigation;
|
||||
|
||||
private ObservableCollection<Pin> pins;
|
||||
|
||||
public ObservableCollection<Pin> Pins
|
||||
{
|
||||
get
|
||||
{
|
||||
if (pins == null)
|
||||
pins = new ObservableCollection<Pin>(); // If view model is not binding context pins collection must be set programmatically.
|
||||
|
||||
return pins;
|
||||
}
|
||||
|
||||
set => pins = value;
|
||||
}
|
||||
|
||||
/// <summary>Delegate to move map to region.</summary>
|
||||
private Action<MapSpan> m_oMoveToRegionDelegate;
|
||||
|
||||
/// <summary> False if user tabed on station marker to show bikes at a given station.</summary>
|
||||
private bool isMapPageEnabled = false;
|
||||
|
||||
IGeolocationService GeolocationService { get; }
|
||||
|
||||
/// <summary> False if user tabed on station marker to show bikes at a given station.</summary>
|
||||
public bool IsMapPageEnabled
|
||||
{
|
||||
get => isMapPageEnabled;
|
||||
private set
|
||||
{
|
||||
if (isMapPageEnabled == value)
|
||||
return;
|
||||
|
||||
isMapPageEnabled = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMapPageEnabled)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Prevents an invalid instane to be created. </summary>
|
||||
/// <param name="shareeBikeApp"> Reference to shareeBike 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(
|
||||
IShareeBikeApp shareeBikeApp,
|
||||
ILocationPermission permissionsService,
|
||||
Plugin.BLE.Abstractions.Contracts.IBluetoothLE bluetoothService,
|
||||
IGeolocationService geolocationService,
|
||||
Action<MapSpan> moveToRegionDelegate,
|
||||
IViewService viewService,
|
||||
INavigation navigation)
|
||||
{
|
||||
ShareeBikeApp = shareeBikeApp
|
||||
?? throw new ArgumentException("Can not instantiate map page view model- object. No shareeBike app object available.");
|
||||
|
||||
PermissionsService = permissionsService ??
|
||||
throw new ArgumentException($"Can not instantiate {nameof(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.");
|
||||
|
||||
|
||||
IsConnected = ShareeBikeApp.GetIsConnected();
|
||||
}
|
||||
|
||||
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 already occurred in past.
|
||||
Log.ForContext<SelectStationPageViewModel>().Error("Position 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 ShareeBike. </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 = GetResourceNameColorPart(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 GetResourceNameColorPart(Color color)
|
||||
{
|
||||
if (color == Color.Blue)
|
||||
{
|
||||
return "Blue";
|
||||
}
|
||||
|
||||
if (color == Color.Green)
|
||||
{
|
||||
return "Green";
|
||||
}
|
||||
|
||||
if (color == Color.LightBlue)
|
||||
{
|
||||
return "LightBlue";
|
||||
}
|
||||
|
||||
if (color == Color.Red)
|
||||
{
|
||||
return "Red";
|
||||
}
|
||||
|
||||
return color.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when page is shown.
|
||||
/// Starts update process.
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
IsProcessWithRunningProcessView = true;
|
||||
// Process map page.
|
||||
|
||||
Log.ForContext<SelectStationPageViewModel>().Information(
|
||||
$"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}.");
|
||||
|
||||
if (Pins.Count <= 0)
|
||||
{
|
||||
// Move and scale before getting stations and bikes which takes some time.
|
||||
if (ShareeBikeApp.CenterMapToCurrentLocation)
|
||||
{
|
||||
ActionText = AppResources.ActivityTextRequestingLocationPermissions;
|
||||
|
||||
// Check location permission
|
||||
var status = await PermissionsService.CheckStatusAsync();
|
||||
if (!GeolocationService.IsSimulation
|
||||
&& status != Status.Granted)
|
||||
{
|
||||
var dialogResult = await ViewService.DisplayAlert(
|
||||
AppResources.MessageHintTitle,
|
||||
AppResources.ErrorMapCenterNoLocationPermissionOpenDialog,
|
||||
AppResources.MessageAnswerYes,
|
||||
AppResources.MessageAnswerNo);
|
||||
|
||||
if (dialogResult)
|
||||
{
|
||||
// User decided to give access to locations permissions.
|
||||
PermissionsService.OpenAppSettings();
|
||||
ActionText = string.Empty;
|
||||
IsProcessWithRunningProcessView = false;
|
||||
IsMapPageEnabled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (status == Status.Granted)
|
||||
{
|
||||
ActionText = AppResources.ActivityTextCenterMap;
|
||||
|
||||
IGeolocation currentLocation = null;
|
||||
try
|
||||
{
|
||||
currentLocation = ShareeBikeApp.CenterMapToCurrentLocation
|
||||
? await GeolocationService.GetAsync()
|
||||
: null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.ForContext<SelectStationPageViewModel>().Error("Getting location failed. {Exception}", ex);
|
||||
}
|
||||
|
||||
MoveAndScale(m_oMoveToRegionDelegate, ShareeBikeApp.Uris.ActiveUri, currentLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes;
|
||||
IsConnected = ShareeBikeApp.GetIsConnected();
|
||||
var resultStationsAndBikes = await ShareeBikeApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync();
|
||||
|
||||
ShareeBikeApp.Stations = resultStationsAndBikes.Response.StationsAll;
|
||||
ShareeBikeApp.ResourceUrls = resultStationsAndBikes.GeneralData.ResourceUrls;
|
||||
|
||||
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 already any pins to the map
|
||||
// i.e detects 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, ShareeBikeApp.Flavor.GetDisplayName()),
|
||||
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, ShareeBikeApp.Flavor.GetDisplayName()),
|
||||
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.ErrorMapPageAuthcookieUndefined,
|
||||
AppResources.MessageAnswerOk);
|
||||
|
||||
IsConnected = ShareeBikeApp.GetIsConnected();
|
||||
|
||||
await ShareeBikeApp.GetConnector(IsConnected).Command.DoLogout();
|
||||
ShareeBikeApp.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.StationsAll,
|
||||
resultStationsAndBikes.Response.BikesOccupied);
|
||||
|
||||
// Update pins color form count of bikes located at station.
|
||||
UpdatePinsColor(colors);
|
||||
|
||||
|
||||
Log.ForContext<SelectStationPageViewModel>().Verbose("Update pins color done.");
|
||||
|
||||
Exception = resultStationsAndBikes.Exception;
|
||||
ActionText = string.Empty;
|
||||
IsProcessWithRunningProcessView = false;
|
||||
IsMapPageEnabled = true;
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
Log.ForContext<SelectStationPageViewModel>().Error($"An error occurred opening select station page.\r\n{l_oException.Message}");
|
||||
|
||||
IsProcessWithRunningProcessView = false;
|
||||
|
||||
await ViewService.DisplayAlert(
|
||||
AppResources.ErrorPageNotLoadedTitle,
|
||||
$"{AppResources.ErrorPageNotLoaded}\r\n{l_oException.Message}",
|
||||
AppResources.MessageAnswerOk);
|
||||
|
||||
IsMapPageEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Moves map and scales visible region depending on active filter. </summary>
|
||||
public static void MoveAndScale(
|
||||
Action<MapSpan> moveToRegionDelegate,
|
||||
Uri activeUri,
|
||||
IGeolocation 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;
|
||||
|
||||
ShareeBikeApp.SelectedStation = ShareeBikeApp.Stations.FirstOrDefault(x => x.Id == selectedStationId)
|
||||
?? new Station(selectedStationId, new List<string>(), null); // Station might not be in list StationDictinaly because this list is not updated in background task.
|
||||
|
||||
await ViewService.ShowPage("//ContactPage");
|
||||
IsMapPageEnabled = true;
|
||||
ActionText = string.Empty;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
IsMapPageEnabled = true;
|
||||
ActionText = string.Empty;
|
||||
|
||||
Log.ForContext<SelectStationPageViewModel>().Error("Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {Exception}", exception);
|
||||
|
||||
await ViewService.DisplayAlert(
|
||||
AppResources.ErrorPageNotLoadedTitle,
|
||||
$"{AppResources.ErrorPageNotLoaded}\r\n{exception.Message}",
|
||||
AppResources.MessageAnswerOk);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of station color for all stations.
|
||||
/// </summary>
|
||||
/// <param name="stationsId">Station id list to get color for.</param>
|
||||
/// <param name="stations">Station object dictionary to get count of available bike from for each station.</param>
|
||||
/// <param name="bikesReserved">Bike collection to get count of reserved/ rented bikes from for each station.</param>
|
||||
/// <returns></returns>
|
||||
internal static IList<Color> GetStationColors(
|
||||
IEnumerable<string> stationsId,
|
||||
IEnumerable<IStation> stations,
|
||||
IEnumerable<BikeInfo> bikesReserved)
|
||||
{
|
||||
if (stationsId == null)
|
||||
{
|
||||
Log.ForContext<SelectStationPageViewModel>().Debug("No stations available to update color for.");
|
||||
return new List<Color>();
|
||||
}
|
||||
|
||||
if (stations == null)
|
||||
{
|
||||
Log.ForContext<SelectStationPageViewModel>().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<SelectStationPageViewModel>().Error("No bikes info available to determine whether a pins is light blue or not.");
|
||||
}
|
||||
|
||||
// Get state for each station.
|
||||
var colors = new List<Color>();
|
||||
foreach (var stationId in stationsId)
|
||||
{
|
||||
// Get color of given station.
|
||||
if (bikesReserved?.Where(x => x.StationId == stationId).Count() > 0)
|
||||
{
|
||||
// There is at least one requested or booked bike
|
||||
colors.Add(Color.LightBlue);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stations?.FirstOrDefault(x => x.Id == stationId)?.AvailableBikesCount > 0)
|
||||
{
|
||||
// There is at least one bike available
|
||||
colors.Add(Color.Green);
|
||||
continue;
|
||||
}
|
||||
|
||||
colors.Add(Color.Red);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception which occurred getting bike information.
|
||||
/// </summary>
|
||||
public Exception Exception
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_oException;
|
||||
}
|
||||
|
||||
private set
|
||||
{
|
||||
var statusInfoText = StatusInfoText;
|
||||
m_oException = value;
|
||||
if (statusInfoText == StatusInfoText)
|
||||
{
|
||||
// Nothing to do because value did not change.
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Holds info about current action. </summary>
|
||||
private string actionText;
|
||||
|
||||
/// <summary> Holds info about current action. </summary>
|
||||
private string ActionText
|
||||
{
|
||||
get => actionText;
|
||||
set
|
||||
{
|
||||
var statusInfoText = StatusInfoText;
|
||||
actionText = value;
|
||||
if (statusInfoText == StatusInfoText)
|
||||
{
|
||||
// Nothing to do because value did not change.
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Used to block more than on copri requests at a given time.</summary>
|
||||
private bool isProcessWithRunningProcessView = false;
|
||||
|
||||
/// <summary>
|
||||
/// True if any action can be performed (request and cancel request)
|
||||
/// </summary>
|
||||
public bool IsProcessWithRunningProcessView
|
||||
{
|
||||
get => isProcessWithRunningProcessView;
|
||||
set
|
||||
{
|
||||
if (value == isProcessWithRunningProcessView)
|
||||
return;
|
||||
|
||||
Log.ForContext<SelectStationPageViewModel>().Debug($"Switch value of {nameof(isProcessWithRunningProcessView)} to {value}.");
|
||||
isProcessWithRunningProcessView = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsProcessWithRunningProcessView)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Holds information whether app is connected to web or not. </summary>
|
||||
private bool? isConnected = null;
|
||||
|
||||
/// <summary>Exposes the is connected state. </summary>
|
||||
private bool IsConnected
|
||||
{
|
||||
get => isConnected ?? false;
|
||||
set
|
||||
{
|
||||
var statusInfoText = StatusInfoText;
|
||||
isConnected = value;
|
||||
if (statusInfoText == StatusInfoText)
|
||||
{
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Holds the status information text. </summary>
|
||||
public string StatusInfoText
|
||||
{
|
||||
get
|
||||
{
|
||||
//if (Exception != null)
|
||||
//{
|
||||
// // An error occurred getting data from copri.
|
||||
// return Exception.GetShortErrorInfoText(ShareeBikeApp.IsReportLevelVerbose);
|
||||
|
||||
//}
|
||||
|
||||
return ActionText ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue