/// Author : Maxime Rohmer
/// Date : 09/06/2023
/// File : Form1.cs
/// Brief : Class that controls the main view of the app
/// Version : Beta 1.0
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
namespace TrackTrends
{
public partial class Main : Form
{
private F1TVEmulator Emulator = null;
private DataWrapper Wrapper = null;
private bool cancelRequested = false;
private SemaphoreSlim semaphore = new SemaphoreSlim(1);
string ConfigFile = "";
string GpUrl = "";
//For the responsive content
Size oldSize = new Size();
Size oldRankingSize = new Size();
Size oldLapTimesSize = new Size();
Size oldBattles = new Size();
Size oldPnlBattles = new Size();
Size oldPnlRankings = new Size();
Size oldPnlFastest = new Size();
Size oldPnlSlowest = new Size();
Point oldRankingPosition = new Point();
Point oldBattlePosition = new Point();
Point oldDriverInfoPosition = new Point();
Point olPnlFastestPosition = new Point();
Point oldPnlSlowestPosition = new Point();
public Main()
{
InitializeComponent();
}
/// <summary>
/// Will update everything that is not data related
/// </summary>
public void RefreshUI()
{
if (Directory.Exists(ConfigurationTool.CONFIGS_FOLDER_NAME))
{
lsbPresets.DataSource = null;
lsbPresets.DataSource = Directory.GetFiles(ConfigurationTool.CONFIGS_FOLDER_NAME);
}
}
/// <summary>
/// Opens the settings page. Also disposes of the browser if there is one opened and all thos things
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSettings_Click(object sender, EventArgs e)
{
if (Emulator != null)
Emulator.ResetDriver();
btnStartDecoding.Enabled = false;
btnStopUpdating.Enabled = false;
btnResetEmulator.Text = "Launch";
Emulator = null;
Wrapper = null;
GC.Collect();
Settings settingsForm = new Settings();
settingsForm.ShowDialog();
RefreshUI();
//MessageBox.Show(settingsForm.GrandPrixUrl + Environment.NewLine + settingsForm.GrandPrixName + Environment.NewLine + settingsForm.GrandPrixYear);
if (settingsForm.GrandPrixUrl != "" && settingsForm.SelectedConfigFile != "")
{
GpUrl = settingsForm.GrandPrixUrl;
tbxGpUrl.Text = GpUrl;
if (File.Exists(settingsForm.SelectedConfigFile))
{
ConfigFile = settingsForm.SelectedConfigFile;
for(int i = 0; i < lsbPresets.Items.Count; i++)
{
if (lsbPresets.Items[i].ToString() == ConfigFile)
lsbPresets.SelectedIndex = i;
}
}
else
{
//Should technically never show up but we never know
MessageBox.Show("The config file has not been found please return to the config and change it");
}
}
else
{
//WE dont care anymore, the user will choose its Grand Prix himself in the main program
//MessageBox.Show("There is no URL for the Grand Prix you want to decode. Please return to the config and add a valid one");
}
}
/// <summary>
/// Will do everything that needs to be done at the first start of the app
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void Form1_Load(object sender, EventArgs e)
{
//Those are the default values but they will need to be changed later when the configuration has been done
ConfigFile = "./Presets/Clean_4K_2023.json";
GpUrl = "https://f1tv.formula1.com/detail/1000006688/2023-azerbaijan-grand-prix?action=play";
tbxGpUrl.Text = GpUrl;
this.DoubleBuffered = true;
oldSize = this.Size;
oldRankingSize = gpbxRanking.Size;
oldLapTimesSize = gpbxLapTimes.Size;
oldBattles = gpbxBattles.Size;
oldPnlRankings = pnlLiveRanking.Size;
oldPnlBattles = pnlBattles.Size;
oldPnlFastest = pnlFastest.Size;
oldPnlSlowest = pnlSlowest.Size;
oldRankingPosition = gpbxRanking.Location;
oldBattlePosition = gpbxBattles.Location;
oldDriverInfoPosition = gpbxDriverInfos.Location;
olPnlFastestPosition = pnlFastest.Location;
oldPnlSlowestPosition = pnlSlowest.Location;
tip1.SetToolTip(btnResetEmulator, "Starts or restarts the emulator. You need to start this to use the app");
tip1.SetToolTip(btnSettings, "Opens the configuration menu");
tip1.SetToolTip(tbxGpUrl, "Insert the URL of the Grand Prix you want to track. Dont forget the \"?action=play\" at the end");
tip1.SetToolTip(lsbPresets, "Select a configuration preset to use with the decoding");
tip1.SetToolTip(pbxResult,"A preview of what the program sees. You should see the DATA page of the F1TV here");
tip1.SetToolTip(lsbOvertakes,"A list of all the activity. You can scroll to see the most recent overtakes");
tip1.SetToolTip(gpbxBattles,"The four first battles in the field. A battle is two drivers less than 3 seconds apart");
tip1.SetToolTip(gpbxLapTimes,"The fastest and slowest drivers on track at the moment. It takes the average lapTime of the last 5 laps to choose who is the fastes or the slowest");
RefreshUI();
}
/// <summary>
/// Will start or stop the process of decoding
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnUpdate_Click(object sender, EventArgs e)
{
cancelRequested = false;
if (Emulator != null && Wrapper != null)
{
// Disable UI controls to prevent re-entrancy
btnResetEmulator.Enabled = false;
btnStartDecoding.Enabled = false;
btnStopUpdating.Enabled = true;
btnSettings.Enabled = false;
while (!cancelRequested)
{
await semaphore.WaitAsync();
try
{
// Start the time-consuming task on a separate thread
await Task.Run(async () =>
{
Stopwatch sw = new Stopwatch();
sw.Start();
Bitmap screen = Emulator.Screenshot();
screen.Save("HopefullyDataScreenshot.png");
Invoke((MethodInvoker)delegate
{
pbxResult.Image = (Bitmap)screen.Clone();
});
Wrapper.ChangeImage(screen);
int errorCode = Wrapper.Refresh();
sw.Stop();
// Task completed
Invoke((MethodInvoker)delegate
{
DisplayResults(errorCode, sw, screen);
DisplayBattles();
DisplayDeltas();
DisplayOvertakes();
});
});
}
finally
{
semaphore.Release();
}
}
// Re-enable UI controls
btnStopUpdating.Text = "Stop";
btnStartDecoding.Enabled = true;
btnStopUpdating.Enabled = false;
btnResetEmulator.Enabled = true;
btnSettings.Enabled = true;
}
}
/// <summary>
/// Will display the overtakes in the overtakes list box
/// </summary>
private void DisplayOvertakes()
{
Wrapper.DisplayOvertakes(lsbOvertakes);
}
/// <summary>
/// Will display the battles in the battles pannel
/// </summary>
private void DisplayBattles()
{
Wrapper.DisplayBattles(pnlBattles, this);
}
/// <summary>
/// Will display the time differences in the faster and slowest pannels
/// </summary>
private void DisplayDeltas()
{
Wrapper.DisplayTimesDeltas(pnlFastest, pnlSlowest, this);
}
/// <summary>
/// Will try to stop the emulator (usually does not work please do not count on it)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (Emulator != null)
{
Emulator.Stop();
}
}
/// <summary>
/// Will display the live ranking on the live ranking pannel. Its called like this because historically it was the method that just recovered the bare results from the OCR
/// </summary>
/// <param name="errorCode"></param>
/// <param name="sw"></param>
/// <param name="screen"></param>
private void DisplayResults(int errorCode, Stopwatch sw, Bitmap screen)
{
if (errorCode != 0)
{
cancelRequested = true;
MessageBox.Show("An error has occured while trying to recover data from live feed. This can happen sometimes. I would advise you to restart a few times. If the problem persists check your configuration.");
}
else
{
Wrapper.DisplayLiveRanking(pnlLiveRanking, this);
}
}
/// <summary>
/// Will stop the data recovering operation and resets some buttons and text
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnStopUpdating_Click(object sender, EventArgs e)
{
// Set the cancellation flag
cancelRequested = true;
btnStopUpdating.Enabled = false;
btnResetEmulator.Enabled = false;
btnStopUpdating.Text = "Stopping";
}
/// <summary>
/// Will start the F1TVEmulator, again this name is historical because back at the start of this project this button did not have a name
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button1_Click(object sender, EventArgs e)
{
lsbOvertakes.Items.Clear();
btnResetEmulator.Text = "Launching";
btnResetEmulator.Enabled = false;
btnSettings.Enabled = true;
btnStartDecoding.Enabled = false;
btnStopUpdating.Enabled = false;
btnSettings.Enabled = false;
int errorCode = -1;
await Task.Run(async () =>
{
if (Emulator != null)
Emulator.ResetDriver();
Emulator = null;
Wrapper = null;
GC.Collect();
Emulator = new F1TVEmulator(GpUrl);
errorCode = await Emulator.Start();
});
if (errorCode != 0)
{
string message = "";
switch (errorCode)
{
case 100:
message = "Error " + errorCode + " Could not recover cookies. It could be because of an improper installation of python or bad cookies in the chrome database. Please try to log on to the F1TV using chrome again";
break;
case 101:
message = "Error " + errorCode + " Could not start the driver. It could be because an other instance is runnin make sure you closed them all before trying again";
break;
case 102:
message = "Error " + errorCode + " Could not navigate on the F1TV site. Make sure the correct URL has been given and that you logged from chrome. It can take a few minutes to update";
break;
case 103:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 104:
message = "Error " + errorCode + " The url is not a valid url";
break;
case 105:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
case 106:
message = "Error " + errorCode + " There has been an error trying to emulate button presses. Please try again";
break;
default:
message = "Could not start the emulator Error " + errorCode;
break;
}
MessageBox.Show(message);
btnResetEmulator.Enabled = true;
btnSettings.Enabled = true;
btnResetEmulator.Text = "Retry";
}
else
{
Wrapper = new DataWrapper(ConfigFile, Emulator.Screenshot());
btnResetEmulator.Text = "Re launch";
btnResetEmulator.Enabled = true;
btnSettings.Enabled = true;
btnStartDecoding.Enabled = true;
}
}
/// <summary>
/// Silly way to remove borders from groupbox and make them look like pannels with titles
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void removeBorders(object sender, PaintEventArgs e)
{
GroupBox gpbx = (GroupBox)sender;
using (Pen pen = new Pen(gpbx.BackColor, 50))
{
e.Graphics.DrawRectangle(pen, 0, 0, gpbx.Width - 1, gpbx.Height - 1);
e.Graphics.DrawRectangle(pen, 0, 0, gpbx.Width - 1, gpbx.Height - 1);
}
using (var brush = new SolidBrush(gpbx.ForeColor))
{
var textPosition = new Point(5, 0); // Adjust the X and Y values as needed
e.Graphics.DrawString(gpbx.Text, gpbx.Font, brush, textPosition);
}
}
/// <summary>
/// Will change the preset to use when starting the emulator
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lsbPresets_SelectedIndexChanged(object sender, EventArgs e)
{
if (lsbPresets.SelectedIndex >= 0)
ConfigFile = lsbPresets.Items[lsbPresets.SelectedIndex].ToString();
}
/// <summary>
/// Will change the URL the emulator will use, historical name again
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void textBox1_TextChanged(object sender, EventArgs e)
{
if (tbxGpUrl.Text != "")
GpUrl = tbxGpUrl.Text;
}
/// <summary>
/// This is called by the automatically generated buttons. Its here to fill in the driver info tab whenever the user clicks on a button that contains the name of a driver
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void btnDriver_Click(object sender, EventArgs e)
{
//Removes the cover
if (pnlCover.Visible = true)
pnlCover.Visible = false;
//Happens when a driver button has been clicked
//MessageBox.Show((sender as Button).Name + " has been selected");
Button btn = (sender as Button);
string[] parts = btn.Name.Split('_');
DriverData driver = Wrapper.GetFullDriverData(parts[0], pnlCurrentDriverLapsHistory, this);
lblCurrentDriverName.Text = driver.Name;
lblCurrentDriverPosition.Text = driver.Position.ToString();
lblCurrentDriverGapToLeader.Text = Reader.ConvertMsToTime(driver.GapToLeader);
lblCurrentDriverLapTime.Text = Reader.ConvertMsToTime(driver.LapTime);
lblCurrentDriverTyreAge.Text = driver.CurrentTyre.NumberOfLaps.ToString();
if (driver.DRS)
{
lblCurrentDriverDRS.Text = "Open";
lblCurrentDriverDRS.ForeColor = Color.FromArgb(0, 164, 46);
}
else
{
lblCurrentDriverDRS.Text = "Closed";
lblCurrentDriverDRS.ForeColor = Color.Black;
}
switch (driver.CurrentTyre.Coumpound)
{
case Tyre.Type.Undefined:
lblCurrentDriverTyreType.Text = "uuuuh...";
lblCurrentDriverTyreType.ForeColor = Color.Violet;
break;
case Tyre.Type.Hard:
lblCurrentDriverTyreType.Text = "Hard";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(164, 165, 168);
break;
case Tyre.Type.Medium:
lblCurrentDriverTyreType.Text = "Medium";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(245, 191, 0);
break;
case Tyre.Type.Soft:
lblCurrentDriverTyreType.Text = "Soft";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(255, 0, 0);
break;
case Tyre.Type.Inter:
lblCurrentDriverTyreType.Text = "Intermediate";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(0, 164, 46);
break;
case Tyre.Type.Wet:
lblCurrentDriverTyreType.Text = "Wet";
lblCurrentDriverTyreType.ForeColor = Color.FromArgb(39, 96, 166);
break;
}
}
/// <summary>
/// This is supposed to be called by an automatically generated button. It should be any button with a laptime info on it
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void btnLapTime_Click(object sender, EventArgs e)
{
//Happens when a lapTime has been clicked
Button btn = sender as Button;
string[] parts = btn.Name.Split('_');
Wrapper.DisplayLapTimeInfos(parts[0], Convert.ToInt32(parts[1]), btn.Text);
}
/// <summary>
/// Will trigger responsive calculation everytime the form changes size
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Main_Resize(object sender, EventArgs e)
{
int xDiff = this.Width - oldSize.Width;
int yDiff = this.Height - oldSize.Height;
int padding = 10;
//This will take half the newly created space
gpbxRanking.Size = new Size(oldRankingSize.Width + xDiff / 2, oldRankingSize.Height + yDiff);
gpbxRanking.Location = new Point(oldRankingPosition.X + xDiff / 2, gpbxRanking.Location.Y);
//Will take half the new height and half the new height
gpbxLapTimes.Size = new Size(oldLapTimesSize.Width + xDiff / 2, oldLapTimesSize.Height + yDiff / 2);
//Will take half the new height and half the new width
gpbxBattles.Size = new Size(oldBattles.Width + xDiff / 2, oldBattles.Height + yDiff / 2);
gpbxBattles.Location = new Point(gpbxBattles.Location.X, oldBattlePosition.Y + yDiff / 2);
//The infos wont change width but will need to be centerd
Point startOfZone = new Point(gpbxOvertakes.Width + gpbxOvertakes.Location.X, gpbxOvertakes.Location.Y);
Point endOfZone = new Point(gpbxRanking.Location.X, gpbxOvertakes.Location.Y);
int totalWidth = endOfZone.X - startOfZone.X;
gpbxDriverInfos.Location = new Point(startOfZone.X + (totalWidth / 2 - gpbxDriverInfos.Width / 2), oldDriverInfoPosition.Y + yDiff);
//Now resizing internals
pnlFastest.Size = new Size(oldPnlFastest.Width + xDiff / 4,oldPnlFastest.Height + yDiff / 4);
pnlFastest.Location = new Point(olPnlFastestPosition.X,olPnlFastestPosition.Y + yDiff / 4);
pnlSlowest.Size = new Size(oldPnlSlowest.Width + xDiff / 4, oldPnlSlowest.Height + yDiff / 4);
pnlSlowest.Location = new Point(oldPnlSlowestPosition.X + xDiff / 4, oldPnlSlowestPosition.Y + yDiff / 4);
pnlBattles.Size = new Size(oldPnlBattles.Width + xDiff / 2,oldPnlBattles.Height + yDiff / 2);
pnlLiveRanking.Size = new Size(oldPnlRankings.Width + xDiff / 2,oldPnlRankings.Height + yDiff);
}
}
}