feat: map of route in /info embed

This commit is contained in:
Kaaninchen
2026-08-19 22:16:56 +02:00
parent cd2d968545
commit 714841df22
11 changed files with 173 additions and 19 deletions
+15 -7
View File
@@ -249,7 +249,7 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
trip_details = {
"long_name": long_name,
"short_name": display_name,
"short_name": train_name,
"station": from_station,
"from": train_from,
"to": goes_to,
@@ -266,7 +266,9 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
train_from_importance = legs["from"]["importance"]
trip_details["stops"][train_from] = {
"arrival": departure_dt,
"importance": train_from_importance
"importance": train_from_importance,
"lat": legs["from"]["lat"],
"lon": legs["from"]["lon"]
}
departure_time_iso = start_time
@@ -276,22 +278,28 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
stop_importance = stop["importance"]
stop_details = {
"arrival": stop_arrival_dt,
"importance": stop_importance
"importance": stop_importance,
"lat": stop["lat"],
"lon": stop["lon"]
}
trip_details["stops"][stop["name"]] = stop_details
if stop.get("name") == from_station:
departure_time_iso = stop["departure"]
trip_details["departure"] = parse_iso(departure_time_iso).strftime("%H:%M")
departure_time = parse_iso(stop["departure"])
trip_details["departure_dt"] = departure_time
trip_details["departure"] = departure_time.strftime("%H:%M")
train_to_importance = legs["to"]["importance"]
trip_details["stops"][goes_to] = {
"arrival": arrival_dt,
"importance": train_to_importance
"importance": train_to_importance,
"lat": legs["to"]["lat"],
"lon": legs["to"]["lon"]
}
valid = validate_connection(start_time, end_time, departure_time_iso)
if not valid:
return None
return trip_details
+1 -1
View File
@@ -4,7 +4,7 @@ channel:
train_via: "{train_name} nach {goes_to} über {from_station}"
embeds:
footer:
notice: "Daten großzügig bereitgestellt von https://transitous.org"
notice: "Daten bereitgestellt von https://transitous.org • Maps (C) CARTO (C) OpenStreetMap.org contributors"
info:
description: "Abfahrt von {station} um {departure}. Ankunft um {arrival}"
via: "Über"
+1 -1
View File
@@ -4,7 +4,7 @@ channel:
train_via: "{train_name} to {goes_to} via {from_station}"
embeds:
footer:
notice: "Data provided by https://transitous.org"
notice: "Data provided by https://transitous.org • Maps (C) CARTO (C) OpenStreetMap.org contributors"
info:
description: "Departure from {station} at {departure}. Arrival by {arrival}"
via: "via"
+4
View File
@@ -92,6 +92,10 @@ OPERATORS = {
"color": 0x812B6D,
"slogan": ["Pour nous tous", "Donner au train des idées d'avance"]
},
"Stadtverkehr Tübingen GmbH": {
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Stadtwerke_T%C3%BCbingen_logo.svg/330px-Stadtwerke_T%C3%BCbingen_logo.svg.png",
"color": 0x439669
},
"Tallink Grupp AS": {
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Tallink_logo.svg/330px-Tallink_logo.svg.png",
"color": 0x225197
+3 -1
View File
@@ -4,4 +4,6 @@ from src.dc.embeds import build_info_embed
def setup_commands(bot: discord.Bot):
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
async def info(ctx):
await ctx.respond(embed=build_info_embed())
await ctx.defer()
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
await ctx.respond(file=image, embed=build_info_embed())
+1
View File
@@ -61,6 +61,7 @@ def build_info_embed() -> discord.Embed:
embed.set_author(name=agency)
embed.set_thumbnail(url=metadata["logo"])
embed.set_image(url="attachment://ride.png")
return embed
def build_announcement_embed(msg):
+17 -6
View File
@@ -3,7 +3,7 @@ import asyncio
import random
from datetime import datetime, timedelta
from src.utils import logger, channel_formatting, choose_connection, get_sound_path, LOCAL_TZ, get_next_station
from src.utils import logger, channel_formatting, choose_connection, get_sound_path, LOCAL_TZ, get_next_station, generate_static_map
from src.config import config
from src.lang.locales import lang
@@ -26,18 +26,25 @@ async def rename_vc(bot: discord.Bot, voice_channel, from_scheduler: bool = Fals
if trip is None:
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
return False
generate_static_map(trip["stops"], trip["mode"], trip["agency"], trip["route_color"])
arrival = trip["arrival"]
long_name = trip["long_name"]
if len(trip["long_name"]) >= 100:
channel_name = trip["short_name"]
else:
channel_name = trip["long_name"]
mode = trip["mode"]
print("-----------------")
logger(f"Transfer: {long_name}; Arrival: {arrival}")
logger(f"Transfer: {channel_name}; Arrival: {arrival}")
logger(f"Agency: {trip["agency"]}, mode: {mode}")
logger(f"Trying to change the channels name. If nothing happens, then the bot is in cooldown... (automatically resolves after up to 10min)")
formatting = channel_formatting(mode)
await voice_channel.edit(name=f"{formatting}{long_name}")
await voice_channel.edit(name=f"{formatting}{channel_name}")
await voice_channel.set_status(None)
start_next_stop_updates(voice_channel)
@@ -59,12 +66,16 @@ async def announcer(announcement: str, voice_channel: discord.VoiceChannel, dest
embed = build_announcement_embed(lang.embeds.announcement.end_of_connection.message())
case "transfer":
embed = build_info_embed()
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
case _:
logger(f"Unknown announcement: {announcement}")
embed = None
if embed:
await voice_channel.send(embed=embed)
if image:
await voice_channel.send(file=image, embed=embed)
else:
await voice_channel.send(embed=embed)
async def voice_announcer(station: str, voice_channel: discord.VoiceChannel) -> bool:
sound_path = get_sound_path(station=station)
@@ -145,7 +156,7 @@ async def _update_next_loop(voice_channel: discord.VoiceChannel):
wait_seconds = (next_stop["arrival"] - datetime.now(LOCAL_TZ)).total_seconds()
if wait_seconds > 0:
await asyncio.sleep(wait_seconds)
await (wait_seconds)
except asyncio.CancelledError:
raise
+127 -1
View File
@@ -1,8 +1,8 @@
import os
import importlib
import random
from pathlib import Path
from datetime import datetime, timedelta, timezone
import staticmaps
from src.config import config
from zoneinfo import ZoneInfo
@@ -202,3 +202,129 @@ def format_stop_list(stops: dict, next_stop: str | None) -> list[tuple[str, str]
fields.append((route_page_name, "\n".join(field_lines)))
return fields
def generate_static_map(stops: dict, mode: str, operator: str, route_color: str):
logger("Generating a new route map..")
operator_metadata = get_operator_metadata(operator, route_color, mode)
operator_color = operator_metadata["color"]
if operator_color == 0xFFFFFF:
stop_color = staticmaps.Color(105, 105, 105, 255)
else:
stop_color = staticmaps.parse_color(f"#{operator_color:06x}")
white = staticmaps.Color(255, 255, 255, 255)
context = staticmaps.Context()
context.set_tile_provider(
staticmaps.TileProvider(
"carto-voyager",
url_pattern=(
"https://$s.basemaps.cartocdn.com/"
"rastertiles/voyager/$z/$x/$y.png"
),
shards=["a", "b", "c", "d"],
attribution="",
)
# It is against the law (and against your morals...) to not give
# OSM and carto credits for their great work
# I only removed the attribution text because I really dislike the
# ugly white box that py-staticmaps adds. Credits are still visible in the
# embeds footer
)
stop_coords = [
staticmaps.create_latlng(
stop["lat"],
stop["lon"],
)
for stop in stops.values()
]
context.add_object(
staticmaps.Line(
stop_coords,
color=white,
width=20,
)
)
context.add_object(
staticmaps.Line(
stop_coords,
color=stop_color,
width=8,
)
)
last_index = len(stop_coords) - 1
for i, point in enumerate(stop_coords):
if i == 0 or i == last_index:
context.add_object(
staticmaps.Circle(
point,
radius_km=0.1,
fill_color=white,
color=stop_color,
width=12
)
)
if i == last_index:
context.add_object(
staticmaps.Marker(
point,
color=stop_color,
size=12
)
)
elif mode not in ["TRAM", "BUS", "SUBWAY", "SUBURBAN", "METRO"]: # wayyy too many stops...
context.add_object(
staticmaps.Circle(
center=point,
fill_color=white,
radius_km=0.1,
color=stop_color,
width=10,
)
)
lats = [c.lat().degrees for c in stop_coords]
lons = [c.lng().degrees for c in stop_coords]
lat_span = max(lats) - min(lats)
lon_span = max(lons) - min(lons)
MIN_SPAN = 0.01
if lat_span < MIN_SPAN or lon_span < MIN_SPAN:
center_lat = (max(lats) + min(lats)) / 2
center_lon = (max(lons) + min(lons)) / 2
pad = MIN_SPAN / 2
context.add_object(
staticmaps.Circle(
staticmaps.create_latlng(center_lat + pad, center_lon + pad),
radius_km=0.01,
fill_color=staticmaps.TRANSPARENT,
color=staticmaps.TRANSPARENT,
width=0,
)
)
context.add_object(
staticmaps.Circle(
staticmaps.create_latlng(center_lat - pad, center_lon - pad),
radius_km=0.01,
fill_color=staticmaps.TRANSPARENT,
color=staticmaps.TRANSPARENT,
width=0,
)
)
try:
image = context.render_cairo(400, 300)
image.write_to_png("src/data/assets/current_map.png")
except RuntimeError:
logger(f"You don't have cairo installed! Because of that I can only give you a low-res png of the route...")
image = context.render_pillow(400, 300)
image.save("src/data/assets/current_map.png")
logger("Map generated!")