19 KiB
19 KiB
DataWrapper.cs
/// Author : Maxime Rohmer
/// Date : 30/05/2023
/// File : DataWrapper.cs
/// Brief : Class that is used to interface between the main Form (vue) and the Storage (wich is a class that wraps the sqlite database, so the model) its almost MVC :D
/// Version : Alpha 1.0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;
namespace TrackTrends
{
internal class DataWrapper
{
private Reader Reader;
private SqliteStorage Storage;
List<List<DriverData>> LiveDriverDataLogs = new List<List<DriverData>>();
//Note : It could be usefull to get the mainForm at the start of the programm and not have to take it in half of the methods.
/// <summary>
/// Constructs a new DataWrapper. It needs the config file so it can create a Reader, It also needs a first screenshot for the same reason
/// </summary>
/// <param name="configFile">The JSON config file that is created by the configuration tool</param>
/// <param name="screenshot">A screenshot of the </param>
public DataWrapper(string configFile, Bitmap screenshot)
{
Reader = new Reader(configFile, screenshot, true);
//The Storage is here and on the Reader. It seems bad but it is ok as we dont use it at all to insert data and are only using it here to read some. The reader takes care of the inserts (Note: We could technically do both here but I did not find it usefull to transfer everything here)
Storage = Reader.Storage;
}
/// <summary>
/// Refreshes the controller so it has the latest driver datas (Be sure to call it everytime you need to use any other method and expects the data to be up to date)
/// </summary>
/// <returns>Error code, 0 is success, 1 is not (Note: Maybe it could be interesting in the future to add some more error handling here)</returns>
public int Refresh()
{
LiveDriverDataLogs.Add(Reader.Decode(Reader.MainZones, Reader.Drivers));
if (LiveDriverDataLogs.Count > 0)
return 0;
return 1;
}
/// <summary>
/// Changes the image to the newest screenshot in all of the zones and windows
/// </summary>
/// <param name="image">The new screenshot to put everywhere (Do not mix resolutions)</param>
public void ChangeImage(Bitmap image)
{
Reader.ChangeImage(image);
}
/// <summary>
/// Gets all the data from one driver and also displays into the given panel the last five laps (or less if its the sart of the race) Note: Its responsive :D
/// </summary>
/// <param name="driverName">The name of the driver (should not be case sensitive but it MUST already exist in the first list that has been inserted into the DB)</param>
/// <param name="lastFiveLapsPanel">The pannel where you want the five last laps to be displayed</param>
/// <param name="form1">The Main form.</param>
/// <returns></returns>
public DriverData GetFullDriverData(string driverName, Panel lastFiveLapsPanel, Main form1)
{
//Note : I know that its a bad idea to ask the Form in this method and some others because it means that it wont work with any main form. And to that Ill say that... your right !
DriverData result = new DriverData();
if (LiveDriverDataLogs.Count > 0)
{
//Searches the most recent live data from the given driverName
foreach (DriverData data in LiveDriverDataLogs[LiveDriverDataLogs.Count - 1])
{
if (data.Name == driverName)
result = data;
}
if (result.Name != "")
{
//Recovers and displays the last five laps from the driver
lastFiveLapsPanel.Controls.Clear();
Size labelDimensions = new Size(lastFiveLapsPanel.Width, lastFiveLapsPanel.Height / 5);
List<(int LapTime, int Lap)> lapsInfos = Storage.GetDriverLaptimes(driverName, 5);
int id = 0;
foreach ((int LapTime, int Lap) lapData in lapsInfos)
{
//Hardcodes the new button.
//Note : It could be smart to have like a default button for all the methods to use without needing to rewrite everything.
Button newButton = new Button();
lastFiveLapsPanel.Controls.Add(newButton);
newButton.Name = driverName + "_" + lapData.Lap;
newButton.Text = Reader.ConvertMsToTime(lapData.LapTime);
newButton.Size = labelDimensions;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Click += form1.btnLapTime_Click;
newButton.Location = new Point(0, id * newButton.Height);
id++;
}
}
}
return result;
}
/// <summary>
/// Runs trough every drivers live data to recover the drivers that are close to each others
/// </summary>
/// <param name="pnlBattles">The control that will host the displayed battles</param>
/// <param name="form1">The main form. It needs to have a method called 'btnDriver_Click' so it can reads the buttons clicks</param>
public void DisplayBattles(Panel pnlBattles,Main form1)
{
DriverData oldDriver = null;
List<(DriverData d1, DriverData d2, int gap)> battles = new List<(DriverData d1, DriverData d2, int gap)>();
//Search trough all the drivers and finds the one battling
foreach (DriverData driver in LiveDriverDataLogs[LiveDriverDataLogs.Count - 1])
{
if (oldDriver != null && driver.Position != -1 && oldDriver.Position != -1)
{
if (driver.GapToLeader < oldDriver.GapToLeader)
{
//There is a problem with the drivers gaps
}
else
{
int gap = driver.GapToLeader - oldDriver.GapToLeader;
//3000ms is 3s. If drivers are that close then they are definitely in battle. If they are farther then maybe not
if (gap <= 3000)
{
battles.Add((oldDriver, driver, gap));
}
}
oldDriver = driver;
}
else
{
oldDriver = driver;
}
}
//We will only display 4 battles max
int maxBattles = 4;
if (battles.Count > 0)
{
pnlBattles.Controls.Clear();
int maxUiHeight = Math.Max(pnlBattles.Height / maxBattles, pnlBattles.Height / battles.Count);
int id = 0;
foreach ((DriverData d1, DriverData d2, int gap) battle in battles)
{
if(id < maxBattles)
{
//*hardcoding* the different controls that needs to be added to the panel.
//Note : this stuff could totally be handled by the Form with method returning a list of the drivers. It was just easier for me at the time to code it this way but its not the prettiest
Button btnFirstDriver = new Button();
Button btnSecondDriver = new Button();
Label lblGap = new Label();
pnlBattles.Controls.Add(btnFirstDriver);
pnlBattles.Controls.Add(lblGap);
pnlBattles.Controls.Add(btnSecondDriver);
btnFirstDriver.Anchor = AnchorStyles.Left | AnchorStyles.Top;
btnSecondDriver.Anchor = AnchorStyles.Right | AnchorStyles.Top;
lblGap.Anchor = AnchorStyles.Right | AnchorStyles.Left | AnchorStyles.Top;
lblGap.TextAlign = ContentAlignment.MiddleCenter;
lblGap.Font = new Font(lblGap.Font.FontFamily, 15);
btnFirstDriver.Click += form1.btnDriver_Click;
btnSecondDriver.Click += form1.btnDriver_Click;
btnFirstDriver.FlatStyle = FlatStyle.Popup;
btnSecondDriver.FlatStyle = FlatStyle.Popup;
lblGap.FlatStyle = FlatStyle.Popup;
btnFirstDriver.Size = new Size(pnlBattles.Width / 3, maxUiHeight);
btnSecondDriver.Size = new Size(pnlBattles.Width / 3, maxUiHeight);
lblGap.Size = new Size(pnlBattles.Width / 3, maxUiHeight);
btnFirstDriver.Location = new Point(pnlBattles.Width / 3 * 0, id * maxUiHeight);
lblGap.Location = new Point(pnlBattles.Width / 3 * 1, id * maxUiHeight);
btnSecondDriver.Location = new Point(pnlBattles.Width / 3 * 2, id * maxUiHeight);
btnFirstDriver.Text = battle.d1.Name;
lblGap.Text = "+ " + Reader.ConvertMsToTime(battle.gap);
if (battle.gap <= 2000)
lblGap.ForeColor = Color.Yellow;
if (battle.gap <= 1000)
lblGap.ForeColor = Color.Green;
btnSecondDriver.Text = battle.d2.Name;
btnFirstDriver.Name = battle.d1.Name + "_" + id;
lblGap.Name = "lbl_Gap_" + id;
btnSecondDriver.Name = battle.d2.Name + "_" + id;
}
else
{
break;
}
id++;
}
}
}
/// <summary>
/// Searches the fastest and slowests drivers and displays them in the given panels
/// </summary>
/// <param name="pnlFastest">Panel that will contain the constructed controls</param>
/// <param name="pnlSlowest">Panel that will contain the constructed controls</param>
/// <param name="form1">The main form that needs to implement the method btnDriver_Click to allow it to recover custom buttons click</param>
public void DisplayTimesDeltas(Panel pnlFastest,Panel pnlSlowest, Main form1)
{
List<(int avg, string driverName)> averages = new List<(int avg, string driverName)>();
foreach (DriverData driver in LiveDriverDataLogs[LiveDriverDataLogs.Count - 1])
{
//We want to recover the last 5 lap times
List<(int lapTime,int lap)> laps = Storage.GetDriverLaptimes(driver.Name,5);
if(laps.Count > 0)
{
int avg = 0;
foreach ((int lapTime, int lap) lap in laps)
{
avg += lap.lapTime;
}
avg = avg / laps.Count;
averages.Add((avg, driver.Name));
}
}
int numberOfDriversToShow = 5;
if (averages.Count > 0 && averages.Count > numberOfDriversToShow)
{
averages = averages.OrderBy(item => item.avg).ToList();
pnlFastest.Controls.Clear();
pnlSlowest.Controls.Clear();
int maxUiSize = pnlFastest.Height / numberOfDriversToShow;
//Displays the fastest drivers
for (int i = 0; i < numberOfDriversToShow; i++)
{
Button newButton = new Button();
(int avg, string driver) data = averages[i];
pnlFastest.Controls.Add(newButton);
newButton.Size = new Size(pnlFastest.Width, maxUiSize);
newButton.Location = new Point(0, i * maxUiSize);
newButton.Text = data.driver;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Name = data.driver + "_fastest_" + i;
newButton.Click += form1.btnDriver_Click;
//We take the average time lost per lap
if (i != 0)
newButton.Text += " + " + Reader.ConvertMsToTime(Convert.ToInt32(((float)data.avg - (float)averages[0].avg) / 5.0f));
}
//Displays the slowests drivers
int badId = 0;
for (int i = averages.Count -1; i >= averages.Count - numberOfDriversToShow; i--)
{
Button newButton = new Button();
(int avg, string driver) data = averages[i];
pnlSlowest.Controls.Add(newButton);
newButton.Size = new Size(pnlFastest.Width, maxUiSize);
newButton.Location = new Point(0, badId * maxUiSize);
newButton.Text = data.driver;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Name = data.driver + "_slowest_" + i;
newButton.Click += form1.btnDriver_Click;
//We take the average time lost per lap
newButton.Text += " + " + Reader.ConvertMsToTime(Convert.ToInt32(((float)data.avg - (float)averages[0].avg) / 5.0f));
badId++;
}
}
}
/// <summary>
/// Will add to the list of overtakes the different changes of position
/// </summary>
/// <param name="lsbResult">The listbox containing all the infos</param>
public void DisplayOvertakes(ListBox lsbResult)
{
//Note : This method SHOULD REALLY not do this but just return a string or a list of string with the new overtakes so the form can handle it as it wishes
if (LiveDriverDataLogs.Count > 1)
{
List<DriverData> oldList = LiveDriverDataLogs[LiveDriverDataLogs.Count - 2];
List<DriverData> newList = LiveDriverDataLogs[LiveDriverDataLogs.Count - 1];
for (int i = 0; i < LiveDriverDataLogs[LiveDriverDataLogs.Count - 1].Count;i++)
{
if (oldList[i].Name != newList[i].Name) {
//There has been a change in the standings
for(int y = 0; y < oldList.Count;y++)
{
if (newList[y].Name == oldList[i].Name)
{
//We found its new location
if (y > i)
{
//The driver overtook someone
lsbResult.Items.Add(newList[y].Name + " climbed to " + y);
}
else
{
//The driver got overtook by someone
lsbResult.Items.Add(newList[y].Name + " fell to " + y);
}
}
}
}
}
}
}
/// <summary>
/// Displays a messageBox containing the infos about a lap time
/// </summary>
/// <param name="driverName">The name of the driver that has done the lapTime</param>
/// <param name="Lap">The number of the lap on wich the lapTime has been set (CAUTION ITS NOT THE RACING LAP ITS FROM THE DB)</param>
/// <param name="LapTime">The time (in ms) of the lap</param>
public void DisplayLapTimeInfos(string driverName, int Lap, string LapTime)
{
List<int> sectors = Storage.GetSectorsFromLapTime(driverName, Lap);
string message = "Lap time infos" + Environment.NewLine;
message += LapTime + Environment.NewLine;
if (sectors.Count > 0)
message += "Sector 1 : " + Reader.ConvertMsToTime(sectors[0]) + Environment.NewLine;
if (sectors.Count > 1)
message += "Sector 2 : " + Reader.ConvertMsToTime(sectors[1]) + Environment.NewLine;
if (sectors.Count > 2)
message += "Sector 3 : " + Reader.ConvertMsToTime(sectors[2]) + Environment.NewLine;
MessageBox.Show(message);
}
/// <summary>
/// Displays the live ranking with the names of the drivers and their gap to the leader in the right order
/// </summary>
/// <param name="pnl">The control that will host all the new controls</param>
/// <param name="form1">The main form</param>
public void DisplayLiveRanking(Panel pnl, Main form1)
{
if (LiveDriverDataLogs.Count > 0)
{
pnl.Controls.Clear();
//Gets the last item that should be the most recent data
List<DriverData> liveData = LiveDriverDataLogs[LiveDriverDataLogs.Count - 1];
Button[] buttons = new Button[liveData.Count];
Size buttonDimensions = new Size(pnl.Width, pnl.Height / liveData.Count);
for (int driverCount = 0; driverCount < liveData.Count; driverCount++)
{
Button newButton = new Button();
newButton.Size = buttonDimensions;
newButton.Location = new Point(0, driverCount * buttonDimensions.Height);
newButton.FlatStyle = FlatStyle.Popup;
DriverData driver = liveData[driverCount];
if (driver.Position == -1)
{
//Its a DNF
newButton.Enabled = false;
}
if (driver.Position > 1)
{
newButton.Text = driver.Name + " +" + Reader.ConvertMsToTime(driver.GapToLeader);
}
else
{
newButton.Text = driver.Name;
}
newButton.Name = liveData[driverCount].Name;
newButton.TextAlign = ContentAlignment.MiddleLeft;
newButton.FlatStyle = FlatStyle.Popup;
newButton.Click += form1.btnDriver_Click;
buttons[driverCount] = newButton;
}
//Note : It could be better to have this directly in the same loop
foreach (Button button in buttons)
{
pnl.Controls.Add(button);
}
}
}
}
}