Compare commits
No commits in common. "452f6ad0413244d548ff62b2bc84b233c58bb1c0" and "48011668e5fc9c9f429697be81d25352d5cb04b8" have entirely different histories.
452f6ad041
...
48011668e5
8
.idea/.gitignore
vendored
8
.idea/.gitignore
vendored
@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -1,7 +0,0 @@
|
||||
<?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>
|
@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
@ -1,7 +0,0 @@
|
||||
<?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>
|
@ -1,8 +0,0 @@
|
||||
<?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>
|
@ -1,8 +0,0 @@
|
||||
<?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>
|
@ -1,13 +0,0 @@
|
||||
<?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
43
display.py
@ -1,43 +0,0 @@
|
||||
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
|
@ -1,56 +0,0 @@
|
||||
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
110
map_display.py
@ -1,110 +0,0 @@
|
||||
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
29
path.py
@ -1,29 +0,0 @@
|
||||
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))
|
@ -1,127 +0,0 @@
|
||||
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
109
units.py
@ -1,109 +0,0 @@
|
||||
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
38
vec.py
@ -1,38 +0,0 @@
|
||||
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