using Serilog; using System; using System.ComponentModel; using System.Text.RegularExpressions; using TINK.Model.Bikes.BikeInfoNS.BikeNS; using TINK.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Model.State; using TINK.Model.User; using TINK.MultilingualResources; using TINK.View; using Xamarin.Forms; using BikeInfoMutable = TINK.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable; namespace TINK.ViewModel.Bikes.Bike { /// /// Defines the type of BikesViewModel child items, i.e. BikesViewModel derives from ObservableCollection<BikeViewModelBase>. /// Holds references to /// - connection state services /// - copri service /// - view service /// public abstract class BikeViewModelBase { /// /// Time format for text "Gebucht seit". /// public const string TIMEFORMAT = "dd. MMMM HH:mm"; /// Provides info about the smart device (phone, tablet, ...). protected ISmartDevice SmartDevice; /// /// Reference on view service to show modal notifications and to perform navigation. /// protected IViewService ViewService { get; } /// Provides a connect object. protected Func ConnectorFactory { get; } /// Delegate to retrieve connected state. protected Func IsConnectedDelegate { get; } /// Removes bike from bikes view model. protected Action BikeRemoveDelegate { get; } /// Object to manage update of view model objects from Copri. public Func ViewUpdateManager { get; } /// /// Holds the bike to display. /// protected BikeInfoMutable Bike; /// Reference on the user protected IUser ActiveUser { get; } /// Holds the view context in which bike view model is used. protected ViewContext ViewContext { get; } /// /// Provides context related info. /// private IInUseStateInfoProvider StateInfoProvider { get; } /// View model to be used for progress report and unlocking/ locking view. protected IBikesViewModel BikesViewModel { get; } /// Delegate to open browser. private Action OpenUrlInBrowser; /// /// Notifies GUI about changes. /// public abstract event PropertyChangedEventHandler PropertyChanged; /// /// Notifies children about changed bike state. /// public abstract void OnSelectedBikeStateChanged(); /// Raises events in order to update GUI. public abstract void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs); /// /// Constructs a bike view model object. /// /// Provides info about the smart device (phone, tablet, ...). /// Bike to be displayed. /// Object holding logged in user or an empty user object. /// Holds the view context in which bike view model is used. /// Provides in use state information. /// View model to be used for progress report and unlocking/ locking view. /// Delegate to open browser. public BikeViewModelBase( Func isConnectedDelegate, Func connectorFactory, Action bikeRemoveDelegate, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, BikeInfoMutable selectedBike, IUser activeUser, ViewContext viewContext, IInUseStateInfoProvider stateInfoProvider, IBikesViewModel bikesViewModel, Action openUrlInBrowser) { IsConnectedDelegate = isConnectedDelegate; ConnectorFactory = connectorFactory; BikeRemoveDelegate = bikeRemoveDelegate; ViewUpdateManager = viewUpdateManager; SmartDevice = smartDevice; ViewService = viewService; Bike = selectedBike ?? throw new ArgumentException(string.Format("Can not construct {0}- object, bike object is null.", typeof(BikeViewModelBase))); ActiveUser = activeUser ?? throw new ArgumentException(string.Format("Can not construct {0}- object, user object is null.", typeof(BikeViewModelBase))); ViewContext = viewContext; StateInfoProvider = stateInfoProvider ?? throw new ArgumentException(string.Format("Can not construct {0}- object, user object is null.", typeof(IInUseStateInfoProvider))); selectedBike.PropertyChanged += (sender, eventargs) => OnSelectedBikePropertyChanged(eventargs.PropertyName); var battery = selectedBike.Drive?.Battery; if (battery != null) { battery.PropertyChanged += (_, args) => { if (args.PropertyName == nameof(BatteryMutable.CurrentChargeBars)) { RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(CurrentChargeBars))); } }; } BikesViewModel = bikesViewModel ?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null."); OpenUrlInBrowser = openUrlInBrowser ?? (url => { Log.ForContext().Error($"No browse service available to open {url}."); }); } /// /// Handles BikeInfoMutable events. /// Helper member to raise events. Maps model event change notification to view model events. /// /// private void OnSelectedBikePropertyChanged(string nameOfProp) { if (nameOfProp == nameof(State)) { OnSelectedBikeStateChanged(); // Notify derived class about change of state. } var state = State; if (LastState != state) { RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(State))); LastState = state; } var stateText = StateText; if (LastStateText != stateText) { RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(StateText))); LastStateText = stateText; } var stateColor = StateColor; if (LastStateColor != stateColor) { RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(StateColor))); LastStateColor = stateColor; } } /// /// Gets the display name of the bike containing of bike id and type of bike.. /// public string Name => Bike.GetDisplayName(); public string TypeOfBike => Bike.GetDisplayTypeOfBike(); /// /// Gets whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations). /// public AaRideType? AaRideType => Bike.AaRideType; public string WheelType => Bike.GetDisplayWheelType(); /// /// Gets the unique Id of bike or an empty string, if no name is defined to avoid duplicate display of id. /// public string DisplayId => Bike.GetDisplayId(); /// /// Gets the unique Id of bike used by derived model to determine which bike to remove. /// public string Id => Bike.Id; public string StationId => $"Station {Bike.StationId}"; public string DisplayName => Bike.GetDisplayName(); public bool IsBikeWithCopriLock => Bike.LockModel == Model.Bikes.BikeInfoNS.BikeNS.LockModel.Sigo; /// Returns if type of bike is a cargo pedelec bike. public bool IsBatteryChargeVisible => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec && (!Bike.Drive.Battery.IsHidden.HasValue /* no value means show battery level */ || Bike.Drive.Battery.IsHidden.Value == false); /// Gets the image path for bike type City bike, CargoLong, Trike or Pedelec. public string DisplayedBikeImageSourceString => $"bike_{Bike.TypeOfBike}_{Bike.Drive.Type}_{Bike.WheelType}.png"; /// /// Gets the current charge level. /// public string CurrentChargeBars => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec ? Bike.Drive.Battery.CurrentChargeBars?.ToString() ?? string.Empty : string.Empty; /// /// Gets the value if current charge level is low ( <= 1 ). /// public bool IsCurrentChargeLow => this.CurrentChargeBars == "1" || this.CurrentChargeBars == "0"; /// /// Gets the current charge level. /// public string MaxChargeBars => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec ? Bike.Drive.Battery.MaxChargeBars?.ToString() ?? string.Empty : string.Empty; /// /// Returns status of a bike as text (binds to GUI). /// /// Log invalid states for diagnose purposes. public string StateText { get { switch (Bike.State.Value) { case InUseStateEnum.FeedbackPending: return AppResources.StatusTextFeedbackPending; case InUseStateEnum.Disposable: return AppResources.StatusTextAvailable; } if (!ActiveUser.IsLoggedIn) { // Nobody is logged in. switch (Bike.State.Value) { case InUseStateEnum.Reserved: return GetReservedInfo( Bike.State.RemainingTime, Bike.StationId, null); // Hide reservation code because no one but active user should see code case InUseStateEnum.Booked: return GetBookedInfo( Bike.State.From, Bike.StationId, null); // Hide reservation code because no one but active user should see code default: return string.Format("Unbekannter status {0}.", Bike.State.Value); } } switch (Bike.State.Value) { case InUseStateEnum.Reserved: return Bike.State.MailAddress == ActiveUser.Mail ? GetReservedInfo( Bike.State.RemainingTime, Bike.StationId, Bike.State.Code) : "Fahrrad bereits reserviert durch anderen Nutzer."; case InUseStateEnum.Booked: return Bike.State.MailAddress == ActiveUser.Mail ? GetBookedInfo( Bike.State.From, Bike.StationId, Bike.State.Code) : "Fahrrad bereits gebucht durch anderen Nutzer."; default: return string.Format("Unbekannter status {0}.", Bike.State.Value); } } } /// Gets the value of property when PropertyChanged was fired. private string LastStateText { get; set; } /// /// Gets reserved into display text. /// /// Log unexpected states. /// /// Display text private string GetReservedInfo( TimeSpan? p_oRemainingTime, string p_strStation = null, string p_strCode = null) { return StateInfoProvider.GetReservedInfo(p_oRemainingTime, p_strStation, p_strCode); } /// /// Gets booked into display text. /// /// Log unexpected states. /// /// Display text private string GetBookedInfo( DateTime? p_oFrom, string p_strStation = null, string p_strCode = null) { return StateInfoProvider.GetBookedInfo(p_oFrom, p_strStation, p_strCode); } /// /// Exposes the bike state. /// public InUseStateEnum State => Bike.State.Value; /// Gets the value of property when PropertyChanged was fired. public InUseStateEnum LastState { get; set; } /// /// Gets the color which visualizes the state of bike in relation to logged in user. /// public Color StateColor { get { if (!ActiveUser.IsLoggedIn) { return Color.Default; } var l_oSelectedBikeState = Bike.State; switch (l_oSelectedBikeState.Value) { case InUseStateEnum.Reserved: return l_oSelectedBikeState.MailAddress == ActiveUser.Mail ? InUseStateEnum.Reserved.GetColor() : Color.Red; // Bike is reserved by someone else case InUseStateEnum.Booked: return l_oSelectedBikeState.MailAddress == ActiveUser.Mail ? InUseStateEnum.Booked.GetColor() : Color.Red; // Bike is booked by someone else default: return Color.Default; } } } /// Holds description about the tariff. public TariffDescriptionViewModel TariffDescription => new TariffDescriptionViewModel(Bike.TariffDescription); /// Gets the value of property when PropertyChanged was fired. public Color LastStateColor { get; set; } /// Gets first url from text. /// url to extract text from. /// Gets first url or an empty string if on url is contained in text. public static string GetUrlFirstOrDefault(string htmlSource) { if (string.IsNullOrEmpty(htmlSource)) return string.Empty; try { var matches = new Regex(@"https://[-a-zA-Z0-9+&@#/%?=~_|!:, .;]*[-a-zA-Z0-9+&@#/%=~_|]").Matches(htmlSource); return matches.Count > 0 ? matches[0].Value : string.Empty; } catch (Exception e) { Log.ForContext().Error("Extracting URL failed. {Exception}", e); return string.Empty; } } /// /// Check if bike has to be removed and if yes invoke remove delegate. /// /// Id of bike to remove. /// Previous state used to decide whether to remove bike or not. public void CheckRemoveBike(string Id, InUseStateEnum lastState) { switch (ViewContext.Page) { case PageContext.MyBikes: // Bike is shown on page My Bikes. switch (Bike.State.Value) { case InUseStateEnum.FeedbackPending: case InUseStateEnum.Reserved: case InUseStateEnum.Booked: // Bike has still to be shown at my bikes page to give feedback or manage bike. break; default: BikeRemoveDelegate(Id); break; } break; case PageContext.BikesAtStation: // Bike is shown on page Bike At Station. switch (lastState != InUseStateEnum.Booked) { case true: // Only remove bike if bike was rented before. break; default: switch (ViewContext.StationId == Bike.StationId) { case true: // Do not remove bike if bike is returned a current station. break; default: BikeRemoveDelegate(Id); break; } break; } break; } } } }