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
vendored
Normal file
8
.idea/.gitignore
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
Normal file
7
.idea/discord.xml
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
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
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
Normal file
7
.idea/misc.xml
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
Normal file
8
.idea/modules.xml
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
Normal file
8
.idea/train-journey-visuals.iml
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
Normal file
13
.idea/vcs.xml
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)
|
Loading…
Reference in New Issue
Block a user