init
This commit is contained in:
commit
073e7d979d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/stations_list
|
1560
Cargo.lock
generated
Normal file
1560
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "bahnreise"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8.5"
|
||||
rand_derive2 = "0.1.21"
|
||||
reqwest = { version = "0.12.9", features = ["blocking"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
tokio = "1.41.1"
|
238
src/game.rs
Normal file
238
src/game.rs
Normal file
@ -0,0 +1,238 @@
|
||||
use std::{error::Error, sync::Arc, time::Instant};
|
||||
|
||||
use crate::stations_list::{Arrivals, Departures, FilterTransports, StationsList};
|
||||
|
||||
pub struct Game {
|
||||
stations: Arc<StationsList>,
|
||||
history: Vec<(String, Option<String>)>,
|
||||
location: String,
|
||||
location_name: Option<String>,
|
||||
departures: Result<Departures, Box<dyn Error>>,
|
||||
target: String,
|
||||
target_name: Option<String>,
|
||||
location_activity_count: usize,
|
||||
target_activity_count: usize,
|
||||
filter_transports: FilterTransports,
|
||||
hint: Option<(Arrivals, Instant)>,
|
||||
}
|
||||
impl Game {
|
||||
pub fn new(
|
||||
filter_transports: FilterTransports,
|
||||
location: Option<String>,
|
||||
target: Option<String>,
|
||||
stations: Arc<StationsList>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let location = if let Some(location) = location {
|
||||
match stations
|
||||
.find_stations(&location)
|
||||
.first()
|
||||
.and_then(|(id, _)| {
|
||||
stations.get_station(id, |station| {
|
||||
station
|
||||
.query_activity_count(id, false, 10, filter_transports.only_known())
|
||||
.map(|c| (id.to_owned(), c))
|
||||
})
|
||||
}) {
|
||||
Some(Ok((id, c))) => (c, id),
|
||||
Some(Err(e)) => Err(e)?,
|
||||
None => Err(format!("start does not exist"))?,
|
||||
}
|
||||
} else {
|
||||
let mut location: Option<(usize, String)> = None;
|
||||
let mut location_counter = 0;
|
||||
for _ in 0..10 {
|
||||
if let Some(st) = stations.get_random_station(3, |id, station| {
|
||||
if let Some(act) = station
|
||||
.query_activity_count(id, false, 10, filter_transports.only_known())
|
||||
.ok()
|
||||
.filter(|v| *v > 1)
|
||||
{
|
||||
Some((act, id.to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
location_counter += 1;
|
||||
if location.as_ref().is_none_or(|p| p.0 < st.0) {
|
||||
location = Some(st);
|
||||
}
|
||||
// after 5 locations with 2+ departures, take the best one
|
||||
if location_counter >= 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
location.ok_or("could not find a start location")?
|
||||
};
|
||||
let target = if let Some(target) = target {
|
||||
match stations.find_stations(&target).first().and_then(|(id, _)| {
|
||||
stations.get_station(id, |station| {
|
||||
station
|
||||
.query_activity_count(id, false, 10, filter_transports.only_known())
|
||||
.map(|c| (id.to_owned(), c))
|
||||
})
|
||||
}) {
|
||||
Some(Ok((id, c))) => (c, id),
|
||||
Some(Err(e)) => Err(e)?,
|
||||
None => Err(format!("ziel does not exist"))?,
|
||||
}
|
||||
} else {
|
||||
let mut target: Option<(usize, String)> = None;
|
||||
let mut target_counter = 0;
|
||||
for _ in 0..10 {
|
||||
if let Some(st) = stations.get_random_station(3, |id, station| {
|
||||
if let Some(act) = (id != location.1.as_str())
|
||||
.then(|| {
|
||||
station
|
||||
.query_activity_count(id, true, 10, filter_transports.only_known())
|
||||
.ok()
|
||||
.filter(|v| *v > 1)
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
Some((act, id.to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
target_counter += 1;
|
||||
if target.as_ref().is_none_or(|p| p.0 < st.0) {
|
||||
target = Some(st);
|
||||
}
|
||||
// after 5 targets with 2+ arrivals, take the best one
|
||||
if target_counter >= 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
target.ok_or("could not find a target station")?
|
||||
};
|
||||
Ok(Self {
|
||||
stations,
|
||||
history: vec![],
|
||||
location: location.1,
|
||||
location_name: None,
|
||||
departures: Err("".into()),
|
||||
target: target.1,
|
||||
target_name: None,
|
||||
location_activity_count: location.0,
|
||||
target_activity_count: target.0,
|
||||
filter_transports,
|
||||
hint: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn history(&self) -> &[(String, Option<String>)] {
|
||||
&self.history
|
||||
}
|
||||
pub fn location(&self) -> &str {
|
||||
&self.location
|
||||
}
|
||||
pub fn location_name(&mut self) -> &str {
|
||||
if self.location_name.is_none() {
|
||||
self.location_name = self
|
||||
.stations
|
||||
.get_station(self.location(), |s| s.name().to_owned());
|
||||
}
|
||||
self.location_name
|
||||
.as_ref()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or(self.location())
|
||||
}
|
||||
pub fn location_name_immut(&self) -> &str {
|
||||
self.location_name
|
||||
.as_ref()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or(self.location())
|
||||
}
|
||||
pub fn target_name_immut(&self) -> &str {
|
||||
self.target_name
|
||||
.as_ref()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or(self.target())
|
||||
}
|
||||
pub fn target(&self) -> &str {
|
||||
&self.target
|
||||
}
|
||||
pub fn target_name(&mut self) -> &str {
|
||||
if self.target_name.is_none() {
|
||||
self.target_name = self
|
||||
.stations
|
||||
.get_station(self.target(), |s| s.name().to_owned());
|
||||
}
|
||||
self.target_name
|
||||
.as_ref()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or(self.target())
|
||||
}
|
||||
pub fn location_activity_count(&self) -> usize {
|
||||
self.location_activity_count
|
||||
}
|
||||
pub fn target_activity_count(&self) -> usize {
|
||||
self.target_activity_count
|
||||
}
|
||||
|
||||
pub fn update_departures(&mut self) {
|
||||
let v = std::mem::replace(&mut self.departures, Err("".into())).or_else(|_| {
|
||||
match self.stations.get_station(&self.location, |location| {
|
||||
location.query_departures(&self.location, 15, self.filter_transports)
|
||||
}) {
|
||||
Some(v) => {
|
||||
if let Ok(v) = &v {
|
||||
self.stations.add_new_from_departures(v);
|
||||
}
|
||||
v
|
||||
}
|
||||
None => Err(
|
||||
"current location no longer exists (this is most likely a server error)".into(),
|
||||
),
|
||||
}
|
||||
});
|
||||
self.departures = v;
|
||||
}
|
||||
pub fn departures_cached(&self) -> Result<&Departures, &Box<dyn Error>> {
|
||||
self.departures.as_ref()
|
||||
}
|
||||
/// Err if check failed, Ok(false) if 0 or 1 departures, Ok(true) otherwise
|
||||
pub fn go_to_check(&self, location: &str) -> Result<usize, Box<dyn Error>> {
|
||||
self.stations
|
||||
.requery_station_if_necessary_or_add_new(location);
|
||||
if let Some(act) = self.stations.get_station(location, |s| {
|
||||
s.query_activity_count(location, false, 10, self.filter_transports.only_known())
|
||||
}) {
|
||||
Ok(act?)
|
||||
} else {
|
||||
Err("no such station".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// true if win
|
||||
pub fn go_to_unchecked(&mut self, location: String) -> bool {
|
||||
let ploc = std::mem::replace(&mut self.location, location);
|
||||
let plocn = self.location_name.take();
|
||||
self.history.push((ploc, plocn));
|
||||
self.departures = Err("".into());
|
||||
self.location == self.target
|
||||
}
|
||||
|
||||
pub fn get_hint(&mut self) -> Result<&Arrivals, Box<dyn Error>> {
|
||||
if self
|
||||
.hint
|
||||
.as_ref()
|
||||
.is_none_or(|v| v.1.elapsed().as_secs_f64() > 60.0)
|
||||
{
|
||||
let o = self
|
||||
.stations
|
||||
.get_station(&self.target, |station| {
|
||||
station.query_arrivals(&self.target, 15, self.filter_transports)
|
||||
})
|
||||
.ok_or_else(|| "failed to find target station");
|
||||
if let Ok(Ok(o)) = o {
|
||||
self.hint = Some((o, Instant::now()));
|
||||
} else if self.hint.is_none() {
|
||||
self.hint = Some((o??, Instant::now()));
|
||||
}
|
||||
}
|
||||
Ok(&self.hint.as_ref().unwrap().0)
|
||||
}
|
||||
}
|
457
src/main.rs
Normal file
457
src/main.rs
Normal file
@ -0,0 +1,457 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use game::Game;
|
||||
use stations_list::{FilterTransports, StationsList, ALL_FILTER_TRANSPORTS};
|
||||
|
||||
mod game;
|
||||
mod stations_list;
|
||||
mod stations_thread;
|
||||
|
||||
fn main() {
|
||||
let stations = StationsList::new_from("stations_list".into()).unwrap();
|
||||
let stations = Arc::new(stations);
|
||||
let _thread = stations_thread::spawn(Arc::clone(&stations), 100);
|
||||
let listener = TcpListener::bind("0.0.0.0:26021").unwrap();
|
||||
let mut threads: Vec<std::thread::JoinHandle<()>> = vec![];
|
||||
let max_threads = 8;
|
||||
loop {
|
||||
if let Ok((mut con, _)) = listener.accept() {
|
||||
if let Some(i) = threads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|v| v.1.is_finished())
|
||||
.map(|v| v.0)
|
||||
{
|
||||
threads.swap_remove(i);
|
||||
}
|
||||
if threads.len() < max_threads {
|
||||
let stations = Arc::clone(&stations);
|
||||
threads.push(std::thread::spawn(move || {
|
||||
let _ = connection(&mut con, stations);
|
||||
let _ = con.shutdown(std::net::Shutdown::Both);
|
||||
}));
|
||||
} else {
|
||||
let _ = write!(&mut con, "server busy, try again later");
|
||||
let _ = con.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn connection(con: &mut TcpStream, stations: Arc<StationsList>) -> std::io::Result<()> {
|
||||
let mut w = BufWriter::new(con);
|
||||
writeln!(w, "–––=== Bahnhofsreise ===̣–––")?;
|
||||
writeln!(w, "Bekannte Bahnhöfe: {}", stations.station_count())?;
|
||||
let filter_transports: FilterTransports = rand::random();
|
||||
let mut filter_transports = filter_transports.and_unknown();
|
||||
let mut start: Vec<(String, String)> = vec![];
|
||||
let mut ziel: Vec<(String, String)> = vec![];
|
||||
loop {
|
||||
for (i, ft) in ALL_FILTER_TRANSPORTS.iter().enumerate() {
|
||||
writeln!(
|
||||
w,
|
||||
"Kategorie {}: [{}] {}",
|
||||
i + 1,
|
||||
if *ft == filter_transports { 'x' } else { ' ' },
|
||||
ft.explined()
|
||||
)?;
|
||||
}
|
||||
write!(w, " Start: ")?;
|
||||
if start.is_empty() {
|
||||
writeln!(w, "Zufällig")?;
|
||||
} else {
|
||||
for (i, v) in start.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(w, " / ")?;
|
||||
}
|
||||
write!(w, "{}", v.1)?;
|
||||
}
|
||||
writeln!(w)?;
|
||||
}
|
||||
write!(w, " Ziel: ")?;
|
||||
if ziel.is_empty() {
|
||||
writeln!(w, "Zufällig")?;
|
||||
} else {
|
||||
for (i, v) in ziel.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(w, " / ")?;
|
||||
}
|
||||
write!(w, "{}", v.1)?;
|
||||
}
|
||||
writeln!(w)?;
|
||||
}
|
||||
writeln!(
|
||||
w,
|
||||
"Enter zum Starten, oder Kategorie <1-6>, oder Start <Bahnhof>, oder Ziel <Bahnhof>"
|
||||
)?;
|
||||
w.flush()?;
|
||||
|
||||
let line = BufReader::new(w.get_mut())
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or(std::io::ErrorKind::BrokenPipe)??
|
||||
.to_lowercase();
|
||||
let line = line.trim();
|
||||
match line {
|
||||
"kategorie 1" => filter_transports = ALL_FILTER_TRANSPORTS[0],
|
||||
"kategorie 2" => filter_transports = ALL_FILTER_TRANSPORTS[1],
|
||||
"kategorie 3" => filter_transports = ALL_FILTER_TRANSPORTS[2],
|
||||
"kategorie 4" => filter_transports = ALL_FILTER_TRANSPORTS[3],
|
||||
"kategorie 5" => filter_transports = ALL_FILTER_TRANSPORTS[4],
|
||||
"kategorie 6" => filter_transports = ALL_FILTER_TRANSPORTS[5],
|
||||
"" => break,
|
||||
line => {
|
||||
if line.starts_with("start ") {
|
||||
start = stations.find_stations(line[6..].trim());
|
||||
} else if line.starts_with("ziel ") {
|
||||
ziel = stations.find_stations(line[5..].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start.is_empty() || ziel.is_empty() {
|
||||
writeln!(
|
||||
w,
|
||||
"Suche nach Bahnhöfen, wo was los ist... (kann etwas dauern)",
|
||||
)?;
|
||||
} else {
|
||||
writeln!(w, "Abfahrt . . .",)?;
|
||||
}
|
||||
w.flush()?;
|
||||
start.truncate(1);
|
||||
let start = start.pop();
|
||||
ziel.truncate(1);
|
||||
let ziel = ziel.pop();
|
||||
let custom_ziel = ziel.is_some();
|
||||
let mut game = match Game::new(
|
||||
filter_transports,
|
||||
start.map(|v| v.0),
|
||||
ziel.map(|v| v.0),
|
||||
Arc::clone(&stations),
|
||||
) {
|
||||
Ok(game) => game,
|
||||
Err(e) => {
|
||||
writeln!(w, "\n[FEHLER] Spielvorbereitung hat nicht geklappt :(\n")?;
|
||||
writeln!(w, "{e}")?;
|
||||
w.flush()?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w, "→ Dein Startbahnhof: {}.", game.location_name())?;
|
||||
writeln!(w, " → Dein Zielbahnhof: {}.", game.target_name())?;
|
||||
writeln!(
|
||||
w,
|
||||
" → Activity Scores: {} → {}",
|
||||
game.location_activity_count(),
|
||||
game.target_activity_count()
|
||||
)?;
|
||||
if custom_ziel && game.target_activity_count() < 3 {
|
||||
writeln!(
|
||||
w,
|
||||
"Geringer Activity Score von {} am Ziel, sicher? [Enter])",
|
||||
game.target_activity_count()
|
||||
)?;
|
||||
w.flush()?;
|
||||
BufReader::new(w.get_mut()).lines().next();
|
||||
}
|
||||
w.flush()?;
|
||||
let mut skip_verbindungen = false;
|
||||
loop {
|
||||
if !skip_verbindungen {
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w, "–– Mögliche Verbindungen ––")?;
|
||||
w.flush()?;
|
||||
game.update_departures();
|
||||
game.location_name();
|
||||
}
|
||||
let departures = match game.departures_cached() {
|
||||
Ok(deps) => deps,
|
||||
Err(e) => {
|
||||
writeln!(w, "\n[FEHLER] Zugsuche hat nicht geklappt :(\n")?;
|
||||
writeln!(w, "{}", e)?;
|
||||
w.flush()?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let mut possible_locations = HashMap::<String, &str>::new();
|
||||
for zug in &departures.entries {
|
||||
for linie in zug {
|
||||
if linie.canceled {
|
||||
if !skip_verbindungen {
|
||||
writeln!(
|
||||
w,
|
||||
"[fällt aus] {} | {} → {}",
|
||||
linie.line_name, linie.stop_place.name, linie.destination.name
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
if !skip_verbindungen {
|
||||
writeln!(
|
||||
w,
|
||||
"{} | {} → {}",
|
||||
linie.line_name, linie.stop_place.name, linie.destination.name
|
||||
)?;
|
||||
}
|
||||
possible_locations.insert(
|
||||
linie.destination.name.trim().to_lowercase(),
|
||||
&linie.destination.slug,
|
||||
);
|
||||
if !linie.via_stops.is_empty() {
|
||||
if !skip_verbindungen {
|
||||
write!(w, " via")?;
|
||||
}
|
||||
let mut width = 3;
|
||||
for (istop, stop) in linie.via_stops.iter().enumerate() {
|
||||
if !skip_verbindungen {
|
||||
if istop > 0 {
|
||||
write!(w, ",")?;
|
||||
width += 1;
|
||||
}
|
||||
if width > 0 && width + stop.name.len() > 70 {
|
||||
writeln!(w)?;
|
||||
write!(w, " ")?;
|
||||
width = 0;
|
||||
}
|
||||
if istop == 0 {
|
||||
width += 1 + stop.name.len();
|
||||
write!(w, " {}", stop.name)?;
|
||||
} else {
|
||||
width += 4 + stop.name.len();
|
||||
write!(w, " {}", stop.name)?;
|
||||
}
|
||||
}
|
||||
possible_locations.insert(stop.name.trim().to_lowercase(), &stop.slug);
|
||||
}
|
||||
if !skip_verbindungen {
|
||||
writeln!(w)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !skip_verbindungen {
|
||||
writeln!(w)?;
|
||||
}
|
||||
}
|
||||
if !skip_verbindungen {
|
||||
writeln!(w)?;
|
||||
}
|
||||
}
|
||||
if !skip_verbindungen {
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w, "→ Aktueller Bahnhof: {}.", game.location_name_immut())?;
|
||||
writeln!(w, " → Dein Zielbahnhof: {}.", game.target_name_immut())?;
|
||||
}
|
||||
skip_verbindungen = false;
|
||||
let mut r_errs = 0;
|
||||
loop {
|
||||
writeln!(w, "Wohin geht die Reise? (`?` für Hilfe)")?;
|
||||
w.flush()?;
|
||||
match BufReader::new(w.get_mut()).lines().next() {
|
||||
None => return Ok(()),
|
||||
Some(line) => {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
if line == "?" {
|
||||
writeln!(w)?;
|
||||
writeln!(w, "–– Erklärung ––")?;
|
||||
writeln!(
|
||||
w,
|
||||
"Bahnreise ist ein Spiel basierend auf Live-Daten von `bahnhof.de`."
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"Ziel ist es, vom Startbahnhof aus den Zielbahnhof zu erreichen."
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"Das Spiel zeigt eine Liste von Verbindungen an, die an dem Bahnhof"
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"abfahren, an dem du gerade bist. Um irgendwohin zu fahren, tippe"
|
||||
)?;
|
||||
writeln!(w, "den Namen eines Bahnhofs eine und bestätige mit Enter.")?;
|
||||
writeln!(
|
||||
w,
|
||||
"Das wiederholst du, bis du am Zielbahnhof angekommen bist."
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"Wenn du nicht weißt, wo der Zielbahnhof ist, kannst du dir"
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"mit `??` einen Tipp anzeigen lassen, der dir verrät, von wo aus"
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"du dein Ziel erreichen kannst. Sonst hilft nur noch eine Landkarte."
|
||||
)?;
|
||||
} else if line == "??" {
|
||||
let target_name = game.target_name_immut().to_owned();
|
||||
match game.get_hint() {
|
||||
Ok(hint) => {
|
||||
let mut arrivals: Vec<(&'_ str, Vec<&'_ str>)> = vec![];
|
||||
for arrival in hint.entries.iter().map(|v| v.iter()).flatten() {
|
||||
if let Some((_, lines)) = arrivals
|
||||
.iter_mut()
|
||||
.find(|v| v.0 == arrival.origin.name.as_str())
|
||||
{
|
||||
if !arrival.canceled {
|
||||
if !lines.contains(&arrival.line_name.as_str()) {
|
||||
lines.push(arrival.line_name.as_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
arrivals.push((
|
||||
arrival.origin.name.as_str(),
|
||||
if !arrival.canceled {
|
||||
vec![arrival.line_name.as_str()]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
writeln!(w, "\n\n[TIPP] Züge, die in {target_name} ankommen:")?;
|
||||
for (origin, lines) in arrivals {
|
||||
writeln!(
|
||||
w,
|
||||
"- {origin} ({})",
|
||||
if lines.is_empty() {
|
||||
format!("all connections cancelled")
|
||||
} else {
|
||||
lines
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
format!("{}{v}", if i == 0 { "" } else { ", " })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(w, "ups, das hat nicht geklappt... ({e})")?;
|
||||
}
|
||||
}
|
||||
skip_verbindungen = true;
|
||||
break;
|
||||
} else if line.is_empty() {
|
||||
r_errs += 1;
|
||||
if r_errs > 25 {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Some(new) = possible_locations
|
||||
.get(line.to_lowercase().as_str())
|
||||
.map(|v| (*v).to_owned())
|
||||
.or_else(|| {
|
||||
stations
|
||||
.find_stations(line)
|
||||
.into_iter()
|
||||
.filter_map(|(id, _)| {
|
||||
possible_locations
|
||||
.values()
|
||||
.find(|v| **v == id.as_str())
|
||||
.map(|_| id.clone())
|
||||
})
|
||||
.next()
|
||||
})
|
||||
{
|
||||
if new.trim().is_empty() {
|
||||
writeln!(
|
||||
w,
|
||||
"Diesen Halt gibt es scheinbar, aber er hat keine Bahnhofs-Seite,"
|
||||
)?;
|
||||
writeln!(w, "d.h. es können leider auch keine Infos/Abfahrten erfragt werden :(")?;
|
||||
w.flush()?;
|
||||
} else {
|
||||
match game.go_to_check(new.as_str()) {
|
||||
Ok(0) if game.target() != new.as_str() => {
|
||||
writeln!(w, "sieht dort recht leer aus... (this is softlock protection btw)")?;
|
||||
w.flush()?;
|
||||
}
|
||||
Ok(act) => {
|
||||
let mut go_to_station = true;
|
||||
if act <= 3 && game.target() != new.as_str() {
|
||||
writeln!(
|
||||
w,
|
||||
"Geringer Activity Score von {act}, sicher? [ja/nein])"
|
||||
)?;
|
||||
w.flush()?;
|
||||
go_to_station = false;
|
||||
if let Some(Ok(v)) =
|
||||
BufReader::new(w.get_mut()).lines().next()
|
||||
{
|
||||
if v.as_str() == "ja" {
|
||||
go_to_station = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if go_to_station {
|
||||
let location = new;
|
||||
if game.go_to_unchecked(location) {
|
||||
writeln!(w)?;
|
||||
writeln!(w)?;
|
||||
writeln!(w, "yay, du bist da :D")?;
|
||||
let ilen = game.history().len().to_string().len();
|
||||
for (i, (id, name)) in game.history().iter().enumerate()
|
||||
{
|
||||
let i = (i + 1).to_string();
|
||||
writeln!(
|
||||
w,
|
||||
"- {}{i}. {}",
|
||||
" ".repeat(ilen.saturating_sub(i.len())),
|
||||
name.as_ref()
|
||||
.map(|v| v.as_str())
|
||||
.unwrap_or(id.as_str())
|
||||
)?;
|
||||
}
|
||||
writeln!(
|
||||
w,
|
||||
"- {} → {}",
|
||||
" ".repeat(ilen.saturating_sub(1)),
|
||||
game.location_name()
|
||||
)?;
|
||||
w.flush()?;
|
||||
return Ok(());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(w, "hm, vielleicht lieber nicht dorthin? ({e})")?;
|
||||
w.flush()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeln!(w, "hm, kenne ich nicht... (typo?)")?;
|
||||
r_errs += 1;
|
||||
if r_errs > 25 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
711
src/stations_list.rs
Normal file
711
src/stations_list.rs
Normal file
@ -0,0 +1,711 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error,
|
||||
fmt::Display,
|
||||
path::PathBuf,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand_derive2::RandGen;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct StationsList {
|
||||
dir: PathBuf,
|
||||
stations: Mutex<HashMap<String, OptStation>>,
|
||||
}
|
||||
struct OptStation {
|
||||
updated: Option<SystemTime>,
|
||||
station: Option<Station>,
|
||||
}
|
||||
|
||||
impl StationsList {
|
||||
pub fn new_from(dir: PathBuf) -> Result<Self, Box<dyn Error>> {
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mut stations: HashMap<String, OptStation> = Default::default();
|
||||
for f in std::fs::read_dir(&dir)? {
|
||||
let f = f?;
|
||||
let fmeta = f.metadata()?;
|
||||
if fmeta.is_file() {
|
||||
let id = f.file_name();
|
||||
let id = id
|
||||
.to_str()
|
||||
.ok_or_else(|| format!("non-utf8 filename {:?}!", f.file_name()))?;
|
||||
let fp = f.path();
|
||||
let save = std::fs::read_to_string(&fp)?;
|
||||
let station = if save.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Station::from_save(id, save)?)
|
||||
};
|
||||
stations.insert(
|
||||
id.trim().to_owned(),
|
||||
OptStation {
|
||||
updated: Some(fmeta.modified().unwrap_or_else(|_| SystemTime::now())),
|
||||
station,
|
||||
},
|
||||
);
|
||||
} else if fmeta.is_dir() {
|
||||
}
|
||||
}
|
||||
let mut count_s: usize = 0;
|
||||
let mut count_n: usize = 0;
|
||||
for station in stations.values() {
|
||||
if station.station.is_some() {
|
||||
count_s += 1;
|
||||
} else {
|
||||
count_n += 1;
|
||||
}
|
||||
}
|
||||
eprintln!("Loaded {count_s} stations and {count_n} non-stations from {dir:?}");
|
||||
Ok(Self {
|
||||
dir,
|
||||
stations: Mutex::new(stations),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn station_count(&self) -> usize {
|
||||
self.stations
|
||||
.blocking_lock()
|
||||
.values()
|
||||
.filter(|v| v.station.is_some())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn add_new_from_departures(&self, deps: &Departures) {
|
||||
let mut stations = self.stations.blocking_lock();
|
||||
for dep in deps.entries.iter().map(|v| v.iter()).flatten() {
|
||||
for id in [&dep.stop_place.slug, &dep.destination.slug]
|
||||
.into_iter()
|
||||
.chain(dep.via_stops.iter().map(|v| &v.slug))
|
||||
.map(|v| v.trim())
|
||||
.filter(|v| !v.is_empty())
|
||||
{
|
||||
if !stations.contains_key(id) {
|
||||
stations.insert(
|
||||
id.to_owned(),
|
||||
OptStation {
|
||||
updated: None,
|
||||
station: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_for_new_stations(&self) -> Result<(), Box<dyn Error>> {
|
||||
// maybe find a new station or update an existing one
|
||||
let html = reqwest::blocking::get("https://bahnhof.de/en/search")?
|
||||
.error_for_status()?
|
||||
.text()?;
|
||||
for (match_index, match_str) in html.match_indices(r#"href="/en/"#) {
|
||||
let id = &html[match_index + match_str.len()..];
|
||||
if let Some(end) = id.find(r#"""#) {
|
||||
let id = id[..end].trim();
|
||||
if !id.contains(['/', '%', '&', '?', '#']) {
|
||||
let mut stations = self.stations.blocking_lock();
|
||||
Self::requery_station_if_necessary_or_add_new_int(id, &self.dir, &mut stations);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn requery_random_station(&self, cache_count: usize) -> Result<(), Box<dyn Error>> {
|
||||
let mut stations = self.stations.blocking_lock();
|
||||
if !stations.is_empty() {
|
||||
for id in stations
|
||||
.iter()
|
||||
.skip(thread_rng().gen_range(0..stations.len()))
|
||||
.take(cache_count)
|
||||
.map(|v| v.0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
if Self::requery_station_if_necessary_or_add_new_int(&id, &self.dir, &mut stations)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn requery_station_if_necessary_or_add_new(&self, id: &str) -> bool {
|
||||
Self::requery_station_if_necessary_or_add_new_int(
|
||||
id,
|
||||
&self.dir,
|
||||
&mut self.stations.blocking_lock(),
|
||||
)
|
||||
}
|
||||
fn requery_station_if_necessary_or_add_new_int(
|
||||
id: &str,
|
||||
dir: &PathBuf,
|
||||
stations: &mut HashMap<String, OptStation>,
|
||||
) -> bool {
|
||||
if let Some(s) = stations.get_mut(id) {
|
||||
// recheck stations after a day, and recheck non-stations after a week
|
||||
if s.updated.is_none_or(|updated| {
|
||||
updated.elapsed().is_ok_and(|elapsed| {
|
||||
elapsed
|
||||
> Duration::from_secs(
|
||||
if s.station.is_some() { 1 } else { 7 } * 24 * 60 * 60,
|
||||
)
|
||||
})
|
||||
}) {
|
||||
match Station::query_station(id) {
|
||||
Ok(station) => {
|
||||
if let Some(prev) = s.station.take() {
|
||||
if prev == station {
|
||||
eprintln!("Confirmed station {id} (unchanged)");
|
||||
} else {
|
||||
eprintln!(
|
||||
"Updated station {id} (changed): {prev:?} -> {station:?}"
|
||||
);
|
||||
if let Err(e) = std::fs::write(dir.join(id), station.to_save()) {
|
||||
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Added new station {id}: non-station -> {station:?}");
|
||||
if let Err(e) = std::fs::write(dir.join(id), station.to_save()) {
|
||||
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||
}
|
||||
}
|
||||
s.station = Some(station);
|
||||
}
|
||||
Err(e) => {
|
||||
if s.updated.is_none_or(|updated| {
|
||||
updated.elapsed().is_ok_and(|elapsed| {
|
||||
elapsed > Duration::from_secs(7 * 24 * 60 * 60)
|
||||
})
|
||||
}) {
|
||||
eprintln!("Error querying station {id} and last updated over a week ago, marking as non-station! Error: {e}");
|
||||
let _ = stations.remove(id);
|
||||
if let Err(e) = std::fs::write(dir.join(id), "") {
|
||||
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"Error querying station {id}, keeping old data for now. Error: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
stations.insert(
|
||||
id.to_owned(),
|
||||
OptStation {
|
||||
updated: Some(SystemTime::now()),
|
||||
station: match Station::query_station(id) {
|
||||
Ok(station) => {
|
||||
eprintln!("Added new station {id}: nothing -> {station:?}");
|
||||
if let Err(e) = std::fs::write(dir.join(id), station.to_save()) {
|
||||
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||
}
|
||||
Some(station)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Marked {id} as not a station. Error: {e}");
|
||||
if let Err(e) = std::fs::write(dir.join(id), "") {
|
||||
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||
}
|
||||
None
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_station<T>(&self, id: &str, map: impl FnOnce(&Station) -> T) -> Option<T> {
|
||||
self.stations
|
||||
.blocking_lock()
|
||||
.get(id)
|
||||
.and_then(|v| v.station.as_ref())
|
||||
.map(map)
|
||||
}
|
||||
|
||||
pub fn get_random_station<T>(
|
||||
&self,
|
||||
limit_tries_cuberoot: usize,
|
||||
map: impl Fn(&str, &Station) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
let stations = self.stations.blocking_lock();
|
||||
for i in 1..=limit_tries_cuberoot {
|
||||
for i in rand::seq::index::sample(
|
||||
&mut thread_rng(),
|
||||
stations.len(),
|
||||
(i * i).min(stations.len()),
|
||||
) {
|
||||
if let Some((id, opt)) = stations.iter().nth(i) {
|
||||
if let Some(station) = &opt.station {
|
||||
if let Some(v) = map(id, station) {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_stations(&self, name: &str) -> Vec<(String, String)> {
|
||||
let stations = self.stations.blocking_lock();
|
||||
let name = name.to_lowercase();
|
||||
if let Some(s) = stations.get(&name).and_then(|v| v.station.as_ref()) {
|
||||
vec![(name.to_owned(), s.name.clone())]
|
||||
} else {
|
||||
let mut o = HashMap::<String, (String, usize)>::new();
|
||||
for (id, station) in stations.iter() {
|
||||
if let Some(station) = &station.station {
|
||||
if station.name.to_lowercase() == name {
|
||||
o.insert(id.clone(), (station.name.clone(), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
if o.len() < 3 {
|
||||
for (id, station) in stations.iter() {
|
||||
if let Some(station) = &station.station {
|
||||
if station.name.to_lowercase().starts_with(&name) {
|
||||
o.insert(
|
||||
id.clone(),
|
||||
(
|
||||
station.name.clone(),
|
||||
station.name.len().saturating_sub(name.len()).min(100),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if o.len() < 3 {
|
||||
for (id, station) in stations.iter() {
|
||||
if let Some(station) = &station.station {
|
||||
if let Some(pos) = station.name.to_lowercase().find(&name) {
|
||||
o.insert(id.clone(), (station.name.clone(), (100 + pos).min(200)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut o = o.into_iter().collect::<Vec<_>>();
|
||||
o.sort_unstable_by_key(|v| v.1 .1);
|
||||
o.into_iter().map(|v| (v.0, v.1 .0)).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Station {
|
||||
name: String,
|
||||
eva_numbers: Vec<u128>,
|
||||
}
|
||||
impl Station {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn from_save(id: &str, save: String) -> Result<Self, Box<dyn Error>> {
|
||||
let mut name = None;
|
||||
let mut eva_numbers = None;
|
||||
for line in save.lines() {
|
||||
match line
|
||||
.split_once('=')
|
||||
.map_or_else(|| ("", line.trim()), |(a, b)| (a.trim(), b.trim()))
|
||||
{
|
||||
("name", v) => name = Some(v.to_owned()),
|
||||
("evanums", v) => eva_numbers = Some(v.to_owned()),
|
||||
("", line) => match line {
|
||||
line => Err(format!("invalid flag-line in bahnhof {id}: {line}"))?,
|
||||
},
|
||||
(key, value) => Err(format!(
|
||||
"invalid key-value-line in bahnhof {id}: {key}={value}"
|
||||
))?,
|
||||
}
|
||||
}
|
||||
Ok(Station {
|
||||
name: name.ok_or_else(|| format!("in station {id}: missing name"))?,
|
||||
eva_numbers: eva_numbers
|
||||
.ok_or_else(|| format!("in station {id}: missing evanums"))?
|
||||
.split(',')
|
||||
.map(|v| {
|
||||
v.trim()
|
||||
.parse()
|
||||
.map_err(|e| format!("eva number {v} could not be parsed: {e}"))
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
fn to_save(&self) -> String {
|
||||
let mut o = String::new();
|
||||
o.push_str("name=");
|
||||
o.push_str(&self.name);
|
||||
o.push('\n');
|
||||
if !self.eva_numbers.is_empty() {
|
||||
o.push_str("evanums=");
|
||||
for (i, num) in self.eva_numbers.iter().enumerate() {
|
||||
if i != 0 {
|
||||
o.push(',');
|
||||
}
|
||||
o.push_str(&format!("{num}"));
|
||||
}
|
||||
o.push('\n');
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
fn query_station(id: &str) -> Result<Self, Box<dyn Error>> {
|
||||
let html =
|
||||
reqwest::blocking::get(format!("https://www.bahnhof.de/{id}/departure"))?.text()?;
|
||||
let start_pat = r#"<title>Abfahrt "#;
|
||||
if let (Some(start), Some(end)) = (html.find(start_pat), html.find("</title>")) {
|
||||
let start = start + start_pat.len();
|
||||
let mut name = if end > start {
|
||||
html[start..end].trim()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
// skip next char (some UTF8 dash)
|
||||
if !name.is_empty() {
|
||||
name = name[name.chars().next().unwrap().len_utf8()..].trim();
|
||||
}
|
||||
let pat = r#"<meta name="bf:evaNumbers" content=""#;
|
||||
if let Some(index) = html.find(pat) {
|
||||
let rest = &html[index + pat.len()..];
|
||||
if let Some(end) = rest.find('"') {
|
||||
let eva_numbers = rest[..end]
|
||||
.trim()
|
||||
.split(',')
|
||||
.map(|v| {
|
||||
v.trim()
|
||||
.parse()
|
||||
.map_err(|e| format!("eva number {v} could not be parsed: {e}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if eva_numbers.is_empty() {
|
||||
Err("no evaNumbers (found empty list)")?;
|
||||
}
|
||||
Ok(Self {
|
||||
name: name.to_owned(),
|
||||
eva_numbers,
|
||||
})
|
||||
} else {
|
||||
Err("missing evaNumbers")?
|
||||
}
|
||||
} else {
|
||||
Err("missing evaNumbers")?
|
||||
}
|
||||
} else {
|
||||
let start_pat = "<title>";
|
||||
if let (Some(start), Some(end)) = (html.find(start_pat), html.find("</title>")) {
|
||||
if start + start_pat.len() < end {
|
||||
Err(format!(
|
||||
"missing title `Abfahrt - <name>`: title was `{}`",
|
||||
&html[start + start_pat.len()..end]
|
||||
))?
|
||||
} else {
|
||||
Err(format!(
|
||||
"missing title `Abfahrt - <name>`: </title> before <title>"
|
||||
))?
|
||||
}
|
||||
} else {
|
||||
Err(format!(
|
||||
"missing title `Abfahrt - <name>`: no <title> found in `{html}`"
|
||||
))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_departures(
|
||||
&self,
|
||||
id: &str,
|
||||
minutes: u8,
|
||||
filter_transports: FilterTransports,
|
||||
) -> Result<Departures, Box<dyn Error>> {
|
||||
let _ = id;
|
||||
if self.eva_numbers.is_empty() {
|
||||
Err("station has no eva numbers")?;
|
||||
}
|
||||
let json =
|
||||
reqwest::blocking::get(self.query_departures_url(false, minutes, filter_transports))?
|
||||
.text()?;
|
||||
Ok(serde_json::from_str(&json).map_err(|e| format!("{e}\nin:\n{json}"))?)
|
||||
}
|
||||
pub fn query_arrivals(
|
||||
&self,
|
||||
id: &str,
|
||||
minutes: u8,
|
||||
filter_transports: FilterTransports,
|
||||
) -> Result<Arrivals, Box<dyn Error>> {
|
||||
let _ = id;
|
||||
if self.eva_numbers.is_empty() {
|
||||
Err("station has no eva numbers")?;
|
||||
}
|
||||
let json =
|
||||
reqwest::blocking::get(self.query_departures_url(true, minutes, filter_transports))?
|
||||
.text()?;
|
||||
Ok(serde_json::from_str(&json).map_err(|e| format!("{e}\nin:\n{json}"))?)
|
||||
}
|
||||
fn query_departures_url(
|
||||
&self,
|
||||
arrivals: bool,
|
||||
minutes: u8,
|
||||
filter_transports: FilterTransports,
|
||||
) -> String {
|
||||
let mut url = format!(
|
||||
"https://www.bahnhof.de/api/boards/{}?",
|
||||
if arrivals { "arrivals" } else { "departures" }
|
||||
);
|
||||
for num in self.eva_numbers.iter() {
|
||||
url.push_str(&format!("evaNumbers={num}&"));
|
||||
}
|
||||
url.push_str(&format!(
|
||||
"duration={minutes}{filter_transports}&locale=de&sortBy=TIME_SCHEDULE"
|
||||
));
|
||||
url
|
||||
}
|
||||
pub fn query_activity_count(
|
||||
&self,
|
||||
id: &str,
|
||||
arrivals: bool,
|
||||
minutes: u8,
|
||||
filter_transports: FilterTransports,
|
||||
) -> Result<usize, Box<dyn Error>> {
|
||||
let _ = id;
|
||||
if self.eva_numbers.is_empty() {
|
||||
Err("station has no eva numbers")?;
|
||||
}
|
||||
let mut url = format!(
|
||||
"https://www.bahnhof.de/api/boards/{}?",
|
||||
if arrivals { "arrivals" } else { "departures" }
|
||||
);
|
||||
for num in self.eva_numbers.iter() {
|
||||
url.push_str(&format!("evaNumbers={num}&"));
|
||||
}
|
||||
url.push_str(&format!(
|
||||
"duration={minutes}{filter_transports}&locale=de&sortBy=TIME_SCHEDULE"
|
||||
));
|
||||
let json = reqwest::blocking::get(url)?.text()?;
|
||||
let arrivals: ArrivalsOrDepartures =
|
||||
serde_json::from_str(&json).map_err(|e| format!("{e}\nin:\n{json}"))?;
|
||||
Ok(arrivals
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|v| !v.iter().any(|v| v.canceled))
|
||||
.count())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArrivalsOrDepartures {
|
||||
pub entries: Vec<Vec<ArrivalOrDeparture>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ArrivalOrDeparture {
|
||||
pub canceled: bool,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Departures {
|
||||
/// Vec<Vec<_>> because there may be a departure of multiple trains (usually coupled together, but still different trains/routes/destinations)
|
||||
pub entries: Vec<Vec<Departure>>,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Departure {
|
||||
pub canceled: bool,
|
||||
pub line_name: String,
|
||||
pub stop_place: SomeStation,
|
||||
pub destination: SomeStation,
|
||||
pub via_stops: Vec<SomeStation>,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SomeStation {
|
||||
// pub eva_number: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub slug: String,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Arrivals {
|
||||
pub entries: Vec<Vec<Arrival>>,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Arrival {
|
||||
pub canceled: bool,
|
||||
pub line_name: String,
|
||||
// pub stop_place: SomeStation,
|
||||
pub origin: SomeStation,
|
||||
// pub via_stops: Vec<SomeStation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, RandGen, PartialEq, Eq)]
|
||||
pub enum FilterTransports {
|
||||
All,
|
||||
AllKnown,
|
||||
AllTrains,
|
||||
AllTrainsKnown,
|
||||
Trains,
|
||||
TrainsKnown,
|
||||
AllRegionalTrains,
|
||||
AllRegionalTrainsKnown,
|
||||
RegionalTrains,
|
||||
RegionalTrainsKnown,
|
||||
HighSpeedTrains,
|
||||
HighSpeedTrainsKnown,
|
||||
}
|
||||
pub const ALL_FILTER_TRANSPORTS: [FilterTransports; 6] = [
|
||||
FilterTransports::All,
|
||||
FilterTransports::AllTrains,
|
||||
FilterTransports::Trains,
|
||||
FilterTransports::AllRegionalTrains,
|
||||
FilterTransports::RegionalTrains,
|
||||
FilterTransports::HighSpeedTrains,
|
||||
];
|
||||
impl Display for FilterTransports {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.allow_high_speed() {
|
||||
write!(
|
||||
f,
|
||||
"&filterTransports=HIGH_SPEED_TRAIN&filterTransports=INTERCITY_TRAIN&filterTransports=INTER_REGIONAL_TRAIN"
|
||||
)?;
|
||||
}
|
||||
if self.allow_regional() {
|
||||
write!(f, "&filterTransports=REGIONAL_TRAIN")?;
|
||||
}
|
||||
if self.allow_local() {
|
||||
write!(f, "&filterTransports=CITY_TRAIN&filterTransports=TRAM")?;
|
||||
}
|
||||
if self.allow_bus() {
|
||||
write!(f, "&filterTransports=BUS")?;
|
||||
}
|
||||
if self.allow_unknown() {
|
||||
write!(f, "&filterTransports=UNKNOWN")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl FilterTransports {
|
||||
fn allow_unknown(&self) -> bool {
|
||||
match self {
|
||||
Self::All
|
||||
| Self::AllTrains
|
||||
| Self::Trains
|
||||
| Self::AllRegionalTrains
|
||||
| Self::RegionalTrains
|
||||
| Self::HighSpeedTrains => true,
|
||||
Self::AllKnown
|
||||
| Self::AllTrainsKnown
|
||||
| Self::TrainsKnown
|
||||
| Self::AllRegionalTrainsKnown
|
||||
| Self::RegionalTrainsKnown
|
||||
| Self::HighSpeedTrainsKnown => false,
|
||||
}
|
||||
}
|
||||
fn allow_bus(&self) -> bool {
|
||||
match self {
|
||||
Self::All | Self::AllKnown => true,
|
||||
Self::AllTrains
|
||||
| Self::AllTrainsKnown
|
||||
| Self::Trains
|
||||
| Self::TrainsKnown
|
||||
| Self::AllRegionalTrains
|
||||
| Self::AllRegionalTrainsKnown
|
||||
| Self::RegionalTrains
|
||||
| Self::RegionalTrainsKnown
|
||||
| Self::HighSpeedTrains
|
||||
| Self::HighSpeedTrainsKnown => false,
|
||||
}
|
||||
}
|
||||
fn allow_local(&self) -> bool {
|
||||
match self {
|
||||
Self::All
|
||||
| Self::AllKnown
|
||||
| Self::AllTrains
|
||||
| Self::AllTrainsKnown
|
||||
| Self::AllRegionalTrains
|
||||
| Self::AllRegionalTrainsKnown => true,
|
||||
Self::Trains
|
||||
| Self::TrainsKnown
|
||||
| Self::RegionalTrains
|
||||
| Self::RegionalTrainsKnown
|
||||
| Self::HighSpeedTrains
|
||||
| Self::HighSpeedTrainsKnown => false,
|
||||
}
|
||||
}
|
||||
fn allow_regional(&self) -> bool {
|
||||
match self {
|
||||
Self::All
|
||||
| Self::AllKnown
|
||||
| Self::AllTrains
|
||||
| Self::AllTrainsKnown
|
||||
| Self::Trains
|
||||
| Self::TrainsKnown
|
||||
| Self::AllRegionalTrains
|
||||
| Self::AllRegionalTrainsKnown
|
||||
| Self::RegionalTrains
|
||||
| Self::RegionalTrainsKnown => true,
|
||||
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => false,
|
||||
}
|
||||
}
|
||||
fn allow_high_speed(&self) -> bool {
|
||||
match self {
|
||||
Self::All
|
||||
| Self::AllKnown
|
||||
| Self::AllTrains
|
||||
| Self::AllTrainsKnown
|
||||
| Self::Trains
|
||||
| Self::TrainsKnown
|
||||
| Self::HighSpeedTrains
|
||||
| Self::HighSpeedTrainsKnown => true,
|
||||
Self::AllRegionalTrains
|
||||
| Self::AllRegionalTrainsKnown
|
||||
| Self::RegionalTrains
|
||||
| Self::RegionalTrainsKnown => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn only_known(&self) -> Self {
|
||||
match self {
|
||||
Self::All | Self::AllKnown => Self::AllKnown,
|
||||
Self::AllTrains | Self::AllTrainsKnown => Self::AllTrainsKnown,
|
||||
Self::Trains | Self::TrainsKnown => Self::TrainsKnown,
|
||||
Self::AllRegionalTrains | Self::AllRegionalTrainsKnown => Self::AllRegionalTrainsKnown,
|
||||
Self::RegionalTrains | Self::RegionalTrainsKnown => Self::RegionalTrainsKnown,
|
||||
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => Self::HighSpeedTrainsKnown,
|
||||
}
|
||||
}
|
||||
pub fn and_unknown(&self) -> Self {
|
||||
match self {
|
||||
Self::All | Self::AllKnown => Self::All,
|
||||
Self::AllTrains | Self::AllTrainsKnown => Self::AllTrains,
|
||||
Self::Trains | Self::TrainsKnown => Self::Trains,
|
||||
Self::AllRegionalTrains | Self::AllRegionalTrainsKnown => Self::AllRegionalTrains,
|
||||
Self::RegionalTrains | Self::RegionalTrainsKnown => Self::RegionalTrains,
|
||||
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => Self::HighSpeedTrains,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn explined(&self) -> &'static str {
|
||||
match self {
|
||||
Self::All | Self::AllKnown => "Öffis (Zug, S+U, Tram, Bus)",
|
||||
Self::AllTrains | Self::AllTrainsKnown => "Schienenverkehr (Zug, S+U, Tram)",
|
||||
Self::Trains | Self::TrainsKnown => "Züge (Zug, ohne Tram)",
|
||||
Self::AllRegionalTrains | Self::AllRegionalTrainsKnown => {
|
||||
"Deutschland-Ticket (Regio, S+U, Tram)"
|
||||
}
|
||||
Self::RegionalTrains | Self::RegionalTrainsKnown => "Regionalzüge (Regio, ohne Tram)",
|
||||
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => "nur Hochgeschwindigkeitszüge",
|
||||
}
|
||||
}
|
||||
}
|
58
src/stations_thread.rs
Normal file
58
src/stations_thread.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::{
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
thread::{sleep, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
use crate::stations_list::StationsList;
|
||||
|
||||
pub struct StationsThread {
|
||||
#[allow(dead_code)]
|
||||
min_stations: Arc<AtomicUsize>,
|
||||
#[allow(dead_code)]
|
||||
join_handle: JoinHandle<()>,
|
||||
}
|
||||
impl StationsThread {
|
||||
#[allow(dead_code)]
|
||||
pub fn turbo(&self, min_stations: usize) {
|
||||
self.min_stations
|
||||
.store(min_stations, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(stations: Arc<StationsList>, min_stations: usize) -> StationsThread {
|
||||
let min_stations = Arc::new(AtomicUsize::new(min_stations));
|
||||
let min_station_count = Arc::clone(&min_stations);
|
||||
let join_handle = std::thread::spawn(move || loop {
|
||||
let turbo =
|
||||
stations.station_count() < min_station_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
sleep(Duration::from_secs(
|
||||
rand::thread_rng().gen_range(10..=80) * if turbo { 1 } else { 60 },
|
||||
));
|
||||
match stations.query_for_new_stations() {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to query for new stations:\n{e}");
|
||||
}
|
||||
}
|
||||
for _ in 0..10 {
|
||||
sleep(Duration::from_secs(if turbo {
|
||||
rand::thread_rng().gen_range(10..=30)
|
||||
} else {
|
||||
rand::thread_rng().gen_range(30..=120)
|
||||
}));
|
||||
match stations.requery_random_station(10) {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to requery stations:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
StationsThread {
|
||||
min_stations,
|
||||
join_handle,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user