mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2025-06-21 13:36:28 +02:00
Version 3.0.381
This commit is contained in:
parent
f963c0a219
commit
3a363acf3a
1525 changed files with 60589 additions and 125098 deletions
14
SharedBusinessLogic/CSharp9.cs
Normal file
14
SharedBusinessLogic/CSharp9.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
// ReSharper disable once CheckNamespace
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
#if USCSHARP9
|
||||
/// <summary>
|
||||
/// Reserved to be used by the compiler for tracking metadata.
|
||||
/// This class should not be used by developers in source code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class IsExternalInit
|
||||
{
|
||||
}
|
||||
#endif
|
||||
}
|
54
SharedBusinessLogic/Model/Bikes/BikeCollection.cs
Normal file
54
SharedBusinessLogic/Model/Bikes/BikeCollection.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using System.Linq;
|
||||
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
|
||||
namespace ShareeBike.Model.Bikes
|
||||
{
|
||||
public class BikeCollection : IBikeDictionary<BikeInfo>
|
||||
{
|
||||
/// <summary> Holds the bike dictionary object.</summary>
|
||||
private Dictionary<string, BikeInfo> BikeDictionary { get; }
|
||||
|
||||
/// <summary>Constructs an empty bike info dictionary object.</summary>
|
||||
public BikeCollection()
|
||||
{
|
||||
BikeDictionary = new Dictionary<string, BikeInfo>();
|
||||
}
|
||||
|
||||
/// <summary> Constructs a bike collection object.</summary>
|
||||
/// <param name="bikeDictionary"></param>
|
||||
public BikeCollection(Dictionary<string, BikeInfo> bikeDictionary)
|
||||
{
|
||||
BikeDictionary = bikeDictionary ??
|
||||
throw new ArgumentNullException(nameof(bikeDictionary), "Can not construct BikeCollection object.");
|
||||
}
|
||||
|
||||
/// <summary> Gets a bike by its id.</summary>
|
||||
/// <param name="id">Id of the bike to get.</param>
|
||||
/// <returns></returns>
|
||||
public BikeInfo GetById(string id)
|
||||
{
|
||||
return BikeDictionary.FirstOrDefault(x => x.Key == id).Value;
|
||||
}
|
||||
|
||||
/// <summary> Gets the count of bikes. </summary>
|
||||
public int Count => BikeDictionary.Count;
|
||||
|
||||
/// <summary> Gets if a bike with given id exists.</summary>
|
||||
/// <param name="id">Id of bike.</param>
|
||||
/// <returns>True if bike is contained, false otherwise.</returns>
|
||||
public bool ContainsKey(string id) => BikeDictionary.Keys.Contains(id);
|
||||
|
||||
/// <summary> Gets the enumerator. </summary>
|
||||
/// <returns>Enumerator object.</returns>
|
||||
public IEnumerator<BikeInfo> GetEnumerator() => BikeDictionary.Values.GetEnumerator();
|
||||
|
||||
/// <summary> Gets the enumerator. </summary>
|
||||
/// <returns>Enumerator object.</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
30
SharedBusinessLogic/Model/Bikes/BikeCollectionFilter.cs
Normal file
30
SharedBusinessLogic/Model/Bikes/BikeCollectionFilter.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ShareeBike.Model.Bikes;
|
||||
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
public static class BikeCollectionFilter
|
||||
{
|
||||
/// <summary> Filters bikes by station. </summary>
|
||||
/// <param name="bikesAtAnyStation">Bikes available, requested and/ or occupied bikes to filter.</param>
|
||||
/// <param name="selectedStation">Id of station, might be null</param>
|
||||
/// <returns>BikeCollection holding bikes at given station or empty BikeCollection, if there are no bikes.</returns>
|
||||
public static BikeCollection GetAtStation(
|
||||
this BikeCollection bikesAtAnyStation,
|
||||
string selectedStation)
|
||||
=> new BikeCollection(bikesAtAnyStation?
|
||||
.Where(bike => !string.IsNullOrEmpty(selectedStation) && bike.StationId == selectedStation)
|
||||
.ToDictionary(bike => bike.Id) ?? new Dictionary<string, BikeInfo>());
|
||||
|
||||
/// <summary> Filters bikes by bike type. </summary>
|
||||
/// <param name="bcAndLockItBikes">Bikes available, requested and/ or occupied bikes to filter.</param>
|
||||
/// <returns>BikeCollection holding LockIt-bikes empty BikeCollection, if there are no LockIt-bikes.</returns>
|
||||
public static BikeCollection GetLockIt(this BikeCollection bcAndLockItBikes)
|
||||
=> new BikeCollection(bcAndLockItBikes?
|
||||
.Where(bike => bike is Bikes.BikeInfoNS.BluetoothLock.BikeInfo)
|
||||
.ToDictionary(x => x.Id) ?? new Dictionary<string, BikeInfo>());
|
||||
}
|
||||
}
|
228
SharedBusinessLogic/Model/Bikes/BikeCollectionMutable.cs
Normal file
228
SharedBusinessLogic/Model/Bikes/BikeCollectionMutable.cs
Normal file
|
@ -0,0 +1,228 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
using ShareeBike.ViewModel;
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
|
||||
|
||||
namespace ShareeBike.Model.Bikes
|
||||
{
|
||||
/// <summary> Holds entity of bikes. </summary>
|
||||
public class BikeCollectionMutable : ObservableCollection<BikeInfoMutable>, IBikeDictionaryMutable<BikeInfoMutable>
|
||||
{
|
||||
/// <summary> Provides a connector object.</summary>
|
||||
private Func<bool, IConnector> ConnectorFactory { get; }
|
||||
|
||||
/// <summary> Provides geolocation information.</summary>
|
||||
private IGeolocationService GeolocationService { get; }
|
||||
|
||||
/// <summary> Provides a connector object.</summary>
|
||||
private ILocksService LockService { get; }
|
||||
|
||||
/// <summary> Delegate to retrieve connected state. </summary>
|
||||
private Func<bool> IsConnectedDelegate { get; }
|
||||
|
||||
/// <summary>Object to manage update of view model objects from Copri.</summary>
|
||||
protected Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
|
||||
|
||||
/// <summary> Constructs a mutable bike collection object. </summary>
|
||||
/// <param name="geolocation">Provides geolocation information.</param>
|
||||
/// <param name="lockService">Manages lock.</param>
|
||||
/// <param name="isConnectedDelegate">Delegate to retrieve connected state.</param>
|
||||
/// <param name="connectorFactory">Provides a connector object.</param>
|
||||
public BikeCollectionMutable(
|
||||
IGeolocationService geolocation,
|
||||
ILocksService lockService,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
Func<IPollingUpdateTaskManager> viewUpdateManager)
|
||||
{
|
||||
SelectedBike = null;
|
||||
|
||||
GeolocationService = geolocation
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeCollectionMutable)}- object. No geolocation object available.");
|
||||
|
||||
LockService = lockService
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeCollectionMutable)}- object. No lock service object available.");
|
||||
|
||||
IsConnectedDelegate = isConnectedDelegate
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeCollectionMutable)}- object. No is connected delegate available.");
|
||||
|
||||
ConnectorFactory = connectorFactory
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeCollectionMutable)}- object. No connector available.");
|
||||
|
||||
ViewUpdateManager = viewUpdateManager
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeCollectionMutable)}- object. No view update manager available.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates bikes dictionary from bikes response, i.e.
|
||||
/// - removes bikes which are no more contained in bikes response
|
||||
/// - updates state of all bikes from state contained in bikes response
|
||||
/// </summary>
|
||||
/// <param name="bikesAll"> Object holding bikes info from copri to update from. Holds station id but not station name.</param>
|
||||
/// <param name="stations"> All stations to get station names from.</param>
|
||||
/// <param name="p_oDateTimeProvider">Provides date time information.</param>
|
||||
public void Update(
|
||||
IEnumerable<BikeInfo> bikesAll,
|
||||
IEnumerable<IStation> stations)
|
||||
{
|
||||
// Get list of current bikes by state(s) to update.
|
||||
// Needed to remove bikes which switched state and have to be removed from collection.
|
||||
var bikesToBeRemoved = this.Select(x => x.Id).ToList();
|
||||
|
||||
foreach (var bikeInfo in bikesAll ?? new List<BikeInfo>())
|
||||
{
|
||||
// Get name of station form station id.
|
||||
var stationName = stations?.FirstOrDefault(x => x.Id == bikeInfo.StationId)?.StationName ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(stationName))
|
||||
Log.ForContext<BikeCollectionMutable>().Debug($"No name for station with id {bikeInfo.StationId} found.");
|
||||
|
||||
// Check if bike has to be added to list of existing station.
|
||||
if (ContainsKey(bikeInfo.Id) == false)
|
||||
{
|
||||
var bikeInfoMutable = BikeInfoMutableFactory.Create(
|
||||
GeolocationService,
|
||||
LockService,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
ViewUpdateManager,
|
||||
bikeInfo,
|
||||
stationName);
|
||||
if (bikeInfoMutable != null)
|
||||
{
|
||||
// Bike does not yet exist in list of bikes.
|
||||
Add(bikeInfoMutable);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update bike.
|
||||
var updateTarget = GetById(bikeInfo.Id);
|
||||
updateTarget.State.Load(bikeInfo.State);
|
||||
updateTarget.DataSource = bikeInfo.DataSource;
|
||||
|
||||
if (bikesToBeRemoved.Contains<string>(bikeInfo.Id))
|
||||
{
|
||||
// Remove list from obsolete list.
|
||||
bikesToBeRemoved.Remove(bikeInfo.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove obsolete bikes.
|
||||
foreach (var stationId in bikesToBeRemoved)
|
||||
{
|
||||
RemoveById(stationId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new bike to collection of bike.
|
||||
/// </summary>
|
||||
/// <param name="newBike">New bike to add.</param>
|
||||
/// <exception cref="Exception">Thrown if bike is not unique.</exception>
|
||||
public new void Add(BikeInfoMutable newBike)
|
||||
{
|
||||
// Ensure that bike id of new bike is unique
|
||||
foreach (BikeInfoMutable bike in Items)
|
||||
{
|
||||
if (bike.Id == newBike.Id)
|
||||
{
|
||||
throw new Exception(string.Format("Can not add bike with {0} to collection ob bike. Id is not unique.", newBike));
|
||||
}
|
||||
}
|
||||
|
||||
base.Add(newBike);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bike selected by user for reserving or cancel reservation.
|
||||
/// </summary>
|
||||
public BikeInfoMutable SelectedBike
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void SetSelectedBike(string id)
|
||||
{
|
||||
SelectedBike = GetById(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a bike by its id.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
public BikeInfoMutable GetById(string id)
|
||||
=> this.FirstOrDefault(bike => bike.Id == id);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a bike by given key exists.
|
||||
/// </summary>
|
||||
/// <param name="p_strKey">Key to check.</param>
|
||||
/// <returns>True if bike exists.</returns>
|
||||
public bool ContainsKey(string id)
|
||||
=> GetById(id) != null;
|
||||
|
||||
/// <summary>
|
||||
/// Removes a bike by its id.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of bike to be removed.</param>
|
||||
public void RemoveById(string id)
|
||||
{
|
||||
var l_oBike = GetById(id);
|
||||
if (l_oBike == null)
|
||||
{
|
||||
// Nothing to do if bike does not exists.
|
||||
return;
|
||||
}
|
||||
|
||||
Remove(l_oBike);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create mutable objects from immutable objects.
|
||||
/// </summary>
|
||||
public static class BikeInfoMutableFactory
|
||||
{
|
||||
/// <param name="viewUpdateManager">Manages update of view model objects from Copri.</param>
|
||||
public static BikeInfoMutable Create(
|
||||
IGeolocationService geolocation,
|
||||
ILocksService lockService,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
Func<IPollingUpdateTaskManager> viewUpdateManager,
|
||||
BikeInfo bikeInfo,
|
||||
string stationName)
|
||||
{
|
||||
if (bikeInfo is BikeInfoNS.BluetoothLock.BikeInfo btBikeInfo)
|
||||
{
|
||||
return new BikeInfoNS.BluetoothLock.BikeInfoMutable(
|
||||
geolocation,
|
||||
lockService,
|
||||
isConnectedDelegate,
|
||||
connectorFactory,
|
||||
viewUpdateManager,
|
||||
btBikeInfo,
|
||||
stationName);
|
||||
}
|
||||
else if (bikeInfo is BikeInfoNS.CopriLock.BikeInfo copriBikeInfo)
|
||||
{
|
||||
return new BikeInfoNS.CopriLock.BikeInfoMutable(copriBikeInfo, stationName);
|
||||
}
|
||||
|
||||
// Unsupported type detected.
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
SharedBusinessLogic/Model/Bikes/BikeCollectionUpdater.cs
Normal file
38
SharedBusinessLogic/Model/Bikes/BikeCollectionUpdater.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShareeBike.Model.Bikes
|
||||
{
|
||||
public static class BikeCollectionUpdater
|
||||
{
|
||||
/// <summary> Updates bikes lock info with the latest lock info from bluetooth service.</summary>
|
||||
/// <param name="bikes">bikes to be updated.</param>
|
||||
/// <param name="locksInfo">locks info to be used for updating bikes.</param>
|
||||
/// <returns></returns>
|
||||
public static BikeCollection UpdateLockInfo(
|
||||
this BikeCollection bikes,
|
||||
IEnumerable<Bikes.BikeInfoNS.BluetoothLock.LockInfo> locksInfo)
|
||||
{
|
||||
|
||||
var updatedBikesCollection = new Dictionary<string, Bikes.BikeInfoNS.BC.BikeInfo>();
|
||||
|
||||
foreach (var bikeInfo in bikes)
|
||||
{
|
||||
if (!(bikeInfo is BikeInfoNS.BluetoothLock.BikeInfo bluetoothBikeInfo))
|
||||
{
|
||||
// No processing needed because bike is not a bluetooth bike
|
||||
updatedBikesCollection.Add(bikeInfo.Id, bikeInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if to update current bike lock info using state from bluetooth service or not.
|
||||
var currentLockInfo = locksInfo.FirstOrDefault(x => x.Id == bluetoothBikeInfo.LockInfo.Id) // Update bike info with latest info from bluethooth service if available
|
||||
?? bluetoothBikeInfo.LockInfo; // Use lock info state object from copri which holds a lock id and a state of value unknown.
|
||||
|
||||
updatedBikesCollection.Add(bluetoothBikeInfo.Id, new BikeInfoNS.BluetoothLock.BikeInfo(bluetoothBikeInfo, currentLockInfo));
|
||||
}
|
||||
|
||||
return new BikeCollection(updatedBikesCollection);
|
||||
}
|
||||
}
|
||||
}
|
112
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BC/BikeInfo.cs
Normal file
112
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BC/BikeInfo.cs
Normal file
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.State;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
|
||||
{
|
||||
public abstract class BikeInfo : IBikeInfo
|
||||
{
|
||||
/// <summary> Default value of demo property. </summary>
|
||||
public const bool DEFAULTVALUEISDEMO = false;
|
||||
|
||||
/// <summary> Holds the info about the bike state. </summary>
|
||||
private readonly IStateInfo _StateInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the bike object.
|
||||
/// </summary>
|
||||
public Bike Bike { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the drive object.
|
||||
/// </summary>
|
||||
public DriveMutable Drive { get; }
|
||||
|
||||
/// <summary> Gets the information where the data origins from. </summary>
|
||||
public DataSource DataSource { get; }
|
||||
|
||||
/// <summary> Constructs a bike object.</summary>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
protected BikeInfo(
|
||||
IStateInfo stateInfo,
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null,
|
||||
string stationId = null,
|
||||
Uri operatorUri = null,
|
||||
RentalDescription tariffDescription = null)
|
||||
{
|
||||
Bike = bike ?? throw new ArgumentNullException(nameof(bike));
|
||||
Drive = drive ?? throw new ArgumentNullException(nameof(drive));
|
||||
DataSource = dataSource;
|
||||
_StateInfo = stateInfo;
|
||||
|
||||
IsDemo = isDemo ?? DEFAULTVALUEISDEMO;
|
||||
Group = group ?? new List<string>();
|
||||
StationId = stationId;
|
||||
OperatorUri = operatorUri;
|
||||
TariffDescription = tariffDescription;
|
||||
}
|
||||
|
||||
public BikeInfo(BikeInfo bikeInfo) : this(
|
||||
bikeInfo != null ? bikeInfo?.State : throw new ArgumentNullException(nameof(bikeInfo)),
|
||||
bikeInfo.Bike,
|
||||
bikeInfo.Drive,
|
||||
bikeInfo.DataSource,
|
||||
bikeInfo.IsDemo,
|
||||
bikeInfo.Group,
|
||||
bikeInfo.StationId,
|
||||
bikeInfo.OperatorUri,
|
||||
bikeInfo.TariffDescription)
|
||||
{ }
|
||||
|
||||
/// <summary> True if device is demo device, false otherwise. </summary>
|
||||
public bool IsDemo { get; }
|
||||
|
||||
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
|
||||
public IEnumerable<string> Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Station a which bike is located, null otherwise.
|
||||
/// </summary>
|
||||
public string StationId { get; }
|
||||
|
||||
/// <summary> Holds description about the tariff. </summary>
|
||||
public RentalDescription TariffDescription { get; }
|
||||
|
||||
/// Holds the rent state of the bike.
|
||||
/// </summary>
|
||||
public IStateInfo State
|
||||
{
|
||||
get { return _StateInfo; }
|
||||
}
|
||||
|
||||
public string Id => Bike.Id;
|
||||
|
||||
public WheelType? WheelType => Bike.WheelType;
|
||||
|
||||
public TypeOfBike? TypeOfBike => Bike.TypeOfBike;
|
||||
|
||||
/// <summary> Gets the model of the lock. </summary>
|
||||
public LockModel LockModel => Bike.LockModel;
|
||||
|
||||
public string Description => Bike.Description;
|
||||
|
||||
/// <summary>
|
||||
/// Uri of the operator or null, in case of single operator setup.
|
||||
/// </summary>
|
||||
public Uri OperatorUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the instance to text.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Id={Bike.Id}{(Bike.WheelType != null ? $", wheel(s)={Bike.WheelType}" : string.Empty)}{(Bike.TypeOfBike != null ? $"type={Bike.TypeOfBike}" : "")}, state={State}, location={(!string.IsNullOrEmpty(StationId) ? $"Station {StationId}" : "On the road")}, is demo={IsDemo}.";
|
||||
}
|
||||
}
|
||||
}
|
157
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BC/BikeInfoMutable.cs
Normal file
157
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BC/BikeInfoMutable.cs
Normal file
|
@ -0,0 +1,157 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.State;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
|
||||
{
|
||||
[DataContract]
|
||||
public abstract class BikeInfoMutable : IBikeInfoMutable, INotifyPropertyChanged
|
||||
{
|
||||
/// <summary> Holds the bike. </summary>
|
||||
private readonly Bike _Bike;
|
||||
|
||||
/// <summary> Holds the drive of the bike. </summary>
|
||||
private readonly DriveMutable _Drive;
|
||||
|
||||
/// <summary> Holds the state info of the bike. </summary>
|
||||
private readonly StateInfoMutable _StateInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bike info object.
|
||||
/// </summary>
|
||||
/// <param name="isDemo">True if device is demo device, false otherwise.</param>
|
||||
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
|
||||
/// <param name="stationId">Name of station where bike is located, null if bike is on the road.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
/// <param name="stateInfo">Bike state info.</param>
|
||||
protected BikeInfoMutable(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
bool isDemo = BikeInfo.DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null,
|
||||
string stationId = null,
|
||||
string stationName = null,
|
||||
Uri operatorUri = null,
|
||||
IRentalDescription tariffDescription = null,
|
||||
Func<DateTime> dateTimeProvider = null,
|
||||
IStateInfo stateInfo = null)
|
||||
{
|
||||
IsDemo = isDemo;
|
||||
Group = group;
|
||||
_Bike = bike;
|
||||
_Drive = drive;
|
||||
DataSource = dataSource;
|
||||
_StateInfo = new StateInfoMutable(dateTimeProvider, stateInfo);
|
||||
_StateInfo.PropertyChanged += (sender, eventargs) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(eventargs.PropertyName));
|
||||
StationId = stationId;
|
||||
StationName = stationName;
|
||||
OperatorUri = operatorUri;
|
||||
TariffDescription = tariffDescription;
|
||||
}
|
||||
|
||||
/// <summary> Id of station a which bike is located, null otherwise.</summary>
|
||||
[DataMember]
|
||||
public string StationId { get; private set; }
|
||||
|
||||
/// <summary> Name of station a which bike is located, null otherwise. </summary>
|
||||
[DataMember]
|
||||
public string StationName { get; }
|
||||
|
||||
/// <summary> Holds description about the tariff. </summary>
|
||||
[DataMember]
|
||||
public IRentalDescription TariffDescription { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the rent state of the bike.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public StateInfoMutable State
|
||||
{
|
||||
get { return _StateInfo; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uri of the operator or null, in case of single operator setup.
|
||||
/// </summary>
|
||||
public Uri OperatorUri { get; }
|
||||
|
||||
/// <summary> Unused member. </summary>
|
||||
IStateInfoMutable IBikeInfoMutable.State => _StateInfo;
|
||||
|
||||
public string Id => _Bike.Id;
|
||||
|
||||
public bool IsDemo { get; }
|
||||
|
||||
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
|
||||
public IEnumerable<string> Group { get; }
|
||||
|
||||
public WheelType? WheelType => _Bike.WheelType;
|
||||
|
||||
public TypeOfBike? TypeOfBike => _Bike.TypeOfBike;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public AaRideType? AaRideType => _Bike.AaRideType;
|
||||
|
||||
|
||||
public LockModel LockModel => _Bike.LockModel;
|
||||
|
||||
public string Description => _Bike.Description;
|
||||
|
||||
public DriveMutable Drive => _Drive;
|
||||
|
||||
/// <summary>
|
||||
/// Fired whenever property of bike changes.
|
||||
/// </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private DataSource _DataSource = DataSource.Copri;
|
||||
|
||||
/// <summary> Gets or sets the information where the data origins from. </summary>
|
||||
public DataSource DataSource
|
||||
{
|
||||
get => _DataSource;
|
||||
set
|
||||
{
|
||||
if (_DataSource == value)
|
||||
return;
|
||||
|
||||
_DataSource = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DataSource)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Loads a bike object from copri server booking_cancel (cancel reservation)/ booking_update (return bike) response.</summary>
|
||||
/// <param name="bike">Bike object to load response into.</param>
|
||||
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
|
||||
/// <param name="stationId">Id of the station if bike station changed, null otherwise.</param>
|
||||
public void Load(
|
||||
NotifyPropertyChangedLevel notifyLevel,
|
||||
string stationId = null)
|
||||
{
|
||||
State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel);
|
||||
if (stationId == null)
|
||||
{
|
||||
// Station did not change.
|
||||
return;
|
||||
}
|
||||
|
||||
StationId = stationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the instance to text.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $", type={TypeOfBike}" : "")}, demo={IsDemo}, state={State.ToString()}, location={(!string.IsNullOrEmpty(StationId) ? $"Station {StationId}" : "On the road")}.";
|
||||
}
|
||||
}
|
||||
}
|
65
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BC/IBikeInfo.cs
Normal file
65
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BC/IBikeInfo.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.State;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows to access bike info.
|
||||
/// </summary>
|
||||
public interface IBikeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the bike object.
|
||||
/// </summary>
|
||||
BikeNS.Bike Bike { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the drive.
|
||||
/// </summary>
|
||||
DriveMutable Drive { get; }
|
||||
|
||||
/// <summary> Gets or sets the information where the data origins from. </summary>
|
||||
DataSource DataSource { get; }
|
||||
|
||||
/// <summary> True if bike is a demo bike. </summary>
|
||||
bool IsDemo { get; }
|
||||
|
||||
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
|
||||
IEnumerable<string> Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Station a which bike is located, null otherwise.
|
||||
/// </summary>
|
||||
string StationId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Uri of the operator or null, in case of single operator setup.
|
||||
/// </summary>
|
||||
Uri OperatorUri { get; }
|
||||
|
||||
/// <summary> Holds description about the tariff. </summary>
|
||||
RentalDescription TariffDescription { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the rent state of the bike.
|
||||
/// </summary>
|
||||
IStateInfo State { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Origin of the data.
|
||||
/// </summary>
|
||||
public enum DataSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Data source copri.
|
||||
/// </summary>
|
||||
Copri,
|
||||
/// <summary>
|
||||
/// Data source cache.
|
||||
/// </summary>
|
||||
Cache
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.State;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BC
|
||||
{
|
||||
public interface IBikeInfoMutable
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the unique id of the bike;
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary> True if bike is a demo bike. </summary>
|
||||
bool IsDemo { get; }
|
||||
|
||||
/// <summary> Returns the group (ShareeBike, Citybike, ...). </summary>
|
||||
IEnumerable<string> Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the count of wheels.
|
||||
/// </summary>
|
||||
WheelType? WheelType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the type of bike.
|
||||
/// </summary>
|
||||
TypeOfBike? TypeOfBike { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
AaRideType? AaRideType { get; }
|
||||
|
||||
/// <summary> Gets the model of the lock. </summary>
|
||||
LockModel LockModel { get; }
|
||||
|
||||
/// <summary> Holds the description of the bike. </summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Station a which bike is located, null otherwise.
|
||||
/// </summary>
|
||||
string StationId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the rent state of the bike.
|
||||
/// </summary>
|
||||
IStateInfoMutable State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Uri of the operator or null, in case of single operator setup.
|
||||
/// </summary>
|
||||
Uri OperatorUri { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Hold the drive object.
|
||||
/// </summary>
|
||||
DriveMutable Drive { get; }
|
||||
|
||||
/// <summary> Gets or sets the information where the data origins from. </summary>
|
||||
DataSource DataSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rental description.
|
||||
/// </summary>
|
||||
IRentalDescription TariffDescription { get; }
|
||||
|
||||
/// <summary> Loads a bike object from copri server booking_cancel (cancel reservation)/ booking_update (return bike) response.</summary>
|
||||
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
|
||||
/// <param name="stationId">Id of the station if bike station changed, null otherwise.</param>
|
||||
void Load(
|
||||
NotifyPropertyChangedLevel notifyLevel,
|
||||
string stationId = null);
|
||||
|
||||
event PropertyChangedEventHandler PropertyChanged;
|
||||
}
|
||||
|
||||
public enum NotifyPropertyChangedLevel
|
||||
{
|
||||
/// <summary> Notify about all property changes.</summary>
|
||||
All,
|
||||
|
||||
/// <summary> Notify about no property changes.</summary>
|
||||
None
|
||||
}
|
||||
}
|
159
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BikeNS/Bike.cs
Normal file
159
SharedBusinessLogic/Model/Bikes/BikeInfoNS/BikeNS/Bike.cs
Normal file
|
@ -0,0 +1,159 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS
|
||||
{
|
||||
/// <summary> Count of wheels. </summary>
|
||||
/// <remarks> Numeric values of enum must match count of wheels</remarks>
|
||||
public enum WheelType
|
||||
{
|
||||
Mono = 1,
|
||||
Two = 2,
|
||||
Trike = 3,
|
||||
Quad = 4
|
||||
}
|
||||
|
||||
/// <summary> Type of bike. </summary>
|
||||
public enum TypeOfBike
|
||||
{
|
||||
Allround = 0,
|
||||
Cargo = 1,
|
||||
City = 2,
|
||||
}
|
||||
|
||||
/// <summary> Holds 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).</summary>
|
||||
public enum AaRideType
|
||||
{
|
||||
NoAaRide = 0,
|
||||
AaRide = 1,
|
||||
}
|
||||
|
||||
/// <summary> Holds the model of lock. </summary>
|
||||
public enum LockModel
|
||||
{
|
||||
ILockIt, // haveltec GbmH Brandenburg, Germany bluetooth lock
|
||||
BordComputer, // TeilRad BC
|
||||
Sigo, // Sigo GmbH Darmstadt, Germany bike lock
|
||||
}
|
||||
|
||||
/// <summary> Holds the type of lock. </summary>
|
||||
public enum LockType
|
||||
{
|
||||
Backend, // Backend, i.e. COPRI controls lock (open, close, ...)
|
||||
Bluethooth, // Lock is controlled.
|
||||
}
|
||||
|
||||
public class Bike : IEquatable<Bike>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a bike.
|
||||
/// </summary>
|
||||
/// <param name="id">Unique id of bike.</param>
|
||||
public Bike(
|
||||
string id,
|
||||
LockModel lockModel,
|
||||
WheelType? wheelType = null,
|
||||
TypeOfBike? typeOfBike = null,
|
||||
AaRideType? aaRideType = null,
|
||||
string description = null)
|
||||
{
|
||||
WheelType = wheelType;
|
||||
TypeOfBike = typeOfBike;
|
||||
AaRideType = aaRideType;
|
||||
LockModel = lockModel;
|
||||
Id = id;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the unique id of the bike;
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the count of wheels.
|
||||
/// </summary>
|
||||
public WheelType? WheelType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the type of bike.
|
||||
/// </summary>
|
||||
public TypeOfBike? TypeOfBike { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public AaRideType? AaRideType { get; }
|
||||
|
||||
/// <summary> Gets the model of the lock. </summary>
|
||||
public LockModel LockModel { get; private set; }
|
||||
|
||||
/// <summary> Holds the description of the bike. </summary>
|
||||
public string Description { get; }
|
||||
|
||||
|
||||
/// <summary> Compares two bike object.</summary>
|
||||
/// <param name="obj">Object to compare with.</param>
|
||||
/// <returns>True if bikes are equal.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var l_oBike = obj as Bike;
|
||||
if (l_oBike == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals(l_oBike);
|
||||
}
|
||||
|
||||
/// <summary> Converts the instance to text.</summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return WheelType == null || TypeOfBike == null
|
||||
? $"Id={Id}{(!string.IsNullOrEmpty(Description) ? $", {Description}" : "")}"
|
||||
: $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $"type={TypeOfBike}" : "")}.";
|
||||
}
|
||||
|
||||
/// <summary> Compares two bike object.</summary>
|
||||
/// <param name="obj">Object to compare with.</param>
|
||||
/// <returns>True if bikes are equal.</returns>
|
||||
public bool Equals(Bike other)
|
||||
{
|
||||
return other != null &&
|
||||
Id == other.Id &&
|
||||
WheelType == other.WheelType &&
|
||||
TypeOfBike == other.TypeOfBike
|
||||
&& Description == other.Description;
|
||||
}
|
||||
|
||||
/// <summary> Compares two bike object.</summary>
|
||||
/// <param name="obj">Object to compare with.</param>
|
||||
/// <returns>True if bikes are equal.</returns>
|
||||
public static bool operator ==(Bike bike1, Bike bike2)
|
||||
{
|
||||
return EqualityComparer<Bike>.Default.Equals(bike1, bike2);
|
||||
}
|
||||
|
||||
/// <summary> Compares two bike object.</summary>
|
||||
/// <param name="obj">Object to compare with.</param>
|
||||
/// <returns>True if bikes are equal.</returns>
|
||||
public static bool operator !=(Bike bike1, Bike bike2)
|
||||
{
|
||||
return !(bike1 == bike2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates hash code for bike object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = -390870100;
|
||||
hashCode = hashCode * -1521134295 + Id.GetHashCode();
|
||||
hashCode = hashCode * -1521134295 + WheelType?.GetHashCode() ?? 0;
|
||||
hashCode = hashCode * -1521134295 + TypeOfBike?.GetHashCode() ?? 0;
|
||||
hashCode = hashCode * -1521134295 + Description?.GetHashCode() ?? 0;
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS
|
||||
{
|
||||
public static class BikeExtension
|
||||
{
|
||||
public static LockType GetLockType(this LockModel model)
|
||||
{
|
||||
switch (model)
|
||||
{
|
||||
case LockModel.ILockIt:
|
||||
return LockType.Bluethooth;
|
||||
|
||||
case LockModel.Sigo:
|
||||
return LockType.Backend;
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported lock model {model} detected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Services.CopriApi.Exception;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality to get the locked bike location.
|
||||
/// </summary>
|
||||
public static class AuthCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of requesting a bike.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
Authenticate,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of requesting a bike.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
WebConnectFailed,
|
||||
GeneralAuthError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of closing process.
|
||||
/// </summary>
|
||||
public interface IAuthCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current location.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
IAuthCommandListener listener)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
//// Step: Authenticate user
|
||||
InvokeCurrentStep(Step.Authenticate);
|
||||
|
||||
try
|
||||
{
|
||||
// Repeat reservation to get a new seed/ k_user value.
|
||||
await connectorFactory(true).Command.CalculateAuthKeys(bike);
|
||||
Log.ForContext<T>().Information("Calculation of AuthKeys successfully.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception is WebConnectFailureException
|
||||
|| exception is RequestNotCachableException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Error("Calculation of AuthKeys failed (Copri server not reachable).");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("Calculation of AuthKeys failed. {@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralAuthError, exception.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
|
||||
using ShareeBike.Services.CopriApi.Exception;
|
||||
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
|
||||
{
|
||||
public static class CancelReservationCommand
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of returning a bike.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
CancelReservation,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of returning a bike.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
WebConnectFailed,
|
||||
InvalidResponse,
|
||||
GeneralCancelReservationError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of returning bike process.
|
||||
/// </summary>
|
||||
public interface ICancelReservationCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current location.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
ICancelReservationCommandListener listener)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
// Cancel Reservation
|
||||
InvokeCurrentStep(Step.CancelReservation);
|
||||
|
||||
try
|
||||
{
|
||||
await connectorFactory(true).Command.DoCancelReservation(bike);
|
||||
Log.ForContext<T>().Information("User canceled reservation of bike {bikeId} successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<ReservedDisconnected>().Information("User selected reserved bike {bikeId} but cancel reservation failed.", bike.Id);
|
||||
if (exception is InvalidAuthorizationResponseException)
|
||||
{
|
||||
// Copri response is invalid.
|
||||
Log.ForContext<T>().Error("Invalid auth. response.");
|
||||
await InvokeCurrentStateAsync(State.InvalidResponse, exception.Message);
|
||||
}
|
||||
else if (exception is WebConnectFailureException
|
||||
|| exception is RequestNotCachableException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Error("Copri server not reachable.");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralCancelReservationError, exception.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
|
||||
using ShareeBike.Repository.Request;
|
||||
using System.Threading;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
using ShareeBike.MultilingualResources;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
|
||||
{
|
||||
public static class EndRentalCommand
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of returning a bike.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
GetLocation,
|
||||
ReturnBike,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of returning a bike.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
GPSNotSupportedException,
|
||||
GPSNotEnabledException,
|
||||
NoGPSPermissionsException,
|
||||
GeneralQueryLocationFailed,
|
||||
|
||||
WebConnectFailed,
|
||||
NotAtStation,
|
||||
NoGPSData,
|
||||
ResponseException,
|
||||
GeneralEndRentalError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of returning bike process.
|
||||
/// </summary>
|
||||
public interface IEndRentalCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End rental.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task<BookingFinishedModel> InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
IGeolocationService geolocation,
|
||||
ILocksService lockService,
|
||||
Func<DateTime> dateTimeProvider = null,
|
||||
Func<bool> isConnectedDelegate = null,
|
||||
Func<bool, IConnector> connectorFactory = null,
|
||||
IEndRentalCommandListener listener = null)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
|
||||
// Get Location
|
||||
//// Step: Start query geolocation data.
|
||||
InvokeCurrentStep(Step.GetLocation);
|
||||
|
||||
// Get geolocation which was requested when closing lock.
|
||||
var closingLockLocation = bike.LockInfo.Location;
|
||||
|
||||
LocationDto endRentalLocation;
|
||||
if (closingLockLocation != null)
|
||||
{
|
||||
// Location was available when closing bike. No further actions required.
|
||||
endRentalLocation =
|
||||
new LocationDto.Builder
|
||||
{
|
||||
Latitude = closingLockLocation.Latitude,
|
||||
Longitude = closingLockLocation.Longitude,
|
||||
Accuracy = closingLockLocation.Accuracy ?? double.NaN,
|
||||
Age = bike.LockInfo.LastLockingStateChange is DateTime lastLockState1 ? lastLockState1.Subtract(closingLockLocation.Timestamp.DateTime) : TimeSpan.MaxValue,
|
||||
}.Build();
|
||||
}
|
||||
else
|
||||
{
|
||||
IGeolocation newLockLocation = null;
|
||||
|
||||
// Check if bike is around
|
||||
var deviceState = lockService[bike.LockInfo.Id].GetDeviceState();
|
||||
|
||||
// Geolocation can not be queried because bike is not around.
|
||||
if (deviceState != DeviceState.Connected)
|
||||
{
|
||||
Log.ForContext<T>().Information("There is no geolocation information available since lock of bike {bikeId} is not connected", bike.Id);
|
||||
// no GPS data exception.
|
||||
//NoGPSDataException.IsNoGPSData("There is no geolocation information available since lock is not connected", out NoGPSDataException exception);
|
||||
await InvokeCurrentStateAsync(State.NoGPSData, "There is no geolocation information available since lock is not connected");
|
||||
|
||||
return null; // return empty BookingFinishedModel
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bike is around -> Query geolocation.
|
||||
var ctsLocation = new CancellationTokenSource();
|
||||
try
|
||||
{
|
||||
newLockLocation = await geolocation.GetAsync(ctsLocation.Token, DateTime.Now);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// No location information available.
|
||||
Log.ForContext<T>().Information("Geolocation query failed.");
|
||||
if (exception is FeatureNotSupportedException)
|
||||
{
|
||||
Log.ForContext<T>().Error("Location service are not supported on device.");
|
||||
await InvokeCurrentStateAsync(State.GPSNotSupportedException, exception.Message);
|
||||
}
|
||||
if (exception is FeatureNotEnabledException)
|
||||
{
|
||||
Log.ForContext<T>().Error("Location service are off.");
|
||||
await InvokeCurrentStateAsync(State.GPSNotEnabledException, exception.Message);
|
||||
}
|
||||
if (exception is PermissionException)
|
||||
{
|
||||
Log.ForContext<T>().Error("No location service permissions granted.");
|
||||
await InvokeCurrentStateAsync(State.NoGPSPermissionsException, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Information("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralQueryLocationFailed, exception.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last lock state time
|
||||
// save geolocation data for sending to backend
|
||||
endRentalLocation = newLockLocation != null
|
||||
? new LocationDto.Builder
|
||||
{
|
||||
Latitude = newLockLocation.Latitude,
|
||||
Longitude = newLockLocation.Longitude,
|
||||
Accuracy = newLockLocation.Accuracy ?? double.NaN,
|
||||
Age = (dateTimeProvider != null ? dateTimeProvider() : DateTime.Now).Subtract(newLockLocation.Timestamp.DateTime),
|
||||
}.Build()
|
||||
: null;
|
||||
}
|
||||
|
||||
// Return bike
|
||||
InvokeCurrentStep(Step.ReturnBike);
|
||||
BookingFinishedModel bookingFinished;
|
||||
try
|
||||
{
|
||||
bookingFinished = await connectorFactory(true).Command.DoReturn(
|
||||
bike,
|
||||
endRentalLocation);
|
||||
Log.ForContext<T>().Information("Rental of bike {bikeId} was terminated successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Rental of bike {bikeId} can not be terminated.", bike.Id);
|
||||
if (exception is WebConnectFailureException)
|
||||
{
|
||||
// No web.
|
||||
Log.ForContext<T>().Error("Copri server not reachable. No web.");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
}
|
||||
else if (exception is NotAtStationException notAtStationException)
|
||||
{
|
||||
// not at station.
|
||||
Log.ForContext<T>().Error("COPRI returned out of GEO fencing error. Position send to COPRI {position}.", endRentalLocation);
|
||||
await InvokeCurrentStateAsync(State.NotAtStation, string.Format(AppResources.ErrorEndRentalNotAtStation, notAtStationException.StationNr, notAtStationException.Distance));
|
||||
|
||||
// reset location -> at next try query new location data
|
||||
bike.LockInfo.Location = null;
|
||||
}
|
||||
else if (exception is NoGPSDataException)
|
||||
{
|
||||
// no GPS data.
|
||||
Log.ForContext<T>().Error("COPRI returned a no-GPS-data error.");
|
||||
await InvokeCurrentStateAsync(State.NoGPSData, exception.Message);
|
||||
|
||||
// reset location -> at next try query new location data
|
||||
bike.LockInfo.Location = null;
|
||||
}
|
||||
else if (exception is ResponseException copriException)
|
||||
{
|
||||
// COPRI exception.
|
||||
Log.ForContext<T>().Error("COPRI returned an error. {response}", copriException.Response);
|
||||
await InvokeCurrentStateAsync(State.ResponseException, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralEndRentalError, exception.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return bookingFinished;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
|
||||
{
|
||||
public static class StartRentalCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of requesting a bike.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
RentBike,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of requesting a bike.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
WebConnectFailed,
|
||||
GeneralStartRentalError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of closing process.
|
||||
/// </summary>
|
||||
public interface IStartRentalCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current location.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
IStartRentalCommandListener listener)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
//// Step: Book bike
|
||||
InvokeCurrentStep(Step.RentBike);
|
||||
try
|
||||
{
|
||||
if (bike.LockInfo.State != LockingState.Open)
|
||||
{
|
||||
await connectorFactory(true).Command.DoBookAsync(bike, LockingAction.Open);
|
||||
}
|
||||
else
|
||||
{
|
||||
await connectorFactory(true).Command.DoBookAsync(bike);
|
||||
}
|
||||
Log.ForContext<T>().Information("User booked bike {bikeId} successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Booking of bike {bikeId} failed.", bike.Id);
|
||||
if (exception is WebConnectFailureException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Error("Copri server not reachable.");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralStartRentalError, exception.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Services.CopriApi.Exception;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides functionality to get the locked bike location.
|
||||
/// </summary>
|
||||
public static class StartReservationCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of requesting a bike.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
ReserveBike,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of requesting a bike.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
TooManyBikesError,
|
||||
WebConnectFailed,
|
||||
GeneralStartReservationError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of closing process.
|
||||
/// </summary>
|
||||
public interface IStartReservationCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current location.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
IStartReservationCommandListener listener)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
//// Step: Reserve Bike
|
||||
InvokeCurrentStep(Step.ReserveBike);
|
||||
|
||||
try
|
||||
{
|
||||
await connectorFactory(true).Command.DoReserve(bike);
|
||||
Log.ForContext<T>().Information("User reserved bike {bikeId} successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Request to reserve bike {bikeId} declined.", bike.Id);
|
||||
if (exception is BookingDeclinedException)
|
||||
{
|
||||
// Too many bikes booked.
|
||||
Log.ForContext<T>().Error("Maximum count of bikes {exception.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount);
|
||||
await InvokeCurrentStateAsync(State.TooManyBikesError, (exception as BookingDeclinedException).MaxBikesCount.ToString());
|
||||
}
|
||||
else if (exception is WebConnectFailureException
|
||||
|| exception is RequestNotCachableException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Error("Copri server not reachable.");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralStartReservationError, exception.Message);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.State;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public class BikeInfo : BC.BikeInfo, IBikeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a bike info object for a available bike.
|
||||
/// </summary>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
/// <param name="lockId">Id of the lock.</param>
|
||||
/// <param name="lockGuid">GUID specifying the lock.</param>
|
||||
/// <param name="currentStationId">Id of station where bike is located.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
public BikeInfo(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
int lockId,
|
||||
Guid lockGuid,
|
||||
string currentStationId,
|
||||
Uri operatorUri = null,
|
||||
RentalDescription tariffDescription = null,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null) : base(
|
||||
new StateInfo(),
|
||||
bike != null
|
||||
? new Bike(
|
||||
bike.Id,
|
||||
LockModel.ILockIt /* Ensure consistent lock model value */,
|
||||
bike.WheelType,
|
||||
bike.TypeOfBike,
|
||||
bike.AaRideType,
|
||||
bike.Description)
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
drive,
|
||||
dataSource,
|
||||
isDemo,
|
||||
group,
|
||||
currentStationId,
|
||||
operatorUri,
|
||||
tariffDescription)
|
||||
{
|
||||
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid }.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bike info object for a requested bike.
|
||||
/// </summary>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
|
||||
/// <param name="lockId">Id of the lock.</param>
|
||||
/// <param name="lockGuid">GUID specifying the lock.</param>
|
||||
/// <param name="requestedAt">Date time when bike was requested</param>
|
||||
/// <param name="mailAddress">Mail address of user which requested bike.</param>
|
||||
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
/// <param name="dateTimeProvider">Date time provider to calculate remaining time.</param>
|
||||
public BikeInfo(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
int lockId,
|
||||
Guid lockGuid,
|
||||
byte[] userKey,
|
||||
byte[] adminKey,
|
||||
byte[] seed,
|
||||
DateTime requestedAt,
|
||||
string mailAddress,
|
||||
string currentStationId,
|
||||
Uri operatorUri,
|
||||
RentalDescription tariffDescription,
|
||||
Func<DateTime> dateTimeProvider,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null) : base(
|
||||
new StateInfo(
|
||||
dateTimeProvider,
|
||||
requestedAt,
|
||||
tariffDescription?.MaxReservationTimeSpan ?? StateRequestedInfo.UNKNOWNMAXRESERVATIONTIMESPAN,
|
||||
mailAddress,
|
||||
"" ), // BC code
|
||||
bike != null
|
||||
? new Bike(
|
||||
bike.Id,
|
||||
LockModel.ILockIt /* Ensure consistent lock model value */,
|
||||
bike.WheelType,
|
||||
bike.TypeOfBike,
|
||||
bike.AaRideType,
|
||||
bike.Description)
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
drive,
|
||||
dataSource,
|
||||
isDemo,
|
||||
group,
|
||||
currentStationId,
|
||||
operatorUri,
|
||||
tariffDescription)
|
||||
{
|
||||
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bike info object for a booked bike.
|
||||
/// </summary>
|
||||
/// <param name="bike">Unique id of bike.</param>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
/// <param name="lockId">Id of the lock.</param>
|
||||
/// <param name="lockGuid">GUID specifying the lock.</param>
|
||||
/// <param name="bookedAt">Date time when bike was booked</param>
|
||||
/// <param name="mailAddress">Mail address of user which booked bike.</param>
|
||||
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
/// <param name="wheelType"></param>
|
||||
public BikeInfo(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
int lockId,
|
||||
Guid lockGuid,
|
||||
byte[] userKey,
|
||||
byte[] adminKey,
|
||||
byte[] seed,
|
||||
DateTime bookedAt,
|
||||
string mailAddress,
|
||||
string currentStationId,
|
||||
Uri operatorUri,
|
||||
RentalDescription tariffDescription = null,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null) : base(
|
||||
new StateInfo(
|
||||
bookedAt,
|
||||
mailAddress,
|
||||
""),
|
||||
bike != null
|
||||
? new Bike(
|
||||
bike.Id,
|
||||
LockModel.ILockIt /* Ensure consistent lock model value */,
|
||||
bike.WheelType,
|
||||
bike.TypeOfBike,
|
||||
bike.AaRideType,
|
||||
bike.Description)
|
||||
: throw new ArgumentNullException(),
|
||||
drive,
|
||||
dataSource,
|
||||
isDemo,
|
||||
group,
|
||||
currentStationId,
|
||||
operatorUri,
|
||||
tariffDescription)
|
||||
{
|
||||
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
|
||||
}
|
||||
|
||||
public BikeInfo(BC.BikeInfo bikeInfo, LockInfo lockInfo) : base(
|
||||
bikeInfo ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source bike info must not be null."))
|
||||
{
|
||||
LockInfo = lockInfo
|
||||
?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source lock object must not be null.");
|
||||
}
|
||||
|
||||
public LockInfo LockInfo { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Repository.Request;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
using ShareeBike.ViewModel;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable
|
||||
{
|
||||
/// <summary> Provides a connector object.</summary>
|
||||
private Func<bool, IConnector> ConnectorFactory { get; }
|
||||
|
||||
/// <summary> Provides geolocation information.</summary>
|
||||
private IGeolocationService GeolocationService { get; }
|
||||
|
||||
/// <summary> Provides geolocation for end rental.</summary>
|
||||
private LocationDto EndRentalLocation { get; }
|
||||
|
||||
/// <summary> Provides a connector object.</summary>
|
||||
private ILocksService LockService { get; }
|
||||
|
||||
/// <summary> Delegate to retrieve connected state. </summary>
|
||||
private Func<bool> IsConnectedDelegate { get; }
|
||||
|
||||
/// <summary>Object to manage update of view model objects from Copri.</summary>
|
||||
protected Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
|
||||
|
||||
/// <summary> Constructs a bike object from source. </summary>
|
||||
/// <param name="viewUpdateManager">Manages update of view model objects from Copri.</param>
|
||||
public BikeInfoMutable(
|
||||
IGeolocationService geolocation,
|
||||
ILocksService lockService,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
Func<IPollingUpdateTaskManager> viewUpdateManager,
|
||||
BikeInfo bike,
|
||||
string stationName) : base(
|
||||
bike != null
|
||||
? bike.Bike
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
bike.Drive,
|
||||
bike.DataSource,
|
||||
bike.IsDemo,
|
||||
bike.Group,
|
||||
bike.StationId,
|
||||
stationName,
|
||||
bike.OperatorUri,
|
||||
bike.TariffDescription,
|
||||
() => DateTime.Now,
|
||||
bike.State)
|
||||
{
|
||||
LockInfo = new LockInfoMutable(
|
||||
bike.LockInfo.Id,
|
||||
bike.LockInfo.Guid,
|
||||
bike.LockInfo.UserKey,
|
||||
bike.LockInfo.AdminKey,
|
||||
bike.LockInfo.Seed,
|
||||
bike.LockInfo.State);
|
||||
|
||||
GeolocationService = geolocation
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No geolocation object available.");
|
||||
|
||||
LockService = lockService
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No lock service object available.");
|
||||
|
||||
IsConnectedDelegate = isConnectedDelegate
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No is connected delegate available.");
|
||||
|
||||
ConnectorFactory = connectorFactory
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No connector available.");
|
||||
|
||||
ViewUpdateManager = viewUpdateManager
|
||||
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No update manger available.");
|
||||
}
|
||||
|
||||
public LockInfoMutable LockInfo { get; }
|
||||
|
||||
ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Connects the lock.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process closing notifications.</param>
|
||||
public async Task ConnectAsync(
|
||||
IConnectAndGetStateCommandListener listener)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
LockService,
|
||||
listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the lock and updates copri.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process closing notifications.</param>
|
||||
/// <param name="stopPollingTask">Task which stops polling.</param>
|
||||
public async Task OpenLockAsync(
|
||||
IOpenCommandListener listener,
|
||||
Task stopPollingTask)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
LockService,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener,
|
||||
stopPollingTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the lock and updates copri.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process closing notifications.</param>
|
||||
/// <param name="stopPollingTask">Task which stops polling.</param>
|
||||
public async Task CloseLockAsync(
|
||||
ICloseCommandListener listener,
|
||||
Task stopPollingTask)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
GeolocationService,
|
||||
LockService,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener,
|
||||
stopPollingTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the lock.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process closing notifications.</param>
|
||||
public async Task DisconnectAsync(
|
||||
IDisconnectCommandListener listener)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
LockService,
|
||||
listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves the bike.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process notifications.</param>
|
||||
/// <returns></returns>
|
||||
public async Task ReserveBikeAsync(
|
||||
IStartReservationCommandListener listener)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the reservation.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process notifications.</param>
|
||||
/// <returns></returns>
|
||||
public async Task CancelReservationAsync(
|
||||
ICancelReservationCommandListener listener)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates the user.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process notifications.</param>
|
||||
/// <returns></returns>
|
||||
public async Task AuthAsync(
|
||||
IAuthCommandListener listener)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rents the bike.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process notifications.</param>
|
||||
/// <returns></returns>
|
||||
public async Task RentBikeAsync(
|
||||
IStartRentalCommandListener listener)
|
||||
{
|
||||
await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bike.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process notifications.</param>
|
||||
/// <returns></returns>
|
||||
public async Task<BookingFinishedModel> ReturnBikeAsync(
|
||||
IEndRentalCommandListener listener)
|
||||
{
|
||||
var bookingFinished = await InvokeAsync<BikeInfoMutable>(
|
||||
this,
|
||||
GeolocationService,
|
||||
LockService,
|
||||
() => DateTime.Now,
|
||||
IsConnectedDelegate,
|
||||
ConnectorFactory,
|
||||
listener);
|
||||
|
||||
return bookingFinished;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Id={Id}{(TypeOfBike != null ? $";type={TypeOfBike}" : "")};state={State.ToString()};Lock id={LockInfo.Id}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Repository.Request;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Services.BluetoothLock.Exception;
|
||||
using ShareeBike.Services.BluetoothLock.Tdo;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
|
||||
{
|
||||
public static class CloseCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of closing a lock.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
StartStopingPolling,
|
||||
StartingQueryingLocation,
|
||||
ClosingLock,
|
||||
WaitStopPollingQueryLocation,
|
||||
/// <summary>
|
||||
/// Notifies back end about modified lock state.
|
||||
/// </summary>
|
||||
UpdateLockingState,
|
||||
QueryLocationTerminated
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible states of closing a lock.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
OutOfReachError,
|
||||
CouldntCloseMovingError,
|
||||
CouldntCloseBoltBlockedError,
|
||||
GeneralCloseError,
|
||||
StartGeolocationException,
|
||||
WaitGeolocationException,
|
||||
WebConnectFailed,
|
||||
ResponseIsInvalid,
|
||||
BackendUpdateFailed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of closing process.
|
||||
/// </summary>
|
||||
public interface ICloseCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the lock and updates copri.
|
||||
/// </summary>
|
||||
/// <param name="listener">Interface to notify view model about steps/ state changes of closing process.</param>
|
||||
/// <param name="stopPolling">Task which stops polling.</param>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
IGeolocationService geolocation,
|
||||
ILocksService lockService,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
ICloseCommandListener listener,
|
||||
Task stopPollingTask)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for geolocation and polling task to stop (on finished or on canceled).
|
||||
async Task<IGeolocation> WaitForPendingTasks(Task<IGeolocation> locationTask)
|
||||
{
|
||||
// Step: Wait until getting geolocation has completed.
|
||||
InvokeCurrentStep(Step.WaitStopPollingQueryLocation);
|
||||
Log.ForContext<T>().Information($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish...");
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(new List<Task> { locationTask, stopPollingTask ?? Task.CompletedTask });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// No location information available.
|
||||
Log.ForContext<T>().Information("Canceling query location/ wait for polling task to finish failed. {@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.WaitGeolocationException, exception.Message);
|
||||
InvokeCurrentStep(Step.QueryLocationTerminated);
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.ForContext<T>().Information($"Steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} finished.");
|
||||
InvokeCurrentStep(Step.QueryLocationTerminated);
|
||||
return locationTask.Result;
|
||||
}
|
||||
|
||||
// Updates locking state
|
||||
async Task UpdateLockingState(IGeolocation location, DateTime timeStamp)
|
||||
{
|
||||
// Step: Update backend.
|
||||
InvokeCurrentStep(Step.UpdateLockingState);
|
||||
try
|
||||
{
|
||||
await connectorFactory(true).Command.UpdateLockingStateAsync(
|
||||
bike,
|
||||
location != null
|
||||
? new LocationDto.Builder
|
||||
{
|
||||
Latitude = location.Latitude,
|
||||
Longitude = location.Longitude,
|
||||
Accuracy = location.Accuracy ?? double.NaN,
|
||||
Age = timeStamp.Subtract(location.Timestamp.DateTime),
|
||||
}.Build()
|
||||
: null);
|
||||
Log.ForContext<T>().Information("Backend updated for bike {bikeId} successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<ReservedOpen>().Information("Updating backend for bike {bikeId} failed.", bike.Id);
|
||||
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
|
||||
if (exception is WebConnectFailureException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Debug("Copri server not reachable.");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
return;
|
||||
}
|
||||
else if (exception is ResponseException copriException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Debug("Message: {Message} Details: {Details}", copriException.Message, copriException.Response);
|
||||
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception);
|
||||
await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
//// Step: Start query geolocation data.
|
||||
Log.ForContext<T>().Debug($"Starting step {Step.StartingQueryingLocation}...");
|
||||
InvokeCurrentStep(Step.StartingQueryingLocation);
|
||||
var ctsLocation = new CancellationTokenSource();
|
||||
Task<IGeolocation> currentLocationTask = Task.FromResult<IGeolocation>(null);
|
||||
var timeStampNow = DateTime.Now;
|
||||
try
|
||||
{
|
||||
currentLocationTask = geolocation.GetAsync(ctsLocation.Token, timeStampNow);
|
||||
Log.ForContext<T>().Information("Starting query location successful.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// No location information available.
|
||||
Log.ForContext<T>().Information("Starting query location failed. {@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.StartGeolocationException, exception.Message);
|
||||
}
|
||||
|
||||
//// Step: Close lock.
|
||||
IGeolocation currentLocation;
|
||||
Log.ForContext<T>().Information($"Starting step {Step.ClosingLock}...");
|
||||
InvokeCurrentStep(Step.ClosingLock);
|
||||
LockitLockingState? lockingState;
|
||||
try
|
||||
{
|
||||
lockingState = await lockService[bike.LockInfo.Id].CloseAsync();
|
||||
Log.ForContext<T>().Information("Lock of bike {bikeId} closed successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be closed.", bike.Id);
|
||||
if (exception is OutOfReachException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Lock is out of reach");
|
||||
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
|
||||
}
|
||||
else if (exception is CouldntCloseMovingException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Lock is moving.");
|
||||
await InvokeCurrentStateAsync(State.CouldntCloseMovingError, exception.Message);
|
||||
}
|
||||
else if (exception is CouldntCloseBoltBlockedException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Bold is blocked.}");
|
||||
await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Debug("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message);
|
||||
}
|
||||
|
||||
// Signal cts to cancel getting geolocation.
|
||||
ctsLocation.Cancel();
|
||||
|
||||
// Wait until getting geolocation and stop polling has completed.
|
||||
currentLocation = await WaitForPendingTasks(currentLocationTask);
|
||||
|
||||
// Update current state from exception
|
||||
bike.LockInfo.State = exception is StateAwareException stateAwareException
|
||||
? stateAwareException.State
|
||||
: LockingState.UnknownDisconnected;
|
||||
|
||||
if (!isConnectedDelegate())
|
||||
{
|
||||
// Lock state can not be updated because there is no connected.
|
||||
throw;
|
||||
}
|
||||
|
||||
if (exception is OutOfReachException)
|
||||
{
|
||||
// Locking state can not be updated because lock is not connected.
|
||||
throw;
|
||||
}
|
||||
|
||||
// Update backend.
|
||||
// Do this even if current lock state is open (lock state must not necessarily be open before try to open, i.e. something undefined between open and closed).
|
||||
await UpdateLockingState(currentLocation, timeStampNow);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
//// Step: Wait until getting geolocation and stop polling has completed.
|
||||
currentLocation = await WaitForPendingTasks(currentLocationTask);
|
||||
|
||||
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
|
||||
|
||||
// Keep geolocation where closing action occurred.
|
||||
bike.LockInfo.Location = currentLocation;
|
||||
|
||||
if (!isConnectedDelegate())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//// Step: Update backend.
|
||||
await UpdateLockingState(currentLocation, timeStampNow);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.MultilingualResources;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Services.BluetoothLock.Exception;
|
||||
using ShareeBike.Services.BluetoothLock.Tdo;
|
||||
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
|
||||
using ShareeBike.ViewModel.Bikes;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
|
||||
{
|
||||
public static class ConnectAndGetStateCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of connecting or disconnecting a lock.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
ConnectLock,
|
||||
GetLockingState,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of connecting or disconnecting a lock.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
BluetoothOff,
|
||||
NoLocationPermission,
|
||||
LocationServicesOff,
|
||||
OutOfReachError,
|
||||
GeneralConnectLockError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of connecting or disconnecting lock process.
|
||||
/// </summary>
|
||||
public interface IConnectAndGetStateCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect or disconnect lock.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
ILocksService lockService,
|
||||
IConnectAndGetStateCommandListener listener = null)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Connect lock
|
||||
InvokeCurrentStep(Step.ConnectLock);
|
||||
|
||||
LockInfoTdo result = null;
|
||||
var continueConnect = true;
|
||||
var retryCount = 1;
|
||||
while (continueConnect && result == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = await lockService.ConnectAsync(
|
||||
new LockInfoAuthTdo.Builder { Id = bike.LockInfo.Id, Guid = bike.LockInfo.Guid, K_seed = bike.LockInfo.Seed, K_u = bike.LockInfo.UserKey }.Build(),
|
||||
lockService.TimeOut.GetSingleConnect(retryCount));
|
||||
Log.ForContext<T>().Information("Connected to lock of bike {bikeId} successfully. Value is {lockState}.", bike.Id, bike.LockInfo.State);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Connection to lock of bike {bikeId} failed. ", bike.Id);
|
||||
if (exception is ConnectBluetoothNotOnException)
|
||||
{
|
||||
continueConnect = false;
|
||||
Log.ForContext<T>().Error("Bluetooth not on.");
|
||||
await InvokeCurrentStateAsync(State.BluetoothOff, exception.Message);
|
||||
}
|
||||
else if (exception is ConnectLocationPermissionMissingException)
|
||||
{
|
||||
continueConnect = false;
|
||||
Log.ForContext<T>().Error("Location permission missing.");
|
||||
await InvokeCurrentStateAsync(State.NoLocationPermission, exception.Message);
|
||||
}
|
||||
else if (exception is ConnectLocationOffException)
|
||||
{
|
||||
continueConnect = false;
|
||||
Log.ForContext<T>().Error("Location services not on.");
|
||||
await InvokeCurrentStateAsync(State.LocationServicesOff, exception.Message);
|
||||
}
|
||||
else if (exception is OutOfReachException)
|
||||
{
|
||||
continueConnect = false;
|
||||
Log.ForContext<T>().Error("Lock is out of reach.");
|
||||
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("{@exception}", exception);
|
||||
|
||||
string message;
|
||||
if (retryCount < 2)
|
||||
{
|
||||
message = AppResources.ErrorConnectLock;
|
||||
}
|
||||
else if (retryCount < 3)
|
||||
{
|
||||
message = AppResources.ErrorConnectLockEscalationLevel1;
|
||||
}
|
||||
else
|
||||
{
|
||||
message = AppResources.ErrorConnectLockEscalationLevel2;
|
||||
continueConnect = false;
|
||||
}
|
||||
await InvokeCurrentStateAsync(State.GeneralConnectLockError, message);
|
||||
}
|
||||
|
||||
if (continueConnect)
|
||||
{
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Get locking state
|
||||
InvokeCurrentStep(Step.GetLockingState);
|
||||
bike.LockInfo.State = result?.State?.GetLockingState() ?? LockingState.UnknownDisconnected;
|
||||
if (bike.LockInfo.State == LockingState.UnknownDisconnected)
|
||||
{
|
||||
// Do not display any messages here, because search is implicit.
|
||||
Log.ForContext<T>().Error("Lock is still not connected.");
|
||||
}
|
||||
bike.LockInfo.Guid = result?.Guid ?? new Guid();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
|
||||
{
|
||||
public static class DisconnectCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of connecting or disconnecting a lock.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
DisconnectLock,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible steps of connecting or disconnecting a lock.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
GeneralDisconnectError,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of connecting or disconnecting lock process.
|
||||
/// </summary>
|
||||
public interface IDisconnectCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect or disconnect lock.
|
||||
/// </summary>
|
||||
/// <param name="listener"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
ILocksService lockService,
|
||||
IDisconnectCommandListener listener = null)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect lock
|
||||
InvokeCurrentStep(Step.DisconnectLock);
|
||||
if (bike.LockInfo.State != LockingState.UnknownDisconnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
bike.LockInfo.State = await lockService.DisconnectAsync(bike.LockInfo.Id, bike.LockInfo.Guid);
|
||||
Log.ForContext<T>().Information("Lock from bike {bikeId} disconnected successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Lock from bike {bikeId} can not be disconnected. {@exception}", bike.Id, exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralDisconnectError, exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Services.BluetoothLock.Exception;
|
||||
using ShareeBike.Services.BluetoothLock.Tdo;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
|
||||
{
|
||||
public static class OpenCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Possible steps of opening a lock.
|
||||
/// </summary>
|
||||
public enum Step
|
||||
{
|
||||
OpeningLock,
|
||||
WaitStopPolling,
|
||||
GetLockInfos,
|
||||
UpdateLockingState,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible states of opening a lock.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
StopPollingFailed,
|
||||
OutOfReachError,
|
||||
CouldntOpenBoldStatusIsUnknownError,
|
||||
CouldntOpenBoldIsBlockedError,
|
||||
CouldntOpenInconsistentStateError,
|
||||
GeneralOpenError,
|
||||
WebConnectFailed,
|
||||
ResponseIsInvalid,
|
||||
BackendUpdateFailed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface to notify view model about steps/ state changes of opening process.
|
||||
/// </summary>
|
||||
public interface IOpenCommandListener
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports current step.
|
||||
/// </summary>
|
||||
/// <param name="currentStep">Current step to report.</param>
|
||||
void ReportStep(Step currentStep);
|
||||
|
||||
/// <summary>
|
||||
/// Reports current state.
|
||||
/// </summary>
|
||||
/// <param name="currentState">Current state to report.</param>
|
||||
/// <param name="message">Message describing the current state.</param>
|
||||
/// <returns></returns>
|
||||
Task ReportStateAsync(State currentState, string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the lock and updates copri.
|
||||
/// </summary>
|
||||
/// <param name="listener">Interface to notify view model about steps/ state changes of opening process.</param>
|
||||
/// <param name="stopPolling">Task which stops polling.</param>
|
||||
public static async Task InvokeAsync<T>(
|
||||
IBikeInfoMutable bike,
|
||||
ILocksService lockService,
|
||||
Func<bool> isConnectedDelegate,
|
||||
Func<bool, IConnector> connectorFactory,
|
||||
IOpenCommandListener listener,
|
||||
Task stopPollingTask)
|
||||
{
|
||||
// Invokes member to notify about step being started.
|
||||
void InvokeCurrentStep(Step step)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
listener.ReportStep(step);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Invokes member to notify about state change.
|
||||
async Task InvokeCurrentStateAsync(State state, string message)
|
||||
{
|
||||
if (listener == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await listener.ReportStateAsync(state, message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait polling task to stop (on finished or on canceled).
|
||||
async Task WaitForPendingTasks()
|
||||
{
|
||||
// Step: Wait until getting geolocation has completed.
|
||||
InvokeCurrentStep(Step.WaitStopPolling);
|
||||
Log.ForContext<T>().Information($"Waiting on stop polling to finish...");
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(new List<Task> { stopPollingTask ?? Task.CompletedTask });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Wait for polling task to finish failed. {@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.StopPollingFailed, exception.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.ForContext<T>().Information($"Stop polling finished.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get lock infos
|
||||
async Task GetLockInfos()
|
||||
{
|
||||
Log.ForContext<T>().Debug($"Starting step {Step.GetLockInfos}...");
|
||||
InvokeCurrentStep(Step.GetLockInfos);
|
||||
// get current charging level
|
||||
try
|
||||
{
|
||||
bike.LockInfo.BatteryPercentage = await lockService[bike.LockInfo.Id].GetBatteryPercentageAsync();
|
||||
Log.ForContext<T>().Information("Lock infos of bike {bikeId} read successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Lock infos could not be read.", bike.Id);
|
||||
if (exception is OutOfReachException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Lock is out of reach");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Debug("{@exception}", exception);
|
||||
}
|
||||
}
|
||||
// get version infos.
|
||||
var versionTdo = lockService[bike.LockInfo.Id].VersionInfo;
|
||||
if (versionTdo != null)
|
||||
{
|
||||
bike.LockInfo.VersionInfo = new VersionInfo.Builder
|
||||
{
|
||||
FirmwareVersion = versionTdo.FirmwareVersion,
|
||||
HardwareVersion = versionTdo.HardwareVersion,
|
||||
LockVersion = versionTdo.LockVersion,
|
||||
}.Build();
|
||||
}
|
||||
}
|
||||
|
||||
// Updates locking state
|
||||
async Task UpdateLockingState()
|
||||
{
|
||||
// Step: Update backend.
|
||||
InvokeCurrentStep(Step.UpdateLockingState);
|
||||
try
|
||||
{
|
||||
await connectorFactory(true).Command.UpdateLockingStateAsync(bike);
|
||||
Log.ForContext<T>().Information("Backend updated for bike {bikeId} successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<ReservedOpen>().Information("Updating backend for bike {bikeId} failed.", bike.Id);
|
||||
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
|
||||
if (exception is WebConnectFailureException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Debug("Copri server not reachable.");
|
||||
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
||||
return;
|
||||
}
|
||||
else if (exception is ResponseException copriException)
|
||||
{
|
||||
// Copri server is not reachable.
|
||||
Log.ForContext<T>().Debug("Message: {Message} Details: {Details}", copriException.Message, copriException.Response);
|
||||
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception);
|
||||
await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//// Start Action
|
||||
|
||||
//// Step: Open lock.
|
||||
Log.ForContext<T>().Information($"Starting step {Step.OpeningLock}...");
|
||||
InvokeCurrentStep(Step.OpeningLock);
|
||||
LockitLockingState? lockingState;
|
||||
try
|
||||
{
|
||||
lockingState = await lockService[bike.LockInfo.Id].OpenAsync();
|
||||
Log.ForContext<T>().Information("Lock of bike {bikeId} opened successfully.", bike.Id);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be opened.", bike.Id);
|
||||
if (exception is OutOfReachException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Lock is out of reach");
|
||||
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
|
||||
}
|
||||
else if (exception is CouldntOpenBoldStatusIsUnknownException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Lock status is unknown.");
|
||||
await InvokeCurrentStateAsync(State.CouldntOpenBoldStatusIsUnknownError, exception.Message);
|
||||
}
|
||||
else if (exception is CouldntOpenBoldIsBlockedException)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Bold is blocked.}");
|
||||
await InvokeCurrentStateAsync(State.CouldntOpenBoldIsBlockedError, exception.Message);
|
||||
}
|
||||
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
|
||||
&& inconsistentState.State == LockingState.Closed)
|
||||
{
|
||||
Log.ForContext<T>().Debug("Lock reports that it is still closed.}");
|
||||
await InvokeCurrentStateAsync(State.CouldntOpenInconsistentStateError, exception.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ForContext<T>().Debug("{@exception}", exception);
|
||||
await InvokeCurrentStateAsync(State.GeneralOpenError, exception.Message);
|
||||
}
|
||||
|
||||
// Update current state from exception
|
||||
bike.LockInfo.State = exception is StateAwareException stateAwareException
|
||||
? stateAwareException.State
|
||||
: LockingState.UnknownDisconnected;
|
||||
|
||||
if (!isConnectedDelegate())
|
||||
{
|
||||
// Lock state can not be updated because there is no connected.
|
||||
throw;
|
||||
}
|
||||
|
||||
if (exception is OutOfReachException)
|
||||
{
|
||||
// Locking state can not be updated because lock is not connected.
|
||||
throw;
|
||||
}
|
||||
|
||||
//// Step: Get Lock Battery and Version info
|
||||
await GetLockInfos();
|
||||
|
||||
//// Step: Wait for stop polling finished
|
||||
await WaitForPendingTasks();
|
||||
|
||||
//// Step: Update backend.
|
||||
//Do this even if current lock state is closed (lock state must not necessarily be closed before try to close, i.e. something undefined between open and opened).
|
||||
await UpdateLockingState();
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
|
||||
|
||||
if (!isConnectedDelegate())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//// Step: Get Lock Battery and Version info
|
||||
await GetLockInfos();
|
||||
|
||||
//// Step: Wait for stop polling finished
|
||||
await WaitForPendingTasks();
|
||||
|
||||
//// Step: Update backend.
|
||||
await UpdateLockingState();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public interface IBikeInfo : BC.IBikeInfo
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
using System.Threading.Tasks;
|
||||
using ShareeBike.Repository.Request;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public interface IBikeInfoMutable : BC.IBikeInfoMutable
|
||||
{
|
||||
ILockInfoMutable LockInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Connects the lock.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process connect notifications.</param>
|
||||
Task ConnectAsync(IConnectAndGetStateCommandListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the lock.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process disconnect notifications.</param>
|
||||
Task DisconnectAsync(IDisconnectCommandListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the lock.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process opening notifications.</param>
|
||||
/// <param name="stopPollingTask">Task which stops polling to be awaited before updating bike object and copri communication.</param>
|
||||
Task OpenLockAsync(IOpenCommandListener listener, Task stopPollingTask);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the lock.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process closing notifications.</param>
|
||||
/// <param name="stopPollingTask">Task which stops polling to be awaited before updating bike object and copri communication.</param>
|
||||
Task CloseLockAsync(ICloseCommandListener listener, Task stopPollingTask);
|
||||
|
||||
/// <summary>
|
||||
/// Reserves the bike.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process reservation notifications.</param>
|
||||
Task ReserveBikeAsync(IStartReservationCommandListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the reservation.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process reservation notifications.</param>
|
||||
Task CancelReservationAsync(ICancelReservationCommandListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates the user.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process reservation notifications.</param>
|
||||
Task AuthAsync(IAuthCommandListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Rents the bike.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process renting notifications.</param>
|
||||
Task RentBikeAsync(IStartRentalCommandListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the bike.
|
||||
/// </summary>
|
||||
/// <param name="listener">View model to process notifications.</param>
|
||||
/// <returns></returns>
|
||||
Task<BookingFinishedModel> ReturnBikeAsync(IEndRentalCommandListener listener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public interface ILockInfoMutable
|
||||
{
|
||||
/// <summary> Identification number of bluetooth lock.</summary>
|
||||
int Id { get; }
|
||||
|
||||
/// <summary> Gets the user key.</summary>
|
||||
byte[] UserKey { get; }
|
||||
|
||||
LockingState State { get; set; }
|
||||
|
||||
/// <summary> Holds the percentage of lock battery.</summary>
|
||||
double BatteryPercentage { get; set; }
|
||||
|
||||
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
|
||||
Guid Guid { get; set; }
|
||||
|
||||
byte[] Seed { get; }
|
||||
|
||||
/// <summary> Timestamp of the last locking state change.</summary>
|
||||
DateTime? LastLockingStateChange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current location of the bike, null if location is unknown.
|
||||
/// </summary>
|
||||
IGeolocation Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version info of the locks.
|
||||
/// </summary>
|
||||
IVersionInfo VersionInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public interface IVersionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the firmware version of the lock.
|
||||
/// </summary>
|
||||
int FirmwareVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the hardware version (revision) of the lock.
|
||||
/// </summary>
|
||||
int HardwareVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds lock version (2 – classic, 3 – plus, 4 – GPS).
|
||||
/// </summary>
|
||||
int LockVersion { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
using System;
|
||||
using ShareeBike.Services.Geolocation;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public class LockInfoMutable : ILockInfoMutable
|
||||
{
|
||||
/// <summary> Lock info object. </summary>
|
||||
private LockInfo LockInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to create time stamp.
|
||||
/// </summary>
|
||||
private Func<DateTime> _nowDelegate;
|
||||
|
||||
/// <summary> Constructs a bluetooth lock info object. </summary>
|
||||
/// <param name="id">Id of lock must always been known when constructing an lock info object.</param>
|
||||
/// <param name="nowDelegate">Delegate to create time stamp if null DateTime.Now is used.</param>
|
||||
public LockInfoMutable(
|
||||
int id,
|
||||
Guid guid,
|
||||
byte[] userKey,
|
||||
byte[] adminKey,
|
||||
byte[] seed,
|
||||
LockingState state,
|
||||
Func<DateTime> nowDelegate = null)
|
||||
{
|
||||
_nowDelegate = nowDelegate ?? (() => DateTime.Now);
|
||||
LockInfo = new LockInfo.Builder() { Id = id, Guid = guid, UserKey = userKey, AdminKey = adminKey, Seed = seed, State = state }.Build();
|
||||
}
|
||||
|
||||
public int Id => LockInfo.Id;
|
||||
|
||||
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
|
||||
public Guid Guid
|
||||
{
|
||||
get => LockInfo.Guid;
|
||||
set => LockInfo = new LockInfo.Builder(LockInfo) { Guid = value }.Build();
|
||||
}
|
||||
|
||||
public byte[] Seed => LockInfo.Seed;
|
||||
|
||||
public byte[] UserKey => LockInfo.UserKey;
|
||||
|
||||
public byte[] AdminKey => LockInfo.AdminKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the locking state.
|
||||
/// </summary>
|
||||
public LockingState State
|
||||
{
|
||||
get => LockInfo.State;
|
||||
set
|
||||
{
|
||||
if (LockInfo.State == value)
|
||||
{
|
||||
// State does not change, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
Location = null; // Invalidate location.
|
||||
LastLockingStateChange = _nowDelegate(); // Get time stamp when state change happened.
|
||||
LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets the time stamp of the last locking state change.</summary>
|
||||
public DateTime? LastLockingStateChange { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current location of the bike, null if location is unknown.
|
||||
/// </summary>
|
||||
public IGeolocation Location { get; set; }
|
||||
|
||||
/// <summary> Holds the percentage of lock battery.</summary>
|
||||
public double BatteryPercentage { get; set; } = double.NaN;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version info of the lock.
|
||||
/// </summary>
|
||||
public IVersionInfo VersionInfo { get; set; } = new VersionInfo.Builder().Build();
|
||||
|
||||
/// <summary> Loads lock info object from values. </summary>
|
||||
public void Load(int id, Guid guid, byte[] seed, byte[] userKey, byte[] adminKey)
|
||||
{
|
||||
LockInfo = new LockInfo.Builder(LockInfo) { Id = id, Guid = guid, Seed = seed, UserKey = userKey, AdminKey = adminKey }.Build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
|
||||
{
|
||||
public class VersionInfo : IVersionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds info about firmware- and hardware version of a lock and the type of lock (lock version).
|
||||
/// </summary>
|
||||
private VersionInfo() { }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the firmware version of the lock.
|
||||
/// </summary>
|
||||
public int FirmwareVersion { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the hardware version (revision) of the lock.
|
||||
/// </summary>
|
||||
public int HardwareVersion { get; private set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Holds lock version (2 – classic, 3 – plus, 4 – GPS).
|
||||
/// </summary>
|
||||
public int LockVersion { get; private set; } = 0;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> Equals(obj as VersionInfo);
|
||||
|
||||
public bool Equals(VersionInfo other)
|
||||
{
|
||||
if (ReferenceEquals(other, null)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
if (GetType() != other.GetType()) return false;
|
||||
|
||||
return ToString() == other.ToString();
|
||||
}
|
||||
|
||||
public override int GetHashCode() => ToString().GetHashCode();
|
||||
|
||||
public override string ToString() => JsonConvert.SerializeObject(this);
|
||||
|
||||
public static bool operator ==(VersionInfo lhs, VersionInfo rhs)
|
||||
{
|
||||
if (ReferenceEquals(lhs, null))
|
||||
return ReferenceEquals(rhs, null) ? true /*null == null = true*/: false;
|
||||
|
||||
return lhs.Equals(rhs);
|
||||
}
|
||||
|
||||
public static bool operator !=(VersionInfo lhs, VersionInfo rhs)
|
||||
=> !(lhs == rhs);
|
||||
public class Builder
|
||||
{
|
||||
private VersionInfo versionInfo = new VersionInfo();
|
||||
|
||||
public int FirmwareVersion { get => versionInfo.FirmwareVersion; set => versionInfo.FirmwareVersion = value; }
|
||||
|
||||
public int HardwareVersion { get => versionInfo.HardwareVersion; set => versionInfo.HardwareVersion = value; }
|
||||
|
||||
public int LockVersion { get => versionInfo.LockVersion; set => versionInfo.LockVersion = value; }
|
||||
|
||||
public VersionInfo Build()
|
||||
{
|
||||
return versionInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
174
SharedBusinessLogic/Model/Bikes/BikeInfoNS/CopriLock/BikeInfo.cs
Normal file
174
SharedBusinessLogic/Model/Bikes/BikeInfoNS/CopriLock/BikeInfo.cs
Normal file
|
@ -0,0 +1,174 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.MiniSurvey;
|
||||
using ShareeBike.Model.State;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
|
||||
{
|
||||
public class BikeInfo : BC.BikeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a bike info object for a available bike or bike for which feed back is pending.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike object.</param>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
/// <param name="currentStationId">Id of station where bike is located.</param>
|
||||
/// <param name="lockInfo">Lock info.</param>
|
||||
/// <param name="isFeedbackPending">If true user has not yet given feedback after returning bike.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
public BikeInfo(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
string currentStationId,
|
||||
LockInfo lockInfo,
|
||||
bool isFeedbackPending = false,
|
||||
Uri operatorUri = null,
|
||||
RentalDescription tariffDescription = null,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null,
|
||||
IMiniSurveyModel miniSurvey = null,
|
||||
string co2Saving = null) : base(
|
||||
new StateInfo(isFeedbackPending),
|
||||
bike != null
|
||||
? new Bike(
|
||||
bike.Id,
|
||||
LockModel.Sigo,
|
||||
bike.WheelType /* Ensure consistent lock model value */,
|
||||
bike.TypeOfBike,
|
||||
bike.AaRideType,
|
||||
bike.Description)
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
drive,
|
||||
dataSource,
|
||||
isDemo,
|
||||
group,
|
||||
currentStationId,
|
||||
operatorUri,
|
||||
tariffDescription)
|
||||
{
|
||||
LockInfo = lockInfo;
|
||||
MiniSurvey = miniSurvey;
|
||||
Co2Saving = co2Saving;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bike info object for a requested bike.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike object.</param>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
/// <param name="requestedAt">Date time when bike was requested</param>
|
||||
/// <param name="mailAddress">Mail address of user which requested bike.</param>
|
||||
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
|
||||
/// <param name="lockInfo">Lock info.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
|
||||
public BikeInfo(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
DateTime requestedAt,
|
||||
string mailAddress,
|
||||
string currentStationId,
|
||||
LockInfo lockInfo,
|
||||
Uri operatorUri,
|
||||
RentalDescription tariffDescription,
|
||||
Func<DateTime> dateTimeProvider,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null) : base(
|
||||
new StateInfo(
|
||||
dateTimeProvider,
|
||||
requestedAt,
|
||||
tariffDescription?.MaxReservationTimeSpan ?? StateRequestedInfo.UNKNOWNMAXRESERVATIONTIMESPAN,
|
||||
mailAddress,
|
||||
""), // BC code
|
||||
bike != null
|
||||
? new Bike(
|
||||
bike.Id,
|
||||
LockModel.Sigo /* Ensure consistent lock model value */,
|
||||
bike.WheelType,
|
||||
bike.TypeOfBike,
|
||||
bike.AaRideType,
|
||||
bike.Description)
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
drive,
|
||||
dataSource,
|
||||
isDemo,
|
||||
group,
|
||||
currentStationId,
|
||||
operatorUri,
|
||||
tariffDescription)
|
||||
{
|
||||
LockInfo = lockInfo;
|
||||
MiniSurvey = new MiniSurveyModel();
|
||||
Co2Saving = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bike info object for a booked bike.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike object.</param>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
/// <param name="bookedAt">Date time when bike was booked</param>
|
||||
/// <param name="mailAddress">Mail address of user which booked bike.</param>
|
||||
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
|
||||
/// <param name="lockInfo">Lock info.</param>
|
||||
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
|
||||
/// <param name="tariffDescription">Hold tariff description of bike.</param>
|
||||
public BikeInfo(
|
||||
Bike bike,
|
||||
DriveMutable drive,
|
||||
DataSource dataSource,
|
||||
DateTime bookedAt,
|
||||
string mailAddress,
|
||||
string currentStationId,
|
||||
LockInfo lockInfo,
|
||||
Uri operatorUri,
|
||||
RentalDescription tariffDescription = null,
|
||||
bool? isDemo = DEFAULTVALUEISDEMO,
|
||||
IEnumerable<string> group = null) : base(
|
||||
new StateInfo(
|
||||
bookedAt,
|
||||
mailAddress,
|
||||
""),
|
||||
bike != null
|
||||
? new Bike(
|
||||
bike.Id,
|
||||
LockModel.Sigo /* Ensure consistent lock model value */,
|
||||
bike.WheelType,
|
||||
bike.TypeOfBike,
|
||||
bike.AaRideType,
|
||||
bike.Description)
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
drive,
|
||||
dataSource,
|
||||
isDemo,
|
||||
group,
|
||||
currentStationId,
|
||||
operatorUri,
|
||||
tariffDescription)
|
||||
{
|
||||
LockInfo = lockInfo;
|
||||
MiniSurvey = new MiniSurveyModel();
|
||||
Co2Saving = string.Empty;
|
||||
}
|
||||
public BikeInfo(BC.BikeInfo bikeInfo, LockInfo lockInfo) : base(
|
||||
bikeInfo ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source bike info must not be null."))
|
||||
{
|
||||
LockInfo = lockInfo
|
||||
?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source lock object must not be null.");
|
||||
}
|
||||
|
||||
/// <summary> Holds the lock info.</summary>
|
||||
public LockInfo LockInfo { get; private set; }
|
||||
|
||||
public IMiniSurveyModel MiniSurvey { get; private set; }
|
||||
|
||||
public string Co2Saving { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
using System;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
|
||||
{
|
||||
public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable
|
||||
{
|
||||
/// <summary> Constructs a bike object from source. </summary>
|
||||
public BikeInfoMutable(BikeInfo bike, string stationName) : base(
|
||||
bike != null
|
||||
? bike.Bike
|
||||
: throw new ArgumentNullException(nameof(bike)),
|
||||
bike.Drive,
|
||||
bike.DataSource,
|
||||
bike.IsDemo,
|
||||
bike.Group,
|
||||
bike.StationId,
|
||||
stationName,
|
||||
bike.OperatorUri,
|
||||
bike.TariffDescription,
|
||||
() => DateTime.Now,
|
||||
bike.State)
|
||||
{
|
||||
LockInfo = new LockInfoMutable(bike.LockInfo.State);
|
||||
|
||||
BookingFinishedModel = new BookingFinishedModel
|
||||
{
|
||||
Co2Saving = bike.Co2Saving,
|
||||
MiniSurvey = new MiniSurvey.MiniSurveyModel()
|
||||
};
|
||||
|
||||
if ((bike?.MiniSurvey?.Questions) == null
|
||||
|| bike.MiniSurvey.Questions.Count <= 0)
|
||||
{
|
||||
// No querries to add.
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a dummy query. Querries are not yet read from COPRI but compiled into the app.
|
||||
BookingFinishedModel.MiniSurvey.Questions.Add("q1", new MiniSurvey.QuestionModel());
|
||||
}
|
||||
|
||||
public LockInfoMutable LockInfo { get; }
|
||||
|
||||
ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo;
|
||||
|
||||
public IBookingFinishedModel BookingFinishedModel { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
|
||||
{
|
||||
public interface IBikeInfoMutable : BikeInfoNS.BC.IBikeInfoMutable
|
||||
{
|
||||
ILockInfoMutable LockInfo { get; }
|
||||
|
||||
IBookingFinishedModel BookingFinishedModel { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
|
||||
{
|
||||
public interface ILockInfoMutable
|
||||
{
|
||||
LockingState State { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.CopriLock
|
||||
{
|
||||
public class LockInfoMutable : ILockInfoMutable
|
||||
{
|
||||
/// <summary> Lock info object. </summary>
|
||||
private LockInfo LockInfo { get; set; }
|
||||
|
||||
/// <summary> Constructs a bluetooth lock info object. </summary>
|
||||
/// <param name="id">Id of lock must always been known when constructing an lock info object.</param>
|
||||
public LockInfoMutable(LockingState state)
|
||||
{
|
||||
LockInfo = new LockInfo.Builder() { State = state }.Build();
|
||||
}
|
||||
|
||||
public LockingState State
|
||||
{
|
||||
get => LockInfo.State;
|
||||
set => LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build();
|
||||
}
|
||||
|
||||
/// <summary> Holds the percentage of lock battery.</summary>
|
||||
public double BatteryPercentage { get; set; } = double.NaN;
|
||||
|
||||
/// <summary> Loads lock info object from values. </summary>
|
||||
public void Load()
|
||||
{
|
||||
LockInfo = new LockInfo.Builder(LockInfo) { }.Build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
using Serilog;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the state of a chargeable battery.
|
||||
/// </summary>
|
||||
public class Battery : IBattery
|
||||
{
|
||||
private Battery() { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current charging level of the battery in percent, double.NaN if unknown.
|
||||
/// </summary>
|
||||
public double CurrentChargePercent { get; private set; } = double.NaN;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
public int? CurrentChargeBars { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
public int? MaxChargeBars { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether backend is aware of battery charging level.
|
||||
/// </summary>
|
||||
public bool? IsBackendAccessible { get; private set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to display battery level or not.
|
||||
/// </summary>
|
||||
public bool? IsHidden { get; private set; } = null;
|
||||
|
||||
public class Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the current charging level of the battery in bars.
|
||||
/// </summary>
|
||||
public int? CurrentChargeBars { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the maximum charging level of the battery in bars.
|
||||
/// </summary>
|
||||
public int? MaxChargeBars { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the current charging level of the battery in percent.
|
||||
/// </summary>
|
||||
public double CurrentChargePercent { get; set; } = double.NaN;
|
||||
|
||||
/// <summary>
|
||||
/// Holds whether backend is aware of battery charging level.
|
||||
/// </summary>
|
||||
public bool? IsBackendAccessible { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Holds whether to display battery level or not.
|
||||
/// </summary>
|
||||
public bool? IsHidden { get; set; } = null;
|
||||
|
||||
public Battery Build()
|
||||
{
|
||||
if (!double.IsNaN(CurrentChargePercent)
|
||||
&& (CurrentChargePercent < 0 || 100 < CurrentChargePercent))
|
||||
{
|
||||
// Invalid filling level detected
|
||||
CurrentChargePercent = double.NaN;
|
||||
}
|
||||
|
||||
if (CurrentChargeBars < 0)
|
||||
{
|
||||
// Current value of bars must never be smaller zero.
|
||||
CurrentChargeBars = null;
|
||||
}
|
||||
|
||||
if (MaxChargeBars < 0)
|
||||
{
|
||||
// Max value of bars must never be smaller zero.
|
||||
MaxChargeBars = null;
|
||||
}
|
||||
|
||||
if (CurrentChargeBars != null
|
||||
&& MaxChargeBars == null)
|
||||
{
|
||||
// If current charge bars is set, max charge must be set as well.
|
||||
Log.ForContext<Battery>().Error($"Current bars value can not be set to {CurrentChargeBars} if max bars is not set.");
|
||||
CurrentChargeBars = null;
|
||||
}
|
||||
|
||||
if (CurrentChargeBars != null
|
||||
&& MaxChargeBars != null
|
||||
&& CurrentChargeBars > MaxChargeBars)
|
||||
{
|
||||
// If current charge bars must never be larger than max charge bars.
|
||||
Log.ForContext<Battery>().Error($"Invalid current bars value {CurrentChargeBars} detected. Value must never be larger than max value bars {MaxChargeBars}.");
|
||||
CurrentChargeBars = null;
|
||||
}
|
||||
|
||||
return new Battery
|
||||
{
|
||||
CurrentChargeBars = CurrentChargeBars,
|
||||
MaxChargeBars = MaxChargeBars,
|
||||
CurrentChargePercent = CurrentChargePercent,
|
||||
IsBackendAccessible = IsBackendAccessible,
|
||||
IsHidden = IsHidden
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the state of a chargeable battery.
|
||||
/// </summary>
|
||||
public class BatteryMutable : IBatteryMutable, INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
IBattery _battery;
|
||||
|
||||
public BatteryMutable(IBattery battery)
|
||||
{
|
||||
_battery = battery;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current charging level of the battery in percent, double.NaN if unknown.
|
||||
/// </summary>
|
||||
public double CurrentChargePercent => _battery.CurrentChargePercent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
public int? CurrentChargeBars
|
||||
{
|
||||
get => _battery.CurrentChargeBars;
|
||||
set
|
||||
{
|
||||
double GetCurrentChargePercent()
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// Filling level is unknown.
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
if (_battery.MaxChargeBars == null || _battery.MaxChargeBars == 0)
|
||||
{
|
||||
// Percentage filling level can not be calculated.
|
||||
return _battery.CurrentChargePercent;
|
||||
}
|
||||
|
||||
return (int)(100 * value / _battery.MaxChargeBars);
|
||||
}
|
||||
|
||||
if (_battery.CurrentChargeBars == value)
|
||||
{
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
_battery = new Battery.Builder
|
||||
{
|
||||
MaxChargeBars = _battery.MaxChargeBars,
|
||||
IsBackendAccessible = _battery.IsBackendAccessible,
|
||||
IsHidden = _battery.IsHidden,
|
||||
CurrentChargeBars = value,
|
||||
CurrentChargePercent = GetCurrentChargePercent(),
|
||||
}.Build();
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentChargeBars)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentChargePercent)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
public int? MaxChargeBars => _battery.MaxChargeBars;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether backend is aware of battery charging level.
|
||||
/// </summary>
|
||||
public bool? IsBackendAccessible => _battery.IsBackendAccessible;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to display battery level or not.
|
||||
/// </summary>
|
||||
public bool? IsHidden => _battery.IsHidden;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
|
||||
{
|
||||
public interface IBattery
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the current charging level of the battery in percent, double.NaN if unknown.
|
||||
/// </summary>
|
||||
double CurrentChargePercent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the current charging level of the battery in bars. Must not be larger than MaxChargeBars, null if unknown.
|
||||
/// </summary>
|
||||
int? CurrentChargeBars { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the maximum charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
int? MaxChargeBars { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds whether backend is aware of battery charging level.
|
||||
/// </summary>
|
||||
bool? IsBackendAccessible { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds whether to display battery level or not.
|
||||
/// </summary>
|
||||
bool? IsHidden { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the state of a chargeable battery.
|
||||
/// </summary>
|
||||
public interface IBatteryMutable : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current charging level of the battery in percent, double.NaN if unknown.
|
||||
/// </summary>
|
||||
double CurrentChargePercent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current charging level of the battery in bars. Must not be larger than MaxChargeBars, null if unknown.
|
||||
/// </summary>
|
||||
int? CurrentChargeBars { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
int? MaxChargeBars { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether backend is aware of battery charging level.
|
||||
/// </summary>
|
||||
bool? IsBackendAccessible { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to display battery level or not.
|
||||
/// </summary>
|
||||
bool? IsHidden { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS
|
||||
{
|
||||
public enum DriveType
|
||||
{
|
||||
/// <summary>
|
||||
/// Bike without pedaling aid.
|
||||
/// </summary>
|
||||
SoleHumanPowered,
|
||||
|
||||
/// <summary>
|
||||
/// pedal electric cycle: Pedaling is assisted by an electric engine.
|
||||
/// </summary>
|
||||
Pedelec
|
||||
}
|
||||
|
||||
public class DriveMutable
|
||||
{
|
||||
public DriveMutable(
|
||||
IEngine engine = null,
|
||||
IBattery battery = null)
|
||||
{
|
||||
if (engine == null)
|
||||
{
|
||||
Engine = new Engine();
|
||||
Battery = new BatteryMutable(new Battery.Builder().Build());
|
||||
Type = DriveType.SoleHumanPowered;
|
||||
return;
|
||||
}
|
||||
|
||||
Engine = engine;
|
||||
Battery = new BatteryMutable(battery ?? new Battery.Builder().Build());
|
||||
Type = DriveType.Pedelec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the drive.
|
||||
/// </summary>
|
||||
public DriveType Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Engine driving the bike.
|
||||
/// </summary>
|
||||
public IEngine Engine { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Battery powering the engine.
|
||||
/// </summary>
|
||||
public IBatteryMutable Battery { get; private set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS
|
||||
{
|
||||
public class Engine : IEngine
|
||||
{
|
||||
public Engine(string manufacturer = null)
|
||||
=> Manufacturer = !string.IsNullOrEmpty(manufacturer) ? manufacturer : null;
|
||||
|
||||
/// <summary>
|
||||
/// Manufacturer of the engine.
|
||||
/// </summary>
|
||||
public string Manufacturer { get; private set; } = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
namespace ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS
|
||||
{
|
||||
public interface IEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Manufacturer of the engine.
|
||||
/// </summary>
|
||||
string Manufacturer { get; }
|
||||
}
|
||||
}
|
23
SharedBusinessLogic/Model/Bikes/BikeInfoNS/DriveNS/IDrive.cs
Normal file
23
SharedBusinessLogic/Model/Bikes/BikeInfoNS/DriveNS/IDrive.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS
|
||||
{
|
||||
public interface IDrive
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type of the drive.
|
||||
/// </summary>
|
||||
DriveNS.DriveType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Engine driving the bike.
|
||||
/// </summary>
|
||||
IEngine Engine { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Battery powering the engine.
|
||||
/// </summary>
|
||||
IBattery Battery { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static ShareeBike.Model.Bikes.BikeInfoNS.RentalDescription;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS
|
||||
{
|
||||
public interface IRentalDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the tariff.
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the time span for which a bike can be reserved.
|
||||
/// </summary>
|
||||
TimeSpan MaxReservationTimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic language aware tariff elements to be displayed to user.
|
||||
/// </summary>
|
||||
Dictionary<string, TariffElement> TariffEntries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Well known language aware elements (AGB, tracking info, ...) to be displayed to user.
|
||||
/// </summary>
|
||||
Dictionary<string, InfoElement> InfoEntries { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS
|
||||
{
|
||||
/// <summary>
|
||||
/// Successor of TarifDescription- object.
|
||||
/// Manages tariff- and rental info.
|
||||
/// </summary>
|
||||
public class RentalDescription : IRentalDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// The different elements of a tariff (example: "Max Gebühr", ) to be displayed by sharee.bike without processing
|
||||
/// </summary>
|
||||
public class TariffElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the tariff element (language aware). To be displayed to user (example of elements: "Gratis Mietzeit", "Mietgebühr", "Max Gebühr").
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the tariff element value (language aware, i.e. value from backend might be english, german, ... depending on smart phone value). To be displayed to user (example: "9.00 € / Tag").
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Info element of general purpose (AGB, tracking info, ...)
|
||||
/// </summary>
|
||||
public class InfoElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Key which identifies the value (required for special processing)
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Text (language aware) to be displayed to user.
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name of the tariff.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Number of the tariff.
|
||||
/// </summary>
|
||||
public int? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the time span for which a bike can be reserved.
|
||||
/// </summary>
|
||||
public TimeSpan MaxReservationTimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic language aware tariff elements to be displayed to user.
|
||||
/// </summary>
|
||||
public Dictionary<string, TariffElement> TariffEntries { get; set; } = new Dictionary<string, TariffElement>();
|
||||
|
||||
/// <summary>
|
||||
/// Well known language aware elements (AGB, tracking info, ...) to be displayed to user.
|
||||
/// </summary>
|
||||
public Dictionary<string, InfoElement> InfoEntries { get; set; } = new Dictionary<string, InfoElement>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
|
||||
namespace ShareeBike.Model.Bikes.BikeInfoNS
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds tariff info for a single bike.
|
||||
/// </summary>
|
||||
#if USCSHARP9
|
||||
public record TariffDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the tariff.
|
||||
/// </summary>
|
||||
public string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of the tariff.
|
||||
/// </summary>
|
||||
public int? Number { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Costs per hour in euro.
|
||||
/// </summary>
|
||||
public double FeeEuroPerHour { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Costs of the abo per month.
|
||||
/// </summary>
|
||||
public double AboEuroPerMonth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Costs per hour in euro.
|
||||
/// </summary>
|
||||
public TimeSpan FreeTimePerSession { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Max. costs per day in euro.
|
||||
/// </summary>
|
||||
public double MaxFeeEuroPerDay { get; init; }
|
||||
}
|
||||
#else
|
||||
public class TariffDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the tariff.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of the tariff.
|
||||
/// </summary>
|
||||
public int? Number { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Costs per hour in euro.
|
||||
/// </summary>
|
||||
public double FeeEuroPerHour { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Costs of the abo per month.
|
||||
/// </summary>
|
||||
public double AboEuroPerMonth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Costs per hour in euro.
|
||||
/// </summary>
|
||||
public TimeSpan FreeTimePerSession { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max. costs per day in euro.
|
||||
/// </summary>
|
||||
public double MaxFeeEuroPerDay { get; set; }
|
||||
|
||||
/// <summary> Info about operator agb as HTML (i.g. text and hyperlink). </summary>
|
||||
public string OperatorAgb { get; set; }
|
||||
|
||||
/// <summary> Text which informs users about GPS tracking if tracking is on. </summary>
|
||||
public string TrackingInfo { get; set; }
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
35
SharedBusinessLogic/Model/Bikes/IBikeCollection.cs
Normal file
35
SharedBusinessLogic/Model/Bikes/IBikeCollection.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Bikes
|
||||
{
|
||||
public interface IBikeDictionary<T> : IReadOnlyCollection<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a bike by its id.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
T GetById(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a bike by given key exists.
|
||||
/// </summary>
|
||||
/// <param name="p_strKey">Key to check.</param>
|
||||
/// <returns>True if bike exists.</returns>
|
||||
bool ContainsKey(string id);
|
||||
}
|
||||
public interface IBikeDictionaryMutable<T> : IBikeDictionary<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes a bike by its id.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of bike to be removed.</param>
|
||||
void RemoveById(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new element to dictionary.
|
||||
/// </summary>
|
||||
/// <param name="newElement">New element to add.</param>
|
||||
void Add(T newElement);
|
||||
}
|
||||
}
|
26
SharedBusinessLogic/Model/BookingFinishedModel.cs
Normal file
26
SharedBusinessLogic/Model/BookingFinishedModel.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using ShareeBike.Model.MiniSurvey;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds tasks to be accomplished/ information shown to user after booking has finished.
|
||||
/// </summary>
|
||||
public class BookingFinishedModel : IBookingFinishedModel
|
||||
{
|
||||
/// <summary> Minisurvey to query user.</summary>
|
||||
public IMiniSurveyModel MiniSurvey { get; set; } = new MiniSurveyModel();
|
||||
|
||||
/// <summary> Holds info about co2 saving accomplished by using cargo bike. </summary>
|
||||
public string Co2Saving { get; set; }
|
||||
|
||||
/// <summary> Holds info about driven distance. </summary>
|
||||
public string Distance { get; set; }
|
||||
|
||||
/// <summary> Holds info about rental duration. </summary>
|
||||
public string Duration { get; set; }
|
||||
|
||||
/// <summary> Holds info about accruing rental costs. </summary>
|
||||
public string RentalCosts { get; set; }
|
||||
|
||||
}
|
||||
}
|
187
SharedBusinessLogic/Model/Connector/Command/Command.cs
Normal file
187
SharedBusinessLogic/Model/Connector/Command/Command.cs
Normal file
|
@ -0,0 +1,187 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Connector.Updater;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Model.User.Account;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Repository.Request;
|
||||
using ShareeBike.Repository.Response;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public class Command : Base, ICommand
|
||||
{
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
public bool IsConnected => CopriServer.IsConnected;
|
||||
|
||||
/// <summary> No user is logged in.</summary>
|
||||
public string SessionCookie => null;
|
||||
|
||||
/// <summary> Is raised whenever login state has changed.</summary>
|
||||
public event LoginStateChangedEventHandler LoginStateChanged;
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="p_oCopriServer">Server which implements communication.</param>
|
||||
public Command(
|
||||
ICopriServerBase p_oCopriServer) : base(p_oCopriServer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs user in.
|
||||
/// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device).
|
||||
/// If log in fails (password modified) session cookie is set to empty.
|
||||
/// If communication fails an exception is thrown.
|
||||
/// </summary>
|
||||
public async Task<IAccount> DoLogin(
|
||||
string mail,
|
||||
string password,
|
||||
string deviceId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mail))
|
||||
{
|
||||
throw new ArgumentNullException("Can not login user. Mail address must not be null or empty.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
{
|
||||
throw new ArgumentNullException("Can not login user. Password must not be null or empty.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException("Can not login user. Device not be null or empty.");
|
||||
}
|
||||
|
||||
AuthorizationResponse response;
|
||||
try
|
||||
{
|
||||
response = (await CopriServer.DoAuthorizationAsync(mail, password, deviceId)).GetIsResponseOk(mail);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
var l_oAccount = response.GetAccount(MerchantId, mail, password);
|
||||
|
||||
// Log in state changes. Notify parent object to update.
|
||||
LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs(l_oAccount.SessionCookie, l_oAccount.Mail));
|
||||
|
||||
return l_oAccount;
|
||||
}
|
||||
|
||||
/// <summary> Logs user out. </summary>
|
||||
public async Task DoLogout()
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected log out request detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to reserve a bike.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
public async Task DoReserve(
|
||||
Bikes.BikeInfoNS.BC.IBikeInfoMutable bike)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected booking request detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary> Request to cancel a reservation.</summary>
|
||||
/// <param name="p_oBike">Bike to book.</param>
|
||||
public async Task DoCancelReservation(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected cancel reservation request detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary> Get authentication keys.</summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
public async Task CalculateAuthKeys(Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable bike)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected request to get authentication keys detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary> Updates COPRI lock state for a booked bike. </summary>
|
||||
/// <param name="bike">Bike to update locking state for.</param>
|
||||
/// <param name="location">Location where lock was opened/ changed.</param>
|
||||
/// <returns>Response on updating locking state.</returns>
|
||||
public async Task StartReturningBike(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected request to notify about start of returning bike. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary> Notifies COPRI about start of returning sequence. </summary>
|
||||
/// <remarks> Operator specific call.</remarks>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <returns>Response on notification about start of returning sequence.</returns>
|
||||
public async Task UpdateLockingStateAsync(Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable bike, LocationDto location)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected request to update locking state detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DoBookAsync(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike, LockingAction? nextAction = null)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected booking request detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task BookAndOpenAync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected request to book and open bike detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<BookingFinishedModel> DoReturn(
|
||||
Bikes.BikeInfoNS.BC.IBikeInfoMutable bike,
|
||||
LocationDto location,
|
||||
ISmartDevice smartDevice)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected returning request detected. No user logged in.");
|
||||
return await Task.FromResult(new BookingFinishedModel());
|
||||
}
|
||||
|
||||
public async Task<BookingFinishedModel> ReturnAndCloseAsync(
|
||||
Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike,
|
||||
ISmartDevice smartDevice)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected close lock and return request detected. No user logged in.");
|
||||
return await Task.FromResult(new BookingFinishedModel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits feedback to copri server.
|
||||
/// </summary>
|
||||
/// <param name="userFeedback">Feedback to submit.</param>
|
||||
#if USCSHARP9
|
||||
public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri)
|
||||
#else
|
||||
public async Task DoSubmitFeedback(IUserFeedback userFeedback, Uri opertorUri)
|
||||
#endif
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected submit feedback request detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary> Submits mini survey to copri server. </summary>
|
||||
/// <param name="answers">Collection of answers.</param>
|
||||
public async Task DoSubmitMiniSurvey(IDictionary<string, string> answers)
|
||||
{
|
||||
Log.ForContext<Command>().Error("Unexpected submit mini survey request detected. No user logged in.");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OpenLockAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task CloseLockAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
321
SharedBusinessLogic/Model/Connector/Command/CommandLoggedIn.cs
Normal file
321
SharedBusinessLogic/Model/Connector/Command/CommandLoggedIn.cs
Normal file
|
@ -0,0 +1,321 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
|
||||
using ShareeBike.Model.Connector.Updater;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Model.User.Account;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Repository.Request;
|
||||
using ShareeBike.Repository.Response;
|
||||
using ShareeBike.Services.CopriApi;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public class CommandLoggedIn : BaseLoggedIn, ICommand
|
||||
{
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
public bool IsConnected => CopriServer.IsConnected;
|
||||
|
||||
/// <summary> Is raised whenever login state has changed.</summary>
|
||||
public event LoginStateChangedEventHandler LoginStateChanged;
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="p_oCopriServer">Server which implements communication.</param>
|
||||
public CommandLoggedIn(ICopriServerBase p_oCopriServer,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
Func<DateTime> dateTimeProvider) : base(p_oCopriServer, sessionCookie, mail, dateTimeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs user in.
|
||||
/// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device).
|
||||
/// If log in fails (password modified) session cookie is set to empty.
|
||||
/// If communication fails an ShareeBike.Repository.Exception is thrown.
|
||||
/// </summary>
|
||||
/// <param name="p_oAccount">Account to use for login.</param>
|
||||
public Task<IAccount> DoLogin(string mail, string password, string deviceId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mail))
|
||||
{
|
||||
throw new ArgumentNullException("Can not login user. Mail address must not be null or empty.");
|
||||
}
|
||||
|
||||
throw new Exception($"Fehler beim Anmelden von unter {mail}. Benutzer {Mail} ist bereits angemeldet.");
|
||||
}
|
||||
|
||||
/// <summary> Logs user out. </summary>
|
||||
public async Task DoLogout()
|
||||
{
|
||||
AuthorizationoutResponse l_oResponse = null;
|
||||
try
|
||||
{
|
||||
l_oResponse = (await CopriServer.DoAuthoutAsync()).GetIsResponseOk();
|
||||
}
|
||||
catch (AuthcookieNotDefinedException)
|
||||
{
|
||||
// Cookie is no more defined, i.e. no need to logout user at copri because user is already logged out.
|
||||
// Just ignore this error.
|
||||
// User logged out, log in state changed. Notify parent object to update.
|
||||
LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs());
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
// User logged out, log in state changed. Notify parent object to update.
|
||||
LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to reserve a bike.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
public async Task DoReserve(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException("Can not reserve bike. No bike object available.");
|
||||
}
|
||||
|
||||
BikeInfoReservedOrBooked response;
|
||||
try
|
||||
{
|
||||
response = (await CopriServer.DoReserveAsync(bike.Id, bike.OperatorUri)).GetIsReserveResponseOk(bike.Id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception was not expected or too many subsequent exceptions detected.
|
||||
throw;
|
||||
}
|
||||
|
||||
bike.Load(response, Mail, Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None);
|
||||
}
|
||||
|
||||
/// <summary> Request to cancel a reservation.</summary>
|
||||
/// <param name="bike">Bike to cancel reservation.</param>
|
||||
public async Task DoCancelReservation(
|
||||
Bikes.BikeInfoNS.BC.IBikeInfoMutable bike)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException("Can not cancel reservation of bike. No bike object available.");
|
||||
}
|
||||
|
||||
BookingActionResponse response;
|
||||
try
|
||||
{
|
||||
response = (await CopriServer
|
||||
.DoCancelReservationAsync(bike.Id, bike.OperatorUri))
|
||||
.GetIsResponseOk(string.Format(MultilingualResources.AppResources.ErrorCancelReservationFailed, bike.Id));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception was not expected or too many subsequent exceptions detected.
|
||||
throw;
|
||||
}
|
||||
|
||||
bike.Load(Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None);
|
||||
}
|
||||
|
||||
/// <summary> Get authentication keys.</summary>
|
||||
/// <param name="bike">Bike to get new keys for.</param>
|
||||
public async Task CalculateAuthKeys(Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable bike)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException("Can not calculate auth keys. No bike object available.");
|
||||
}
|
||||
|
||||
switch (bike.State.Value)
|
||||
{
|
||||
case State.InUseStateEnum.Reserved:
|
||||
case State.InUseStateEnum.Booked:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentNullException($"Can not calculate auth keys. Unexpected bike state {bike.State.Value} detected.");
|
||||
}
|
||||
|
||||
BikeInfoReservedOrBooked response;
|
||||
Guid guid = (bike is BikeInfoMutable btBike) ? btBike.LockInfo.Guid : new Guid();
|
||||
try
|
||||
{
|
||||
response = (await CopriServer.CalculateAuthKeysAsync(bike.Id, bike.OperatorUri)).GetIsBookingResponseOk(bike.Id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception was not expected or too many subsequent exceptions detected.
|
||||
throw;
|
||||
}
|
||||
|
||||
UpdaterJSON.Load(
|
||||
bike,
|
||||
response,
|
||||
Mail,
|
||||
Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None);
|
||||
}
|
||||
|
||||
/// <summary> Notifies COPRI about start of returning sequence. </summary>
|
||||
/// <remarks> Operator specific call.</remarks>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <returns>Response on notification about start of returning sequence.</returns>
|
||||
public async Task StartReturningBike(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException("Can not notify about start returning bike. No bike object available.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
(await CopriServer.StartReturningBike(
|
||||
bike.Id,
|
||||
bike.OperatorUri)).GetIsResponseOk("Start returning bike");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception was not expected or too many subsequent exceptions detected.
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary> Updates COPRI lock state for a booked or reserved bike. </summary>
|
||||
/// <param name="bike">Bike to update locking state for.</param>
|
||||
/// <param name="location">Location of the bike.</param>
|
||||
/// <returns>Response on updating locking state.</returns>
|
||||
public async Task UpdateLockingStateAsync(
|
||||
IBikeInfoMutable bike,
|
||||
LocationDto location)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException("Can not update locking state of bike. No bike object available.");
|
||||
}
|
||||
|
||||
if (bike.State.Value != State.InUseStateEnum.Booked && bike.State.Value != State.InUseStateEnum.Reserved)
|
||||
{
|
||||
throw new ArgumentNullException($"Can not update locking state of bike. Unexpected booking state {bike.State} detected.");
|
||||
}
|
||||
|
||||
lock_state? state = RequestBuilderHelper.GetLockState(bike.LockInfo.State);
|
||||
|
||||
if (!state.HasValue)
|
||||
{
|
||||
throw new ArgumentNullException($"Can not update locking state of bike. Unexpected locking state {bike.LockInfo.State} detected.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
(await CopriServer.UpdateLockingStateAsync(
|
||||
bike.Id,
|
||||
state.Value,
|
||||
bike.OperatorUri,
|
||||
location,
|
||||
bike.LockInfo.BatteryPercentage,
|
||||
bike.LockInfo.VersionInfo)).GetIsBookingResponseOk(bike.Id);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception was not expected or too many subsequent exceptions detected.
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Request to book a bike. </summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
/// <param name="nextAction">If not null next locking action which is performed after booking.</param>
|
||||
public async Task DoBookAsync(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike, LockingAction? nextAction = null)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bike), "Can not book bike. No bike object available.");
|
||||
}
|
||||
|
||||
BikeInfoReservedOrBooked response;
|
||||
var btBike = bike as BikeInfoMutable;
|
||||
Guid guid = btBike != null ? btBike.LockInfo.Guid : new Guid();
|
||||
double batteryPercentage = btBike != null ? btBike.LockInfo.BatteryPercentage : double.NaN;
|
||||
|
||||
response = (await CopriServer.DoBookAsync(
|
||||
bike.OperatorUri,
|
||||
bike.Id,
|
||||
guid,
|
||||
batteryPercentage,
|
||||
nextAction)).GetIsBookingResponseOk(bike.Id);
|
||||
|
||||
bike.Load(
|
||||
response,
|
||||
Mail,
|
||||
Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Books a bike and opens the lock.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike to book and open.</param>
|
||||
public async Task BookAndOpenAync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike)
|
||||
=> await Polling.BookAndOpenAync(CopriServer, bike, Mail);
|
||||
|
||||
/// <summary> Request to return a bike.</summary>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <param name="locaton">Position of the bike for bluetooth locks.</param>
|
||||
/// <param name="smartDevice">Provides info about hard and software.</param>
|
||||
public async Task<BookingFinishedModel> DoReturn(
|
||||
Bikes.BikeInfoNS.BC.IBikeInfoMutable bike,
|
||||
LocationDto location = null,
|
||||
ISmartDevice smartDevice = null)
|
||||
{
|
||||
if (bike == null)
|
||||
{
|
||||
throw new ArgumentNullException("Can not return bike. No bike object available.");
|
||||
}
|
||||
|
||||
DoReturnResponse response
|
||||
= (await CopriServer.DoReturn(bike.Id, location, bike.OperatorUri)).GetIsReturnBikeResponseOk(bike.Id);
|
||||
|
||||
bike.Load(
|
||||
Bikes.BikeInfoNS.BC.NotifyPropertyChangedLevel.None,
|
||||
response.bike_returned.station ?? string.Empty);
|
||||
|
||||
return response?.Create() ?? new BookingFinishedModel();
|
||||
}
|
||||
|
||||
/// <summary> Request to return bike and close the lock.</summary>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <param name="smartDevice">Provides info about hard and software.</param>
|
||||
public async Task<BookingFinishedModel> ReturnAndCloseAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike, ISmartDevice smartDevice = null)
|
||||
=> await Polling.ReturnAndCloseAync(CopriServer, smartDevice, bike);
|
||||
|
||||
/// <summary>
|
||||
/// Submits feedback to copri server.
|
||||
/// </summary>
|
||||
/// <param name="userFeedback">Feedback to submit.</param>
|
||||
#if USCSHARP9
|
||||
public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri)
|
||||
=> await CopriServer.DoSubmitFeedback(userFeedback.BikeId, userFeedback.Message, userFeedback.IsBikeBroken, opertorUri);
|
||||
#else
|
||||
/// <summary> Submits feedback for a renting operation.</summary>
|
||||
public async Task DoSubmitFeedback(
|
||||
IUserFeedback userFeedback,
|
||||
Uri opertorUri)
|
||||
=> await CopriServer.DoSubmitFeedback(userFeedback.BikeId, userFeedback.CurrentChargeBars, userFeedback.Message, userFeedback.IsBikeBroken, opertorUri);
|
||||
#endif
|
||||
|
||||
/// <summary> Submits mini survey to copri server. </summary>
|
||||
/// <param name="answers">Collection of answers.</param>
|
||||
public async Task DoSubmitMiniSurvey(IDictionary<string, string> answers)
|
||||
=> await CopriServer.DoSubmitMiniSurvey(answers);
|
||||
|
||||
public async Task OpenLockAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike)
|
||||
=> await CopriServer.OpenAync(bike);
|
||||
public async Task CloseLockAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike)
|
||||
=> await CopriServer.CloseAync(bike);
|
||||
}
|
||||
}
|
177
SharedBusinessLogic/Model/Connector/Command/ICommand.cs
Normal file
177
SharedBusinessLogic/Model/Connector/Command/ICommand.cs
Normal file
|
@ -0,0 +1,177 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Model.User.Account;
|
||||
using ShareeBike.Repository.Request;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public interface ICommand
|
||||
{
|
||||
/// <summary> Is raised whenever login state has changed.</summary>
|
||||
event LoginStateChangedEventHandler LoginStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Logs user in.
|
||||
/// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device).
|
||||
/// If log in fails (password modified) session cookie is set to empty.
|
||||
/// If communication fails an exception is thrown.
|
||||
/// </summary>
|
||||
Task<IAccount> DoLogin(string p_strMail, string p_strPassword, string p_strDeviceId);
|
||||
|
||||
/// <summary> Logs user out. </summary>
|
||||
Task DoLogout();
|
||||
|
||||
/// <summary> Request to reserve a bike.</summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
Task DoReserve(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Request to cancel a reservation.</summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
Task DoCancelReservation(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Get authentication keys to connect to lock.</summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
Task CalculateAuthKeys(Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Notifies COPRI about start of returning sequence. </summary>
|
||||
/// <remarks> Operator specific call.</remarks>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <returns>Response on notification about start of returning sequence.</returns>
|
||||
Task StartReturningBike(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Updates COPRI lock state for a booked bike. </summary>
|
||||
/// <param name="bike">Bike to update locking state for.</param>
|
||||
/// <param name="location">Geolocation of lock when returning bike.</param>
|
||||
/// <returns>Response on updating locking state.</returns>
|
||||
Task UpdateLockingStateAsync(Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable bike, LocationDto location = null);
|
||||
|
||||
/// <summary> Request to book a bike.</summary>
|
||||
/// <param name="bike">Bike to book.</param>
|
||||
/// <param name="nextAction">If not null next locking action which is performed after booking.</param>
|
||||
Task DoBookAsync(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike, LockingAction? nextAction = null);
|
||||
|
||||
/// <summary> Request to book a bike and open its lock.</summary>
|
||||
/// <param name="bike">Bike to book and to open lock for.</param>
|
||||
Task BookAndOpenAync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Request to open lock.</summary>
|
||||
/// <param name="bike">Bike for which lock has to be opened.</param>
|
||||
Task OpenLockAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Request to close lock.</summary>
|
||||
/// <param name="bike">Bike for which lock has to be closed.</param>
|
||||
Task CloseLockAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike);
|
||||
|
||||
/// <summary> Request to return a bike.</summary>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <param name="location">Geolocation of lock when returning bike.</param>
|
||||
/// <param name="smartDevice">Provides info about hard and software.</param>
|
||||
Task<BookingFinishedModel> DoReturn(Bikes.BikeInfoNS.BC.IBikeInfoMutable bike, LocationDto geolocation = null, ISmartDevice smartDevice = null);
|
||||
|
||||
/// <summary> Request to return bike and close the lock.</summary>
|
||||
/// <param name="bike">Bike to return.</param>
|
||||
/// <param name="smartDevice">Provides info about hard and software.</param>
|
||||
Task<BookingFinishedModel> ReturnAndCloseAsync(Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable bike, ISmartDevice smartDevice = null);
|
||||
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary> True if user is logged in false if not. </summary>
|
||||
string SessionCookie { get; }
|
||||
|
||||
/// <summary> Submits feedback for a renting operation.</summary>
|
||||
Task DoSubmitFeedback(
|
||||
IUserFeedback userFeedback,
|
||||
Uri opertorUri);
|
||||
|
||||
/// <summary> Submits mini survey to copri server. </summary>
|
||||
/// <param name="answers">Collection of answers.</param>
|
||||
Task DoSubmitMiniSurvey(IDictionary<string, string> answers);
|
||||
|
||||
#if USCSHARP9
|
||||
/// <summary>
|
||||
/// Feedback given by user when returning bike.
|
||||
/// </summary>
|
||||
public interface IUserFeedback
|
||||
{
|
||||
/// <summary> Id of the bike to which the feedback is related to.</summary>
|
||||
string BikeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds whether bike is broken or not.
|
||||
/// </summary>
|
||||
bool IsBikeBroken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds either
|
||||
/// - general feedback
|
||||
/// - error description of broken bike
|
||||
/// or both.
|
||||
/// </summary>
|
||||
string Message { get; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Defines delegate to be raised whenever login state changes.</summary>
|
||||
/// <param name="eventArgs">Holds session cookie and mail address if user logged in successfully.</param>
|
||||
public delegate void LoginStateChangedEventHandler(object sender, LoginStateChangedEventArgs eventArgs);
|
||||
|
||||
#if !USCSHARP9
|
||||
/// <summary>
|
||||
/// Feedback given by user when returning bike.
|
||||
/// </summary>
|
||||
public interface IUserFeedback
|
||||
{
|
||||
/// <summary> Id of the bike to which the feedback is related to.</summary>
|
||||
string BikeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the current charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
int? CurrentChargeBars { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds whether bike is broken or not.
|
||||
/// </summary>
|
||||
bool IsBikeBroken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds either
|
||||
/// - general feedback
|
||||
/// - error description of broken bike
|
||||
/// or both.
|
||||
/// </summary>
|
||||
string Message { get; }
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary> Event arguments to notify about changes of logged in state.</summary>
|
||||
public class LoginStateChangedEventArgs : EventArgs
|
||||
{
|
||||
public LoginStateChangedEventArgs() : this(string.Empty, string.Empty)
|
||||
{ }
|
||||
|
||||
public LoginStateChangedEventArgs(string sessionCookie, string mail)
|
||||
{
|
||||
SessionCookie = sessionCookie;
|
||||
Mail = mail;
|
||||
}
|
||||
|
||||
public string SessionCookie { get; }
|
||||
|
||||
public string Mail { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a action to be performed with an lock.
|
||||
/// </summary>
|
||||
public enum LockingAction
|
||||
{
|
||||
Close,
|
||||
Open,
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
#if USCSHARP9
|
||||
public record UserFeedbackDto : ICommand.IUserFeedback
|
||||
{
|
||||
public string BikeId { get; init; }
|
||||
public bool IsBikeBroken { get; init; }
|
||||
public string Message { get; init; }
|
||||
}
|
||||
#else
|
||||
#if USCSHARP9
|
||||
public class UserFeedbackDto : ICommand.IUserFeedback
|
||||
#else
|
||||
public class UserFeedbackDto : IUserFeedback
|
||||
#endif
|
||||
{
|
||||
public string BikeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Holds the current charging level of the battery in bars, null if unknown.
|
||||
/// </summary>
|
||||
public int? CurrentChargeBars { get; set; }
|
||||
|
||||
public bool IsBikeBroken { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
71
SharedBusinessLogic/Model/Connector/Connector.cs
Normal file
71
SharedBusinessLogic/Model/Connector/Connector.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Repository;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects app to copri data by getting data from copri.
|
||||
/// </summary>
|
||||
public class Connector : IConnector
|
||||
{
|
||||
/// <summary>Constructs a copri connector object to connect to copri by https with cache fall back.</summary>
|
||||
/// <param name="activeUri"> Uri to connect to.</param>
|
||||
/// <param name="appContextInfo">Provides app related info (app name and version, merchant id) to pass to COPRI.</param>
|
||||
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
|
||||
/// <param name="sessionCookie"> Holds the session cookie.</param>
|
||||
/// <param name="mail">Mail of user.</param>
|
||||
/// <param name="smartDevice">Holds info about smart device.</param>
|
||||
/// <param name="expiresAfter">Timespan which holds value after which cache expires.</param>
|
||||
/// <param name="server"> Is null in production and might be a mock in testing context.</param>
|
||||
public Connector(
|
||||
Uri activeUri,
|
||||
AppContextInfo appContextInfo,
|
||||
string uiIsoLangugageName,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
ISmartDevice smartDevice = null,
|
||||
TimeSpan? expiresAfter = null,
|
||||
ICachedCopriServer server = null)
|
||||
{
|
||||
var cachedServer = server ?? new CopriProviderHttps(
|
||||
activeUri,
|
||||
appContextInfo.MerchantId,
|
||||
appContextInfo,
|
||||
uiIsoLangugageName,
|
||||
smartDevice,
|
||||
sessionCookie,
|
||||
expiresAfter);
|
||||
|
||||
Command = CreateCommand(
|
||||
cachedServer,
|
||||
sessionCookie,
|
||||
mail);
|
||||
|
||||
Query = CreateQuery(
|
||||
cachedServer,
|
||||
sessionCookie,
|
||||
mail);
|
||||
}
|
||||
|
||||
/// <summary> Object for querying stations and bikes.</summary>
|
||||
public ICommand Command { get; private set; }
|
||||
|
||||
/// <summary> Object for querying stations and bikes.</summary>
|
||||
public IQuery Query { get; private set; }
|
||||
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
public bool IsConnected => Command.IsConnected;
|
||||
|
||||
/// <summary> Creates a command object to perform copri commands. </summary>
|
||||
public static ICommand CreateCommand(ICopriServerBase copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie)
|
||||
? new Command(copri)
|
||||
: new CommandLoggedIn(copri, sessioncookie, mail, () => DateTime.Now) as ICommand;
|
||||
|
||||
/// <summary> Creates a command object to perform copri queries. </summary>
|
||||
private static IQuery CreateQuery(ICachedCopriServer copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie)
|
||||
? new CachedQuery(copri) as IQuery
|
||||
: new CachedQueryLoggedIn(copri, sessioncookie, mail, () => DateTime.Now);
|
||||
}
|
||||
}
|
53
SharedBusinessLogic/Model/Connector/ConnectorCache.cs
Normal file
53
SharedBusinessLogic/Model/Connector/ConnectorCache.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Repository;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects app to copri data by getting data from cache.
|
||||
/// </summary>
|
||||
public class ConnectorCache : IConnector
|
||||
{
|
||||
/// <summary>Constructs a copri connector object to connect to cache.</summary>
|
||||
/// <remarks>Used for offline scenario to ensure responsiveness of app by preventing hopeless tries to communicate with COPRI. </remarks>
|
||||
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
|
||||
/// <param name="sessionCookie"> Holds the session cookie.</param>
|
||||
/// <param name="mail">Mail of user.</param>
|
||||
/// <param name="smartDevice">Holds info about smart device.</param>
|
||||
/// <param name="server"> Is null in production and might be a mock in testing context.</param>
|
||||
public ConnectorCache(
|
||||
AppContextInfo appContextInfo,
|
||||
string uiIsoLangugageName,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
ISmartDevice smartDevice = null,
|
||||
ICopriServer server = null)
|
||||
{
|
||||
Command = Connector.CreateCommand(
|
||||
server ?? new CopriProviderMonkeyStore(appContextInfo.MerchantId, uiIsoLangugageName, sessionCookie, smartDevice),
|
||||
sessionCookie,
|
||||
mail);
|
||||
|
||||
Query = GetQuery(
|
||||
server ?? new CopriProviderMonkeyStore(appContextInfo.MerchantId, uiIsoLangugageName, sessionCookie, smartDevice),
|
||||
sessionCookie,
|
||||
mail);
|
||||
}
|
||||
|
||||
/// <summary> Object for querying stations and bikes.</summary>
|
||||
public ICommand Command { get; private set; }
|
||||
|
||||
/// <summary> Object for querying stations and bikes.</summary>
|
||||
public IQuery Query { get; private set; }
|
||||
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
public bool IsConnected => Command.IsConnected;
|
||||
|
||||
/// <summary> Gets a command object to perform copri queries. </summary>
|
||||
private static IQuery GetQuery(ICopriServer copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie)
|
||||
? new Query(copri) as IQuery
|
||||
: new QueryLoggedIn(copri, sessioncookie, mail, () => DateTime.Now);
|
||||
}
|
||||
}
|
34
SharedBusinessLogic/Model/Connector/ConnectorFactory.cs
Normal file
34
SharedBusinessLogic/Model/Connector/ConnectorFactory.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Repository;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public class ConnectorFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a connector object depending on whether beein onlin or offline.
|
||||
/// </summary>
|
||||
/// <param name="isConnected">
|
||||
/// True if online, false if offline.
|
||||
/// If offline cache connector is returned to avoid performance penalty which would happen when trying to communicate with backend in offline scenario.
|
||||
/// </param>
|
||||
/// <param name="appContextInfo">Provides app related info (app name and version, merchantid) to pass to COPRI.</param>
|
||||
/// <param name="uiIsoLangugageName">Two letter ISO language name.</param>
|
||||
/// <param name="smartDevice">Holds info about smart device.</param>
|
||||
public static IConnector Create(
|
||||
bool isConnected,
|
||||
Uri activeUri,
|
||||
AppContextInfo appContextInfo,
|
||||
string uiIsoLangugageName,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
ISmartDevice smartDevice = null,
|
||||
TimeSpan? expiresAfter = null)
|
||||
{
|
||||
return isConnected
|
||||
? new Connector(activeUri, appContextInfo, uiIsoLangugageName, sessionCookie, mail, smartDevice, expiresAfter: expiresAfter) as IConnector
|
||||
: new ConnectorCache(appContextInfo, uiIsoLangugageName, sessionCookie, mail, smartDevice);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Filter
|
||||
{
|
||||
public static class GroupFilterFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates filter object.
|
||||
/// </summary>
|
||||
/// <param name="group">if value consists
|
||||
/// - list of strings entries are used to filter (intersect) with or if value is
|
||||
/// - null or an empty list null filter is applied, i.e. filtering is off.</param>
|
||||
/// <returns>Filtering object.</returns>
|
||||
/// <remarks>
|
||||
/// Tread group values of null and empty lists as marker to turn filtering off to handle COPRI responses maximal flexible.
|
||||
/// </remarks>
|
||||
public static IGroupFilter Create(IEnumerable<string> group)
|
||||
{
|
||||
return group != null && group.Count() > 0
|
||||
? (IGroupFilter)new IntersectGroupFilter(group) :
|
||||
new NullGroupFilter();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Filter
|
||||
{
|
||||
public interface IGroupFilter
|
||||
{
|
||||
IEnumerable<string> DoFilter(IEnumerable<string> filter);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Filter
|
||||
{
|
||||
/// <summary> Filters to enumerations of string by intersecting.</summary>
|
||||
public class IntersectGroupFilter : IGroupFilter
|
||||
{
|
||||
private IEnumerable<string> Group { get; set; }
|
||||
|
||||
public IntersectGroupFilter(IEnumerable<string> group) => Group = group ?? new List<string>();
|
||||
|
||||
/// <summary> Applies filtering. </summary>
|
||||
/// <param name="filter">Enumeration of filter values to filter with or null if no filtering has to be applied.</param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<string> DoFilter(IEnumerable<string> filter) => filter != null
|
||||
? Group.IntersectByGoupId(filter)
|
||||
: Group;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Filter
|
||||
{
|
||||
public static class IntersectGroupFilterHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Transforms a group (each element consists of an operator prefix and a numeric bike category) to bike category enumeration (numeric elements).
|
||||
/// </summary>
|
||||
/// <param name="group">Group to transform.</param>
|
||||
/// <returns>Enumeration of numeric bike categories.</returns>
|
||||
public static IEnumerable<string> ToBikeCategory(this IEnumerable<string> group)
|
||||
=> group?.Select(x => x.GetBikeCategory())?.Where(x => !string.IsNullOrEmpty(x))
|
||||
?? new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Extracts bike group category umber from a group string.
|
||||
/// </summary>
|
||||
/// <param name="group">Group to transform. Example KN_300101 (Stadtrad located in Konstanz), FR_300102 (Lastenrad located in Freiburg).</param>
|
||||
/// <returns>Enumeration of numeric bike categories.</returns>
|
||||
public static string GetBikeCategory(this string group)
|
||||
=> Regex.Match(group, "[0-9]+")?.Value ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Intersects two goups only taking into accout the numeric bike group category part.
|
||||
/// </summary>
|
||||
/// <param name="group">Group to filter.</param>
|
||||
/// <param name="filter">Filter to apply</param>
|
||||
public static IEnumerable<string> IntersectByGoupId(this IEnumerable<string> group, IEnumerable<string> filter)
|
||||
=> group.Where(x => filter.ContainsGroupId(x));
|
||||
|
||||
/// <summary>
|
||||
/// Gets if group contains a filter element.
|
||||
/// </summary>
|
||||
/// <param name="group"></param>
|
||||
/// <param name="filterElement"></param>
|
||||
/// <returns></returns>
|
||||
public static bool ContainsGroupId(this IEnumerable<string> group, string filterElement)
|
||||
=> group.ToBikeCategory().Contains(filterElement.GetBikeCategory());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Filter
|
||||
{
|
||||
public class NullGroupFilter : IGroupFilter
|
||||
{
|
||||
public IEnumerable<string> DoFilter(IEnumerable<string> filter) => filter;
|
||||
}
|
||||
}
|
11
SharedBusinessLogic/Model/Connector/FilterHelper.cs
Normal file
11
SharedBusinessLogic/Model/Connector/FilterHelper.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public static class FilterHelper
|
||||
{
|
||||
/// <summary> Holds the Citybike group (city bikes).</summary>
|
||||
public const string CITYBIKE = "300103";
|
||||
|
||||
/// <summary> Holds the Cargo group (Lastenräder).</summary>
|
||||
public const string CARGOBIKE = "300101";
|
||||
}
|
||||
}
|
155
SharedBusinessLogic/Model/Connector/FilteredConnector.cs
Normal file
155
SharedBusinessLogic/Model/Connector/FilteredConnector.cs
Normal file
|
@ -0,0 +1,155 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Connector.Filter;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Model.Stations;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using ShareeBike.Model.Stations.StationNS.Operator;
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary> Filters connector responses.</summary>
|
||||
/// <remarks>Former name: Filter</remarks>
|
||||
public class FilteredConnector : IFilteredConnector
|
||||
{
|
||||
/// <summary> Constructs a filter object. </summary>
|
||||
/// <param name="group">Filter group.</param>
|
||||
/// <param name="connector">Connector object.</param>
|
||||
public FilteredConnector(
|
||||
IEnumerable<string> group,
|
||||
IConnector connector)
|
||||
{
|
||||
Connector = connector;
|
||||
|
||||
if (Connector == null)
|
||||
{
|
||||
throw new ArgumentException("Can not construct filter object. Connector- and command objects must not be null.");
|
||||
}
|
||||
|
||||
Query = new QueryProvider(Connector.Query, GroupFilterFactory.Create(group));
|
||||
}
|
||||
|
||||
/// <summary> Inner connector object.</summary>
|
||||
public IConnector Connector { get; }
|
||||
|
||||
/// <summary> Command object. </summary>
|
||||
public ICommand Command => Connector.Command;
|
||||
|
||||
/// <summary> Object to query information. </summary>
|
||||
public IQuery Query { get; }
|
||||
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
public bool IsConnected => Connector.IsConnected;
|
||||
|
||||
/// <summary> Object to perform filtered queries.</summary>
|
||||
private class QueryProvider : IQuery
|
||||
{
|
||||
/// <summary> Holds the filter. </summary>
|
||||
private IGroupFilter Filter { get; }
|
||||
|
||||
/// <summary> Holds the reference to object which performs copri queries.</summary>
|
||||
private IQuery m_oInnerQuery;
|
||||
|
||||
/// <summary> Constructs a query object.</summary>
|
||||
/// <param name="innerQuerry"></param>
|
||||
/// <param name="filter"></param>
|
||||
public QueryProvider(IQuery innerQuerry, IGroupFilter filter)
|
||||
{
|
||||
m_oInnerQuery = innerQuerry;
|
||||
Filter = filter;
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
public async Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
|
||||
{
|
||||
var result = await m_oInnerQuery.GetBikesAsync(operatorUri, stationId, bikeId);
|
||||
if (bikeId == null)
|
||||
{
|
||||
// Do filter
|
||||
return new Result<BikeCollection>(
|
||||
result.Source,
|
||||
new BikeCollection(DoFilter(result.Response, Filter)),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Do NOT filter on SelectBikePage = use function from NullFilterConnector: https://dev.azure.com/TeilRad/sharee.bike%20Buchungsplattform/_workitems/edit/904
|
||||
return new Result<BikeCollection>(
|
||||
result.Source,
|
||||
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied if a user is logged in. </summary>
|
||||
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
|
||||
{
|
||||
var result = await m_oInnerQuery.GetBikesOccupiedAsync();
|
||||
return new Result<BikeCollection>(
|
||||
result.Source,
|
||||
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
/// <summary> Gets all station applying filter rules. </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
|
||||
{
|
||||
// Bikes and stations from COPRI or cache
|
||||
var providerBikesAndStations = await m_oInnerQuery.GetBikesAndStationsAsync();
|
||||
|
||||
// Do filtering.
|
||||
var filteredStationsDictionary = new StationDictionary(
|
||||
providerBikesAndStations.Response.StationsAll.CopriVersion,
|
||||
DoFilter(providerBikesAndStations.Response.StationsAll, Filter));
|
||||
|
||||
var filteredBikesOccupiedDictionary = new BikeCollection(
|
||||
DoFilter(providerBikesAndStations.Response.BikesOccupied, Filter));
|
||||
|
||||
var filteredBikesAndStations = new Result<StationsAndBikesContainer>(
|
||||
providerBikesAndStations.Source,
|
||||
new StationsAndBikesContainer(
|
||||
filteredStationsDictionary,
|
||||
filteredBikesOccupiedDictionary),
|
||||
providerBikesAndStations.GeneralData,
|
||||
providerBikesAndStations.Exception);
|
||||
|
||||
return filteredBikesAndStations;
|
||||
}
|
||||
|
||||
/// <summary> Filter bikes by group. </summary>
|
||||
/// <param name="bikes">Bikes to filter.</param>
|
||||
/// <returns>Filtered bikes.</returns>
|
||||
private static Dictionary<string, BikeInfo> DoFilter(BikeCollection bikes, IGroupFilter filter) =>
|
||||
bikes
|
||||
.Where(x => filter.DoFilter(x.Group).Count() > 0)
|
||||
.ToDictionary(x => x.Id);
|
||||
|
||||
/// <summary> Filter stations by group and removes bike group collection entries which do not match group filter. </summary>
|
||||
/// <returns>Matching stations.</returns>
|
||||
private static Dictionary<string, IStation> DoFilter(StationDictionary stations, IGroupFilter filter) =>
|
||||
stations
|
||||
.Where(station => filter.DoFilter(station.Group).Count() > 0)
|
||||
.Select(station => new Station(
|
||||
station.Id,
|
||||
station.Group,
|
||||
station.Position,
|
||||
station.StationName,
|
||||
station.OperatorUri,
|
||||
station.OperatorData,
|
||||
new BikeGroupCol(station.BikeGroups
|
||||
.Where(group => filter.DoFilter(new List<string> { group.Group }).Count() > 0))) as IStation)
|
||||
.ToDictionary(x => x.Id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public static class FilteredConnectorFactory
|
||||
{
|
||||
/// <summary> Creates a filter object. </summary>
|
||||
/// <param name="group">Filter to apply on stations and bikes.</param>
|
||||
/// <param name="connector">Connector to connect to COPRI.</param>
|
||||
public static IFilteredConnector Create(IEnumerable<string> group, IConnector connector)
|
||||
{
|
||||
return group != null
|
||||
? (IFilteredConnector)new FilteredConnector(group, connector)
|
||||
: new NullFilterConnector(connector); // Do not apply filtering.
|
||||
}
|
||||
}
|
||||
}
|
14
SharedBusinessLogic/Model/Connector/IConnector.cs
Normal file
14
SharedBusinessLogic/Model/Connector/IConnector.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public interface IConnector
|
||||
{
|
||||
/// <summary> Object for querying stations and bikes.</summary>
|
||||
ICommand Command { get; }
|
||||
|
||||
/// <summary> Object for querying stations and bikes.</summary>
|
||||
IQuery Query { get; }
|
||||
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
bool IsConnected { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public interface IFilteredConnector : IConnector
|
||||
{
|
||||
IConnector Connector { get; }
|
||||
}
|
||||
}
|
117
SharedBusinessLogic/Model/Connector/NullFilterConnector.cs
Normal file
117
SharedBusinessLogic/Model/Connector/NullFilterConnector.cs
Normal file
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Model.Stations;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary> Filters connector responses.</summary>
|
||||
public class NullFilterConnector : IFilteredConnector
|
||||
{
|
||||
/// <summary> Constructs a filter object. </summary>
|
||||
/// <param name="p_oGroup">Filter group.</param>
|
||||
/// <param name="connector">Connector object.</param>
|
||||
public NullFilterConnector(
|
||||
IConnector connector)
|
||||
{
|
||||
Connector = connector;
|
||||
|
||||
if (Connector == null)
|
||||
{
|
||||
throw new ArgumentException("Can not construct filter object. Connector- and command objects must not be null.");
|
||||
}
|
||||
|
||||
Query = new QueryProvider(Connector.Query);
|
||||
}
|
||||
|
||||
/// <summary> Inner connector object.</summary>
|
||||
public IConnector Connector { get; }
|
||||
|
||||
/// <summary> Command object. </summary>
|
||||
public ICommand Command => Connector.Command;
|
||||
|
||||
/// <summary> Object to query information. </summary>
|
||||
public IQuery Query { get; }
|
||||
|
||||
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
|
||||
public bool IsConnected => Connector.IsConnected;
|
||||
|
||||
/// <summary> Object to perform filtered queries.</summary>
|
||||
private class QueryProvider : IQuery
|
||||
{
|
||||
/// <summary> Holds the reference to object which performs copri queries.</summary>
|
||||
private IQuery m_oInnerQuery;
|
||||
|
||||
/// <summary> Constructs a query object.</summary>
|
||||
/// <param name="p_oInnerQuery"></param>
|
||||
/// <param name="p_oFilter"></param>
|
||||
public QueryProvider(IQuery p_oInnerQuery)
|
||||
{
|
||||
m_oInnerQuery = p_oInnerQuery;
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
public async Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
|
||||
{
|
||||
var result = await m_oInnerQuery.GetBikesAsync(operatorUri, stationId, bikeId);
|
||||
return new Result<BikeCollection>(
|
||||
result.Source,
|
||||
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied if a user is logged in. </summary>
|
||||
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
|
||||
{
|
||||
var result = await m_oInnerQuery.GetBikesOccupiedAsync();
|
||||
return new Result<BikeCollection>(
|
||||
result.Source,
|
||||
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
/// <summary> Gets all station applying filter rules. </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
|
||||
{
|
||||
var result = await m_oInnerQuery.GetBikesAndStationsAsync();
|
||||
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
result.Source,
|
||||
new StationsAndBikesContainer(
|
||||
new StationDictionary(
|
||||
result.Response.StationsAll.CopriVersion,
|
||||
result.Response.StationsAll.ToDictionary(x => x.Id)),
|
||||
new BikeCollection(
|
||||
result.Response.BikesOccupied?.ToDictionary(x => x.Id) ?? new Dictionary<string, BikeInfo>())),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
|
||||
/// <summary> Filter bikes by group. </summary>
|
||||
/// <param name="bikes">Bikes to filter.</param>
|
||||
/// <returns>Filtered bikes.</returns>
|
||||
public static Dictionary<string, BikeInfo> DoFilter(BikeCollection bikes, IEnumerable<string> filter)
|
||||
{
|
||||
return bikes.Where(x => x.Group.Intersect(filter).Count() > 0).ToDictionary(x => x.Id);
|
||||
}
|
||||
|
||||
/// <summary> Filter stations by group. </summary>
|
||||
/// <returns></returns>
|
||||
public static Dictionary<string, IStation> DoFilter(StationDictionary stations, IEnumerable<string> p_oFilter)
|
||||
{
|
||||
return stations.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary(x => x.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
SharedBusinessLogic/Model/Connector/Query/Base.cs
Normal file
27
SharedBusinessLogic/Model/Connector/Query/Base.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using ShareeBike.Repository;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides information required for copri commands/ query operations.
|
||||
/// </summary>
|
||||
public class Base
|
||||
{
|
||||
/// <summary> Reference to object which provides access to copri server. </summary>
|
||||
protected ICopriServerBase CopriServer { get; }
|
||||
|
||||
/// <summary> Gets the merchant id.</summary>
|
||||
protected string MerchantId => CopriServer.MerchantId;
|
||||
|
||||
/// <summary> Constructs a query base object.</summary>
|
||||
/// <param name="p_oCopriServer">Server which implements communication.</param>
|
||||
/// <param name="p_oErrorStack">Object which hold communication objects.</param>
|
||||
protected Base(
|
||||
ICopriServerBase p_oCopriServer)
|
||||
{
|
||||
CopriServer = p_oCopriServer
|
||||
?? throw new ArgumentException("Can not instantiate command/ query base- object. Copri server object must never be null or emtpy.");
|
||||
}
|
||||
}
|
||||
}
|
39
SharedBusinessLogic/Model/Connector/Query/BaseLoggedIn.cs
Normal file
39
SharedBusinessLogic/Model/Connector/Query/BaseLoggedIn.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using ShareeBike.Repository;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary>Holds user infromation required for copri related commands/ query operations. </summary>
|
||||
public class BaseLoggedIn : Base
|
||||
{
|
||||
/// <summary>Session cookie used to sign in to copri.</summary>
|
||||
public string SessionCookie { get; }
|
||||
|
||||
/// <summary> Mail address of the user. </summary>
|
||||
protected string Mail { get; }
|
||||
|
||||
/// <summary> Object which provides date time info. </summary>
|
||||
protected readonly Func<DateTime> DateTimeProvider;
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="copriServer">Server which implements communication.</param>
|
||||
public BaseLoggedIn(ICopriServerBase copriServer,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
Func<DateTime> p_oDateTimeProvider) : base(copriServer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sessionCookie))
|
||||
throw new ArgumentException("Can not instantiate query object- object. Session cookie must never be null or emtpy.");
|
||||
|
||||
if (string.IsNullOrEmpty(mail))
|
||||
throw new ArgumentException("Can not instantiate query object- object. Mail address must never be null or emtpy.");
|
||||
|
||||
DateTimeProvider = p_oDateTimeProvider
|
||||
?? throw new ArgumentException("Can not instantiate connector- object. No date time provider object available.");
|
||||
|
||||
SessionCookie = sessionCookie;
|
||||
|
||||
Mail = mail;
|
||||
}
|
||||
}
|
||||
}
|
96
SharedBusinessLogic/Model/Connector/Query/CachedQuery.cs
Normal file
96
SharedBusinessLogic/Model/Connector/Query/CachedQuery.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Connector.Updater;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Services.CopriApi;
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary> Provides query functionality for use without log in. </summary>
|
||||
public class CachedQuery : Base, IQuery
|
||||
{
|
||||
/// <summary> Cached copri server (connection to copri backed up by cache). </summary>
|
||||
private readonly ICachedCopriServer server;
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="copriServer">Server which implements communication.</param>
|
||||
public CachedQuery(
|
||||
ICopriServerBase copriServer) : base(copriServer)
|
||||
{
|
||||
server = copriServer as ICachedCopriServer;
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentException($"Copri server is not of expected type. Type detected is {copriServer.GetType()}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets all stations including positions and bikes.</summary>
|
||||
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
|
||||
{
|
||||
var resultStations = await server.GetStations();
|
||||
|
||||
if (resultStations.Source == typeof(CopriCallsMonkeyStore))
|
||||
{
|
||||
// Communication with copri in order to get stations failed.
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
resultStations.Source,
|
||||
new StationsAndBikesContainer(
|
||||
resultStations.Response.GetStationsAllMutable(),
|
||||
new BikeCollection() /* There are no bikes occupied because user is not logged in. */),
|
||||
resultStations.GeneralData,
|
||||
resultStations.Exception);
|
||||
}
|
||||
|
||||
// Communication with copri succeeded.
|
||||
server.AddToCache(resultStations);
|
||||
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
resultStations.Source,
|
||||
new StationsAndBikesContainer(
|
||||
resultStations.Response.GetStationsAllMutable(),
|
||||
new BikeCollection()),
|
||||
resultStations.GeneralData);
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied. </summary>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
|
||||
{
|
||||
Log.ForContext<CachedQuery>().Error("Unexpected call to get be bikes occupied detected. No user is logged in.");
|
||||
|
||||
return new Result<BikeCollection>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
await Task.FromResult(new BikeCollection(new Dictionary<string, BikeInfo>())),
|
||||
new GeneralData(),
|
||||
new Exception("Abfrage der reservierten/ gebuchten Räder nicht möglich. Kein Benutzer angemeldet."));
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes available. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
|
||||
{
|
||||
var result = await server.GetBikesAvailable(operatorUri: operatorUri, stationId: stationId, bikeId: bikeId);
|
||||
|
||||
if (result.Source != typeof(CopriCallsMonkeyStore))
|
||||
{
|
||||
server.AddToCache(result, operatorUri, stationId, bikeId);
|
||||
}
|
||||
|
||||
return new Result<BikeCollection>(
|
||||
result.Source,
|
||||
result.Response.GetBikesAvailable(result.Source == typeof(CopriCallsMonkeyStore)
|
||||
? Bikes.BikeInfoNS.BC.DataSource.Cache
|
||||
: Bikes.BikeInfoNS.BC.DataSource.Copri),
|
||||
result.GeneralData,
|
||||
result.Exception);
|
||||
}
|
||||
}
|
||||
}
|
212
SharedBusinessLogic/Model/Connector/Query/CachedQueryLoggedIn.cs
Normal file
212
SharedBusinessLogic/Model/Connector/Query/CachedQueryLoggedIn.cs
Normal file
|
@ -0,0 +1,212 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Connector.Updater;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Repository.Response;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary> Provides query functionality for a logged in user. </summary>
|
||||
public class CachedQueryLoggedIn : BaseLoggedIn, IQuery
|
||||
{
|
||||
/// <summary> Cached copri server (connection to copri backed up by cache). </summary>
|
||||
private ICachedCopriServer Server { get; }
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="copriServer">Server which implements communication.</param>
|
||||
public CachedQueryLoggedIn(ICopriServerBase copriServer,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
Func<DateTime> dateTimeProvider) : base(copriServer, sessionCookie, mail, dateTimeProvider)
|
||||
{
|
||||
Server = copriServer as ICachedCopriServer;
|
||||
if (Server == null)
|
||||
{
|
||||
throw new ArgumentException($"Copri server is not of expected type. Type detected is {copriServer.GetType()}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets all stations including positions.</summary>
|
||||
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
|
||||
{
|
||||
BikeCollection GetBikeCollection(IEnumerable<BikeInfoReservedOrBooked> bikeInfoEnumerable, Bikes.BikeInfoNS.BC.DataSource dataSource) =>
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
null, // Bikes available are no more of interest because count of available bikes at each given station is was added to station object.
|
||||
bikeInfoEnumerable ?? new Dictionary<string, BikeInfoReservedOrBooked>().Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
dataSource);
|
||||
|
||||
var stationsResponse = await Server.GetStations();
|
||||
|
||||
if (stationsResponse.Source == typeof(CopriCallsMonkeyStore)
|
||||
|| stationsResponse.Exception != null)
|
||||
{
|
||||
// Stations were read from cache ==> get bikes available and occupied from cache as well to avoid inconsistencies
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
stationsResponse.Source,
|
||||
new StationsAndBikesContainer(
|
||||
stationsResponse.Response.GetStationsAllMutable(),
|
||||
GetBikeCollection(stationsResponse.Response.bikes_occupied?.Values, Bikes.BikeInfoNS.BC.DataSource.Cache)),
|
||||
stationsResponse.GeneralData,
|
||||
stationsResponse.Exception);
|
||||
}
|
||||
|
||||
|
||||
// Both types bikes could read from copri => update cache
|
||||
Server.AddToCache(stationsResponse);
|
||||
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
stationsResponse.Source,
|
||||
new StationsAndBikesContainer(
|
||||
stationsResponse.Response.GetStationsAllMutable(),
|
||||
GetBikeCollection(stationsResponse.Response.bikes_occupied?.Values, Bikes.BikeInfoNS.BC.DataSource.Copri)),
|
||||
stationsResponse.GeneralData,
|
||||
stationsResponse?.Exception);
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied. </summary>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
|
||||
{
|
||||
var bikesAvailableResponse = await Server.GetBikesAvailable(false);
|
||||
if (bikesAvailableResponse.Source == typeof(CopriCallsMonkeyStore)
|
||||
|| bikesAvailableResponse.Exception != null)
|
||||
{
|
||||
// Bikes available were read from cache ==> get bikes occupied from cache as well to avoid inconsistencies.
|
||||
Log.ForContext<CachedQueryLoggedIn>().Debug("Bikes available read from cache. Reading bikes occupied from cache as well.");
|
||||
return new Result<BikeCollection>(
|
||||
bikesAvailableResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse.Response?.bikes?.Values?.Where(bike => bike.GetState() == State.InUseStateEnum.FeedbackPending),
|
||||
(await Server.GetBikesOccupied(true))?.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesAvailableResponse.GeneralData,
|
||||
bikesAvailableResponse.Exception);
|
||||
}
|
||||
|
||||
var bikesOccupiedResponse = await Server.GetBikesOccupied(false);
|
||||
if (bikesOccupiedResponse.Source == typeof(CopriCallsMonkeyStore)
|
||||
|| bikesOccupiedResponse.Exception != null)
|
||||
{
|
||||
// Bikes occupied were read from cache ==> get bikes available from cache as well to avoid inconsistencies
|
||||
Log.ForContext<CachedQueryLoggedIn>().Debug("Bikes occupied read from cache. Reread bikes available from cache as well.");
|
||||
return new Result<BikeCollection>(
|
||||
bikesOccupiedResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
(await Server.GetBikesAvailable(true)).Response?.bikes?.Values?.Where(bike => bike.GetState() == State.InUseStateEnum.FeedbackPending),
|
||||
bikesOccupiedResponse.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesOccupiedResponse.GeneralData,
|
||||
bikesOccupiedResponse.Exception);
|
||||
}
|
||||
|
||||
// Both types bikes could read from copri => update bikes occupied cache.
|
||||
// // Do not add bikes available to cache because this might lead to conflicts calls GetBikesAsync() and bikes with FeedbackPending state are of no use offline.
|
||||
Server.AddToCache(bikesOccupiedResponse);
|
||||
|
||||
return new Result<BikeCollection>(
|
||||
bikesOccupiedResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse?.Response.bikes?.Values?.Select(bike => bike)?.Where(bike => bike.GetState() == State.InUseStateEnum.FeedbackPending),
|
||||
bikesOccupiedResponse?.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Copri),
|
||||
bikesOccupiedResponse.GeneralData,
|
||||
bikesOccupiedResponse.Exception);
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes available and bikes occupied. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
|
||||
{
|
||||
var bikesAvailableResponse = await Server.GetBikesAvailable(operatorUri: operatorUri, stationId: stationId, bikeId: bikeId);
|
||||
|
||||
if (bikesAvailableResponse.Source == typeof(CopriCallsMonkeyStore)
|
||||
|| bikesAvailableResponse.Exception != null)
|
||||
{
|
||||
// Bikes were read from cache.
|
||||
Log.ForContext<CachedQueryLoggedIn>().Debug("Bikes available and bikes occupied from cache invoking one single call.");
|
||||
return new Result<BikeCollection>(
|
||||
bikesAvailableResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse.Response?.bikes?.Values,
|
||||
operatorUri?.AbsoluteUri == null ?
|
||||
(await Server.GetBikesOccupied(true)).Response?.bikes_occupied?.Values // Get bikes occupied from cache as well to avoid inconsistencies.
|
||||
: bikesAvailableResponse.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesAvailableResponse.GeneralData,
|
||||
bikesAvailableResponse.Exception);
|
||||
}
|
||||
|
||||
if (operatorUri?.AbsoluteUri != null)
|
||||
{
|
||||
// Both types bikes could read from copri successfully => update cache
|
||||
Server.AddToCache(bikesAvailableResponse, operatorUri, stationId, bikeId);
|
||||
|
||||
Log.ForContext<CachedQueryLoggedIn>().Debug("Bikes available and occupied read successfully from server invoking one single request.");
|
||||
return new Result<BikeCollection>(
|
||||
bikesAvailableResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse.Response?.bikes?.Values,
|
||||
bikesAvailableResponse.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Copri),
|
||||
bikesAvailableResponse.GeneralData,
|
||||
bikesAvailableResponse.Exception != null ? new AggregateException(new[] { bikesAvailableResponse.Exception }) : null);
|
||||
}
|
||||
|
||||
/// Legacy implementation: GetBikesOccupied are not returned in <see cref="ICachedCopriServer.GetBikesAvailable"/> call.
|
||||
/// A separate call <see cref="ICachedCopriServer.GetBikesOccupied"/> is required to retrieve all bikes.
|
||||
var bikesOccupiedResponse = await Server.GetBikesOccupied(); /* Only query bikes occupied if operator uri is unknown. */
|
||||
if (bikesOccupiedResponse.Source == typeof(CopriCallsMonkeyStore)
|
||||
|| bikesOccupiedResponse.Exception != null)
|
||||
{
|
||||
// Bikes occupied were read from cache ==> get bikes available from cache as well to avoid inconsistencies
|
||||
Log.ForContext<CachedQueryLoggedIn>().Debug("Bikes occupied read from cache. Reread bikes available from cache as well.");
|
||||
return new Result<BikeCollection>(
|
||||
bikesOccupiedResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
(await Server.GetBikesAvailable(true, operatorUri, stationId, bikeId)).Response?.bikes?.Values,
|
||||
bikesOccupiedResponse.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesOccupiedResponse.GeneralData,
|
||||
bikesOccupiedResponse.Exception);
|
||||
}
|
||||
|
||||
// Both types bikes could read from copri => update cache
|
||||
Server.AddToCache(bikesAvailableResponse, operatorUri, stationId, bikeId);
|
||||
Server.AddToCache(bikesOccupiedResponse);
|
||||
|
||||
Log.ForContext<CachedQueryLoggedIn>().Debug("Bikes available and occupied read successfully from server.");
|
||||
return new Result<BikeCollection>(
|
||||
bikesAvailableResponse.Source,
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse.Response?.bikes?.Values,
|
||||
bikesOccupiedResponse.Response?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Copri),
|
||||
bikesAvailableResponse.GeneralData,
|
||||
bikesAvailableResponse.Exception != null || bikesOccupiedResponse.Exception != null ? new AggregateException(new[] { bikesAvailableResponse.Exception, bikesOccupiedResponse.Exception }) : null);
|
||||
}
|
||||
}
|
||||
}
|
23
SharedBusinessLogic/Model/Connector/Query/IQuery.cs
Normal file
23
SharedBusinessLogic/Model/Connector/Query/IQuery.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
public interface IQuery
|
||||
{
|
||||
/// <summary> Gets all stations including positions.</summary>
|
||||
Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync();
|
||||
|
||||
/// <summary> Gets bikes occupied is a user is logged in. </summary>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
Task<Result<BikeCollection>> GetBikesOccupiedAsync();
|
||||
|
||||
/// <summary> Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null);
|
||||
}
|
||||
}
|
72
SharedBusinessLogic/Model/Connector/Query/Query.cs
Normal file
72
SharedBusinessLogic/Model/Connector/Query/Query.cs
Normal file
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Connector.Updater;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Services.CopriApi;
|
||||
using BikeInfo = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfo;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary> Provides query functionality from cache without login. </summary>
|
||||
public class Query : Base, IQuery
|
||||
{
|
||||
/// <summary> Cached copri server. </summary>
|
||||
private readonly ICopriServer server;
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="copriServer">Server which implements communication.</param>
|
||||
public Query(ICopriServerBase copriServer) : base(copriServer)
|
||||
{
|
||||
server = copriServer as ICopriServer;
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentException($"Copri server is not of expected type. Type detected is {copriServer.GetType()}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets all stations including positions.</summary>
|
||||
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
|
||||
{
|
||||
var stationsAllResponse = await server.GetStationsAsync();
|
||||
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
new StationsAndBikesContainer(
|
||||
stationsAllResponse.GetStationsAllMutable(),
|
||||
new BikeCollection() /* There are no bikes occupied because user is not logged in. */),
|
||||
stationsAllResponse.GetGeneralData());
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied. </summary>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
|
||||
{
|
||||
Log.ForContext<Query>().Error("Unexpected call to get be bikes occupied detected. No user is logged in.");
|
||||
return new Result<BikeCollection>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
await Task.FromResult(new BikeCollection(new Dictionary<string, BikeInfo>())),
|
||||
new GeneralData(),
|
||||
new Exception("Abfrage der reservierten/ gebuchten Räder fehlgeschlagen. Kein Benutzer angemeldet."));
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <returns> Collection of bikes. </returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
|
||||
{
|
||||
var bikesAvailableResponse = await server.GetBikesAvailableAsync(operatorUri, stationId, bikeId);
|
||||
return new Result<BikeCollection>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
bikesAvailableResponse != null
|
||||
? bikesAvailableResponse.GetBikesAvailable(Bikes.BikeInfoNS.BC.DataSource.Cache)
|
||||
: await Task.FromResult(new BikeCollection(new Dictionary<string, BikeInfo>())),
|
||||
bikesAvailableResponse?.GetGeneralData());
|
||||
}
|
||||
}
|
||||
}
|
106
SharedBusinessLogic/Model/Connector/Query/QueryLoggedIn.cs
Normal file
106
SharedBusinessLogic/Model/Connector/Query/QueryLoggedIn.cs
Normal file
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Model.Connector.Updater;
|
||||
using ShareeBike.Model.Services.CopriApi;
|
||||
using ShareeBike.Repository;
|
||||
using ShareeBike.Repository.Response;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary> Provides query functionality from cache for a logged in user. </summary>
|
||||
public class QueryLoggedIn : BaseLoggedIn, IQuery
|
||||
{
|
||||
/// <summary> Copri server. </summary>
|
||||
private readonly ICopriServer server;
|
||||
|
||||
/// <summary>Constructs a copri query object.</summary>
|
||||
/// <param name="copriServer">Server which implements communication.</param>
|
||||
public QueryLoggedIn(ICopriServerBase copriServer,
|
||||
string sessionCookie,
|
||||
string mail,
|
||||
Func<DateTime> dateTimeProvider) : base(copriServer, sessionCookie, mail, dateTimeProvider)
|
||||
{
|
||||
server = copriServer as ICopriServer;
|
||||
if (server == null)
|
||||
{
|
||||
throw new ArgumentException($"Copri server is not of expected type. Type detected is {copriServer.GetType()}.");
|
||||
}
|
||||
|
||||
server = copriServer as ICopriServer;
|
||||
}
|
||||
|
||||
/// <summary> Gets all stations including positions.</summary>
|
||||
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
|
||||
{
|
||||
var stationResponse = await server.GetStationsAsync();
|
||||
|
||||
return new Result<StationsAndBikesContainer>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
new StationsAndBikesContainer(
|
||||
stationResponse.GetStationsAllMutable(),
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
null, // Bikes available are no more of interest because count of available bikes at each given station is was added to station object.
|
||||
stationResponse.bikes_occupied?.Values ?? new Dictionary<string, BikeInfoReservedOrBooked>().Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache)),
|
||||
stationResponse.GetGeneralData());
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes occupied and bikes for which feedback is required. </summary>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
|
||||
{
|
||||
var bikesFeedbackRequired = await server.GetBikesAvailableAsync();
|
||||
var bikesOccupiedResponse = await server.GetBikesOccupiedAsync();
|
||||
|
||||
return new Result<BikeCollection>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesFeedbackRequired.bikes?.Values?.Select(bike => bike)?.Where(bike => bike.GetState() == State.InUseStateEnum.FeedbackPending),
|
||||
bikesOccupiedResponse?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesOccupiedResponse.GetGeneralData());
|
||||
}
|
||||
|
||||
/// <summary> Gets bikes available and bikes occupied. </summary>
|
||||
/// <param name="operatorUri">Uri of the operator host to get bikes from or null if bikes have to be gotten form primary host.</param>
|
||||
/// <param name="stationId"> Id of station which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <param name="bikeId"> Id of bike which is used for filtering bikes. Null if no filtering should be applied.</param>
|
||||
/// <returns>Collection of bikes.</returns>
|
||||
public async Task<Result<BikeCollection>> GetBikesAsync(Uri operatorUri = null, string stationId = null, string bikeId = null)
|
||||
{
|
||||
var bikesAvailableResponse = await server.GetBikesAvailableAsync(operatorUri, stationId, bikeId);
|
||||
|
||||
if (operatorUri?.AbsoluteUri != null)
|
||||
{
|
||||
return new Result<BikeCollection>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse?.bikes?.Values,
|
||||
bikesAvailableResponse?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesAvailableResponse?.GetGeneralData());
|
||||
}
|
||||
|
||||
var bikesOccupiedResponse = await server.GetBikesOccupiedAsync();
|
||||
|
||||
return new Result<BikeCollection>(
|
||||
typeof(CopriCallsMonkeyStore),
|
||||
BikeCollectionFactory.GetBikesAll(
|
||||
bikesAvailableResponse?.bikes?.Values,
|
||||
bikesOccupiedResponse?.bikes_occupied?.Values,
|
||||
Mail,
|
||||
DateTimeProvider,
|
||||
Bikes.BikeInfoNS.BC.DataSource.Cache),
|
||||
bikesAvailableResponse?.GetGeneralData());
|
||||
}
|
||||
}
|
||||
}
|
489
SharedBusinessLogic/Model/Connector/TextToTypeHelper.cs
Normal file
489
SharedBusinessLogic/Model/Connector/TextToTypeHelper.cs
Normal file
|
@ -0,0 +1,489 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.Services.CopriApi.ServerUris;
|
||||
using ShareeBike.Model.State;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using ShareeBike.Model.Stations.StationNS.Operator;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Repository.Response;
|
||||
using ShareeBike.Repository.Response.Stations.Station;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace ShareeBike.Model.Connector
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts weak typed JSON data (mostly string) to strong typed c# data (base types, enumerations, objects, ...).
|
||||
/// JSON is received from COPRI and deserialized using Json.NET.
|
||||
/// </summary>
|
||||
public static class TextToTypeHelper
|
||||
{
|
||||
/// <summary> Holds the text for demo bikes. </summary>
|
||||
private const string DEMOBIKEMARKER = "DEMO";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the position from StationInfo object.
|
||||
/// </summary>
|
||||
/// <param name="stationInfo">Object to get information from.</param>
|
||||
/// <returns>Position information.</returns>
|
||||
public static IPosition GetPosition(this StationInfo stationInfo)
|
||||
=> GetPosition(stationInfo.gps);
|
||||
|
||||
/// <summary> Gets the position from StationInfo object. </summary>
|
||||
/// <param name="authorizationResponse">Object to get information from.</param>
|
||||
/// <returns>Position information.</returns>
|
||||
public static IEnumerable<string> GetGroup(this AuthorizationResponse authorizationResponse)
|
||||
{
|
||||
try
|
||||
{
|
||||
return authorizationResponse.user_group.GetGroup();
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
throw new Exception($"Can not get group of user from text \"{authorizationResponse.user_group}\".", l_oException);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets the position from StationInfo object. </summary>
|
||||
/// <param name="group">Object to get information from.</param>
|
||||
/// <returns>Position information.</returns>
|
||||
public static IEnumerable<string> GetGroup(this string[] group)
|
||||
{
|
||||
if (group == null || group.Length == 0)
|
||||
{
|
||||
// If not logged in stations groups are empty form COPRI version v4.1.
|
||||
Log.Debug("Can not get group form string. Group text can not be null.");
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
return new HashSet<string>(group).ToList();
|
||||
}
|
||||
|
||||
/// <summary> Gets if user acknowledged AGBs or not. </summary>
|
||||
/// <param name="authorizationResponse">Object to get information from.</param>
|
||||
/// <returns>Position information.</returns>
|
||||
public static bool GetIsAgbAcknowledged(this AuthorizationResponse authorizationResponse)
|
||||
=> int.TryParse(authorizationResponse?.agb_checked, out int result)
|
||||
&& result != 0;
|
||||
|
||||
/// <summary> Gets the position from StationInfo object. </summary>
|
||||
/// <param name="group">Object to get information from.</param>
|
||||
/// <returns>Position information.</returns>
|
||||
public static string GetGroup(this IEnumerable<string> group)
|
||||
{
|
||||
return string.Join(",", group);
|
||||
}
|
||||
|
||||
/// <summary> Gets the position from StationInfo object. </summary>
|
||||
/// <param name="stationInfo">Object to get information from.</param>
|
||||
/// <returns>Position information.</returns>
|
||||
public static IEnumerable<string> GetGroup(this StationInfo stationInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
return stationInfo.station_group.GetGroup();
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
throw new Exception($"Can not get group of stations from text \"{stationInfo.station_group}\".", l_oException);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the position from BikeInfoBase object.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">Object to get information from.</param>
|
||||
/// <returns>Rental state.</returns>
|
||||
public static InUseStateEnum GetState(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
var stateText = bikeInfo.state?.ToLower()?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(stateText))
|
||||
{
|
||||
throw new InvalidResponseException<BikeInfoBase>(
|
||||
"Bike state must not be empty.",
|
||||
bikeInfo);
|
||||
}
|
||||
|
||||
if (Enum.TryParse(stateText, out InUseStateEnum state))
|
||||
return state;
|
||||
|
||||
if (stateText == "available")
|
||||
{
|
||||
return InUseStateEnum.Disposable;
|
||||
}
|
||||
else if (stateText == "reserved" ||
|
||||
stateText == "requested")
|
||||
{
|
||||
return InUseStateEnum.Reserved;
|
||||
}
|
||||
else if (stateText == "booked" ||
|
||||
stateText == "occupied")
|
||||
{
|
||||
return InUseStateEnum.Booked;
|
||||
}
|
||||
|
||||
throw new CommunicationException(string.Format("Unknown bike state detected. State is {0}.", stateText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the position from BikeInfoAvailable object.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">Object to get information from.</param>
|
||||
/// <returns>Rental state.</returns>
|
||||
public static InUseStateEnum GetState(this BikeInfoAvailable bikeInfo)
|
||||
{
|
||||
var state = GetState((BikeInfoBase)bikeInfo);
|
||||
|
||||
if (state != InUseStateEnum.Disposable)
|
||||
return state;
|
||||
|
||||
return bikeInfo.GetIsFeedbackPending()
|
||||
? InUseStateEnum.FeedbackPending
|
||||
: InUseStateEnum.Disposable;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the from date information from JSON.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>From information if bike hold this information 0001-01-01 (DateTime.MinValue) otherwise.</returns>
|
||||
public static DateTime GetFrom(this BikeInfoReservedOrBooked bikeInfo)
|
||||
=> DateTime.TryParse(bikeInfo?.start_time, out DateTime dateFrom) ? dateFrom : DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the bike is a trike or not.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static bool? GetIsDemo(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
return bikeInfo?.description != null
|
||||
? bikeInfo.description.ToUpper().Contains(DEMOBIKEMARKER)
|
||||
: (bool?)null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the bike is a trike or not.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from.</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static IEnumerable<string> GetGroup(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
return bikeInfo?.bike_group?.GetGroup()?.ToList() ?? new List<string>();
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
throw new Exception($"Can not get group of user from text \"{bikeInfo.bike_group}\".", l_oException);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets whether the bike has a bord computer or not. </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from.</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static bool GetIsManualLockBike(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
return !string.IsNullOrEmpty(bikeInfo.system)
|
||||
&& bikeInfo.system.ToUpper().StartsWith("LOCK");
|
||||
}
|
||||
|
||||
/// <summary> Gets whether the bike has a bord computer or not. </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static bool GetIsBluetoothLockBike(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
return !string.IsNullOrEmpty(bikeInfo.system)
|
||||
&& bikeInfo.system.ToUpper().StartsWith("ILOCKIT");
|
||||
}
|
||||
|
||||
/// <summary> Gets whether the bike Is a Sigo bike or not. </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>True if bike is a Sigo.</returns>
|
||||
public static bool GetIsSigoBike(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
return !string.IsNullOrEmpty(bikeInfo.system)
|
||||
&& bikeInfo.system.ToUpper().StartsWith("SIGO");
|
||||
}
|
||||
|
||||
public static LockModel? GetLockModel(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
if (GetIsBluetoothLockBike(bikeInfo))
|
||||
return LockModel.ILockIt;
|
||||
|
||||
if (GetIsManualLockBike(bikeInfo))
|
||||
return LockModel.BordComputer;
|
||||
|
||||
if (GetIsSigoBike(bikeInfo))
|
||||
return LockModel.Sigo;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary> Gets whether the bike has a bord computer or not. </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static int GetBluetoothLockId(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
return TextToLockItTypeHelper.GetBluetoothLockId(bikeInfo?.Ilockit_ID);
|
||||
}
|
||||
|
||||
/// <summary> Gets whether the bike has a bord computer or not. </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static Guid GetBluetoothLockGuid(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
// return new Guid("00000000-0000-0000-0000-e57e6b9aee16");
|
||||
return Guid.TryParse(bikeInfo?.Ilockit_GUID, out Guid lockGuid)
|
||||
? lockGuid
|
||||
: TextToLockItTypeHelper.INVALIDLOCKGUID;
|
||||
}
|
||||
|
||||
public static byte[] GetUserKey(this BikeInfoReservedOrBooked bikeInfo)
|
||||
{
|
||||
return GetKey(bikeInfo.K_u);
|
||||
}
|
||||
|
||||
public static byte[] GetAdminKey(this BikeInfoReservedOrBooked bikeInfo)
|
||||
{
|
||||
return GetKey(bikeInfo.K_a);
|
||||
}
|
||||
|
||||
public static byte[] GetSeed(this BikeInfoReservedOrBooked bikeInfo)
|
||||
{
|
||||
return GetKey(bikeInfo.K_seed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get array of keys from string of format "[12, -9, 5]"
|
||||
/// </summary>
|
||||
/// <param name="keyArrayText"></param>
|
||||
/// <returns></returns>
|
||||
private static byte[] GetKey(string keyArrayText)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyArrayText))
|
||||
return new byte[0];
|
||||
|
||||
return Regex.Replace(keyArrayText, @"\[(.*)\]", "$1").Split(',').Select(x => (byte)sbyte.Parse(x)).ToArray();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("Can not extract K_u/ K_a/ or K_seed. Key {ArrayText} does not is not of expected format. {Exception}", keyArrayText, exception);
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the bike is a trike or not.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">JSON to get information from..</param>
|
||||
/// <returns>From information.</returns>
|
||||
public static WheelType? GetWheelType(this BikeInfoBase bikeInfo)
|
||||
=> Enum.TryParse(bikeInfo?.bike_type?.wheels, true, out WheelType wheelType)
|
||||
? wheelType
|
||||
: (WheelType?)null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of bike.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">Object to get bike type from.</param>
|
||||
/// <returns>Type of bike.</returns>
|
||||
public static TypeOfBike? GetTypeOfBike(this BikeInfoBase bikeInfo)
|
||||
=> Enum.TryParse(bikeInfo?.bike_type?.category, true, out TypeOfBike typeOfBike)
|
||||
? typeOfBike
|
||||
: (TypeOfBike?)null;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo">Object to get AA info from.</param>
|
||||
/// <returns>AA info.</returns>
|
||||
public static AaRideType? GetAaRideType(this BikeInfoBase bikeInfo)
|
||||
=> Enum.TryParse(bikeInfo?.aa_ride, true, out AaRideType aaRide)
|
||||
? aaRide
|
||||
: (AaRideType?)null;
|
||||
|
||||
|
||||
/// <summary> Get position from a ,- separated string. </summary>
|
||||
/// <param name="gps">Text to extract position from.</param>
|
||||
/// <returns>Position object.</returns>
|
||||
public static IPosition GetPosition(Repository.Response.Position gps)
|
||||
=> PositionFactory.Create(
|
||||
double.TryParse(gps?.latitude, NumberStyles.Float, CultureInfo.InvariantCulture, out double latitude) ? latitude : double.NaN,
|
||||
double.TryParse(gps?.longitude, NumberStyles.Float, CultureInfo.InvariantCulture, out double longitude) ? longitude : double.NaN);
|
||||
|
||||
/// <summary> Get position from a ,- separated string. </summary>
|
||||
/// <param name="gps">Text to extract position from.</param>
|
||||
/// <returns>Position object.</returns>
|
||||
public static Map.IMapSpan GetMapSpan(this MapSpan mapSpan)
|
||||
=> Map.MapSpanFactory.Create(
|
||||
GetPosition(mapSpan?.center),
|
||||
double.TryParse(mapSpan?.radius, NumberStyles.Float, CultureInfo.InvariantCulture, out double radius) ? radius : double.NaN);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locking state from response.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo"> Response locking state from.</param>
|
||||
/// <returns>Locking state</returns>
|
||||
public static Bikes.BikeInfoNS.CopriLock.LockingState GetCopriLockingState(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bikeInfo?.lock_state))
|
||||
return Bikes.BikeInfoNS.CopriLock.LockingState.UnknownDisconnected;
|
||||
|
||||
if (bikeInfo.lock_state.ToUpper().Trim() == "locked".ToUpper())
|
||||
return Bikes.BikeInfoNS.CopriLock.LockingState.Closed;
|
||||
|
||||
if (bikeInfo.lock_state.ToUpper().Trim() == "locking".ToUpper())
|
||||
return Bikes.BikeInfoNS.CopriLock.LockingState.Closing;
|
||||
|
||||
if (bikeInfo.lock_state.ToUpper().Trim() == "unlocked".ToUpper())
|
||||
return Bikes.BikeInfoNS.CopriLock.LockingState.Open;
|
||||
|
||||
if (bikeInfo.lock_state.ToUpper().Trim() == "unlocking".ToUpper())
|
||||
return Bikes.BikeInfoNS.CopriLock.LockingState.Opening;
|
||||
|
||||
return Bikes.BikeInfoNS.CopriLock.LockingState.UnknownDisconnected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operator Uri from response.
|
||||
/// </summary>
|
||||
/// <param name="bikeInfo"> Response to get uri from.</param>
|
||||
/// <returns>Operator Uri</returns>
|
||||
public static Uri GetOperatorUri(this BikeInfoBase bikeInfo)
|
||||
{
|
||||
if (Uri.TryCreate(bikeInfo?.uri_operator, UriKind.Absolute, out var operatorUri))
|
||||
{
|
||||
// Valid uri detected.
|
||||
return new Uri($"{operatorUri.AbsoluteUri}/{CopriServerUriList.REST_RESOURCE_ROOT}");
|
||||
}
|
||||
|
||||
Log.Error(!string.IsNullOrEmpty(bikeInfo?.uri_operator)
|
||||
? $"Operator uri can not be extracted from bike info base object {bikeInfo.uri_operator}. Uri is not valid."
|
||||
: "Operator uri can not be extracted from bike info base object. Entry is null or empty.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operator Uri from response.
|
||||
/// </summary>
|
||||
/// <param name="stationInfo"> Response to get uri from.</param>
|
||||
/// <returns>Operator Uri</returns>
|
||||
public static Uri GetOperatorUri(this StationInfo stationInfo)
|
||||
{
|
||||
if (Uri.TryCreate(stationInfo?.uri_operator, UriKind.Absolute, out var operatorUri))
|
||||
{
|
||||
// Valid uri detected.
|
||||
return new Uri($"{operatorUri.AbsoluteUri}/{CopriServerUriList.REST_RESOURCE_ROOT}");
|
||||
}
|
||||
|
||||
Log.Error(!string.IsNullOrEmpty(stationInfo?.uri_operator)
|
||||
? $"Operator uri can not be extracted from station object {stationInfo.uri_operator}. Uri is not valid."
|
||||
: "Operator uri can not be extracted from station object. Entry is null or empty.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary> Tries to get the copri version from response.</summary>
|
||||
/// <param name="response">Response to get version info from.</param>
|
||||
/// <returns>COPRI version</returns>
|
||||
public static bool TryGetCopriVersion(this CopriVersion response, out Version copriVersion)
|
||||
{
|
||||
copriVersion = new Version(0, 0);
|
||||
return response != null
|
||||
&& !string.IsNullOrEmpty(response.copri_version)
|
||||
&& Version.TryParse(response.copri_version, out copriVersion);
|
||||
}
|
||||
|
||||
/// <summary> Gets the copri version from.</summary>
|
||||
/// <param name="response">Response to get version info from.</param>
|
||||
/// <returns>COPRI version</returns>
|
||||
public static Version GetCopriVersion(this CopriVersion response)
|
||||
=> response.TryGetCopriVersion(out Version copriVersion)
|
||||
? copriVersion
|
||||
: throw new InvalidResponseException($"Can not get version info from copri response {response?.copri_version}.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets bike advanced bike state. If entry Co2Saving exists feedback is required.
|
||||
/// </summary>
|
||||
/// <param name="bike">Bike get to state from.</param>
|
||||
public static bool GetIsFeedbackPending(this BikeInfoAvailable bike)
|
||||
=> bike.co2saving != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of bikes available at station.
|
||||
/// </summary>
|
||||
/// <param name="stationInfo">Object to get information from.</param>
|
||||
/// <returns>Count of bikes available or null if information is unknown.</returns>
|
||||
public static int? GetBikesAvailableCount(this StationInfo stationInfo)
|
||||
=> TryGetBikesAvailableCount(stationInfo, out int bikeCount)
|
||||
? bikeCount
|
||||
: (int?)null;
|
||||
|
||||
public static bool TryGetBikesAvailableCount(this StationInfo stationInfo, out int bikesCount)
|
||||
=> int.TryParse(stationInfo?.bike_count, out bikesCount);
|
||||
|
||||
public static void SetBikesAvailableCount(this StationInfo stationInfo, int bikesCount)
|
||||
=> stationInfo.bike_count = bikesCount.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets station object from response object.
|
||||
/// </summary>
|
||||
/// <param name="station">Response object to get station object from.</param>
|
||||
/// <returns>Station object.</returns>
|
||||
public static Station GetStation(this StationInfo station) =>
|
||||
new Station(
|
||||
station.station,
|
||||
station.GetGroup(),
|
||||
station.GetPosition(),
|
||||
station.description,
|
||||
station.GetOperatorUri(),
|
||||
new Data(station.operator_data?.operator_name,
|
||||
station.operator_data?.operator_phone,
|
||||
station.operator_data?.operator_hours,
|
||||
station.operator_data?.operator_email,
|
||||
!string.IsNullOrEmpty(station.operator_data?.operator_color)
|
||||
? Color.FromHex(station.operator_data?.operator_color)
|
||||
: (Color?)null),
|
||||
new BikeGroupCol(station.station_type?.Select(x => new BikeGroupCol.Entry(
|
||||
x.Key,
|
||||
int.TryParse(x.Value.bike_count, out int count) ? count : 0,
|
||||
x.Value.bike_group)) ?? new List<BikeGroupCol.Entry>()
|
||||
));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bike group object from response object.
|
||||
/// </summary>
|
||||
/// <param name="bikeGroup">Response object to get station from.</param>
|
||||
/// <param name="name">Name of the bike group.</param>
|
||||
/// <returns></returns>
|
||||
public static BikeGroupCol.Entry GetBikeGroup(this BikeGroup bikeGroup, string name) =>
|
||||
new BikeGroupCol.Entry(
|
||||
name,
|
||||
int.TryParse(bikeGroup?.bike_count ?? "0", out var countCity) ? countCity : 0,
|
||||
bikeGroup?.bike_group ?? string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Default value for reserve_timerange.
|
||||
/// </summary>
|
||||
private static int DEFAULTMAXRESERVATIONTIMESPAN = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reservation time span from response.
|
||||
/// </summary>
|
||||
/// <param name="description">Response to get time span from.</param>
|
||||
/// <returns>Time span.</returns>
|
||||
public static TimeSpan GetMaxReservationTimeSpan(this RentalDescription description) =>
|
||||
TimeSpan.FromMinutes(int.TryParse(description?.reserve_timerange, out int minutes)
|
||||
? minutes
|
||||
: DEFAULTMAXRESERVATIONTIMESPAN );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
|
||||
using ShareeBike.Model.Bikes;
|
||||
using ShareeBike.Repository.Response;
|
||||
using Serilog;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
public static class BikeCollectionFactory
|
||||
{
|
||||
/// <summary> Gets bikes available from copri server response.</summary>
|
||||
/// <param name="bikesAvailableResponse">Response to create collection from.</param>
|
||||
/// <param name="dataSource">Specified the data source</param>
|
||||
/// <returns>New collection of available bikes.</returns>
|
||||
public static BikeCollection GetBikesAvailable(
|
||||
this BikesAvailableResponse bikesAvailableResponse,
|
||||
DataSource dataSource)
|
||||
=> GetBikesAll(
|
||||
bikesAvailableResponse?.bikes?.Values,
|
||||
bikesAvailableResponse?.bikes_occupied?.Values,
|
||||
string.Empty,
|
||||
() => DateTime.Now,
|
||||
dataSource);
|
||||
|
||||
/// <summary> Gets bikes occupied from copri server response. </summary>
|
||||
/// <param name="bikesOccupiedResponse">Response to create bikes from.</param>
|
||||
/// <param name="dataSource">Specified the data source</param>
|
||||
/// <returns>New collection of occupied bikes.</returns>
|
||||
public static BikeCollection GetBikesOccupied(
|
||||
this BikesReservedOccupiedResponse bikesOccupiedResponse,
|
||||
string mail,
|
||||
Func<DateTime> dateTimeProvider,
|
||||
DataSource dataSource)
|
||||
=> GetBikesAll(
|
||||
new BikesAvailableResponse()?.bikes?.Values,
|
||||
bikesOccupiedResponse?.bikes_occupied?.Values,
|
||||
mail,
|
||||
dateTimeProvider,
|
||||
dataSource);
|
||||
|
||||
/// <summary> Gets bikes occupied from copri server response. </summary>
|
||||
/// <param name="bikesAvailable">Response to create bikes available from.</param>
|
||||
/// <param name="bikesOccupied">Response to create bikes occupied from.</param>
|
||||
/// <returns>New collection of occupied bikes.</returns>
|
||||
public static BikeCollection GetBikesAll(
|
||||
IEnumerable<BikeInfoAvailable> bikesAvailable,
|
||||
IEnumerable<BikeInfoReservedOrBooked> bikesOccupied,
|
||||
string mail,
|
||||
Func<DateTime> dateTimeProvider,
|
||||
DataSource dataSource)
|
||||
{
|
||||
var bikesDictionary = new Dictionary<string, BikeInfo>();
|
||||
var duplicates = new Dictionary<string, BikeInfo>();
|
||||
|
||||
// Get bikes from Copri/ file/ memory, ....
|
||||
if (bikesAvailable != null)
|
||||
{
|
||||
foreach (var bikeInfoResponse in bikesAvailable)
|
||||
{
|
||||
var bikeInfo = BikeInfoFactory.Create(bikeInfoResponse, dataSource);
|
||||
if (bikeInfo == null)
|
||||
{
|
||||
// Response is not valid.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bikesDictionary.ContainsKey(bikeInfo.Id))
|
||||
{
|
||||
// Duplicates are not allowed.
|
||||
Log.Error($"Duplicate bike with id {bikeInfo.Id} detected evaluating bikes available. Bike status is {bikeInfo.State.Value}.");
|
||||
|
||||
if (!duplicates.ContainsKey(bikeInfo.Id))
|
||||
{
|
||||
duplicates.Add(bikeInfo.Id, bikeInfo);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
bikesDictionary.Add(bikeInfo.Id, bikeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get bikes from Copri/ file/ memory, ....
|
||||
if (bikesOccupied != null)
|
||||
{
|
||||
foreach (var bikeInfoResponse in bikesOccupied)
|
||||
{
|
||||
BikeInfo bikeInfo = BikeInfoFactory.Create(
|
||||
bikeInfoResponse,
|
||||
mail,
|
||||
dateTimeProvider,
|
||||
dataSource);
|
||||
|
||||
if (bikeInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bikesDictionary.ContainsKey(bikeInfo.Id))
|
||||
{
|
||||
// Duplicates are not allowed.
|
||||
Log.Error($"Duplicate bike with id {bikeInfo.Id} detected evaluating bikes occupied. Bike status is {bikeInfo.State.Value}.");
|
||||
if (!duplicates.ContainsKey(bikeInfo.Id))
|
||||
{
|
||||
duplicates.Add(bikeInfo.Id, bikeInfo);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
bikesDictionary.Add(bikeInfo.Id, bikeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove entries which are not unique.
|
||||
foreach (var l_oDuplicate in duplicates)
|
||||
{
|
||||
bikesDictionary.Remove(l_oDuplicate.Key);
|
||||
}
|
||||
|
||||
return new BikeCollection(bikesDictionary);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
303
SharedBusinessLogic/Model/Connector/Updater/BikeInfoFactory.cs
Normal file
303
SharedBusinessLogic/Model/Connector/Updater/BikeInfoFactory.cs
Normal file
|
@ -0,0 +1,303 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
|
||||
using ShareeBike.Model.MiniSurvey;
|
||||
using ShareeBike.Model.State;
|
||||
using ShareeBike.Repository.Response;
|
||||
using BikeExtension = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.BikeExtension;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs bike info instances/ bike info derived instances.
|
||||
/// </summary>
|
||||
public static class BikeInfoFactory
|
||||
{
|
||||
/// <summary> Set default lock type to . </summary>
|
||||
public static LockModel DEFAULTLOCKMODEL = LockModel.Sigo;
|
||||
|
||||
/// <summary> Creates a bike info object from copri response. </summary>
|
||||
/// <param name="bikeInfo">Copri response for a disposable bike. </param>
|
||||
/// <param name="dataSource">Specifies the data source.</param>
|
||||
public static BikeInfo Create(
|
||||
BikeInfoAvailable bikeInfo,
|
||||
DataSource dataSource)
|
||||
{
|
||||
if (bikeInfo == null) throw new ArgumentNullException(nameof(bikeInfo));
|
||||
|
||||
var lockModel = bikeInfo.GetLockModel();
|
||||
|
||||
if (lockModel.HasValue
|
||||
&& lockModel.Value == LockModel.BordComputer)
|
||||
{
|
||||
// Manual lock bikes are no more supported.
|
||||
Log.Error(
|
||||
$"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " +
|
||||
"Manual lock bikes are no more supported." +
|
||||
$"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $"station number {bikeInfo.station}" : string.Empty)}."
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (bikeInfo.GetState())
|
||||
{
|
||||
case InUseStateEnum.Disposable:
|
||||
case InUseStateEnum.FeedbackPending:
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(bikeInfo.station))
|
||||
{
|
||||
// Bike available must always have a station id because bikes can only be returned at a station.
|
||||
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. No station info set.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var lockType = lockModel.HasValue
|
||||
? BikeExtension.GetLockType(lockModel.Value)
|
||||
: BikeExtension.GetLockType(DEFAULTLOCKMODEL); // Map bikes without "system"- entry in response to back end- locks.
|
||||
|
||||
try
|
||||
{
|
||||
switch (lockType)
|
||||
{
|
||||
case LockType.Backend:
|
||||
return new Bikes.BikeInfoNS.CopriLock.BikeInfo(
|
||||
new Bike(
|
||||
bikeInfo.bike,
|
||||
LockModel.Sigo,
|
||||
bikeInfo.GetWheelType(),
|
||||
bikeInfo.GetTypeOfBike(),
|
||||
bikeInfo.GetAaRideType(),
|
||||
bikeInfo.description),
|
||||
DriveFactory.Create(bikeInfo?.bike_type),
|
||||
dataSource,
|
||||
bikeInfo.station,
|
||||
new Bikes.BikeInfoNS.CopriLock.LockInfo.Builder { State = bikeInfo.GetCopriLockingState() }.Build(),
|
||||
bikeInfo.GetState() == InUseStateEnum.FeedbackPending,
|
||||
bikeInfo.GetOperatorUri(),
|
||||
bikeInfo.rental_description != null
|
||||
? RentalDescriptionFactory.Create(bikeInfo.rental_description)
|
||||
: TariffDescriptionFactory.Create(bikeInfo.tariff_description),
|
||||
bikeInfo.GetIsDemo(),
|
||||
bikeInfo.GetGroup(),
|
||||
miniSurvey: bikeInfo.user_miniquery != null
|
||||
? new MiniSurveyModel(new Dictionary<string, IQuestionModel> {
|
||||
{ "q1", new QuestionModel()} // Add a dummy query. Queries are not yet read from COPRI but compiled into the app.
|
||||
})
|
||||
: new MiniSurveyModel(),
|
||||
co2Saving: bikeInfo.co2saving);
|
||||
|
||||
case LockType.Bluethooth:
|
||||
return new Bikes.BikeInfoNS.BluetoothLock.BikeInfo(
|
||||
new Bike(
|
||||
bikeInfo.bike,
|
||||
LockModel.ILockIt,
|
||||
bikeInfo.GetWheelType(),
|
||||
bikeInfo.GetTypeOfBike(),
|
||||
bikeInfo.GetAaRideType(),
|
||||
bikeInfo.description),
|
||||
DriveFactory.Create(bikeInfo?.bike_type),
|
||||
dataSource,
|
||||
bikeInfo.GetBluetoothLockId(),
|
||||
bikeInfo.GetBluetoothLockGuid(),
|
||||
bikeInfo.station,
|
||||
bikeInfo.GetOperatorUri(),
|
||||
bikeInfo.rental_description != null
|
||||
? RentalDescriptionFactory.Create(bikeInfo.rental_description)
|
||||
: TariffDescriptionFactory.Create(bikeInfo.tariff_description),
|
||||
bikeInfo.GetIsDemo(),
|
||||
bikeInfo.GetGroup());
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported lock type {lockType} detected.");
|
||||
}
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
// Constructor reported invalid arguments (missing lock id, ....).
|
||||
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Invalid response detected. Available bike with id {bikeInfo.bike} skipped. {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Creates a bike info object from copri response. </summary>
|
||||
/// <param name="bikeInfo">Copri response. </param>
|
||||
/// <param name="mailAddress">Mail address of user.</param>
|
||||
/// <param name="dateTimeProvider">Date and time provider function.</param>
|
||||
/// <param name="dataSource">Specified the source of the data.</param>
|
||||
public static BikeInfo Create(
|
||||
BikeInfoReservedOrBooked bikeInfo,
|
||||
string mailAddress,
|
||||
Func<DateTime> dateTimeProvider,
|
||||
DataSource dataSource)
|
||||
{
|
||||
if (bikeInfo == null) throw new ArgumentNullException(nameof(bikeInfo));
|
||||
|
||||
var lockModel = bikeInfo.GetLockModel();
|
||||
|
||||
if (lockModel.HasValue
|
||||
&& lockModel.Value == LockModel.BordComputer)
|
||||
{
|
||||
// Manual lock bikes are no more supported.
|
||||
Log.Error(
|
||||
$"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " +
|
||||
"Manual lock bikes are no more supported." +
|
||||
$"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $", station number {bikeInfo.station}" : string.Empty)}."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
var lockType = lockModel.HasValue
|
||||
? BikeExtension.GetLockType(lockModel.Value)
|
||||
: BikeExtension.GetLockType(DEFAULTLOCKMODEL); // Map bikes without "system"- entry in response to backend- locks.
|
||||
|
||||
// Check if bike is a bluetooth lock bike.
|
||||
int lockSerial = bikeInfo.GetBluetoothLockId();
|
||||
Guid lockGuid = bikeInfo.GetBluetoothLockGuid();
|
||||
|
||||
switch (bikeInfo.GetState())
|
||||
{
|
||||
case InUseStateEnum.Reserved:
|
||||
try
|
||||
{
|
||||
switch (lockType)
|
||||
{
|
||||
case LockType.Bluethooth:
|
||||
return new Bikes.BikeInfoNS.BluetoothLock.BikeInfo(
|
||||
new Bike(
|
||||
bikeInfo.bike,
|
||||
LockModel.ILockIt,
|
||||
bikeInfo.GetWheelType(),
|
||||
bikeInfo.GetTypeOfBike(),
|
||||
bikeInfo.GetAaRideType(),
|
||||
bikeInfo.description),
|
||||
DriveFactory.Create(bikeInfo?.bike_type),
|
||||
dataSource,
|
||||
lockSerial,
|
||||
lockGuid,
|
||||
bikeInfo.GetUserKey(),
|
||||
bikeInfo.GetAdminKey(),
|
||||
bikeInfo.GetSeed(),
|
||||
bikeInfo.GetFrom(),
|
||||
mailAddress,
|
||||
bikeInfo.station,
|
||||
bikeInfo.GetOperatorUri(),
|
||||
bikeInfo.rental_description != null
|
||||
? RentalDescriptionFactory.Create(bikeInfo.rental_description)
|
||||
: TariffDescriptionFactory.Create(bikeInfo.tariff_description),
|
||||
dateTimeProvider,
|
||||
bikeInfo.GetIsDemo(),
|
||||
bikeInfo.GetGroup());
|
||||
|
||||
case LockType.Backend:
|
||||
return new Bikes.BikeInfoNS.CopriLock.BikeInfo(
|
||||
new Bike(
|
||||
bikeInfo.bike,
|
||||
LockModel.Sigo,
|
||||
bikeInfo.GetWheelType(),
|
||||
bikeInfo.GetTypeOfBike(),
|
||||
bikeInfo.GetAaRideType(),
|
||||
bikeInfo.description),
|
||||
DriveFactory.Create(bikeInfo?.bike_type),
|
||||
dataSource,
|
||||
bikeInfo.GetFrom(),
|
||||
mailAddress,
|
||||
bikeInfo.station,
|
||||
new Bikes.BikeInfoNS.CopriLock.LockInfo.Builder { State = bikeInfo.GetCopriLockingState() }.Build(),
|
||||
bikeInfo.GetOperatorUri(),
|
||||
bikeInfo.rental_description != null
|
||||
? RentalDescriptionFactory.Create(bikeInfo.rental_description)
|
||||
: TariffDescriptionFactory.Create(bikeInfo.tariff_description),
|
||||
dateTimeProvider,
|
||||
bikeInfo.GetIsDemo(),
|
||||
bikeInfo.GetGroup());
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported lock type {lockType} detected.");
|
||||
}
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
// Constructor reported invalid arguments (missing lock id, ....).
|
||||
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Reserved bike with id {bikeInfo.bike} skipped. {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
case InUseStateEnum.Booked:
|
||||
try
|
||||
{
|
||||
switch (lockModel)
|
||||
{
|
||||
case LockModel.ILockIt:
|
||||
return new Bikes.BikeInfoNS.BluetoothLock.BikeInfo(
|
||||
new Bike(
|
||||
bikeInfo.bike,
|
||||
LockModel.ILockIt,
|
||||
bikeInfo.GetWheelType(),
|
||||
bikeInfo.GetTypeOfBike(),
|
||||
bikeInfo.GetAaRideType(),
|
||||
bikeInfo.description),
|
||||
DriveFactory.Create(bikeInfo?.bike_type),
|
||||
dataSource,
|
||||
lockSerial,
|
||||
bikeInfo.GetBluetoothLockGuid(),
|
||||
bikeInfo.GetUserKey(),
|
||||
bikeInfo.GetAdminKey(),
|
||||
bikeInfo.GetSeed(),
|
||||
bikeInfo.GetFrom(),
|
||||
mailAddress,
|
||||
bikeInfo.station,
|
||||
bikeInfo.GetOperatorUri(),
|
||||
bikeInfo.rental_description != null
|
||||
? RentalDescriptionFactory.Create(bikeInfo.rental_description)
|
||||
: TariffDescriptionFactory.Create(bikeInfo.tariff_description),
|
||||
bikeInfo.GetIsDemo(),
|
||||
bikeInfo.GetGroup());
|
||||
|
||||
case LockModel.BordComputer:
|
||||
throw new NotSupportedException($"Bikes with lock model of type {lockModel} are no more supported.");
|
||||
|
||||
default:
|
||||
return new Bikes.BikeInfoNS.CopriLock.BikeInfo(
|
||||
new Bike(
|
||||
bikeInfo.bike,
|
||||
LockModel.Sigo,
|
||||
bikeInfo.GetWheelType(),
|
||||
bikeInfo.GetTypeOfBike(),
|
||||
bikeInfo.GetAaRideType(),
|
||||
bikeInfo.description),
|
||||
DriveFactory.Create(bikeInfo?.bike_type),
|
||||
DataSource.Copri,
|
||||
bikeInfo.GetFrom(),
|
||||
mailAddress,
|
||||
bikeInfo.station,
|
||||
new Bikes.BikeInfoNS.CopriLock.LockInfo.Builder { State = bikeInfo.GetCopriLockingState() }.Build(),
|
||||
bikeInfo.GetOperatorUri(),
|
||||
bikeInfo.rental_description != null
|
||||
? RentalDescriptionFactory.Create(bikeInfo.rental_description)
|
||||
: TariffDescriptionFactory.Create(bikeInfo.tariff_description),
|
||||
bikeInfo.GetIsDemo(),
|
||||
bikeInfo.GetGroup());
|
||||
}
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
// Constructor reported invalid arguments (missing lock id, ....).
|
||||
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Booked bike with id {bikeInfo.bike} skipped. {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
default:
|
||||
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ShareeBike.Model.MiniSurvey;
|
||||
using ShareeBike.Repository.Response;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
public static class BookingFinishedModelFactory
|
||||
{
|
||||
/// <summary> Creates a booking finished object from response.</summary>
|
||||
/// <param name="response">Response to create survey object from.</param>
|
||||
public static BookingFinishedModel Create(this DoReturnResponse response)
|
||||
{
|
||||
var bookingFinished = new BookingFinishedModel
|
||||
{
|
||||
Co2Saving = response?.bike_returned.co2saving,
|
||||
RentalCosts = response?.bike_returned.total_price,
|
||||
Duration = response?.bike_returned.real_clock,
|
||||
Distance = response?.bike_returned.distance,
|
||||
};
|
||||
|
||||
if (response?.user_miniquery == null)
|
||||
|
||||
{
|
||||
return bookingFinished;
|
||||
}
|
||||
|
||||
var miniquery = response.user_miniquery;
|
||||
bookingFinished.MiniSurvey = new MiniSurveyModel
|
||||
{
|
||||
Title = miniquery.title,
|
||||
Subtitle = miniquery.subtitle,
|
||||
Footer = miniquery.footer
|
||||
};
|
||||
|
||||
foreach (var question in miniquery?.questions?.OrderBy(x => x.Key) ?? new Dictionary<string, MiniSurveyResponse.Question>().OrderBy(x => x.Key))
|
||||
{
|
||||
if (string.IsNullOrEmpty(question.Key.Trim())
|
||||
|| question.Value.query == null)
|
||||
{
|
||||
// Skip invalid entries.
|
||||
continue;
|
||||
}
|
||||
|
||||
bookingFinished.MiniSurvey.Questions.Add(
|
||||
question.Key,
|
||||
new QuestionModel());
|
||||
}
|
||||
|
||||
return bookingFinished;
|
||||
}
|
||||
}
|
||||
}
|
45
SharedBusinessLogic/Model/Connector/Updater/DriveFactory.cs
Normal file
45
SharedBusinessLogic/Model/Connector/Updater/DriveFactory.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.EngineNS;
|
||||
using ShareeBike.Repository.Response;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
public static class DriveFactory
|
||||
{
|
||||
public static DriveMutable Create(this BikeType bikeType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bikeType?.engine?.manufacturer))
|
||||
{
|
||||
// Bike is has no engine
|
||||
return new DriveMutable();
|
||||
}
|
||||
|
||||
// Bike is a pedelec.
|
||||
return new DriveMutable(
|
||||
new Engine(bikeType?.engine?.manufacturer),
|
||||
new Battery.Builder
|
||||
{
|
||||
CurrentChargePercent = double.TryParse(bikeType?.battery?.charge_current_percent, out double currentChargePercent)
|
||||
? currentChargePercent
|
||||
: double.NaN,
|
||||
|
||||
CurrentChargeBars = int.TryParse(bikeType?.battery?.charge_current_bars, out int currentChargeBars)
|
||||
? (int?)currentChargeBars
|
||||
: null,
|
||||
|
||||
MaxChargeBars = int.TryParse(bikeType?.battery?.charge_max_bars, out int maxChargeBars)
|
||||
? (int?)maxChargeBars
|
||||
: null,
|
||||
|
||||
IsBackendAccessible = bikeType?.battery?.backend_accessible != null && int.TryParse(bikeType.battery.backend_accessible, out int accessible)
|
||||
? (bool?)(accessible > 0)
|
||||
: null,
|
||||
|
||||
IsHidden = bikeType?.battery?.hidden != null && int.TryParse(bikeType.battery.hidden, out int hidden)
|
||||
? (bool?)(hidden > 0)
|
||||
: null
|
||||
}.Build());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
public static class RentalDescriptionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates rental description object from JSON- tariff description object.
|
||||
/// </summary>
|
||||
/// <param name="rentalDesciption">Source JSON object.</param>
|
||||
/// <returns>Tariff description object.</returns>
|
||||
|
||||
public static RentalDescription Create(this Repository.Response.RentalDescription rentalDesciption)
|
||||
{
|
||||
RentalDescription.TariffElement CreateTarifEntry(string[] elementValue) =>
|
||||
new RentalDescription.TariffElement
|
||||
{
|
||||
Description = elementValue != null && elementValue.Length > 0 ? elementValue[0] : string.Empty,
|
||||
Value = elementValue != null && elementValue.Length > 1 ? elementValue[1] : string.Empty,
|
||||
};
|
||||
|
||||
RentalDescription.InfoElement CreateInfoElement(string[] elementValue) =>
|
||||
new RentalDescription.InfoElement
|
||||
{
|
||||
Key = elementValue != null && elementValue.Length > 0 ? elementValue[0] : string.Empty,
|
||||
Value = elementValue != null && elementValue.Length > 1 ? elementValue[1] : string.Empty,
|
||||
};
|
||||
|
||||
// Read tariff elements.
|
||||
var tarifEntries = rentalDesciption?.tarif_elements != null
|
||||
? rentalDesciption.tarif_elements.Select(x => new
|
||||
{
|
||||
x.Key,
|
||||
Value = CreateTarifEntry(x.Value)
|
||||
}).ToLookup(x => x.Key, x => x.Value).ToDictionary(x => x.Key, x => x.First())
|
||||
: new Dictionary<string, RentalDescription.TariffElement>();
|
||||
|
||||
// Read info elements.
|
||||
var InfoEntries = rentalDesciption?.rental_info != null
|
||||
? rentalDesciption.rental_info.Select(x => new
|
||||
{
|
||||
x.Key,
|
||||
Value = CreateInfoElement(x.Value)
|
||||
}).ToLookup(x => x.Key, x => x.Value).ToDictionary(x => x.Key, x => x.First())
|
||||
: new Dictionary<string, RentalDescription.InfoElement>();
|
||||
|
||||
var bike = new RentalDescription
|
||||
{
|
||||
Name = rentalDesciption?.name ?? string.Empty,
|
||||
Id = int.TryParse(rentalDesciption?.id ?? string.Empty, out int number) ? number : (int?)null,
|
||||
MaxReservationTimeSpan = rentalDesciption.GetMaxReservationTimeSpan(),
|
||||
TariffEntries = tarifEntries,
|
||||
InfoEntries = InfoEntries
|
||||
};
|
||||
|
||||
return bike;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
using System.Globalization;
|
||||
using ShareeBike.MultilingualResources;
|
||||
using ShareeBike.Repository.Response;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
public static class TariffDescriptionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates rental description object from JSON- tarif description object.
|
||||
/// </summary>
|
||||
/// <param name="tariffDesciption">Source JSON object.</param>
|
||||
/// <returns>Tariff description object.</returns>
|
||||
public static Bikes.BikeInfoNS.RentalDescription Create(this TariffDescription tariffDesciption)
|
||||
{
|
||||
var bike = new Bikes.BikeInfoNS.RentalDescription
|
||||
{
|
||||
Name = tariffDesciption?.name,
|
||||
#if USCSHARP9
|
||||
Number = int.TryParse(tariffDesciption?.number, out int number) ? number : null,
|
||||
#else
|
||||
Id = int.TryParse(tariffDesciption?.number, out int number) ? number : (int?)null,
|
||||
#endif
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(tariffDesciption?.free_hours)
|
||||
&& double.TryParse(tariffDesciption?.free_hours, NumberStyles.Any, CultureInfo.InvariantCulture, out double freeHours))
|
||||
{
|
||||
// Free time. Unit hours,format floating point number.
|
||||
bike.TariffEntries.Add("1", new Bikes.BikeInfoNS.RentalDescription.TariffElement
|
||||
{
|
||||
Description = AppResources.MessageBikesManagementTariffDescriptionFreeTimePerSession,
|
||||
Value = string.Format("{0} {1}", freeHours.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionHour)
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tariffDesciption?.eur_per_hour)
|
||||
&& double.TryParse(tariffDesciption?.eur_per_hour, NumberStyles.Any, CultureInfo.InvariantCulture, out double euroPerHour))
|
||||
{
|
||||
// Euro per hour. Format floating point.
|
||||
bike.TariffEntries.Add("2", new Bikes.BikeInfoNS.RentalDescription.TariffElement
|
||||
{
|
||||
Description = AppResources.MessageBikesManagementTariffDescriptionFeeEuroPerHour,
|
||||
Value = string.Format("{0} {1}", euroPerHour.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerHour)
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tariffDesciption?.max_eur_per_day)
|
||||
&& double.TryParse(tariffDesciption.max_eur_per_day, NumberStyles.Any, CultureInfo.InvariantCulture, out double maxEuroPerDay))
|
||||
{
|
||||
// Max euro per day. Format floating point.
|
||||
bike.TariffEntries.Add("3", new Bikes.BikeInfoNS.RentalDescription.TariffElement
|
||||
{
|
||||
Description = AppResources.MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay,
|
||||
Value = string.Format("{0} {1}", maxEuroPerDay.ToString("0.00"), AppResources.MessageBikesManagementMaxFeeEuroPerDay)
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tariffDesciption?.abo_eur_per_month)
|
||||
&& double.TryParse(tariffDesciption.abo_eur_per_month, NumberStyles.Any, CultureInfo.InvariantCulture, out double aboEuroPerMonth))
|
||||
{
|
||||
// Abo per month
|
||||
bike.TariffEntries.Add("4", new Bikes.BikeInfoNS.RentalDescription.TariffElement
|
||||
{
|
||||
Description = AppResources.MessageBikesManagementTariffDescriptionAboEuroPerMonth,
|
||||
Value = string.Format("{0} {1}", aboEuroPerMonth.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerMonth)
|
||||
});
|
||||
}
|
||||
|
||||
return bike;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
149
SharedBusinessLogic/Model/Connector/Updater/UpdaterJSON.cs
Normal file
149
SharedBusinessLogic/Model/Connector/Updater/UpdaterJSON.cs
Normal file
|
@ -0,0 +1,149 @@
|
|||
using System;
|
||||
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
|
||||
using ShareeBike.Model.State;
|
||||
using ShareeBike.Model.Stations;
|
||||
using ShareeBike.Model.User.Account;
|
||||
using ShareeBike.Repository.Exception;
|
||||
using ShareeBike.Repository.Response;
|
||||
using ShareeBike.Repository.Response.Stations;
|
||||
using ShareeBike.Services.CopriApi;
|
||||
using IBikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.IBikeInfoMutable;
|
||||
|
||||
namespace ShareeBike.Model.Connector.Updater
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects ShareeBike app to copri using JSON as input data format.
|
||||
/// </summary>
|
||||
/// <todo>Rename to UpdateFromCopri.</todo>
|
||||
public static class UpdaterJSON
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all station for station provider and add them into station list.
|
||||
/// </summary>
|
||||
/// <param name="stationsAllResponse">List of stations to update.</param>
|
||||
public static StationDictionary GetStationsAllMutable(this StationsAvailableResponse stationsAllResponse)
|
||||
{
|
||||
// Get stations from Copri/ file/ memory, ....
|
||||
if (stationsAllResponse == null
|
||||
|| stationsAllResponse.stations == null)
|
||||
{
|
||||
// Latest list of stations could not be retrieved from provider.
|
||||
return new StationDictionary();
|
||||
}
|
||||
|
||||
Version.TryParse(stationsAllResponse.copri_version, out Version copriVersion);
|
||||
|
||||
var stations = new StationDictionary(version: copriVersion);
|
||||
|
||||
foreach (var station in stationsAllResponse.stations)
|
||||
{
|
||||
if (stations.GetById(station.Value.station) != null)
|
||||
{
|
||||
// Can not add station to list of station. Id is not unique.
|
||||
throw new InvalidResponseException<StationsAvailableResponse>(
|
||||
string.Format("Station id {0} is not unique.", station.Value.station), stationsAllResponse);
|
||||
}
|
||||
|
||||
stations.Add(station.Value.GetStation());
|
||||
}
|
||||
|
||||
return stations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets general data from COPRI response.
|
||||
/// </summary>
|
||||
/// <param name="response">Response to get data from.</param>
|
||||
/// <returns>General data object initialized form COPRI response.</returns>
|
||||
public static GeneralData GetGeneralData(this ResponseBase response)
|
||||
=> new GeneralData(
|
||||
response.init_map.GetMapSpan(),
|
||||
response.merchant_message,
|
||||
response.TryGetCopriVersion(out Version copriVersion)
|
||||
? new Version(0, 0)
|
||||
: copriVersion,
|
||||
new ResourceUrls(response.tariff_info_html, response.bike_info_html, response.agb_html, response.privacy_html, response.impress_html));
|
||||
|
||||
/// <summary> Gets account object from login response.</summary>
|
||||
/// <param name="merchantId">Needed to extract cookie from authorization response.</param>
|
||||
/// <param name="loginResponse">Response to get session cookie and debug level from.</param>
|
||||
/// <param name="mail">Mail address needed to construct a complete account object (is not part of response).</param>
|
||||
/// <param name="password">Password needed to construct a complete account object (is not part of response).</param>
|
||||
public static IAccount GetAccount(
|
||||
this AuthorizationResponse loginResponse,
|
||||
string merchantId,
|
||||
string mail,
|
||||
string password)
|
||||
{
|
||||
if (loginResponse == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loginResponse));
|
||||
}
|
||||
|
||||
return new Account(
|
||||
mail,
|
||||
password,
|
||||
loginResponse.GetIsAgbAcknowledged(),
|
||||
loginResponse.authcookie?.Replace(merchantId, ""),
|
||||
loginResponse.GetGroup(),
|
||||
loginResponse.debuglevel == 1
|
||||
? Permissions.All :
|
||||
(Permissions)loginResponse.debuglevel);
|
||||
}
|
||||
|
||||
/// <summary> Load bike object from booking response. </summary>
|
||||
/// <param name="bike">Bike object to load from response.</param>
|
||||
/// <param name="bikeInfo">Booking response.</param>
|
||||
/// <param name="mailAddress">Mail address of user which books bike.</param>
|
||||
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
|
||||
public static void Load(
|
||||
this IBikeInfoMutable bike,
|
||||
BikeInfoReservedOrBooked bikeInfo,
|
||||
string mailAddress,
|
||||
NotifyPropertyChangedLevel notifyLevel = NotifyPropertyChangedLevel.All)
|
||||
{
|
||||
if (bike is Bikes.BikeInfoNS.BluetoothLock.BikeInfoMutable btBikeInfo)
|
||||
{
|
||||
btBikeInfo.LockInfo.Load(
|
||||
bikeInfo.GetBluetoothLockId(),
|
||||
bikeInfo.GetBluetoothLockGuid(),
|
||||
bikeInfo.GetSeed(),
|
||||
bikeInfo.GetUserKey(),
|
||||
bikeInfo.GetAdminKey());
|
||||
}
|
||||
|
||||
var l_oState = bikeInfo.GetState();
|
||||
switch (l_oState)
|
||||
{
|
||||
case InUseStateEnum.Disposable:
|
||||
bike.State.Load(
|
||||
InUseStateEnum.Disposable,
|
||||
notifyLevel: notifyLevel);
|
||||
break;
|
||||
|
||||
case InUseStateEnum.Reserved:
|
||||
bike.State.Load(
|
||||
InUseStateEnum.Reserved,
|
||||
bikeInfo.GetFrom(),
|
||||
bikeInfo.rental_description.GetMaxReservationTimeSpan(),
|
||||
mailAddress,
|
||||
bikeInfo.timeCode,
|
||||
notifyLevel);
|
||||
break;
|
||||
|
||||
case InUseStateEnum.Booked:
|
||||
bike.State.Load(
|
||||
InUseStateEnum.Booked,
|
||||
bikeInfo.GetFrom(),
|
||||
mailAddress: mailAddress,
|
||||
code: bikeInfo.timeCode,
|
||||
notifyLevel: notifyLevel);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception(string.Format("Unexpected bike state detected. state is {0}.", l_oState));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
9
SharedBusinessLogic/Model/CurrentAppInfos.cs
Normal file
9
SharedBusinessLogic/Model/CurrentAppInfos.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using Xamarin.Essentials;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
public static class CurrentAppInfos
|
||||
{
|
||||
public static string CurrentAppVersion => VersionTracking.CurrentVersion;
|
||||
}
|
||||
}
|
14
SharedBusinessLogic/Model/Device/IAppInfo.cs
Normal file
14
SharedBusinessLogic/Model/Device/IAppInfo.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
/// <summary> Interface to get version info. </summary>
|
||||
public interface IAppInfo
|
||||
{
|
||||
/// <summary> Gets the app version to display to user.</summary>
|
||||
Version Version { get; }
|
||||
|
||||
/// <summary> Gets the URL to the app store. </summary>
|
||||
/// <value>The store URL.</value>
|
||||
string StoreUrl { get; }
|
||||
}
|
||||
}
|
9
SharedBusinessLogic/Model/Device/IDevice.cs
Normal file
9
SharedBusinessLogic/Model/Device/IDevice.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
public interface IDevice
|
||||
{
|
||||
/// <summary> Gets unitque device identifier. </summary>
|
||||
/// <returns>Gets the identifies specifying device.</returns>
|
||||
string GetIdentifier();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
public interface IExternalBrowserService
|
||||
{
|
||||
/// <summary> Opens an external browser. </summary>
|
||||
/// <param name="url">Url to open.</param>
|
||||
void OpenUrl(string url);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
public interface IGeolodationDependent
|
||||
{
|
||||
bool IsGeolcationEnabled { get; }
|
||||
}
|
||||
}
|
23
SharedBusinessLogic/Model/Device/ISmartDevice.cs
Normal file
23
SharedBusinessLogic/Model/Device/ISmartDevice.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using Xamarin.Essentials;
|
||||
|
||||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
public interface ISmartDevice
|
||||
{
|
||||
/// <summary> Gets unitque device identifier. </summary>
|
||||
/// <returns>Gets the identifies specifying device.</returns>
|
||||
string Identifier { get; }
|
||||
|
||||
/// <summary> Manufacturer (Samsung). </summary>
|
||||
string Manufacturer { get; }
|
||||
|
||||
/// <summary> Device Model (SMG-950U, iPhone10,6). </summary>
|
||||
string Model { get; }
|
||||
|
||||
/// <summary> Operation system. </summary>
|
||||
DevicePlatform Platform { get; }
|
||||
|
||||
/// <summary> Operating System Version Number (7.0) as text</summary>
|
||||
string VersionText { get; }
|
||||
}
|
||||
}
|
15
SharedBusinessLogic/Model/Device/ISpecialFolder.cs
Normal file
15
SharedBusinessLogic/Model/Device/ISpecialFolder.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
public interface ISpecialFolder
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the folder name of external folder to write to.
|
||||
/// </summary>
|
||||
/// <returns>External directory.</returns>
|
||||
string GetExternalFilesDir();
|
||||
|
||||
/// <summary> Gets the folder name of the personal data folder dir on internal storage. </summary>
|
||||
/// <returns>Directory name.</returns>
|
||||
string GetInternalPersonalDir();
|
||||
}
|
||||
}
|
8
SharedBusinessLogic/Model/Device/IWebView.cs
Normal file
8
SharedBusinessLogic/Model/Device/IWebView.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace ShareeBike.Model.Device
|
||||
{
|
||||
public interface IWebView
|
||||
{
|
||||
/// <summary> Clears the cookie cache for all web views. </summary>
|
||||
void ClearCookies();
|
||||
}
|
||||
}
|
34
SharedBusinessLogic/Model/EnumExtensions.cs
Normal file
34
SharedBusinessLogic/Model/EnumExtensions.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
public static class EnumExtensions
|
||||
{
|
||||
public static string GetDisplayName(this Enum enu)
|
||||
{
|
||||
var attr = GetDisplayAttribute(enu);
|
||||
return attr != null ? attr.Name : enu.ToString();
|
||||
}
|
||||
|
||||
public static string GetDescription(this Enum enu)
|
||||
{
|
||||
var attr = GetDisplayAttribute(enu);
|
||||
return attr != null ? attr.Description : enu.ToString();
|
||||
}
|
||||
|
||||
private static DisplayAttribute GetDisplayAttribute(object value)
|
||||
{
|
||||
Type type = value?.GetType() ?? typeof(object);
|
||||
if (!type.IsEnum)
|
||||
{
|
||||
throw new ArgumentException(string.Format("Type {0} is not an enum", type));
|
||||
}
|
||||
|
||||
// Get the enum field.
|
||||
var field = type.GetField(value.ToString());
|
||||
return field == null ? null : field.GetCustomAttribute<DisplayAttribute>();
|
||||
}
|
||||
}
|
||||
}
|
11
SharedBusinessLogic/Model/FileOperationException.cs
Normal file
11
SharedBusinessLogic/Model/FileOperationException.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
/// <summary> Operation fired when a file operation fails.</summary>
|
||||
public class FileOperationException : Exception
|
||||
{
|
||||
public FileOperationException(string message, Exception innerException) : base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
}
|
13
SharedBusinessLogic/Model/FilterCollectionStore.cs
Normal file
13
SharedBusinessLogic/Model/FilterCollectionStore.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
public static class FilterCollectionStore
|
||||
{
|
||||
/// <summary> Writes filter collection state to string. </summary>
|
||||
/// <returns> Filter collection to write to string.</returns>
|
||||
/// <param name="p_oDictionary">P o dictionary.</param>
|
||||
public static string ToString(this IDictionary<string, FilterState> p_oDictionary) => "{" + string.Join(", ", p_oDictionary.Select(x => "(" + x.Key + "= " + x.Value + ")")) + "}";
|
||||
}
|
||||
}
|
45
SharedBusinessLogic/Model/GroupFilterHelper.cs
Normal file
45
SharedBusinessLogic/Model/GroupFilterHelper.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System.Collections.Generic;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.ViewModel.Map;
|
||||
using ShareeBike.ViewModel.Settings;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
/// <summary> Holds collection of filters to filter options (Cargo, Citybike, ....). </summary>
|
||||
/// <remarks> Former name: FilterCollection.</remarks>
|
||||
public static class GroupFilterHelper
|
||||
{
|
||||
/// <summary> Gets default filter set.</summary>
|
||||
public static IGroupFilterSettings GetSettingsFilterDefaults
|
||||
{
|
||||
get
|
||||
{
|
||||
return new GroupFilterSettings(new Dictionary<string, FilterState> {
|
||||
{ FilterHelper.CARGOBIKE, FilterState.On },
|
||||
{FilterHelper.CITYBIKE, FilterState.On }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets default filter set.</summary>
|
||||
public static IGroupFilterMapPage GetMapPageFilterDefaults
|
||||
{
|
||||
get
|
||||
{
|
||||
return new GroupFilterMapPage(new Dictionary<string, FilterState> {
|
||||
{ FilterHelper.CARGOBIKE, FilterState.On },
|
||||
{FilterHelper.CITYBIKE, FilterState.Off }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Holds value whether filter (on Cargo, Citybike, ....) is on or off. </summary>
|
||||
public enum FilterState
|
||||
{
|
||||
/// <summary> Option (Cargo, Citybike, ....) is available.</summary>
|
||||
On,
|
||||
/// <summary> Option is off</summary>
|
||||
Off,
|
||||
}
|
||||
}
|
22
SharedBusinessLogic/Model/IBookingFinishedModel.cs
Normal file
22
SharedBusinessLogic/Model/IBookingFinishedModel.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using ShareeBike.Model.MiniSurvey;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
public interface IBookingFinishedModel
|
||||
{
|
||||
/// <summary> Minisurvey to query user.</summary>
|
||||
IMiniSurveyModel MiniSurvey { get; set; }
|
||||
|
||||
/// <summary> Holds info about co2 saving accomplished by using cargo bike. </summary>
|
||||
string Co2Saving { get; set; }
|
||||
|
||||
/// <summary> Holds info about driven distance. </summary>
|
||||
string Distance { get; set; }
|
||||
|
||||
/// <summary> Holds info about rental duration. </summary>
|
||||
string Duration { get; set; }
|
||||
|
||||
/// <summary> Holds info about accruing rental costs. </summary>
|
||||
string RentalCosts { get; set; }
|
||||
}
|
||||
}
|
16
SharedBusinessLogic/Model/IPosition.cs
Normal file
16
SharedBusinessLogic/Model/IPosition.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds a exact position.
|
||||
/// </summary>
|
||||
public interface IPosition : IEquatable<IPosition>
|
||||
{
|
||||
double Latitude { get; }
|
||||
|
||||
double Longitude { get; }
|
||||
|
||||
bool IsValid { get; }
|
||||
}
|
||||
}
|
133
SharedBusinessLogic/Model/IShareeBikeApp.cs
Normal file
133
SharedBusinessLogic/Model/IShareeBikeApp.cs
Normal file
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
using Serilog.Events;
|
||||
using ShareeBike.Model.Connector;
|
||||
using ShareeBike.Model.Device;
|
||||
using ShareeBike.Model.Services.CopriApi.ServerUris;
|
||||
using ShareeBike.Model.Settings;
|
||||
using ShareeBike.Model.Stations.StationNS;
|
||||
using ShareeBike.Services;
|
||||
using ShareeBike.Services.BluetoothLock;
|
||||
using ShareeBike.Settings;
|
||||
using ShareeBike.ViewModel.Map;
|
||||
using ShareeBike.ViewModel.Settings;
|
||||
|
||||
namespace ShareeBike.Model
|
||||
{
|
||||
public enum AppFlavor
|
||||
{
|
||||
[Display(Name = "sharee.bike")]
|
||||
ShareeBike,
|
||||
[Display(Name = "Lastenrad Bayern")]
|
||||
LastenradBayern,
|
||||
}
|
||||
|
||||
public interface IShareeBikeApp
|
||||
{
|
||||
/// <summary> Update connector from depending on whether user is logged in or not.</summary>
|
||||
void UpdateConnector();
|
||||
|
||||
/// <summary> Saves object to file. </summary>
|
||||
void Save();
|
||||
|
||||
/// <summary> Holds the filter which is applied on the map view. Either Cargo or Citybike stations are displayed. </summary>
|
||||
IGroupFilterMapPage GroupFilterMapPage { get; set; }
|
||||
|
||||
/// <summary> Holds the user of the app. </summary>
|
||||
User.User ActiveUser { get; }
|
||||
|
||||
/// <summary> Holds the system to copri.</summary>
|
||||
IFilteredConnector GetConnector(bool isConnected);
|
||||
|
||||
/// <summary> Name of the station which is selected. </summary>
|
||||
IStation SelectedStation { get; set; }
|
||||
|
||||
/// <summary>Polling period.</summary>
|
||||
PollingParameters Polling { get; set; }
|
||||
|
||||
TimeSpan ExpiresAfter { get; set; }
|
||||
|
||||
/// <summary> Holds status about whats new page. </summary>
|
||||
WhatsNew WhatsNew { get; }
|
||||
|
||||
/// <summary> Gets whether device is connected to Internet or not. </summary>
|
||||
bool GetIsConnected();
|
||||
|
||||
/// <summary> Action to post to GUI thread.</summary>
|
||||
Action<SendOrPostCallback, object> PostAction { get; }
|
||||
|
||||
/// <summary> Holds the uri which is applied after restart. </summary>
|
||||
Uri NextActiveUri { get; set; }
|
||||
|
||||
/// <summary> Holds the filters loaded from settings. </summary>
|
||||
IGroupFilterSettings FilterGroupSetting { get; set; }
|
||||
|
||||
/// <summary>Settings determining the startup behavior of the app.</summary>
|
||||
IStartupSettings StartupSettings { get; }
|
||||
|
||||
/// <summary> Value indicating whether map is centered to current position or not. </summary>
|
||||
bool CenterMapToCurrentLocation { get; set; }
|
||||
|
||||
/// <summary> Holds the map area where user is or was located or null if position is unknown. </summary>
|
||||
Xamarin.Forms.GoogleMaps.MapSpan UserMapSpan { get; set; }
|
||||
|
||||
/// <summary> Holds the map span to display either default span or span centered to current position depending on option <see cref="CenterMapToCurrentLocation"/>.</summary>
|
||||
Xamarin.Forms.GoogleMaps.MapSpan ActiveMapSpan { get; }
|
||||
|
||||
bool LogToExternalFolder { get; set; }
|
||||
|
||||
bool IsSiteCachingOn { get; set; }
|
||||
|
||||
/// <summary> Gets the minimum logging level. </summary>
|
||||
LogEventLevel MinimumLogEventLevel { get; set; }
|
||||
|
||||
/// <summary> Gets a value indicating whether reporting level is verbose or not.</summary>
|
||||
bool IsReportLevelVerbose { get; set; }
|
||||
|
||||
/// <summary> Updates logging level. </summary>
|
||||
/// <param name="newLogLevel">New level to set.</param>
|
||||
void UpdateLoggingLevel(LogEventLevel newLogLevel);
|
||||
|
||||
/// <summary>Holds uris of copri servers. </summary>
|
||||
CopriServerUriList Uris { get; }
|
||||
|
||||
/// <summary> Holds the different lock service implementations.</summary>
|
||||
LocksServicesContainerMutable LocksServices { get; }
|
||||
|
||||
/// <summary> Holds the flavor of the app, i.e. specifies if app is sharee.bike or LastenRad Bayern.</summary>
|
||||
AppFlavor Flavor { get; }
|
||||
|
||||
/// <summary> Holds available app themes.</summary>
|
||||
ServicesContainerMutable Themes { get; }
|
||||
|
||||
/// <summary> Reference of object which provides device information. </summary>
|
||||
ISmartDevice SmartDevice { get; }
|
||||
|
||||
/// <summary> Holds the folder where settings files are stored. </summary>
|
||||
string SettingsFileFolder { get; }
|
||||
|
||||
/// <summary> Holds the external path. </summary>
|
||||
string ExternalFolder { get; }
|
||||
|
||||
/// <summary> Holds the stations centered. </summary>
|
||||
IEnumerable<IStation> Stations { get; set; }
|
||||
|
||||
/// <summary> Holds the Urs to query resources from. </summary>
|
||||
IResourceUrls ResourceUrls { get; set; }
|
||||
}
|
||||
|
||||
public interface IResourceUrls
|
||||
{
|
||||
string TariffsResourcePath { get; }
|
||||
|
||||
string ManualResourcePath { get; }
|
||||
|
||||
string GtcResourcePath { get; }
|
||||
|
||||
string PrivacyResourcePath { get; }
|
||||
|
||||
string ImpressResourcePath { get; }
|
||||
}
|
||||
}
|
16
SharedBusinessLogic/Model/Logging/AppAndEnvironmentInfo.cs
Normal file
16
SharedBusinessLogic/Model/Logging/AppAndEnvironmentInfo.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using Serilog;
|
||||
using ShareeBike.Model.Device;
|
||||
|
||||
namespace ShareeBike.Model.Logging
|
||||
{
|
||||
public class AppAndEnvironmentInfo
|
||||
{
|
||||
public void LogHeader(ISmartDevice device, AppFlavor appFlavor, Version appVersion)
|
||||
{
|
||||
Log.ForContext<AppAndEnvironmentInfo>().Information($"App: {appFlavor.GetDisplayName()}, version {appVersion}");
|
||||
Log.ForContext<AppAndEnvironmentInfo>().Information($"OS: {device.Platform}, version: {device.VersionText}");
|
||||
Log.ForContext<AppAndEnvironmentInfo>().Information($"Device: {device.Model}, manufacturer: {device.Manufacturer}");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Logging
|
||||
{
|
||||
public class EmptyDirectoryLoggingManger : ILoggingDirectoryManager
|
||||
{
|
||||
public void DeleteObsoleteLogs() { }
|
||||
|
||||
/// <summary> Gets the log file name. </summary>
|
||||
public string LogFileName { get { return string.Empty; } }
|
||||
|
||||
/// <summary> Gets all log files in logging directory. </summary>
|
||||
/// <returns>List of log files.</returns>
|
||||
public IList<string> GetLogFiles() { return new List<string>(); }
|
||||
|
||||
/// <summary> Gets path where log files are located. </summary>
|
||||
/// <returns>Path to log files.</returns>
|
||||
public string LogFilePath { get { return string.Empty; } }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace ShareeBike.Model.Logging
|
||||
{
|
||||
public interface ILoggingDirectoryManager
|
||||
{
|
||||
/// <summary> Deletes files which are out of retainment scope. </summary>
|
||||
void DeleteObsoleteLogs();
|
||||
|
||||
/// <summary> Gets the log file name. </summary>
|
||||
string LogFileName { get; }
|
||||
|
||||
/// <summary> Gets all log files in logging directory. </summary>
|
||||
/// <returns>List of log files.</returns>
|
||||
IList<string> GetLogFiles();
|
||||
|
||||
/// <summary> Gets path where log files are located. </summary>
|
||||
/// <returns>Path to log files.</returns>
|
||||
string LogFilePath { get; }
|
||||
}
|
||||
}
|
35
SharedBusinessLogic/Model/Logging/LogEntryClassifyHelper.cs
Normal file
35
SharedBusinessLogic/Model/Logging/LogEntryClassifyHelper.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using Serilog;
|
||||
using ShareeBike.Repository.Exception;
|
||||
|
||||
namespace ShareeBike.Model.Logging
|
||||
{
|
||||
public static class LogEntryClassifyHelper
|
||||
{
|
||||
/// <summary> Classifies exception and logs information or error depending on result of classification. </summary>
|
||||
/// <typeparam name="T0">Type of first message parameter.</typeparam>
|
||||
/// <typeparam name="T1">Type of second message parameter.</typeparam>
|
||||
/// <param name="p_oLogger">Object to use for logging.</param>
|
||||
/// <param name="messageTemplate">Templated used to output message.</param>
|
||||
/// <param name="propertyValue0">First message parameter.</param>
|
||||
/// <param name="propertyValue1">Second message parameter.</param>
|
||||
/// <param name="p_oException">Exception to classify.</param>
|
||||
public static void InformationOrError<T0, T1>(this ILogger p_oLogger, string messageTemplate, T0 propertyValue0, T1 propertyValue1, Exception p_oException)
|
||||
{
|
||||
if (p_oException == null)
|
||||
{
|
||||
p_oLogger.Error(messageTemplate, propertyValue0, propertyValue1, string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p_oException.GetIsConnectFailureException())
|
||||
{
|
||||
// Expected exception (LAN or mobile data off/ not reachable, proxy, ...)
|
||||
p_oLogger.Information(messageTemplate, propertyValue0, propertyValue1, p_oException);
|
||||
return;
|
||||
}
|
||||
|
||||
p_oLogger.Error(messageTemplate, propertyValue0, propertyValue1, p_oException);
|
||||
}
|
||||
}
|
||||
}
|
110
SharedBusinessLogic/Model/Logging/LoggerConfigurationHelper.cs
Normal file
110
SharedBusinessLogic/Model/Logging/LoggerConfigurationHelper.cs
Normal file
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Serilog;
|
||||
using Serilog.Configuration;
|
||||
using Serilog.Formatting.Json;
|
||||
|
||||
namespace ShareeBike.Model.Logging
|
||||
{
|
||||
/// <summary> Holds new logging levels. </summary>
|
||||
public enum RollingInterval
|
||||
{
|
||||
/// <summary> Create a new log file for each session (start of app).</summary>
|
||||
Session,
|
||||
}
|
||||
|
||||
/// <summary> Provides logging file name helper functionality.</summary>
|
||||
public static class LoggerConfigurationHelper
|
||||
{
|
||||
// Max count of retained logs count.
|
||||
private const int RETAINEDFILECOUNT = 10;
|
||||
|
||||
// Max size of logging file.
|
||||
private const int MAXLOGFILESSIZEBYTESBYTES = 1024 * 1024 * 8; // 8MB
|
||||
|
||||
/// <summary> Holds the log file name. </summary>
|
||||
private static ILoggingDirectoryManager DirectoryManager { get; set; } = new EmptyDirectoryLoggingManger();
|
||||
|
||||
/// <summary> Sets up logging to file.</summary>
|
||||
/// <param name="loggerConfiguration">Object to set up logging with.</param>
|
||||
/// <param name="p_oDevice">Object to get file information from.</param>
|
||||
/// <param name="rollingInterval">Specifies rolling type.</param>
|
||||
/// <param name="retainedFilesCountLimit">Count of file being retained.</param>
|
||||
/// <returns>Logger object.</returns>
|
||||
public static LoggerConfiguration File(
|
||||
this LoggerSinkConfiguration loggerConfiguration,
|
||||
string logFileFolder,
|
||||
RollingInterval rollingInterval = RollingInterval.Session,
|
||||
int retainedFilesCountLimit = RETAINEDFILECOUNT)
|
||||
{
|
||||
if (DirectoryManager is EmptyDirectoryLoggingManger)
|
||||
{
|
||||
// Roll file only once per app session.
|
||||
try
|
||||
{
|
||||
DirectoryManager = new LoggingDirectoryManager(
|
||||
Directory.GetFiles,
|
||||
Directory.Exists,
|
||||
(path) => Directory.CreateDirectory(path),
|
||||
System.IO.File.Delete,
|
||||
logFileFolder,
|
||||
Path.DirectorySeparatorChar,
|
||||
retainedFilesCountLimit);
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
Log.Error("Log directory manager could not be instantiated successfully. {@l_oException}", l_oException);
|
||||
DirectoryManager = new EmptyDirectoryLoggingManger();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DirectoryManager.DeleteObsoleteLogs();
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
Log.Error("Not all obsolete log files could be deleted successfully. {@l_oException}", l_oException);
|
||||
}
|
||||
|
||||
if (loggerConfiguration == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return loggerConfiguration.File(
|
||||
new JsonFormatter(),
|
||||
DirectoryManager.LogFileName,
|
||||
fileSizeLimitBytes: MAXLOGFILESSIZEBYTESBYTES / RETAINEDFILECOUNT,
|
||||
/*shared: true, // Leads to exception if activated.*/
|
||||
rollingInterval: Serilog.RollingInterval.Infinite,
|
||||
retainedFileCountLimit: retainedFilesCountLimit);
|
||||
}
|
||||
|
||||
/// <summary> Gets all log files in logging directory. </summary>
|
||||
/// <param name="p_oLogger"></param>
|
||||
/// <returns>List of log files.</returns>
|
||||
public static IList<string> GetLogFiles(this ILogger p_oLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DirectoryManager.GetLogFiles();
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
Log.Error("Getting list of log files failed. Empty list is returned instead. {@l_oException}", l_oException);
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets path where log files are located. </summary>
|
||||
/// <param name="p_oLogger"></param>
|
||||
/// <returns>List of log files.</returns>
|
||||
public static string GetLogFilePath(
|
||||
this ILogger p_oLogger)
|
||||
{
|
||||
return DirectoryManager.LogFilePath;
|
||||
}
|
||||
}
|
||||
}
|
146
SharedBusinessLogic/Model/Logging/LoggingDirectoryManager.cs
Normal file
146
SharedBusinessLogic/Model/Logging/LoggingDirectoryManager.cs
Normal file
|
@ -0,0 +1,146 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShareeBike.Model.Logging
|
||||
{
|
||||
public class LoggingDirectoryManager : ILoggingDirectoryManager
|
||||
{
|
||||
/// <summary> Name of logging subdirectory.</summary>
|
||||
private const string LOGDIRECTORYTITLE = "Log";
|
||||
|
||||
/// <summary> Prevents an invalid instance to be created. </summary>
|
||||
private LoggingDirectoryManager() { }
|
||||
|
||||
public LoggingDirectoryManager(
|
||||
Func<string, IList<string>> fileListProvider,
|
||||
Func<string, bool> directoryExistsChecker,
|
||||
Action<string> directoryCreator,
|
||||
Action<string> fileEraser,
|
||||
string logFilePath,
|
||||
char directorySeparatorChar,
|
||||
int p_iRetainedFilesCountLimit)
|
||||
{
|
||||
m_oFileListProvider = fileListProvider ?? throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. File list provider delegate can not be null.");
|
||||
|
||||
if (directoryExistsChecker == null)
|
||||
{
|
||||
throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Directory existence checker delegate can not be null.");
|
||||
}
|
||||
|
||||
if (directoryCreator == null)
|
||||
{
|
||||
throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Directory creator delegate can not be null.");
|
||||
}
|
||||
|
||||
m_oFileEraser = fileEraser ?? throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. File eraser delegate can not be null.");
|
||||
|
||||
if (string.IsNullOrEmpty(logFilePath))
|
||||
{
|
||||
throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Log file path can not be null or empty.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(directorySeparatorChar.ToString()))
|
||||
{
|
||||
throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Directory separator character can not be null or empty.");
|
||||
}
|
||||
|
||||
if (p_iRetainedFilesCountLimit < 1)
|
||||
{
|
||||
throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Count of retained log files is {p_iRetainedFilesCountLimit} but must be equal or larger one.");
|
||||
}
|
||||
|
||||
DirectorySeparatorChar = directorySeparatorChar.ToString();
|
||||
|
||||
LogFilePath = $"{logFilePath}{DirectorySeparatorChar}{LOGDIRECTORYTITLE}";
|
||||
|
||||
|
||||
m_iRetainedFilesCountLimit = p_iRetainedFilesCountLimit;
|
||||
|
||||
if (string.IsNullOrEmpty(LogFileTitle))
|
||||
{
|
||||
LogFileTitle = $"{DateTime.Now:yyyy_MM_dd_HH_mm_ss}.jsnl";
|
||||
}
|
||||
|
||||
// Create directory if direcotry does not exist.
|
||||
if (directoryExistsChecker(LogFilePath) == false)
|
||||
{
|
||||
try
|
||||
{
|
||||
directoryCreator(LogFilePath);
|
||||
}
|
||||
catch (Exception l_oException)
|
||||
{
|
||||
|
||||
throw new FileOperationException($"Logging directory {LogFilePath} could not be created successfully.", l_oException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Deletes files which are out of retainment scope. </summary>
|
||||
public void DeleteObsoleteLogs()
|
||||
{
|
||||
var l_oExceptions = new List<Exception>();
|
||||
var l_oSortedFileArray = m_oFileListProvider(LogFilePath).OrderByDescending(x => x).ToArray();
|
||||
|
||||
for (int l_iIndex = l_oSortedFileArray.Length - 1;
|
||||
l_iIndex >= m_iRetainedFilesCountLimit - 1; /* files remaining count must be m_iRetainedFilesCountLimit - 1 because new log file will be added afterwards */
|
||||
l_iIndex--)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_oFileEraser(l_oSortedFileArray[l_iIndex]);
|
||||
}
|
||||
catch (Exception l_oExpetion)
|
||||
{
|
||||
// Getting list of log files found.
|
||||
l_oExceptions.Add(l_oExpetion);
|
||||
}
|
||||
}
|
||||
|
||||
if (l_oExceptions.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AggregateException("Deleting obsolete log files failed.", l_oExceptions.ToArray());
|
||||
}
|
||||
|
||||
/// <summary> Gets all log files in logging directory. </summary>
|
||||
/// <param name="p_oLogger"></param>
|
||||
/// <returns>List of log files.</returns>
|
||||
public IList<string> GetLogFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
return m_oFileListProvider(LogFilePath).OrderBy(x => x).ToList();
|
||||
}
|
||||
catch (Exception l_oExpetion)
|
||||
{
|
||||
// Getting list of log files found.
|
||||
throw new FileOperationException("Getting list of log files failed.", l_oExpetion);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Holds delegate to provide file names.</summary>
|
||||
private readonly Func<string, IList<string>> m_oFileListProvider;
|
||||
|
||||
/// <summary>Holds delegate to delete files.</summary>
|
||||
private readonly Action<string> m_oFileEraser;
|
||||
|
||||
/// <summary>Holds delegate to provide file names.</summary>
|
||||
private int m_iRetainedFilesCountLimit;
|
||||
|
||||
/// <summary> Holds the log file name. </summary>
|
||||
private string LogFileTitle { get; }
|
||||
|
||||
/// <summary> Holds the log file name. </summary>
|
||||
public string LogFilePath { get; }
|
||||
|
||||
/// <summary> Holds the directory separator character. </summary>
|
||||
private string DirectorySeparatorChar { get; }
|
||||
|
||||
/// <summary> Holds the log file name. </summary>
|
||||
public string LogFileName { get { return $"{LogFilePath}{DirectorySeparatorChar}{LogFileTitle}"; } }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue