Compare commits
	
		
			16 Commits
		
	
	
		
			48011668e5
			...
			452f6ad041
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 452f6ad041 | |||
| 57808d6f2f | |||
| 477f479712 | |||
| 1ddb840e60 | |||
| 0c30f4ea24 | |||
| 369db54b5a | |||
| ca393c6678 | |||
| c5ac5f48ad | |||
| 13ff7dacd8 | |||
| 22bd82fb56 | |||
| 3784973647 | |||
| 68a9d853c5 | |||
| c0063a5d75 | |||
| e32085c4c3 | |||
| 3b2108b7b3 | |||
| a2c9708213 | 
							
								
								
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | # Default ignored files | ||||||
|  | /shelf/ | ||||||
|  | /workspace.xml | ||||||
|  | # Editor-based HTTP Client requests | ||||||
|  | /httpRequests/ | ||||||
|  | # Datasource local storage ignored files | ||||||
|  | /dataSources/ | ||||||
|  | /dataSources.local.xml | ||||||
							
								
								
									
										7
									
								
								.idea/discord.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/discord.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="DiscordProjectSettings"> | ||||||
|  |     <option name="show" value="PROJECT_FILES" /> | ||||||
|  |     <option name="description" value="" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <settings> | ||||||
|  |     <option name="USE_PROJECT_PROFILE" value="false" /> | ||||||
|  |     <version value="1.0" /> | ||||||
|  |   </settings> | ||||||
|  | </component> | ||||||
							
								
								
									
										7
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="Black"> | ||||||
|  |     <option name="sdkName" value="Python 3.10" /> | ||||||
|  |   </component> | ||||||
|  |   <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" /> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/.idea/train-journey-visuals.iml" filepath="$PROJECT_DIR$/.idea/train-journey-visuals.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/train-journey-visuals.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/train-journey-visuals.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <module type="PYTHON_MODULE" version="4"> | ||||||
|  |   <component name="NewModuleRootManager"> | ||||||
|  |     <content url="file://$MODULE_DIR$" /> | ||||||
|  |     <orderEntry type="inheritedJdk" /> | ||||||
|  |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|  |   </component> | ||||||
|  | </module> | ||||||
							
								
								
									
										13
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="CommitMessageInspectionProfile"> | ||||||
|  |     <profile version="1.0"> | ||||||
|  |       <inspection_tool class="SubjectLimit" enabled="true" level="WARNING" enabled_by_default="true"> | ||||||
|  |         <option name="RIGHT_MARGIN" value="50" /> | ||||||
|  |       </inspection_tool> | ||||||
|  |     </profile> | ||||||
|  |   </component> | ||||||
|  |   <component name="VcsDirectoryMappings"> | ||||||
|  |     <mapping directory="$PROJECT_DIR$" vcs="Git" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										43
									
								
								display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import pygame | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Display: | ||||||
|  |     APP_NAME = "Train Journey" | ||||||
|  |  | ||||||
|  |     def __init__(self, surf: pygame.Surface): | ||||||
|  |         self.surf: pygame.Surface = surf | ||||||
|  |         self.font: pygame.font.Font = pygame.font.SysFont("ubuntu", 20) | ||||||
|  |         self._tooltip_surf: Optional[pygame.Surface] = None | ||||||
|  |  | ||||||
|  |     def mainloop(self) -> None: | ||||||
|  |         running = True | ||||||
|  |  | ||||||
|  |         self.init_interactive() | ||||||
|  |  | ||||||
|  |         clock = pygame.time.Clock() | ||||||
|  |         while running: | ||||||
|  |             for event in pygame.event.get(): | ||||||
|  |                 if event.type == pygame.QUIT: | ||||||
|  |                     running = False | ||||||
|  |  | ||||||
|  |                 elif event.type == pygame.KEYDOWN: | ||||||
|  |                     if event.key == pygame.K_ESCAPE: | ||||||
|  |                         running = False | ||||||
|  |  | ||||||
|  |                     elif event.key == pygame.K_s and event.mod & pygame.KMOD_CTRL: | ||||||
|  |                         path = "/tmp/image.jpg" | ||||||
|  |                         pygame.image.save(self.surf, path) | ||||||
|  |                         print(f"Saved as {path}") | ||||||
|  |  | ||||||
|  |             pygame.display.set_caption(f"{self.APP_NAME} - {clock.get_fps():.2f}fps") | ||||||
|  |             self.render() | ||||||
|  |             pygame.display.flip() | ||||||
|  |             clock.tick(30) | ||||||
|  |  | ||||||
|  |     def init_interactive(self) -> None: | ||||||
|  |         self._tooltip_surf = pygame.Surface(self.surf.get_size(), pygame.SRCALPHA) | ||||||
|  |  | ||||||
|  |     def render(self) -> None: | ||||||
|  |         pass | ||||||
							
								
								
									
										56
									
								
								gps_loader.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								gps_loader.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | from units import Value, Unit | ||||||
