Compare commits
	
		
			17 Commits
		
	
	
		
			48011668e5
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7c3700168a | |||
| 452f6ad041 | |||
| 57808d6f2f | |||
| 477f479712 | |||
| 1ddb840e60 | |||
| 0c30f4ea24 | |||
| 369db54b5a | |||
| ca393c6678 | |||
| c5ac5f48ad | |||
| 13ff7dacd8 | |||
| 22bd82fb56 | |||
| 3784973647 | |||
| 68a9d853c5 | |||
| c0063a5d75 | |||
| e32085c4c3 | |||
| 3b2108b7b3 | |||
| a2c9708213 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -160,3 +160,4 @@ cython_debug/ | ||||
| #  option (not recommended) you can uncomment the following to ignore the entire idea folder. | ||||
| #.idea/ | ||||
|  | ||||
| data/ | ||||
							
								
								
									
										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"data/{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