mirror of
https://github.com/kaaninchen/Gleiswechsel.git
synced 2026-09-17 16:52:47 +00:00
feat: map of route in /info embed
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
src/data/announcements/*
|
src/data/announcements/*
|
||||||
|
src/data/assets/current_map.png
|
||||||
!src/data/announcements/.gitkeep
|
!src/data/announcements/.gitkeep
|
||||||
config.json
|
config.json
|
||||||
stations.json
|
stations.json
|
||||||
@@ -158,7 +158,7 @@ The bot can join the voice chat, play an audio file, and disconnect from the voi
|
|||||||
Place the audio file of your desired station in [src/data/announcements](src/data/announcements/) with the EXACT name of the station. The bot will automatically check if an audio file with the stations name exists, and if it does, play it.
|
Place the audio file of your desired station in [src/data/announcements](src/data/announcements/) with the EXACT name of the station. The bot will automatically check if an audio file with the stations name exists, and if it does, play it.
|
||||||
#### http
|
#### http
|
||||||
|
|
||||||
- `"user_agent"`: The user agent of the bot for the API. If you don't know what that is, then you shouldn't have to change that. Even if you do, you still probably don't have to
|
- `"user_agent"`: The user agent of the bot for the API. If you don't know what that is, then you shouldn't have to change that.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
After you've set everything up, you're ready to start the bot!
|
After you've set everything up, you're ready to start the bot!
|
||||||
@@ -186,5 +186,5 @@ Covered in [locales](#locales)
|
|||||||
### [announcements/](src/data/announcements/)
|
### [announcements/](src/data/announcements/)
|
||||||
Covered in [voice announcements](#voice)
|
Covered in [voice announcements](#voice)
|
||||||
|
|
||||||
### [announcements](src/data/announcements/)
|
### [assets](src/data/assets/)
|
||||||
Assets (such as images) which the bot uses. You shouldn't have to change anything there
|
Assets (such as images) which the bot uses. You shouldn't have to change anything there
|
||||||
@@ -13,6 +13,7 @@ numpy==2.5.1
|
|||||||
pandas==3.0.3
|
pandas==3.0.3
|
||||||
propcache==0.5.2
|
propcache==0.5.2
|
||||||
py-cord==2.8.0
|
py-cord==2.8.0
|
||||||
|
py-staticmaps==0.5.0
|
||||||
pyyaml==6.0.3
|
pyyaml==6.0.3
|
||||||
pycparser==3.0
|
pycparser==3.0
|
||||||
pynacl==1.6.2
|
pynacl==1.6.2
|
||||||
|
|||||||
+15
-7
@@ -249,7 +249,7 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
|
|
||||||
trip_details = {
|
trip_details = {
|
||||||
"long_name": long_name,
|
"long_name": long_name,
|
||||||
"short_name": display_name,
|
"short_name": train_name,
|
||||||
"station": from_station,
|
"station": from_station,
|
||||||
"from": train_from,
|
"from": train_from,
|
||||||
"to": goes_to,
|
"to": goes_to,
|
||||||
@@ -266,7 +266,9 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
train_from_importance = legs["from"]["importance"]
|
train_from_importance = legs["from"]["importance"]
|
||||||
trip_details["stops"][train_from] = {
|
trip_details["stops"][train_from] = {
|
||||||
"arrival": departure_dt,
|
"arrival": departure_dt,
|
||||||
"importance": train_from_importance
|
"importance": train_from_importance,
|
||||||
|
"lat": legs["from"]["lat"],
|
||||||
|
"lon": legs["from"]["lon"]
|
||||||
}
|
}
|
||||||
|
|
||||||
departure_time_iso = start_time
|
departure_time_iso = start_time
|
||||||
@@ -276,22 +278,28 @@ def get_trip_details(random_connection: dict | None) -> dict | None:
|
|||||||
stop_importance = stop["importance"]
|
stop_importance = stop["importance"]
|
||||||
stop_details = {
|
stop_details = {
|
||||||
"arrival": stop_arrival_dt,
|
"arrival": stop_arrival_dt,
|
||||||
"importance": stop_importance
|
"importance": stop_importance,
|
||||||
|
"lat": stop["lat"],
|
||||||
|
"lon": stop["lon"]
|
||||||
}
|
}
|
||||||
trip_details["stops"][stop["name"]] = stop_details
|
trip_details["stops"][stop["name"]] = stop_details
|
||||||
|
|
||||||
if stop.get("name") == from_station:
|
if stop.get("name") == from_station:
|
||||||
departure_time_iso = stop["departure"]
|
departure_time = parse_iso(stop["departure"])
|
||||||
trip_details["departure"] = parse_iso(departure_time_iso).strftime("%H:%M")
|
trip_details["departure_dt"] = departure_time
|
||||||
|
trip_details["departure"] = departure_time.strftime("%H:%M")
|
||||||
|
|
||||||
train_to_importance = legs["to"]["importance"]
|
train_to_importance = legs["to"]["importance"]
|
||||||
trip_details["stops"][goes_to] = {
|
trip_details["stops"][goes_to] = {
|
||||||
"arrival": arrival_dt,
|
"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)
|
valid = validate_connection(start_time, end_time, departure_time_iso)
|
||||||
if not valid:
|
if not valid:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return trip_details
|
return trip_details
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ channel:
|
|||||||
train_via: "{train_name} nach {goes_to} über {from_station}"
|
train_via: "{train_name} nach {goes_to} über {from_station}"
|
||||||
embeds:
|
embeds:
|
||||||
footer:
|
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:
|
info:
|
||||||
description: "Abfahrt von {station} um {departure}. Ankunft um {arrival}"
|
description: "Abfahrt von {station} um {departure}. Ankunft um {arrival}"
|
||||||
via: "Über"
|
via: "Über"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ channel:
|
|||||||
train_via: "{train_name} to {goes_to} via {from_station}"
|
train_via: "{train_name} to {goes_to} via {from_station}"
|
||||||
embeds:
|
embeds:
|
||||||
footer:
|
footer:
|
||||||
notice: "Data provided by https://transitous.org"
|
notice: "Data provided by https://transitous.org • Maps (C) CARTO (C) OpenStreetMap.org contributors"
|
||||||
info:
|
info:
|
||||||
description: "Departure from {station} at {departure}. Arrival by {arrival}"
|
description: "Departure from {station} at {departure}. Arrival by {arrival}"
|
||||||
via: "via"
|
via: "via"
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ OPERATORS = {
|
|||||||
"color": 0x812B6D,
|
"color": 0x812B6D,
|
||||||
"slogan": ["Pour nous tous", "Donner au train des idées d'avance"]
|
"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": {
|
"Tallink Grupp AS": {
|
||||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Tallink_logo.svg/330px-Tallink_logo.svg.png",
|
"logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/Tallink_logo.svg/330px-Tallink_logo.svg.png",
|
||||||
"color": 0x225197
|
"color": 0x225197
|
||||||
|
|||||||
+3
-1
@@ -4,4 +4,6 @@ from src.dc.embeds import build_info_embed
|
|||||||
def setup_commands(bot: discord.Bot):
|
def setup_commands(bot: discord.Bot):
|
||||||
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
|
@bot.slash_command(description="Informationen über die aktuelle Fahrt")
|
||||||
async def info(ctx):
|
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())
|
||||||
@@ -61,6 +61,7 @@ def build_info_embed() -> discord.Embed:
|
|||||||
embed.set_author(name=agency)
|
embed.set_author(name=agency)
|
||||||
embed.set_thumbnail(url=metadata["logo"])
|
embed.set_thumbnail(url=metadata["logo"])
|
||||||
|
|
||||||
|
embed.set_image(url="attachment://ride.png")
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
def build_announcement_embed(msg):
|
def build_announcement_embed(msg):
|
||||||
|
|||||||
+17
-6
@@ -3,7 +3,7 @@ import asyncio
|
|||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta
|
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.config import config
|
||||||
from src.lang.locales import lang
|
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:
|
if trip is None:
|
||||||
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
|
logger(f"Failed to select route after {max_attempt} attempts", "fatal")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
generate_static_map(trip["stops"], trip["mode"], trip["agency"], trip["route_color"])
|
||||||
|
|
||||||
arrival = trip["arrival"]
|
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"]
|
mode = trip["mode"]
|
||||||
|
|
||||||
print("-----------------")
|
print("-----------------")
|
||||||
logger(f"Transfer: {long_name}; Arrival: {arrival}")
|
logger(f"Transfer: {channel_name}; Arrival: {arrival}")
|
||||||
logger(f"Agency: {trip["agency"]}, mode: {mode}")
|
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)")
|
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)
|
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)
|
await voice_channel.set_status(None)
|
||||||
start_next_stop_updates(voice_channel)
|
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())
|
embed = build_announcement_embed(lang.embeds.announcement.end_of_connection.message())
|
||||||
case "transfer":
|
case "transfer":
|
||||||
embed = build_info_embed()
|
embed = build_info_embed()
|
||||||
|
image = discord.File("src/data/assets/current_map.png", filename="ride.png")
|
||||||
case _:
|
case _:
|
||||||
logger(f"Unknown announcement: {announcement}")
|
logger(f"Unknown announcement: {announcement}")
|
||||||
embed = None
|
embed = None
|
||||||
|
|
||||||
if embed:
|
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:
|
async def voice_announcer(station: str, voice_channel: discord.VoiceChannel) -> bool:
|
||||||
sound_path = get_sound_path(station=station)
|
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()
|
wait_seconds = (next_stop["arrival"] - datetime.now(LOCAL_TZ)).total_seconds()
|
||||||
if wait_seconds > 0:
|
if wait_seconds > 0:
|
||||||
await asyncio.sleep(wait_seconds)
|
await (wait_seconds)
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
|
|||||||
+127
-1
@@ -1,8 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import importlib
|
import importlib
|
||||||
import random
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import staticmaps
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from zoneinfo import ZoneInfo
|
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)))
|
fields.append((route_page_name, "\n".join(field_lines)))
|
||||||
|
|
||||||
return fields
|
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!")
|
||||||
|
|||||||
Reference in New Issue
Block a user