use crate::parsers::common::{Location, PlaceType, TravelClass, Travelers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExploreDuration {
Weekend,
OneWeek,
TwoWeeks,
}
impl ExploreDuration {
pub fn as_wire_code(self) -> i32 {
match self {
ExploreDuration::Weekend => 1,
ExploreDuration::OneWeek => 2,
ExploreDuration::TwoWeeks => 3,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExploreDate {
pub month: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct MapBounds {
pub sw: (f64, f64),
pub ne: (f64, f64),
}
pub mod Interest {
#![allow(non_snake_case)]
pub const OUTDOORS: &str = "/g/11bc58l13w";
pub const BEACHES: &str = "/m/0b3yr";
pub const MUSEUMS: &str = "/m/09cmq";
pub const HISTORY: &str = "/m/03g3w";
pub const SKIING: &str = "/m/071k0";
pub const CLIMBING: &str = "/m/01rwk";
}
pub fn mid_from_name(name: &str) -> Option<&'static str> {
if name.starts_with("/m/") || name.starts_with("/g/") {
return None; }
let lower = name.to_lowercase();
const TABLE: &[(&str, &str)] = &[
("outdoors", Interest::OUTDOORS),
("nature", Interest::OUTDOORS),
("outdoor", Interest::OUTDOORS),
("beaches", Interest::BEACHES),
("beach", Interest::BEACHES),
("coast", Interest::BEACHES),
("coastal", Interest::BEACHES),
("museums", Interest::MUSEUMS),
("museum", Interest::MUSEUMS),
("art", Interest::MUSEUMS),
("history", Interest::HISTORY),
("culture", Interest::HISTORY),
("historical", Interest::HISTORY),
("heritage", Interest::HISTORY),
("skiing", Interest::SKIING),
("ski", Interest::SKIING),
("snowboarding", Interest::SKIING),
("snow", Interest::SKIING),
("climbing", Interest::CLIMBING),
("rock climbing", Interest::CLIMBING),
("bouldering", Interest::CLIMBING),
("mountaineering", Interest::CLIMBING),
];
TABLE
.iter()
.find(|(alias, _)| *alias == lower.as_str())
.map(|(_, mid)| *mid)
}
pub fn known_interest_names() -> &'static [&'static str] {
&[
"outdoors", "beaches", "museums", "history", "skiing", "climbing",
]
}
pub fn resolve_interest(raw: &str) -> anyhow::Result<String> {
if raw.starts_with("/m/") || raw.starts_with("/g/") {
return Ok(raw.to_string());
}
if let Some(mid) = mid_from_name(raw) {
return Ok(mid.to_string());
}
let names = known_interest_names().join(", ");
anyhow::bail!(
"unknown interest {raw:?}\n\
Known names: {names}\n\
Or pass a raw Knowledge-Graph MID, e.g. /m/01rwk"
)
}
pub mod Region {
#![allow(non_snake_case)]
pub const NORTHERN_EUROPE: &str = "/m/01531v";
pub const ALPS: &str = "/m/0lcd";
}
pub fn region_from_name(name: &str) -> Option<&'static str> {
if name.starts_with("/m/") || name.starts_with("/g/") {
return None; }
let lower = name.to_lowercase();
const TABLE: &[(&str, &str)] = &[
("northern europe", Region::NORTHERN_EUROPE),
("scandinavia", Region::NORTHERN_EUROPE),
("nordic", Region::NORTHERN_EUROPE),
("alps", Region::ALPS),
("alpine", Region::ALPS),
];
TABLE
.iter()
.find(|(alias, _)| *alias == lower.as_str())
.map(|(_, mid)| *mid)
}
pub fn known_region_names() -> &'static [&'static str] {
&["northern europe", "alps"]
}
pub fn resolve_destination(raw: &str) -> anyhow::Result<Location> {
if raw.starts_with("/m/") || raw.starts_with("/g/") {
return Ok(Location {
loc_identifier: raw.to_string(),
loc_type: PlaceType::Region,
location_name: None,
});
}
if let Some(mid) = region_from_name(raw) {
return Ok(Location {
loc_identifier: mid.to_string(),
loc_type: PlaceType::Region,
location_name: Some(raw.to_string()),
});
}
if raw.len() <= 4 && raw.chars().all(|c| c.is_ascii_alphanumeric()) {
return Ok(Location {
loc_identifier: raw.to_uppercase(),
loc_type: PlaceType::Airport,
location_name: None,
});
}
let regions = known_region_names().join(", ");
anyhow::bail!(
"unknown destination {raw:?}\n\
Use an IATA airport code (e.g. BCN), a region name ({regions}),\n\
or a raw Knowledge-Graph MID (e.g. /m/01531v)"
)
}
#[derive(Debug, Clone)]
pub struct ExploreConfig {
pub origin: Vec<Location>,
pub destination: Option<Location>,
pub trip_date: Option<ExploreDate>,
pub trip_duration: ExploreDuration,
pub max_price: Option<i32>,
pub interest: Option<String>,
pub airline_alliance: Option<crate::parsers::common::Alliance>,
pub max_flight_duration_minutes: Option<u32>,
pub baggage: Option<(u8, u8)>,
pub map_bounds: Option<MapBounds>,
pub travellers: Travelers,
pub travel_class: TravelClass,
}
impl Default for ExploreConfig {
fn default() -> Self {
Self {
origin: Vec::new(),
destination: None,
trip_date: None,
trip_duration: ExploreDuration::OneWeek,
max_price: None,
interest: None,
airline_alliance: None,
max_flight_duration_minutes: None,
baggage: None,
map_bounds: None,
travellers: Travelers::default(),
travel_class: TravelClass::Economy,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExploreResult {
pub place_id: String,
pub name: String,
pub country: String,
pub coords: (f64, f64),
pub image_url: Option<String>,
pub nearest_airport: String,
pub flight_airport: Option<String>,
pub date_from: Option<chrono::NaiveDate>,
pub date_to: Option<chrono::NaiveDate>,
pub price: Option<i32>,
pub airline: Option<String>,
pub stops: Option<u8>,
pub flight_duration_minutes: Option<u32>,
pub accommodation_price: Option<i32>,
pub booking_token: String,
}