|  | from vec import Vec2 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GPSLoader: | ||||||
|  |     @staticmethod | ||||||
|  |     def load_data(filename: str) -> dict: | ||||||
|  |         data = { | ||||||
|  |             "times": [], | ||||||
|  |             "points": [], | ||||||
|  |             "altitudes": [], | ||||||
|  |             "altitudes_wgs84": [], | ||||||
|  |             "speeds": [], | ||||||
|  |             "directions": [], | ||||||
|  |             "distances": [], | ||||||
|  |             "accuracies": [], | ||||||
|  |             "satellites": [] | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         # time [s], lat, lon, alt [m], alt_wgs84 [m], speed [m/s], dir [°], dist [km], x_acc [m], y_acc [m], sats | ||||||
|  |  | ||||||
|  |         with open(filename, "r") as f: | ||||||
|  |             content = f.read() | ||||||
|  |             lines = content.splitlines() | ||||||
|  |             headers, lines = lines[0], lines[1:] | ||||||
|  |  | ||||||
|  |         for line in lines: | ||||||
|  |             values = line.split(",") | ||||||
|  |             values = list(map(lambda v: None if v == "NaN" else float(v), values)) | ||||||
|  |  | ||||||
|  |             if None in values: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             time = Value(values[0], Unit.SEC) | ||||||
|  |             lat = Value(values[1], Unit.DEG) | ||||||
|  |             lon = Value(values[2], Unit.DEG) | ||||||
|  |             alt = Value(values[3], Unit.M) | ||||||
|  |             alt_wgs84 = Value(values[4], Unit.M) | ||||||
|  |             speed = Value(values[5], Unit.M_S) | ||||||
|  |             dir_ = Value(values[6], Unit.DEG) | ||||||
|  |             dist = Value(values[7], Unit.KM) | ||||||
|  |             x_acc = Value(values[8], Unit.M) | ||||||
|  |             y_acc = Value(values[9], Unit.M) | ||||||
|  |             sats = Value(values[10], Unit.NONE) | ||||||
|  |  | ||||||
|  |             data["times"].append(time) | ||||||
|  |             data["points"].append(Vec2(lon.value, lat.value)) | ||||||
|  |             data["altitudes"].append(alt) | ||||||
|  |             data["altitudes_wgs84"].append(alt_wgs84) | ||||||
|  |             data["speeds"].append(speed) | ||||||
|  |             data["directions"].append(dir_) | ||||||
|  |             data["distances"].append(dist) | ||||||
|  |             data["accuracies"].append(Vec2(x_acc.value, y_acc.value)) | ||||||
|  |             data["satellites"].append(sats.value) | ||||||
|  |  | ||||||
|  |         return data | ||||||
							
								
								
									
										110
									
								
								map_display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								map_display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import pygame | ||||||
|  |  | ||||||
|  | from display import Display | ||||||
|  | from path import Path | ||||||
|  | from vec import Vec2 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MapDisplay(Display): | ||||||
|  |     PATH_WIDTH = 5 | ||||||
|  |     SEGMENT_SIZE = 20 | ||||||
|  |     MIN_SEGMENT_LENGTH = 5 | ||||||
|  |     APP_NAME = "Train Journey - Map Display" | ||||||
|  |  | ||||||
|  |     def __init__(self, | ||||||
|  |                  surf: pygame.Surface, | ||||||
|  |                  min_lon: float, | ||||||
|  |                  max_lon: float, | ||||||
|  |                  min_lat: float, | ||||||
|  |                  max_lat: float, | ||||||
|  |                  cities: list[tuple[Vec2, str, str]]): | ||||||
|  |  | ||||||
|  |         super().__init__(surf) | ||||||
|  |         self.min_lon: float = min_lon | ||||||
|  |         self.max_lon: float = max_lon | ||||||
|  |         self.min_lat: float = min_lat | ||||||
|  |         self.max_lat: float = max_lat | ||||||
|  |         self._precomputeDisplayValues() | ||||||
|  |         self.cities: list[tuple[Vec2, str, str]] = cities | ||||||
|  |  | ||||||
|  |     def _precomputeDisplayValues(self) -> None: | ||||||
|  |         width, height = self.surf.get_size() | ||||||
|  |         self._lon_span = self.max_lon - self.min_lon | ||||||
|  |         self._lat_span = self.max_lat - self.min_lat | ||||||
|  |         r1 = width / self._lon_span | ||||||
|  |         r2 = height / self._lat_span | ||||||
|  |  | ||||||
|  |         self._r = min(r1, r2) | ||||||
|  |  | ||||||
|  |         self._ox = (width - self._lon_span * self._r) / 2 | ||||||
|  |         self._oy = (height - self._lat_span * self._r) / 2 | ||||||
|  |  | ||||||
|  |     def real_to_screen(self, lon: float, lat: float) -> tuple[float, float]: | ||||||
|  |         x = (lon - self.min_lon) * self._r + self._ox | ||||||
|  |         y = (lat - self.min_lat) * self._r + self._oy | ||||||
|  |  | ||||||
|  |         return x, self.surf.get_height() - y | ||||||
|  |  | ||||||
|  |     def draw_path(self, path: Path) -> None: | ||||||
|  |         self.draw_colored_path(path, None) | ||||||
|  |  | ||||||
|  |     def draw_colored_path(self, path: Path, colors: Optional[list[tuple[int, int, int]]] = None) -> None: | ||||||
|  |         for i, pt in enumerate(path.points): | ||||||
|  |             lon = pt.x | ||||||
|  |             lat = pt.y | ||||||
|  |             x, y = self.real_to_screen(lon, lat) | ||||||
|  |             col = (255, 255, 255) if colors is None else colors[i] | ||||||
|  |             pygame.draw.circle(self.surf, col, (x, y), self.PATH_WIDTH) | ||||||
|  |  | ||||||
|  |     def draw_segment(self, path: Path, start_i: int, end_i: int) -> None: | ||||||
|  |         if end_i - start_i < self.MIN_SEGMENT_LENGTH: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         points = [] | ||||||
|  |         for i in range(start_i, end_i): | ||||||
|  |             pt = path.points[i] | ||||||
|  |             pt = Vec2(*self.real_to_screen(pt.x, pt.y)) | ||||||
|  |             n = path.normals[i] | ||||||
|  |             n = Vec2(n.x, -n.y) | ||||||
|  |             pt1 = pt + n * self.SEGMENT_SIZE | ||||||
|  |             pt2 = pt - n * self.SEGMENT_SIZE | ||||||
|  |             points.insert(0, (pt1.x, pt1.y)) | ||||||
|  |             points.append((pt2.x, pt2.y)) | ||||||
|  |  | ||||||
|  |         pygame.draw.lines(self.surf, (255, 255, 255), True, points) | ||||||
|  |  | ||||||
|  |     def draw_cities(self) -> None: | ||||||
|  |         for city in self.cities: | ||||||
|  |             self.draw_city(*city) | ||||||
|  |  | ||||||
|  |     def draw_city(self, pos: Vec2, name: str, label_side: str) -> None: | ||||||
|  |         pos2 = Vec2(*self.real_to_screen(pos.x, pos.y)) | ||||||
|  |  | ||||||
|  |         pygame.draw.circle(self.surf, (180, 180, 180), (pos2.x, pos2.y), 10) | ||||||
|  |  | ||||||
|  |         label = self.font.render(name, True, (255, 255, 255)) | ||||||
|  |         label_size = Vec2(*label.get_size()) | ||||||
|  |  | ||||||
|  |         line_end = pos2 | ||||||
|  |         label_pos = line_end - label_size / 2 | ||||||
|  |  | ||||||
|  |         if label_side == "above": | ||||||
|  |             line_end -= Vec2(0, 20) | ||||||
|  |             label_pos = line_end - label_size.scale(Vec2(0.5, 1)) | ||||||
|  |  | ||||||
|  |         elif label_side == "below": | ||||||
|  |             line_end += Vec2(0, 20) | ||||||
|  |             label_pos = line_end - label_size.scale(Vec2(0.5, 0)) | ||||||
|  |  | ||||||
|  |         elif label_side == "left": | ||||||
|  |             line_end -= Vec2(20, 0) | ||||||
|  |             label_pos = line_end - label_size.scale(Vec2(1, 0.5)) | ||||||
|  |  | ||||||
|  |         elif label_side == "right": | ||||||
|  |             line_end += Vec2(20, 0) | ||||||
|  |             label_pos = line_end - label_size.scale(Vec2(0, 0.5)) | ||||||
|  |  | ||||||
|  |         pygame.draw.line(self.surf, (255, 255, 255), (pos2.x, pos2.y), (line_end.x, line_end.y)) | ||||||
|  |         self.surf.blit(label, (label_pos.x, label_pos.y)) | ||||||
							
								
								
									
										29
									
								
								path.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								path.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | from vec import Vec2 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Path: | ||||||
|  |     def __init__(self, points: list[Vec2], extra_data: list): | ||||||
|  |         self.points: list[Vec2] = points | ||||||
|  |         self.extra_data = extra_data | ||||||
|  |         self.normals: list[Vec2] = [] | ||||||
|  |         self._init_normals() | ||||||
|  |  | ||||||
|  |     def _init_normals(self) -> None: | ||||||
|  |         pt0 = self.points[0] | ||||||
|  |         self.normals.append(Vec2(pt0.y, -pt0.x)) | ||||||
|  |         for i in range(1, len(self.points) - 1): | ||||||
|  |             pt1 = self.points[i-1] | ||||||
|  |             pt2 = self.points[i] | ||||||
|  |             pt3 = self.points[i+1] | ||||||
|  |             d1 = (pt1 - pt2).normalized() | ||||||
|  |             d2 = (pt3 - pt2).normalized() | ||||||
|  |  | ||||||
|  |             d = (d1 + d2).normalized() | ||||||
|  |  | ||||||
|  |             if d2.cross(d) < 0: | ||||||
|  |                 d = -d | ||||||
|  |  | ||||||
|  |             self.normals.append(d) | ||||||
|  |  | ||||||
|  |         ptl = self.points[-1] | ||||||
|  |         self.normals.append(Vec2(ptl.y, -ptl.x)) | ||||||
							
								
								
									
										127
									
								
								speed_map_display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								speed_map_display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | |||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | import pygame | ||||||
|  |  | ||||||
|  | from gps_loader import GPSLoader | ||||||
|  | from map_display import MapDisplay | ||||||
|  | from path import Path | ||||||
|  | from units import Unit | ||||||
|  | from vec import Vec2 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SpeedMapDisplay(MapDisplay): | ||||||
|  |     APP_NAME = "Train Journey - Speed Map Display" | ||||||
|  |  | ||||||
|  |     def __init__(self, | ||||||
|  |                  surf: pygame.Surface, | ||||||
|  |                  min_lon: float, | ||||||
|  |                  max_lon: float, | ||||||
|  |                  min_lat: float, | ||||||
|  |                  max_lat: float, | ||||||
|  |                  cities: list[tuple[Vec2, str, str]], | ||||||
|  |                  min_speed_col: tuple[int, int, int], | ||||||
|  |                  max_speed_col: tuple[int, int, int], | ||||||
|  |                  segment_threshold: float): | ||||||
|  |         super().__init__(surf, min_lon, max_lon, min_lat, max_lat, cities) | ||||||
|  |         self.min_speed_col: tuple[int, int, int] = min_speed_col | ||||||
|  |         self.max_speed_col: tuple[int, int, int] = max_speed_col | ||||||
|  |         self.segment_threshold: float = segment_threshold | ||||||
|  |  | ||||||
|  |         self._path: Optional[Path] = None | ||||||
|  |  | ||||||
|  |     def draw_path(self, path: Path) -> None: | ||||||
|  |         min_speed = min(path.extra_data) | ||||||
|  |         max_speed = max(path.extra_data) | ||||||
|  |  | ||||||
|  |         colors = list(map(lambda s: self.interpolate_color(s, min_speed, max_speed), path.extra_data)) | ||||||
|  |  | ||||||
|  |         self.draw_colored_path(path, colors) | ||||||
|  |  | ||||||
|  |         in_segment = False | ||||||
|  |         start_i = 0 | ||||||
|  |         for i, speed in enumerate(path.extra_data): | ||||||
|  |             if speed >= self.segment_threshold: | ||||||
|  |                 if not in_segment: | ||||||
|  |                     in_segment = True | ||||||
|  |                     start_i = i | ||||||
|  |  | ||||||
|  |             elif in_segment: | ||||||
|  |                 in_segment = False | ||||||
|  |                 self.draw_segment(path, start_i, i) | ||||||
|  |  | ||||||
|  |     def interpolate_color(self, speed: float, min_speed: float, max_speed: float) -> tuple[int, int, int]: | ||||||
|  |         r_span = self.max_speed_col[0] - self.min_speed_col[0] | ||||||
|  |         g_span = self.max_speed_col[1] - self.min_speed_col[1] | ||||||
|  |         b_span = self.max_speed_col[2] - self.min_speed_col[2] | ||||||
|  |  | ||||||
|  |         f = (speed - min_speed) / (max_speed - min_speed) | ||||||
|  |         r = int(r_span * f + self.min_speed_col[0]) | ||||||
|  |         g = int(g_span * f + self.min_speed_col[1]) | ||||||
|  |         b = int(b_span * f + self.min_speed_col[2]) | ||||||
|  |  | ||||||
|  |         return r, g, b | ||||||
|  |  | ||||||
|  |     def render(self) -> None: | ||||||
|  |         self.surf.fill((0, 0, 0)) | ||||||
|  |         self._tooltip_surf.fill((0, 0, 0, 0)) | ||||||
|  |         self.draw_cities() | ||||||
|  |  | ||||||
|  |         if self._path is not None: | ||||||
|  |             self.draw_path(self._path) | ||||||
|  |             mpos = Vec2(*pygame.mouse.get_pos()) | ||||||
|  |             self.tooltip_nearest(mpos) | ||||||
|  |             self.surf.blit(self._tooltip_surf, (0, 0)) | ||||||
|  |  | ||||||
|  |     def set_path(self, path: Path) -> None: | ||||||
|  |         self._path = path | ||||||
|  |  | ||||||
|  |     def tooltip_nearest(self, mpos: Vec2) -> None: | ||||||
|  |         closest = None | ||||||
|  |         closest_i = 0 | ||||||
|  |         min_dist = 0 | ||||||
|  |  | ||||||
|  |         for i, pt in enumerate(self._path.points): | ||||||
|  |             pt = Vec2(*self.real_to_screen(pt.x, pt.y)) | ||||||
|  |             dist = (pt - mpos).mag | ||||||
|  |             if i == 0 or dist < min_dist: | ||||||
|  |                 closest = pt | ||||||
|  |                 closest_i = i | ||||||
|  |                 min_dist = dist | ||||||
|  |  | ||||||
|  |         pt = closest | ||||||
|  |         speed = self._path.extra_data[closest_i] | ||||||
|  |         col = (200, 150, 50) | ||||||
|  |         pygame.draw.circle(self.surf, col, (pt.x, pt.y), 2 * self.PATH_WIDTH, 2) | ||||||
|  |         pygame.draw.line(self.surf, col, (pt.x, pt.y), (mpos.x, mpos.y), 2) | ||||||
|  |         txt = self.font.render(f"{speed:.1f} km/h", True, (150, 200, 255)) | ||||||
|  |         txt_size = Vec2(*txt.get_size()) | ||||||
|  |         txt_pos = mpos - txt_size.scale(Vec2(0.5, 1)) | ||||||
|  |         pygame.draw.rect(self._tooltip_surf, (0, 0, 0, 150), (txt_pos.x, txt_pos.y, txt_size.x, txt_size.y)) | ||||||
|  |         self._tooltip_surf.blit(txt, (txt_pos.x, txt_pos.y)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     name = "data_28-03" | ||||||
|  |     cities = [ | ||||||
|  |         (Vec2(7.359119, 46.227302), "Sion", "above"), | ||||||
|  |         (Vec2(7.079001, 46.105981), "Martigny", "below"), | ||||||
|  |         (Vec2(7.001849, 46.216559), "Saint-Maurice", "right") | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     data = GPSLoader.load_data(f"{name}.csv") | ||||||
|  |     speeds = list(map(lambda s: s.convert(Unit.KM_H).value, data["speeds"])) | ||||||
|  |     path = Path(data["points"], speeds) | ||||||
|  |  | ||||||
|  |     pygame.init() | ||||||
|  |  | ||||||
|  |     win = pygame.display.set_mode([600, 600]) | ||||||
|  |     display = SpeedMapDisplay( | ||||||
|  |         win, | ||||||
|  |         6.971094, 7.430611, | ||||||
|  |         46.076312, 46.253036, | ||||||
|  |         cities, | ||||||
|  |         (255, 0, 0), (0, 255, 0), 155) | ||||||
|  |  | ||||||
|  |     display.set_path(path) | ||||||
|  |  | ||||||
|  |     display.mainloop() | ||||||
							
								
								
									
										109
									
								
								units.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								units.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | |||||||
|  | from enum import Enum, auto | ||||||
|  | from math import pi | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UnitClass(Enum): | ||||||
|  |     NONE = auto() | ||||||
|  |     LENGTH = auto() | ||||||
|  |     SPEED = auto() | ||||||
|  |     TIME = auto() | ||||||
|  |     ANGLE = auto() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Unit(Enum): | ||||||
|  |     KM_H = auto(), UnitClass.SPEED | ||||||
|  |     M_S = auto(), UnitClass.SPEED | ||||||
|  |     KM = auto(), UnitClass.LENGTH | ||||||
|  |     M = auto(), UnitClass.LENGTH | ||||||
|  |     SEC = auto(), UnitClass.TIME | ||||||
|  |     MIN = auto(), UnitClass.TIME | ||||||
|  |     HOUR = auto(), UnitClass.TIME | ||||||
|  |     DEG = auto(), UnitClass.ANGLE | ||||||
|  |     RAD = auto(), UnitClass.ANGLE | ||||||
|  |     NONE = auto(), UnitClass.NONE | ||||||
|  |  | ||||||
|  |     def __init__(self, v: int, cls: UnitClass): | ||||||
|  |         self.cls: UnitClass = cls | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Value: | ||||||
|  |     def __init__(self, value: float, unit: Unit): | ||||||
|  |         self.value: float = value | ||||||
|  |         self.unit: Unit = unit | ||||||
|  |  | ||||||
|  |     def convert(self, to_unit: Unit) -> "Value": | ||||||
|  |         if self.unit == to_unit: | ||||||
|  |             return self | ||||||
|  |  | ||||||
|  |         from_cls = self.unit.cls | ||||||
|  |         to_cls = to_unit.cls | ||||||
|  |  | ||||||
|  |         if from_cls != to_cls: | ||||||
|  |             raise ValueError(f"Cannot convert from {from_cls} to {to_cls}") | ||||||
|  |  | ||||||
|  |         cls = from_cls | ||||||
|  |  | ||||||
|  |         if cls == UnitClass.NONE: | ||||||
|  |             to_value = self.value | ||||||
|  |  | ||||||
|  |         elif cls == UnitClass.LENGTH: | ||||||
|  |             value = self.value | ||||||
|  |             if self.unit == Unit.KM: | ||||||
|  |                 value *= 1000 | ||||||
|  |  | ||||||
|  |             to_value = value | ||||||
|  |  | ||||||
|  |             if to_unit == Unit.KM: | ||||||
|  |                 to_value /= 1000 | ||||||
|  |  | ||||||
|  |         elif cls == UnitClass.TIME: | ||||||
|  |             value = self.value | ||||||
|  |             if self.unit == Unit.MIN: | ||||||
|  |                 value *= 60 | ||||||
|  |             elif self.unit == Unit.HOUR: | ||||||
|  |                 value *= 3600 | ||||||
|  |  | ||||||
|  |             to_value = value | ||||||
|  |  | ||||||
|  |             if to_unit == Unit.MIN: | ||||||
|  |                 to_value /= 60 | ||||||
|  |             elif self.unit == Unit.HOUR: | ||||||
|  |                 to_value /= 3600 | ||||||
|  |  | ||||||
|  |         elif cls == UnitClass.SPEED: | ||||||
|  |             value = self.value | ||||||
|  |             if self.unit == Unit.KM_H: | ||||||
|  |                 value /= 3.6 | ||||||
|  |  | ||||||
|  |             to_value = value | ||||||
|  |  | ||||||
|  |             if to_unit == Unit.KM_H: | ||||||
|  |                 to_value *= 3.6 | ||||||
|  |  | ||||||
|  |         elif cls == UnitClass.ANGLE: | ||||||
|  |             value = self.value | ||||||
|  |             if self.unit == Unit.RAD: | ||||||
|  |                 value *= 180 / pi | ||||||
|  |  | ||||||
|  |             to_value = value | ||||||
|  |  | ||||||
|  |             if to_unit == Unit.RAD: | ||||||
|  |                 value *= pi / 180 | ||||||
|  |  | ||||||
|  |         else: | ||||||
|  |             raise ValueError(f"Unknown unit class: {cls}") | ||||||
|  |  | ||||||
|  |         return Value(to_value, to_unit) | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         return f"{self.value} [{self.unit}]" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     v1 = Value(765, Unit.KM_H) | ||||||
|  |     print(v1) | ||||||
|  |     print(v1.convert(Unit.M_S)) | ||||||
|  |  | ||||||
|  |     v2 = Value(120, Unit.SEC) | ||||||
|  |     print(v2) | ||||||
|  |     print(v2.convert(Unit.MIN)) | ||||||
							
								
								
									
										38
									
								
								vec.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								vec.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | from math import sqrt | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Vec2: | ||||||
|  |     def __init__(self, x: float = 0, y: float = 0): | ||||||
|  |         self.x: float = x | ||||||
|  |         self.y: float = y | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def mag(self) -> float: | ||||||
|  |         return sqrt(self.x ** 2 + self.y ** 2) | ||||||
|  |  | ||||||
|  |     def normalized(self) -> "Vec2": | ||||||
|  |         mag = self.mag | ||||||
|  |         if mag == 0: | ||||||
|  |             return Vec2() | ||||||
|  |         return self / mag | ||||||
|  |  | ||||||
|  |     def __add__(self, v: "Vec2") -> "Vec2": | ||||||
|  |         return Vec2(self.x + v.x, self.y + v.y) | ||||||
|  |  | ||||||
|  |     def __sub__(self, v: "Vec2") -> "Vec2": | ||||||
|  |         return Vec2(self.x - v.x, self.y - v.y) | ||||||
|  |  | ||||||
|  |     def __mul__(self, f: float) -> "Vec2": | ||||||
|  |         return Vec2(self.x * f, self.y * f) | ||||||
|  |  | ||||||
|  |     def __truediv__(self, f: float) -> "Vec2": | ||||||
|  |         return Vec2(self.x / f, self.y / f) | ||||||
|  |  | ||||||
|  |     def __neg__(self) -> "Vec2": | ||||||
|  |         return Vec2(-self.x, -self.y) | ||||||
|  |  | ||||||
|  |     def cross(self, v: "Vec2") -> float: | ||||||
|  |         return self.x * v.y - self.y * v.x | ||||||
|  |  | ||||||
|  |     def scale(self, v: "Vec2") -> "Vec2": | ||||||
|  |         return Vec2(self.x * v.x, self.y * v.y) | ||||||
		Reference in New Issue
	
	Block a user