diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..26d33521af10bcc7fd8cea344038eaaeb78d0ef5
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/F1-OvertakeAnalyser-Python.iml b/.idea/F1-OvertakeAnalyser-Python.iml
new file mode 100644
index 0000000000000000000000000000000000000000..591c422bea5969b4f4e6edaa9412ebff3ae03ad2
--- /dev/null
+++ b/.idea/F1-OvertakeAnalyser-Python.iml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.venv" />
+    </content>
+    <orderEntry type="jdk" jdkName="Python 3.12 (F1-OvertakeAnalyser-Python)" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9e377b7026972e4f9db0500ff67f4363d416e02f
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="Python 3.12 (F1-OvertakeAnalyser-Python)" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2fbde88f2ebaad8ac855add891be3fabc613e6d5
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/F1-OvertakeAnalyser-Python.iml" filepath="$PROJECT_DIR$/.idea/F1-OvertakeAnalyser-Python.iml" />
+    </modules>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..94a25f7f4cb416c083d265558da75d457237d671
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/DataAnalyser.py b/DataAnalyser.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a2af99b02c46ee2f8931a2bfb99fe304370fdaf
--- /dev/null
+++ b/DataAnalyser.py
@@ -0,0 +1,242 @@
+import math
+from typing import List
+
+from fastf1.core import Session, Lap
+
+from DataHandler import DataHandler
+
+
+class DataAnalyser(DataHandler):
+
+
+
+    # ===== Overtakes =====
+
+    def analyseRacesForOvertakes(self, races: List[Session]):
+        overtakesInRaces: List[List[int]] = [[]]
+        for race in races:
+            overtakesInRaces.append(self.analyseRaceForOvertakes(race))
+        return overtakesInRaces
+
+    def analyseRaceForOvertakes(self, race: Session):
+
+
+        # Collect grid positions
+        allLapPositions: List[dict[str, int]] = []
+
+        gridPositions: dict[str, int] = self.getGridPositions(race)
+        allLapPositions.append(gridPositions)
+
+        runningLapsPositions: List[dict[str, int]] = self.getRunningLapsPositions(race)
+        for runningLapPositions in runningLapsPositions:
+            allLapPositions.append(runningLapPositions)
+
+        # Count position changes between all lap pairs
+        overtakesInLaps: List[int] = self.countOvertakesInLaps(allLapPositions)
+
+        return overtakesInLaps
+
+    def getGridPositions(self, race: Session):
+        sessionResults = race.results
+        gridPositions: dict[str, int] = {}  # driverId & lapNumber
+        for i in range(self.numberOfDrivers):
+            driverPosition = sessionResults['GridPosition'].iloc[i]
+            driverAbbreviation = sessionResults['Abbreviation'].iloc[i]
+            gridPositions[driverAbbreviation] = driverPosition
+
+        if self.activateDebugOvertakeAnalysis:
+            print(f"\nLap: 0")
+            for position in range(len(gridPositions)):
+                gridPositions.values()
+                driverAtPosition = list(gridPositions.keys())[list(gridPositions.values()).index(position + 1)] # get dictionary keys (driverIds) by values (current race position)
+                print(f"P{position + 1}: {driverAtPosition}")
+
+        return gridPositions
+
+    def getRunningLapsPositions(self, race: Session):
+        runningLapsPositions: List[dict[str, int]] = []
+        allRaceLaps = race.laps
+
+        for raceLapIndex in range(race.total_laps):
+            runningLapPositions: dict[str, int] = {}  # driverId & lapNumber
+            for driver in race.drivers:
+                raceLap = allRaceLaps.pick_laps(raceLapIndex + 1) # Lap 0 doesn't exist
+                raceLap = raceLap.pick_drivers(driver)
+                try :
+                    driverAbbreviation = raceLap['Driver'].iloc[0]
+                    driverPosition = raceLap['Position'].iloc[0]
+                    runningLapPositions[driverAbbreviation] = driverPosition
+                except: # triggered when not all drivers that took part reached lap, probably by crashing or being behind
+                    x = 0 # do nothing
+            runningLapsPositions.append(runningLapPositions)
+
+        # debug
+        if self.activateDebugOvertakeAnalysis:
+            for raceLapIndex in range(race.total_laps):
+                print(f"\nLap: {raceLapIndex + 1}")
+                runningLapPositions = runningLapsPositions[raceLapIndex]
+                for position in range(len(runningLapPositions)):
+                    runningLapPositions.values()
+                    driverAtPosition = list(runningLapPositions.keys())[list(runningLapPositions.values()).index(position + 1)]
+                    print(f"P{position + 1}: {driverAtPosition}")
+
+        return runningLapsPositions
+
+    def countOvertakesInLaps(self, allLapPositions: List[dict[str, int]]):
+        overtakesInLaps: List[int] = []
+        for i in range(len(allLapPositions) - 1):
+            overtakesInLap: int = self.countOvertakesInLap(allLapPositions[i], allLapPositions[i + 1])
+            overtakesInLaps.append(overtakesInLap)
+            if self.activateDebugOvertakeAnalysis: print(f"Overtakes in lap {i+1}: {overtakesInLap}")
+        return overtakesInLaps
+
+    def countOvertakesInLap(self, startOrder: dict[str, int], endOrder: dict[str, int]):
+        out: int = self.numberOfDrivers
+        orderToSort: List[int] = [self.numberOfDrivers] * self.numberOfDrivers
+        for driver in startOrder:
+            if driver not in endOrder: # add missing drivers for sorting/counting purposes
+                endOrder[driver] = out # heaviest weighting to drop driver to bottom, as they do irl when crashing or similar
+            if math.isnan(startOrder[driver]): startOrder[driver] = out
+            if math.isnan(endOrder[driver]): endOrder[driver] = out
+            driverStartPosition: int = int(startOrder[driver])
+            driverEndPosition: int = int(endOrder[driver])
+
+            orderToSort[driverStartPosition - 1] = driverEndPosition
+
+        if self.activateDebugOvertakeAnalysis:
+            #print("Weighting:")
+            for i in range(len(orderToSort)):
+                x=0
+                #print(orderToSort[i])
+
+
+
+        overtakes: int = 0
+        index: int = 0
+        while index < len(orderToSort) - 1:
+            if orderToSort[index] > orderToSort[index + 1]:
+                temp: int = orderToSort[index]
+                orderToSort[index] = orderToSort[index + 1]
+                orderToSort[index + 1] = temp
+                overtakes += 1
+                index = -1
+            index += 1
+
+        return overtakes
+
+    # ===== Weather =====
+
+    def analyseRacesForWeather(self, races: List[Session]):
+        weatherInRaces: List[List[bool]] = [[]] # isRaining per lap per race
+        for race in races:
+            weatherInRace = self.analyseRaceForWeather(race)
+            weatherInRaces.append(weatherInRace)
+        return weatherInRaces
+
+    def analyseRaceForWeather(self, race: Session):
+        x = 0
+
+    # ===== Tire Changes =====
+
+    def getEarliestTireChanges(self, races: List[Session]):
+        earliestTireChanges: List[int] = [[]]  # isRaining per lap per race
+        for race in races:
+            earliestTireChange = self.getEarliestTireChange(race)
+            earliestTireChanges.append(earliestTireChange)
+        return earliestTireChanges
+
+    # Returns -1 if no tire change occured
+    def getEarliestTireChange(self, race: Session):
+        earliestTireChangeLap: int = -1
+        compoundsPerLap: List[List[str]] = self.getCompoundsForRace(race)
+        compoundsPerLap[0] = compoundsPerLap[1] # presume grid tires same as 1st lap; races are only picked if weather change after first 10 laps anyways, so its ok
+        startingCompound: str = self.getPredominantCompound(compoundsPerLap[0])
+        earliestTireChangeLap = self.getFirstLapWithOppositeCompound(compoundsPerLap, startingCompound)
+
+        return earliestTireChangeLap
+
+    def getLatestTireChanges(self, races: List[Session]):
+        latestTireChanges: List[int] = [[]]  # isRaining per lap per race
+        for race in races:
+            latestTireChange = self.getLatestTireChange(race)
+            latestTireChanges.append(latestTireChange)
+        return latestTireChanges
+
+    # Returns -1 if no tire change occured
+    def getLatestTireChange(self, race: Session):
+        latestTireChangeLap: int = -1
+        compoundsPerLap: List[List[str]] = self.getCompoundsForRace(race)
+        compoundsPerLap[0] = compoundsPerLap[1]  # presume grid tires same as 1st lap; races are only picked if weather change after first 10 laps anyways, so its ok
+        startingCompound: str = self.getPredominantCompound(compoundsPerLap[0])
+        latestTireChangeLap = self.getFirstLapWithoutCompound(compoundsPerLap, startingCompound)
+
+        return latestTireChangeLap
+
+    def getFirstLapWithoutCompound(self, compoundsPerLap: List[List[str]], startingCompound: str):
+        currentLap = 0
+        filter = self.setFilter(startingCompound)
+        for compoundsThisLap in compoundsPerLap:
+            noStartingCompoundsLeft = True
+            for compound in compoundsThisLap:
+                if compound in filter:
+                    noStartingCompoundsLeft = False
+            if noStartingCompoundsLeft: return currentLap
+            currentLap += 1
+
+
+    def getCompoundsForRace(self, race: Session):
+        compoundsPerLap: List[List[str]] = [[]]
+        allRaceLaps = race.laps
+
+        for raceLapIndex in range(race.total_laps):
+            compoundsThisLap: List[str] = []
+            for driver in race.drivers:
+                raceLap = allRaceLaps.pick_laps(raceLapIndex + 1)  # Lap 0 doesn't exist
+                raceLap = raceLap.pick_drivers(driver)
+                try:
+                    driverAbbreviation = raceLap['Driver'].iloc[0]
+                    compound = raceLap['Compound'].iloc[0]
+                    compoundsThisLap.append(compound)
+                except:  # triggered when not all drivers that took part reached lap, probably by crashing or being behind
+                    x = 0  # do nothing
+            compoundsPerLap.append(compoundsThisLap)
+
+        return compoundsPerLap
+
+    def getPredominantCompound(self, compoundsThisLap: List[str]):
+        slickCounter = 0
+        interCounter = 0
+        wetCounter = 0
+        for compound in compoundsThisLap:
+            if compound in self.slickCompounds: slickCounter += 1
+            if compound == 'INTERMEDIATE': interCounter += 1
+            if compound == 'WET': wetCounter += 1
+        mostUsed = max(slickCounter, interCounter, wetCounter)
+        if slickCounter == mostUsed: return 'SLICK'
+        if interCounter == mostUsed: return 'INTERMEDIATE'
+        if wetCounter == mostUsed: return 'WET'
+        return 'error'
+
+    def getFirstLapWithOppositeCompound(self, compoundsPerLap: List[List[str]], startingCompound: str):
+        filter = self.setFilter(startingCompound)
+        currentLap = 0
+        for compoundsThisLap in compoundsPerLap:
+            for compound in compoundsThisLap:
+                if compound not in filter:
+                    return currentLap
+            currentLap += 1
+
+    def setFilter(self, startingCompound: str):
+        if startingCompound == 'SLICK': return self.slickCompounds
+        return startingCompound
+
+    # ===== Crashes =====
+
+    def analyseRacesForCrashes(self, races):
+        x = 0
+
+    # ===== Events =====
+
+    def analyseRacesForEvents(self, races):
+        x = 0
+
diff --git a/DataChecker.py b/DataChecker.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ddbfa08f76dab291b67b1c8eca0b9e3e4684c81
--- /dev/null
+++ b/DataChecker.py
@@ -0,0 +1,19 @@
+from DataHandler import DataHandler
+
+
+class DataChecker(DataHandler):
+
+    def check_imported_race(self, race_data):
+        for current_lap in range(len(race_data)):
+            print(f"Lap {current_lap}")
+            for position in range(1, len(race_data[0]) + 1):
+                driver_at_position = race_data[current_lap][position - 1]
+                if driver_at_position == self.invalidDriverId:
+                    break  # skip entries without drivers
+                print(f"P{position}: {race_data[current_lap][position - 1]}")
+            print()
+        print(f"Number of laps run: {len(race_data) - 1}")
+
+    def check_imported_races(self, races):
+        for race in races:
+            self.check_imported_race(race)
\ No newline at end of file
diff --git a/DataHandler.py b/DataHandler.py
new file mode 100644
index 0000000000000000000000000000000000000000..c718a439337e95e47d859e77b70da8cc75129b50
--- /dev/null
+++ b/DataHandler.py
@@ -0,0 +1,10 @@
+from abc import ABC, abstractmethod
+
+class DataHandler(ABC):
+
+    def __init__(self):
+        self.numberOfDrivers = 20
+        self.invalidDriverId = "NO_DRIVER"
+        self.enableTestingMode = False  # only partially import data to speed up testing, because HTTP 429 limits import speed
+        self.activateDebugOvertakeAnalysis = False
+        self.slickCompounds = ('SOFT', 'MEDIUM', 'HARD')
\ No newline at end of file
diff --git a/DataImporter.py b/DataImporter.py
new file mode 100644
index 0000000000000000000000000000000000000000..40e7e2b44008f79233ffeabed9e1a7fef865bafe
--- /dev/null
+++ b/DataImporter.py
@@ -0,0 +1,30 @@
+import json
+import time
+
+import fastf1
+import requests
+from abc import ABC
+from typing import List
+
+from fastf1.core import Session
+
+from DataHandler import DataHandler
+
+
+
+
+
+class DataImporter(DataHandler, ABC):
+
+    def importRaceSessions(self, races):
+        sessions: List[Session] = []
+        for race in races:
+            sessions.append(self.importRaceSession(race))
+        return sessions
+
+
+    def importRaceSession(self, race):
+        season, raceIndex = race
+        session = fastf1.get_session(season, raceIndex, 'R')
+        session.load(laps=True, weather=True, messages=False, telemetry=False)
+        return session
\ No newline at end of file
diff --git a/DataPlotter.py b/DataPlotter.py
new file mode 100644
index 0000000000000000000000000000000000000000..31bc35cf563d0c66dc8f912e9b1d840614860c33
--- /dev/null
+++ b/DataPlotter.py
@@ -0,0 +1,48 @@
+import math
+from abc import ABC
+from typing import List
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+from fastf1.core import Session, Lap
+
+from DataHandler import DataHandler
+
+
+class DataPlotter(DataHandler):
+    def plotOvertakesWithTireChangeWindow(self, overtakesPerLap: List[int], earliestTireChange: int, latestTireChange: int):
+
+        overtakesPerLap.insert(0, 0) # Insert 0th lap, which cannot have overtakes
+        laps: int = len(overtakesPerLap)
+
+        figure, axis = plt.subplots(figsize=(8.0, 4.9))
+
+        axis.plot(overtakesPerLap)
+
+
+        axis.set_xlabel('Lap')
+        axis.set_ylabel('Position changes in lap')
+
+        major_ticks_laps = np.arange(0, laps, 5)
+        major_ticks_overtakes = np.arange(0, max(overtakesPerLap) + 1, 5)
+        minor_ticks_laps = np.arange(0, laps, 1)
+        minor_ticks_overtakes = np.arange(0, max(overtakesPerLap) + 1, 1)
+
+        axis.set_xticks(major_ticks_laps)
+        axis.set_xticks(minor_ticks_laps, minor=True)
+        axis.set_yticks(major_ticks_overtakes)
+        axis.set_yticks(minor_ticks_overtakes, minor=True)
+
+        # And a corresponding grid
+        axis.grid(which='both')
+
+        # Or if you want different settings for the grids:
+        axis.grid(which='minor', alpha=0.2)
+        axis.grid(which='major', alpha=0.5)
+
+
+        axis.legend(bbox_to_anchor=(1.0, 1.02))
+        plt.tight_layout()
+        plt.show()
\ No newline at end of file
diff --git a/__pycache__/DataAnalyser.cpython-312.pyc b/__pycache__/DataAnalyser.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ecccefbfe08c806dc0f19ec65dbea28f80374b1f
Binary files /dev/null and b/__pycache__/DataAnalyser.cpython-312.pyc differ
diff --git a/__pycache__/DataChecker.cpython-312.pyc b/__pycache__/DataChecker.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3cc2c91a482e442f42e50c9323e7620b7bb2410a
Binary files /dev/null and b/__pycache__/DataChecker.cpython-312.pyc differ
diff --git a/__pycache__/DataHandler.cpython-312.pyc b/__pycache__/DataHandler.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b158e8a2cb16c80f6620ac6055ec6e4497358d2d
Binary files /dev/null and b/__pycache__/DataHandler.cpython-312.pyc differ
diff --git a/__pycache__/DataImporter.cpython-312.pyc b/__pycache__/DataImporter.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0423a9fea9c28a4df0751aa5ec208ce2b742b005
Binary files /dev/null and b/__pycache__/DataImporter.cpython-312.pyc differ
diff --git a/__pycache__/DataPlotter.cpython-312.pyc b/__pycache__/DataPlotter.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e6aab7555863e182c257ac9dab1a0cc088b39228
Binary files /dev/null and b/__pycache__/DataPlotter.cpython-312.pyc differ
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb9736e123d740785f6f2b6b62beae1d78482283
--- /dev/null
+++ b/main.py
@@ -0,0 +1,131 @@
+from typing import List
+
+import matplotlib.pyplot as plt
+
+import fastf1.plotting
+
+
+from DataAnalyser import DataAnalyser
+from DataChecker import DataChecker
+from DataImporter import DataImporter
+from DataPlotter import DataPlotter
+
+
+
+
+
+class Main:
+
+    def main(self):
+        # contains pairs of season & race number to identify specific races
+        race_indexes = [
+            [2022, 4]  # Imola 2022 (DWR)
+            # [2024, 7]  # Imola 2024 (SWR)
+            # [2024, 9] # Canada 2024 (DWR)
+            #
+            # TODO: add more races
+        ]
+
+        importer = DataImporter()
+        analyser = DataAnalyser()
+        plotter = DataPlotter()
+
+        for race_index in race_indexes:
+            # Import
+            raceSession = importer.importRaceSession(race_index)
+            print("Import done")
+
+            # Analyse
+            overtakesInRaces: List[int] = analyser.analyseRaceForOvertakes(raceSession)
+            print("Overtake analysis done")
+            # weatherInRaces = analyser.analyseRaceForWeather(raceSession)
+            earliestTireChange: int = analyser.getEarliestTireChange(raceSession) # first lap where someone switched from slicks to non slicks or vice versa, denoted by lap number
+            print("First tire change done")
+            latestTireChange: int = analyser.getLatestTireChange(raceSession)
+            print("Last tire change done")
+
+            plotter.plotOvertakesWithTireChangeWindow(overtakesInRaces, earliestTireChange, latestTireChange)
+            print("Plot done")
+
+
+
+            print(f"\n\n===== Data for race {race_index[0]}/{race_index[1]} =====")
+            print("Lap\tOvertakes")
+            currentLap = 0
+            for overtakesInLap in overtakesInRaces:
+                print(f"{currentLap}\t{overtakesInLap}")
+                currentLap += 1
+            print(f"First tire compound: lap {earliestTireChange}")
+            print(f"Last tire compound: lap {latestTireChange}")
+            print(f"Weather change window is therefore: laps {earliestTireChange-1} - {latestTireChange+1}")
+
+
+
+
+    def fastF1Example(self):
+        # Load FastF1's dark color scheme
+        fastf1.plotting.setup_mpl(mpl_timedelta_support=False, misc_mpl_mods=False,
+                                  color_scheme='fastf1')
+
+
+        session = fastf1.get_session(2024, 7, 'R')
+        session.load(laps=True, weather=True)
+
+        figure, axis = plt.subplots(figsize=(8.0, 4.9))
+
+
+        for driver in session.drivers:
+            driverLaps = session.laps.pick_drivers(driver)
+
+            driverAbbreviation = driverLaps['Driver'].iloc[0]
+            style = fastf1.plotting.get_driver_style(identifier=driverAbbreviation,
+                                                     style=['color', 'linestyle'],
+                                                     session=session)
+
+            axis.plot(driverLaps['LapNumber'], driverLaps['Position'],
+                    label=driverAbbreviation, **style)
+
+
+        axis.set_ylim([20.5, 0.5])
+        axis.set_yticks([1, 5, 10, 15, 20])
+        axis.set_xlabel('Lap')
+        axis.set_ylabel('Position')
+
+
+        axis.legend(bbox_to_anchor=(1.0, 1.02))
+        plt.tight_layout()
+
+        plt.show()
+
+
+    def oldJavaTransfer(self):
+        # contains pairs of season & race number to identify specific races
+        race_indexes = [
+            [2024, 7],  # Imola 2024
+            [2022, 4]   # Imola 2022
+            # TODO: add more races
+        ]
+
+        importer = DataImporter()
+        checker = DataChecker()
+        analyser = DataAnalyser()
+        # plotter = DataPlotter()  # Commented as it's not used
+
+        races = importer.import_laps_for_races(race_indexes)
+        checker.check_imported_races(races)
+        overtakes_in_races = analyser.count_overtakes_in_races(races)
+
+        print(f"Data for race {race_indexes[0][1]}/{race_indexes[0][0]}")
+        for current_lap in range(len(overtakes_in_races[0])):
+            print(f"Overtakes in lap {current_lap + 1}: {overtakes_in_races[0][current_lap]}")
+
+        return overtakes_in_races
+
+
+
+
+if __name__ == '__main__':
+    app = Main()
+
+    # Call the get_overtakes_in_races method
+    app.main()
\ No newline at end of file