diff --git a/src/exercises/type_hints.py b/src/exercises/type_hints.py new file mode 100644 index 0000000..f704701 --- /dev/null +++ b/src/exercises/type_hints.py @@ -0,0 +1,11 @@ +def load_scores(data: list[tuple[str, int | str]]) -> dict[str, int]: + result: dict[str, int] = {} + + for name, points in data: + result[name] = int(points) + + return result + + +if __name__ == "__main__": + print(load_scores([("Housi", 40), ("Theodolf", 45), ("Mehmet", 15)])) diff --git a/src/lap_detection/api.py b/src/lap_detection/api.py new file mode 100644 index 0000000..8013300 --- /dev/null +++ b/src/lap_detection/api.py @@ -0,0 +1,35 @@ +from dataclasses import field, dataclass +from typing import List + + +@dataclass +class Coordinate: + lon: float + lat: float + + +@dataclass +class Geometry: + coordinates: list[list[float]] + type: str + + +@dataclass +class Feature: + type: str + properties: dict + geometry: Geometry + + +@dataclass +class Collection: + type: str + features: list[Feature] + + +@dataclass +class Lap: + start_index: int + end_index: int + distance_m: float + coords: List[Coordinate] = field(default_factory=list) diff --git a/src/lap_detection/data/usecase1-luetzelsee.png b/src/lap_detection/data/usecase1-luetzelsee.png new file mode 100644 index 0000000..b431cd1 Binary files /dev/null and b/src/lap_detection/data/usecase1-luetzelsee.png differ diff --git a/src/lap_detection/data/usecase1.json b/src/lap_detection/data/usecase1.json new file mode 100644 index 0000000..df6f7e0 --- /dev/null +++ b/src/lap_detection/data/usecase1.json @@ -0,0 +1,254 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + 8.765906945161646, + 47.258748443007846 + ], + [ + 8.766561810054753, + 47.259064494933625 + ], + [ + 8.767042044310301, + 47.25977560486854 + ], + [ + 8.770243606009103, + 47.26097064310798 + ], + [ + 8.773154116644207, + 47.261770612311835 + ], + [ + 8.773779876431064, + 47.262817467331416 + ], + [ + 8.774842212812928, + 47.26337051445671 + ], + [ + 8.7770542008966, + 47.262955729654294 + ], + [ + 8.779077005788508, + 47.261819992730636 + ], + [ + 8.77968821302224, + 47.26189900130527 + ], + [ + 8.78080875961777, + 47.26096570498896 + ], + [ + 8.779790080895253, + 47.26043238542155 + ], + [ + 8.778887822597824, + 47.25847683437709 + ], + [ + 8.776704939621482, + 47.25818053246326 + ], + [ + 8.775424314941517, + 47.25800275051935 + ], + [ + 8.771378705157787, + 47.25681752230369 + ], + [ + 8.767187569842747, + 47.25661998168874 + ], + [ + 8.765848734950225, + 47.258081764790205 + ], + [ + 8.76619799622648, + 47.258891654270315 + ], + [ + 8.766983834097772, + 47.25985955471677 + ], + [ + 8.769748819201538, + 47.260886695021895 + ], + [ + 8.771960807285268, + 47.26149901918447 + ], + [ + 8.773328747283983, + 47.26185455899676 + ], + [ + 8.773707113666546, + 47.262842157058344 + ], + [ + 8.774929528133953, + 47.26341495549261 + ], + [ + 8.776530308982785, + 47.263158184547166 + ], + [ + 8.778975137916603, + 47.261933567519776 + ], + [ + 8.779760975789031, + 47.261933567519776 + ], + [ + 8.780954285149079, + 47.26102496238727 + ], + [ + 8.779906501320283, + 47.26043238542155 + ], + [ + 8.778858717491573, + 47.25843732755084 + ], + [ + 8.776035522175249, + 47.25816077894348 + ], + [ + 8.773532483027822, + 47.257469401105396 + ], + [ + 8.771931702178989, + 47.25699530822746 + ], + [ + 8.769341347712782, + 47.25665948987063 + ], + [ + 8.767333095374056, + 47.25663973578335 + ], + [ + 8.766634572821516, + 47.25742989352784 + ], + [ + 8.765848734950225, + 47.25810151833983 + ], + [ + 8.766692783034046, + 47.259365730169066 + ], + [ + 8.768148038352706, + 47.26015584723416 + ], + [ + 8.769865239627705, + 47.26082743746883 + ], + [ + 8.771698861327764, + 47.261261991316445 + ], + [ + 8.772513804305333, + 47.26157802823809 + ], + [ + 8.773445167709042, + 47.26185455899676 + ], + [ + 8.773707113666546, + 47.26282240527772 + ], + [ + 8.77498773834651, + 47.26335570076975 + ], + [ + 8.776792254940261, + 47.26319768785049 + ], + [ + 8.778684086853957, + 47.261913815399794 + ], + [ + 8.77917887366155, + 47.261913815399794 + ], + [ + 8.780081131959008, + 47.261736045991285 + ], + [ + 8.78089607493655, + 47.26104471483848 + ], + [ + 8.779935606426562, + 47.260471890758794 + ], + [ + 8.778829612385294, + 47.25855584794044 + ], + [ + 8.776704939621482, + 47.25827929995211 + ], + [ + 8.773328747283983, + 47.25744964732027 + ], + [ + 8.771320494945257, + 47.256758260196676 + ], + [ + 8.769021191542777, + 47.25671875208849 + ], + [ + 8.767216674949026, + 47.25667924395049 + ], + [ + 8.765906945162811, + 47.25806201123356 + ], + [ + 8.766110680907673, + 47.25904968004181 + ] + ], + "type": "LineString" + } + } + ] +} diff --git a/src/lap_detection/data/usecase2-la_stadion.png b/src/lap_detection/data/usecase2-la_stadion.png new file mode 100644 index 0000000..e8a2aaa Binary files /dev/null and b/src/lap_detection/data/usecase2-la_stadion.png differ diff --git a/src/lap_detection/data/usecase2.json b/src/lap_detection/data/usecase2.json new file mode 100644 index 0000000..b14ca3c --- /dev/null +++ b/src/lap_detection/data/usecase2.json @@ -0,0 +1,230 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + 8.680274515612865, + 47.26389838973671 + ], + [ + 8.680175818438386, + 47.264053132092755 + ], + [ + 8.680053297808996, + 47.264117800406325 + ], + [ + 8.679849096758915, + 47.26413858663324 + ], + [ + 8.679621072253923, + 47.264030036247476 + ], + [ + 8.679512165026779, + 47.263729789341 + ], + [ + 8.679399854450054, + 47.26327941578782 + ], + [ + 8.67940325780063, + 47.26311081342081 + ], + [ + 8.679512165026779, + 47.263018428334334 + ], + [ + 8.679712962726285, + 47.262967616467904 + ], + [ + 8.679968214037757, + 47.2630138090758 + ], + [ + 8.680117961474764, + 47.263189340617544 + ], + [ + 8.680165608385664, + 47.263471113717145 + ], + [ + 8.680240482104153, + 47.263815244403105 + ], + [ + 8.680260902209596, + 47.26396536782704 + ], + [ + 8.680169011737263, + 47.264050822508835 + ], + [ + 8.680080524615533, + 47.26412241956851 + ], + [ + 8.679842290057849, + 47.26414089621335 + ], + [ + 8.679631282305564, + 47.2640462033404 + ], + [ + 8.679549601886038, + 47.26386374586352 + ], + [ + 8.679410064501724, + 47.263344085046214 + ], + [ + 8.679386241046757, + 47.263138528915164 + ], + [ + 8.679481534869694, + 47.26304614387709 + ], + [ + 8.679665315814304, + 47.26296299720494 + ], + [ + 8.679937583880672, + 47.26299764166757 + ], + [ + 8.680083927966052, + 47.263131600042726 + ], + [ + 8.680189431841683, + 47.263551949985214 + ], + [ + 8.680271112261238, + 47.26394458153209 + ], + [ + 8.6801962385438, + 47.2640554416769 + ], + [ + 8.679971617388333, + 47.26413858663324 + ], + [ + 8.679801449846963, + 47.26412934831126 + ], + [ + 8.679685735919747, + 47.26408315667672 + ], + [ + 8.679580232044117, + 47.26398384452665 + ], + [ + 8.679471324816973, + 47.26360276129074 + ], + [ + 8.67942708125662, + 47.263376419646136 + ], + [ + 8.679382837695215, + 47.263170863640255 + ], + [ + 8.679539391833316, + 47.26302304759247 + ], + [ + 8.679675525867026, + 47.26296299720494 + ], + [ + 8.679883130267626, + 47.26299995129776 + ], + [ + 8.680032877703553, + 47.26308078828532 + ], + [ + 8.680090734668198, + 47.26315931552659 + ], + [ + 8.68018262514056, + 47.26351730588564 + ], + [ + 8.680240482104153, + 47.26386605545562 + ], + [ + 8.680264305560144, + 47.26398846370054 + ], + [ + 8.680124768175858, + 47.264078537511125 + ], + [ + 8.679944390581795, + 47.26415013453331 + ], + [ + 8.6797299794801, + 47.26409701417123 + ], + [ + 8.679573425341943, + 47.26397460617764 + ], + [ + 8.679508761676175, + 47.26376212370499 + ], + [ + 8.679450904712553, + 47.263494209806254 + ], + [ + 8.679382837695215, + 47.26316162514942 + ], + [ + 8.679508761676175, + 47.26305076313281 + ], + [ + 8.679658509113182, + 47.26296992609943 + ], + [ + 8.679876323565509, + 47.262972235730814 + ] + ], + "type": "LineString" + } + } + ] +} diff --git a/src/lap_detection/data/usecase3.json b/src/lap_detection/data/usecase3.json new file mode 100644 index 0000000..3fe2b7d --- /dev/null +++ b/src/lap_detection/data/usecase3.json @@ -0,0 +1,146 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + 9.834547025245826, + 46.80193545785821 + ], + [ + 9.835708992332172, + 46.802531998995164 + ], + [ + 9.83625366440333, + 46.803004256037525 + ], + [ + 9.836653090589152, + 46.804048177947294 + ], + [ + 9.83708882824638, + 46.805340624627235 + ], + [ + 9.8373793200183, + 46.80653362477142 + ], + [ + 9.838105549447391, + 46.807726598457776 + ], + [ + 9.83908595917572, + 46.80879528158499 + ], + [ + 9.840320549204648, + 46.809689976710644 + ], + [ + 9.840828909804458, + 46.810311284014006 + ], + [ + 9.841192024519046, + 46.81028643185974 + ], + [ + 9.842281368662782, + 46.81051010083516 + ], + [ + 9.843407024276331, + 46.8112059539223 + ], + [ + 9.844278499590757, + 46.81185209515593 + ], + [ + 9.84467792577658, + 46.811379915787256 + ], + [ + 9.845585712562325, + 46.81155387708952 + ], + [ + 9.845948827276885, + 46.81073376888085 + ], + [ + 9.845767269920316, + 46.81016217091576 + ], + [ + 9.84522259784913, + 46.809640271815994 + ], + [ + 9.845004729019848, + 46.80894439847279 + ], + [ + 9.844605302833997, + 46.80894439847279 + ], + [ + 9.844060630762868, + 46.808298222306405 + ], + [ + 9.843298089862373, + 46.80767689174925 + ], + [ + 9.843007598090423, + 46.80755262477646 + ], + [ + 9.844205876648118, + 46.807055554014994 + ], + [ + 9.844205876648118, + 46.806483916960445 + ], + [ + 9.84464161430526, + 46.805440042316604 + ], + [ + 9.84308022103312, + 46.805738294281895 + ], + [ + 9.840901532747182, + 46.805713440014216 + ], + [ + 9.839703254190908, + 46.805564314169374 + ], + [ + 9.840865221275834, + 46.80317824442912 + ], + [ + 9.842499237490642, + 46.80133891010621 + ], + [ + 9.844060630762868, + 46.79915151175959 + ] + ], + "type": "LineString" + } + } + ] +} diff --git a/src/lap_detection/data/usecase3_davos_gelaende.png b/src/lap_detection/data/usecase3_davos_gelaende.png new file mode 100644 index 0000000..91fe690 Binary files /dev/null and b/src/lap_detection/data/usecase3_davos_gelaende.png differ diff --git a/src/lap_detection/data/usecase4-aarau_stadtkurs.png b/src/lap_detection/data/usecase4-aarau_stadtkurs.png new file mode 100644 index 0000000..379c259 Binary files /dev/null and b/src/lap_detection/data/usecase4-aarau_stadtkurs.png differ diff --git a/src/lap_detection/data/usecase4.json b/src/lap_detection/data/usecase4.json new file mode 100644 index 0000000..66a662b --- /dev/null +++ b/src/lap_detection/data/usecase4.json @@ -0,0 +1,150 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + 8.04235788233035, + 47.39493290884846 + ], + [ + 8.042885285759382, + 47.394196547811134 + ], + [ + 8.043511577329639, + 47.393493648131596 + ], + [ + 8.043841204472528, + 47.393627534507715 + ], + [ + 8.044368607901589, + 47.39304735775431 + ], + [ + 8.04623100125653, + 47.39356059136196 + ], + [ + 8.047516547113759, + 47.39120636999834 + ], + [ + 8.04857135396935, + 47.39139605049661 + ], + [ + 8.049543754040798, + 47.3916973063549 + ], + [ + 8.05041726596852, + 47.39192045773251 + ], + [ + 8.05003819475445, + 47.39296925655 + ], + [ + 8.049922825254868, + 47.393962248959525 + ], + [ + 8.049774493041156, + 47.39416307661054 + ], + [ + 8.04880209296968, + 47.394107291228494 + ], + [ + 8.047219882685113, + 47.393817206291374 + ], + [ + 8.046263963970716, + 47.393605220135385 + ], + [ + 8.04657710975647, + 47.39276842434944 + ], + [ + 8.04708803182848, + 47.39186466997646 + ], + [ + 8.0474835843996, + 47.391184054600785 + ], + [ + 8.048258208184848, + 47.39134026218545 + ], + [ + 8.049016350612987, + 47.391552257452844 + ], + [ + 8.049856899826551, + 47.391753094288106 + ], + [ + 8.050433747325599, + 47.39200971801864 + ], + [ + 8.050153564255254, + 47.39256759138394 + ], + [ + 8.049972269326105, + 47.393393233126204 + ], + [ + 8.049856899826551, + 47.39391762048891 + ], + [ + 8.049560235397877, + 47.39415191953893 + ], + [ + 8.0485548726123, + 47.39416307661054 + ], + [ + 8.047153957256796, + 47.393850677712095 + ], + [ + 8.046263963970716, + 47.39361637732273 + ], + [ + 8.045588228329194, + 47.39344901926367 + ], + [ + 8.04600026225745, + 47.39275726698256 + ], + [ + 8.045720079185827, + 47.39214360816524 + ], + [ + 8.044599346900696, + 47.391753094288106 + ] + ], + "type": "LineString" + } + } + ] +} diff --git a/src/lap_detection/lap_detector.py b/src/lap_detection/lap_detector.py new file mode 100644 index 0000000..3487408 --- /dev/null +++ b/src/lap_detection/lap_detector.py @@ -0,0 +1,53 @@ +from haversine import haversine, Unit + +from lap_detection.api import Coordinate + + +def dist(a: Coordinate, b: Coordinate): + return haversine((a.lat, a.lon), (b.lat, b.lon), Unit.METERS) + + +def count_laps(coords: list[Coordinate], start_coord: Coordinate, radius: int) -> int: + """Count the number of laps completed + by tracking the number of times that the starting coordinate was passed by the given coordinate list + Args: + coords (list[Coordinate]): the list of coordinates + start_coord (Coordinate): the coordinate to compare to + radius (int): the radius around the starting coordinate + Returns: + int: the number of laps completed + """ + if not coords: + return 0 + + laps = 0 + + distance_since_last = 0 + + # Do we start inside or outside the start/finish area? + d_to_first = dist(coords[0], start_coord) + inside = d_to_first < radius + + for i in range(1, len(coords)): + prev = coords[i - 1] + curr = coords[i] + + # Don't compare the starting coordinate with itself + if curr == start_coord: + continue + + d = dist(prev, curr) + distance_since_last += d + + # Did we enter the start/finish area? + d_to_start = dist(curr, start_coord) + now_inside = d_to_start < radius + if not inside and now_inside: + # Ignore possible gps stutters with minimum lap distance threshold + if distance_since_last > 100: + laps += 1 + distance_since_last = 0 + + inside = now_inside + + return laps diff --git a/src/lap_detection/main.py b/src/lap_detection/main.py new file mode 100644 index 0000000..e33527d --- /dev/null +++ b/src/lap_detection/main.py @@ -0,0 +1,131 @@ +# TASK: Rundenzählung Stoppuhr + +# Hintergrund: Sportuhren zeichnen oft mittels GPS alle x Sekunden die Koordination (lat/lon) des jeweiligen Standorts auf. +# Ein automatischer Modus detektiert, wenn der User eine Runde (Lap) gelaufen/gefahren ist und nimmt +# automatisch die Zwischenzeit. +# +# Aufgabe: Unsere Aufgabe ist es nun diese Rundendetektion selbst in Python zu schreiben. Der Programmierer bekommt +# eine .json-File mit Koordinaten. Diese sind immer als Liste [lon, lat], also z.B. [[8.5417, 47.3769], +# [8.5434, 47.3772], [8.5446, 47.3783], ...]. Unser Programm soll als Antwort einen Integer mit der +# ermittelten Anzahl Runden zurückgeben, also z.B. 2. +# +# - Die Aufgabe ist bewusst so offen gestellt, damit eure Kreativität nicht eingeschränkt ist. Ihr könnt den +# Schwierigkeitsgrad selbst steuern, wie stark ihr ins Detail wollt, was ihr berücksichtigen wollt und was +# nicht etc. Es geht damit auch nicht so sehr um die technischen Details, sondern darum, denn Code mithilfe +# von Hints und Docstrings möglichst aussagekräftig und selbsterklärend zu machen. Nutzt auch 2-3 Tests mit +# Pytest, um eueren Code 'sicherer' zu machen und zu validieren. +# +# Denkanstösse: - Was macht eine Runde aus? +# - Wie entscheide ich, was eine Runde ist (geometrisch, Randbedingungen) -> 'Kochrezept' machen +# - Wo schränke ich mich ein, wo mache ich Annahmen, ... +# +# Ziel: - Ein anderer User/Programmierer kann eure Funktionen und die darin hints/docstrings/tests lesen und +# versteht was in eurem Code passiert, ohne den eigentlichen Code anschauen zu müssen. +# - Es muss keine Perfektion angestrebt werden, es geht um die praktische Umsetzung der Theorie in einer +# Praxisanwendung! Macht Annahmen/Vereinfachungen! +# - Diejenigen, welche das Beispiel abgeben wollen, machen am besten einen +# - neuen Branch (Aufgabe Rundenzählung) +# - main-Funktion mit der Hauptfunktion, allfällige Helferfunktionen in einem separaten Modul +# - test-Ordner mit 2-3 Pytests +# - ein kurzer Readme mit Erklärung der Funktionsweise eures Rundenzählers und allfällige +# Einschränkungen/Annahmen +# - alle Funktionen mit sauberen Typehints und docstrings (Validation falls nötig) +# -> commit und PR +# +# Hilfsmittel: - für benötigte geometrische Berechnungen dürfen auch KI-Hilfsmittel (Prompt Engineering) genutzt werden! +# - Bitte nützt aber die KI nicht, um das ganze Programm zu erstellen, dokumentieren und testen :-) +# +# Hints: - für eine allfällige Distanz zwischen 2 Punkten -> haversine +# +# Usecases: - Im Ordner 'data' befinden sich 4 usecases mit Koordinaten. +# - Lösungen für die usecases: +# - usecase1: 3 +# - usecase2: 3 (3 3/4) +# - usecase3: 0 +# - usecase4: 2 +# - Schreibt aber auch noch 1-2 eigene Testcases für allfällige wichtige Hilfsfunktionen. + + +from pathlib import Path +import json + +from dacite import from_dict + +from lap_detection.api import Collection, Coordinate +from lap_detection.lap_detector import count_laps, dist + + +def _check_path(path: str) -> Path: + """Checks the existence of the provided path + Args: + path (str): The path + Returns: + a Path object + Raises: + ValueError: If the path is not present + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"{path} does not exist") + return path + + +def _read_file(path: Path) -> Collection: + """Reads the provided file + Args: + path (str): The path of the file + Returns: + an object of type Collection + """ + with path.open("r", encoding="utf-8") as f: + loaded_file = json.load(f) + collection = from_dict(data_class=Collection, data=loaded_file) + return collection + + +def _build_coordinates(collection: Collection) -> list[Coordinate]: + """Build a list of Coordinate objects from a Collection + Args: + collection (Collection): the feature collection + Returns: + a list of Coordinate objects + """ + return [ + Coordinate(coordinate[0], coordinate[1]) + for coordinate in collection.features[0].geometry.coordinates + ] + + +def get_coordinates(path: str) -> list[Coordinate]: + """Validate the path, read the file and extract the coordinates + Args: + path (str): The path of the file + Returns: + a list of Coordinate objects + """ + validated_path = _check_path(path) + data = _read_file(validated_path) + coordinates = _build_coordinates(data) + + return coordinates + + +def main(path: str) -> None: + coords = get_coordinates(path=path) + + # detect laps -> business logic (build functions) + + # Set the radius of the start/finish area based of the minimum distance of two following points + radius = min([dist(coords[i - 1], coords[i]) for i in range(len(coords))]) + + # Count laps from each coordinate + # The maximum indicates that the starting point is in a high traffic area + n_rounds = max([count_laps(coords, start, radius) for start in coords]) + print(f"Anzahl Runden: {n_rounds}") + + +if __name__ == "__main__": + main("data/usecase1.json") + main("data/usecase2.json") + main("data/usecase3.json") + main("data/usecase4.json") diff --git a/tests/lap_detection/test_main.py b/tests/lap_detection/test_main.py new file mode 100644 index 0000000..c6dd668 --- /dev/null +++ b/tests/lap_detection/test_main.py @@ -0,0 +1,57 @@ +import pytest + +from lap_detection.api import Collection, Feature, Geometry, Coordinate +import lap_detection.main as main + + +# arrange +@pytest.fixture +def sample_collection(): + return Collection( + "FeatureCollection", + [ + Feature( + "Feature", + {}, + Geometry([[123.456, 654.321], [987.654, 101.010]], "LineString"), + ) + ], + ) + + +# arrange +@pytest.mark.parametrize( + "path,expected_laps", + [ + ("src/lap_detection/data/usecase1.json", 3), + ("src/lap_detection/data/usecase2.json", 3), + ("src/lap_detection/data/usecase3.json", 0), + ("src/lap_detection/data/usecase4.json", 2), + ], +) +def test_main(path, expected_laps, capsys): + # act + main.main(path) + + # assert + captured = capsys.readouterr() + assert captured.out.strip() == f"Anzahl Runden: {expected_laps}" + + +def test_check_invalid_path(): + # assert + with pytest.raises(FileNotFoundError, match="invalid_path.json does not exist"): + # act + main._check_path("invalid_path.json") + + +def test_build_coordinates(sample_collection): + # act + coordinates = main._build_coordinates(sample_collection) + + # assert + assert isinstance(coordinates, list) + assert len(coordinates) == 2 + assert isinstance(coordinates[1], Coordinate) + assert coordinates[1].lon == 987.654 + assert coordinates[1].lat == 101.010 diff --git a/tests/test_lap_detector.py b/tests/test_lap_detector.py new file mode 100644 index 0000000..d5e62b6 --- /dev/null +++ b/tests/test_lap_detector.py @@ -0,0 +1,54 @@ +from lap_detection.api import Coordinate +from lap_detection.lap_detector import count_laps + + +def test_no_laps(): + # arrange + start = Coordinate(lat=47.0, lon=8.0) + coords = [ + Coordinate(lat=47.0001, lon=8.0001), + Coordinate(lat=47.0002, lon=8.0002), + Coordinate(lat=47.0003, lon=8.0003), + ] + + # act + result = count_laps(coords, start, radius=20) + + # assert + assert result == 0 + + +def test_single_lap(): + # arrange + start = Coordinate(lat=47.0, lon=8.0) + coords = [ + Coordinate(lat=47.0030, lon=8.0000), # far from start + Coordinate(lat=47.0020, lon=8.0010), + Coordinate(lat=47.0010, lon=8.0020), + Coordinate(lat=47.0001, lon=8.0001), # back near start -> lap counted + Coordinate(lat=47.0030, lon=8.0000), # far again -> no second lap + ] + + # act + result = count_laps(coords, start, radius=20) + + # assert + assert result == 1 + + +def test_two_laps(): + # arrange + start = Coordinate(lat=47.0, lon=8.0) + coords = [ + Coordinate(lat=47.0030, lon=8.0000), # far from start + Coordinate(lat=47.0001, lon=8.0001), # back near start -> lap 1 + Coordinate(lat=47.0002, lon=8.0002), # still near start -> don't count twice + Coordinate(lat=47.0030, lon=8.0000), # far again + Coordinate(lat=47.0001, lon=8.0001), # back near start -> lap 2 + ] + + # act + result = count_laps(coords, start, radius=20) + + # assert + assert result == 2