train-journey-visuals/map_display.py
2024-04-17 17:52:25 +02:00

131 lines
4.3 KiB
Python

from typing import Optional
import pygame
from path import Path
from vec import Vec2
class MapDisplay:
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]]):
self.surf: pygame.Surface = 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.cities: list[tuple[Vec2, str, str]] = cities
self.font: pygame.font.Font = pygame.font.SysFont("ubuntu", 20)
self._tooltip_surf: Optional[pygame.Surface] = None
def real_to_screen(self, lon: float, lat: float) -> tuple[float, float]:
x = (lon - self.min_lon) / (self.max_lon - self.min_lon)
y = (lat - self.min_lat) / (self.max_lat - self.min_lat)
w, h = self.surf.get_size()
return x * w, h - y * h
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))
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