include combined time + weather windows
This commit is contained in:
parent
c089f20b87
commit
a1ca56dc82
4 changed files with 138 additions and 42 deletions
15
src/clock.rs
15
src/clock.rs
|
@ -1,22 +1,35 @@
|
|||
// Mostly nicked from https://github.com/icykoneko/ff14-fish-tracker-app/blob/master/js/app/time.js. Thanks!
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Timelike, Utc};
|
||||
|
||||
const EARTH_TO_EORZEA: f64 = 3600. / 175.;
|
||||
const EORZEA_TO_EARTH: f64 = 1. / EARTH_TO_EORZEA;
|
||||
|
||||
/// Converts a human timestamp to an Eorzean one.
|
||||
pub fn to_eorzea_time(earth_time: DateTime<Utc>) -> DateTime<Utc> {
|
||||
let et_ts = earth_time.timestamp();
|
||||
let new_ts = et_ts.abs() as f64 * EARTH_TO_EORZEA;
|
||||
DateTime::from_timestamp(new_ts.floor() as i64, 0).unwrap()
|
||||
}
|
||||
|
||||
/// Gets the current date and time in Eorzea. Arguably you will never need the date, though,
|
||||
/// because it isn't shown anywhere in the game, the time is what matters.
|
||||
pub fn get_current_eorzea_date() -> DateTime<Utc> {
|
||||
to_eorzea_time(Utc::now())
|
||||
}
|
||||
|
||||
/// Converts an Eorzean timestamp to human time.
|
||||
pub fn to_earth_time(ez_time: DateTime<Utc>) -> DateTime<Utc> {
|
||||
let ez_ts = ez_time.timestamp();
|
||||
let new_ts = (ez_ts.abs() as f64 * EORZEA_TO_EARTH).ceil();
|
||||
DateTime::from_timestamp(new_ts as i64, 0).unwrap()
|
||||
}
|
||||
|
||||
/// Sets hours and minutes for a date, based on a f32.
|
||||
pub fn set_hm_from_float(date: &DateTime<Utc>, hm: f32) -> DateTime<Utc> {
|
||||
let minutes = ((hm % 1.) * 100.) as u32;
|
||||
date.date_naive()
|
||||
.and_hms_opt(hm as u32, minutes, date.second())
|
||||
.unwrap()
|
||||
.and_utc()
|
||||
}
|
||||
|
|
110
src/data.rs
110
src/data.rs
|
@ -1,11 +1,13 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Timelike;
|
||||
use chrono::{DateTime, Duration, Timelike, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
clock,
|
||||
forecast::{Forecast, ForecastSet, Rate},
|
||||
forecast::{
|
||||
self, modify_weather_time, round_to_last_weather_time, Forecast, ForecastSet, Rate,
|
||||
},
|
||||
};
|
||||
|
||||
const DATA: &'static str = include_str!("../data.json");
|
||||
|
@ -32,7 +34,7 @@ pub struct SubData {
|
|||
pub weather_types: HashMap<u32, WeatherType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
#[serde(rename_all(deserialize = "camelCase"))]
|
||||
pub struct FishEntry {
|
||||
#[serde(alias = "_id")]
|
||||
|
@ -83,7 +85,7 @@ pub struct Location {
|
|||
pub map_coords: (f32, f32, u32),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct FishMeta {
|
||||
pub id: u32,
|
||||
pub name_en: String,
|
||||
|
@ -92,14 +94,23 @@ pub struct FishMeta {
|
|||
pub struct CombinedFish<'a> {
|
||||
pub entry: &'a FishEntry,
|
||||
pub meta: &'a FishMeta,
|
||||
pub is_up: bool,
|
||||
pub next_uptime: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl<'a> CombinedFish<'a> {
|
||||
pub fn is_in_time_range(&self) -> bool {
|
||||
/// Fills in the rest of the struct.
|
||||
pub fn find_uptime(&mut self, data: &Data) {
|
||||
self.is_up = self.is_up_now(data);
|
||||
self.next_uptime = self.get_next_uptime(data);
|
||||
}
|
||||
|
||||
/// Finds out whether the fish is technically up at the given time (discounting factors like weather).
|
||||
pub fn is_in_time_range_at(&self, date: DateTime<Utc>) -> bool {
|
||||
if self.entry.start_hour.is_none() || self.entry.end_hour.is_none() {
|
||||
return false;
|
||||
}
|
||||
let et = clock::get_current_eorzea_date();
|
||||
let et = clock::to_eorzea_time(date);
|
||||
let start_hour = self.entry.start_hour.unwrap();
|
||||
let end_hour = self.entry.end_hour.unwrap();
|
||||
let spans_midnight = start_hour > end_hour;
|
||||
|
@ -111,6 +122,7 @@ impl<'a> CombinedFish<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the associated forecast struct.
|
||||
pub fn get_forecast<'b>(&self, data: &'b Data) -> Option<&'b Forecast> {
|
||||
if self.entry.location.is_none() {
|
||||
return None;
|
||||
|
@ -124,12 +136,13 @@ impl<'a> CombinedFish<'a> {
|
|||
data.forecasts.get(&spot.forecast_id)
|
||||
}
|
||||
|
||||
pub fn is_in_correct_weather(&self, forecast: &Forecast) -> bool {
|
||||
/// Finds out whether the fish is in favorable weather conditions at a specific Eorzean time.
|
||||
pub fn is_in_correct_weather_at(&self, forecast: &Forecast, date: DateTime<Utc>) -> bool {
|
||||
if self.entry.weather_set.is_empty() {
|
||||
return true;
|
||||
}
|
||||
// Check if the current weather is right!
|
||||
let cur_weather = forecast.weather_now();
|
||||
let cur_weather = forecast.weather_at(date);
|
||||
let is_current = self
|
||||
.entry
|
||||
.weather_set
|
||||
|
@ -141,7 +154,8 @@ impl<'a> CombinedFish<'a> {
|
|||
let is_past = if self.entry.previous_weather_set.is_empty() {
|
||||
true
|
||||
} else {
|
||||
let prev_weather = forecast.nth_weather(-1);
|
||||
let prev_date = date - Duration::hours(8);
|
||||
let prev_weather = forecast.weather_at(prev_date);
|
||||
self.entry
|
||||
.previous_weather_set
|
||||
.iter()
|
||||
|
@ -151,10 +165,71 @@ impl<'a> CombinedFish<'a> {
|
|||
is_current && is_past
|
||||
}
|
||||
|
||||
pub fn is_up(&self, data: &Data) -> bool {
|
||||
/// Finds out if the fish is up now, includes weather and time bounds.
|
||||
pub fn is_up_now(&self, data: &Data) -> bool {
|
||||
self.is_up_at_n(data, 0)
|
||||
}
|
||||
|
||||
/// Finds out if the fish is up at a specific time, `n` Eorzean hours away.
|
||||
pub fn is_up_at_n(&self, data: &Data, n: i64) -> bool {
|
||||
let forecast = self.get_forecast(data);
|
||||
self.is_in_time_range()
|
||||
&& forecast.is_none_or(|forecast| self.is_in_correct_weather(forecast))
|
||||
let date = clock::get_current_eorzea_date() + Duration::hours(n);
|
||||
self.is_in_time_range_at(date)
|
||||
&& forecast.is_none_or(|forecast| self.is_in_correct_weather_at(forecast, date))
|
||||
}
|
||||
|
||||
/// Get the next uptime for a fish that is only restricted by time.
|
||||
pub fn get_next_time_uptime(&self) -> DateTime<Utc> {
|
||||
let ez_date = clock::get_current_eorzea_date();
|
||||
// Check if we have a minute part to our start time, which can happen.
|
||||
let adjusted_date = clock::set_hm_from_float(&ez_date, self.entry.start_hour.unwrap());
|
||||
clock::to_earth_time(adjusted_date)
|
||||
}
|
||||
|
||||
/// Get the next times when the weather is favorable for a fish.
|
||||
pub fn get_next_weather_uptimes(&self, data: &Data, cycles: u32) -> Vec<DateTime<Utc>> {
|
||||
let forecast = self.get_forecast(data).unwrap();
|
||||
// Skip ahead by 8-hour intervals, to quickly find the next favorable weather.
|
||||
let mut date = round_to_last_weather_time(clock::get_current_eorzea_date());
|
||||
// Start with the next weather cycle.
|
||||
date = date + Duration::hours(8);
|
||||
let mut results = Vec::new();
|
||||
for i in 1..cycles {
|
||||
while !self.is_in_correct_weather_at(forecast, date) {
|
||||
date = date + Duration::hours(8);
|
||||
}
|
||||
results.push(date.clone());
|
||||
date = date + Duration::hours(8);
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
// Go until you find the next uptime slot, and return its date.
|
||||
pub fn get_next_uptime(&self, data: &Data) -> DateTime<Utc> {
|
||||
// Is this a time-only fish? If so, just return the next time.
|
||||
if self.entry.weather_set.is_empty() && self.entry.start_hour.is_some() {
|
||||
return clock::to_earth_time(self.get_next_time_uptime());
|
||||
}
|
||||
|
||||
// If not, check if there's a weather-only fish
|
||||
let next_weather_uptimes = self.get_next_weather_uptimes(data, 50);
|
||||
if !self.entry.weather_set.is_empty() && self.entry.start_hour.is_none() {
|
||||
return clock::to_earth_time(next_weather_uptimes.first().unwrap().clone());
|
||||
}
|
||||
|
||||
// Now it gets a little bit tricky: We need to find a weather slot that also includes the start point for the
|
||||
// time window.
|
||||
for date in next_weather_uptimes {
|
||||
// Does the start time lie within this window?
|
||||
let uptime_end = date + Duration::hours(8);
|
||||
let start_hour = self.entry.start_hour.unwrap();
|
||||
if date.hour() as f32 <= start_hour && start_hour <= uptime_end.hour() as f32 {
|
||||
return clock::to_earth_time(clock::set_hm_from_float(&date, start_hour));
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This should never be called?
|
||||
Utc::now()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,7 +267,16 @@ impl Data {
|
|||
.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
let corresponding_meta = self.fish_entries.iter().find(|m| &m.id == k);
|
||||
corresponding_meta.map(|m| (k.clone(), CombinedFish { entry: v, meta: m }))
|
||||
corresponding_meta.map(|m| {
|
||||
let mut cfish = CombinedFish {
|
||||
entry: v,
|
||||
meta: m,
|
||||
is_up: false, // fake default values for now
|
||||
next_uptime: Utc::now(), // dito
|
||||
};
|
||||
cfish.find_uptime(self);
|
||||
(k.clone(), cfish)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::HashMap, f32::consts::PI};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Duration, Timelike, Utc};
|
||||
|
||||
|
@ -27,44 +27,51 @@ pub struct Rate {
|
|||
pub type ForecastSet = HashMap<u32, Forecast>;
|
||||
|
||||
impl Forecast {
|
||||
/// Calculates the current weather for the given target magic number.
|
||||
pub fn weather_for_target(&self, target: u32) -> &Rate {
|
||||
// TODO: Don't unwrap here!
|
||||
self.rates.iter().find(|r| target < r.rate).unwrap()
|
||||
}
|
||||
|
||||
/// Calculates the weather right now.
|
||||
pub fn weather_now(&self) -> &Rate {
|
||||
let utc = Utc::now();
|
||||
let target = calculate_target(utc);
|
||||
self.weather_for_target(target)
|
||||
}
|
||||
|
||||
/// Returns the weather `n` cycles before or after the current weather.
|
||||
pub fn nth_weather(&self, n: i32) -> &Rate {
|
||||
let last_weather_time = round_to_last_weather_time(&clock::get_current_eorzea_date());
|
||||
let hours = n * 8;
|
||||
let td = Duration::hours(hours.abs() as i64);
|
||||
let new_date = if hours > 0 {
|
||||
last_weather_time.checked_add_signed(td).unwrap()
|
||||
} else {
|
||||
last_weather_time.checked_sub_signed(td).unwrap()
|
||||
};
|
||||
let target = calculate_target(new_date);
|
||||
/// Calculates the weather at a specific point in Eorzean time.
|
||||
pub fn weather_at(&self, date: DateTime<Utc>) -> &Rate {
|
||||
let new_date = round_to_last_weather_time(date);
|
||||
let target = calculate_target(clock::to_earth_time(new_date));
|
||||
self.weather_for_target(target)
|
||||
}
|
||||
}
|
||||
|
||||
/// Rounds to the last weather "start". These happen three times a day, at 0:00,
|
||||
/// at 8:00, and at 16:00.
|
||||
pub fn round_to_last_weather_time(date: &DateTime<Utc>) -> DateTime<Utc> {
|
||||
pub fn round_to_last_weather_time(date: DateTime<Utc>) -> DateTime<Utc> {
|
||||
let cur_hour = date.hour();
|
||||
// This essentially performs division without rest.
|
||||
let last_hour = (cur_hour / 8) * 8;
|
||||
date.date_naive()
|
||||
.and_hms_opt(last_hour, date.minute(), date.second())
|
||||
.and_hms_opt(last_hour, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc()
|
||||
}
|
||||
|
||||
/// Adds or subtracts `n` intervals of 8 hours to the last weather time.
|
||||
pub fn modify_weather_time(n: i32) -> DateTime<Utc> {
|
||||
let last_weather_time = round_to_last_weather_time(clock::get_current_eorzea_date());
|
||||
let hours = n * 8;
|
||||
let td = Duration::hours(hours.abs() as i64);
|
||||
if hours > 0 {
|
||||
last_weather_time.checked_add_signed(td).unwrap()
|
||||
} else {
|
||||
last_weather_time.checked_sub_signed(td).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the magic target number for a specific date.
|
||||
/// It's important to note that this expects a human time, not an Eorzean time.
|
||||
pub fn calculate_target(m: DateTime<Utc>) -> u32 {
|
||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -36,24 +36,16 @@ where
|
|||
#[axum::debug_handler]
|
||||
async fn main_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> {
|
||||
let meta = state.data.fish_with_meta();
|
||||
let mut values: Vec<(&CombinedFish, bool)> = meta
|
||||
.values()
|
||||
.filter_map(|fish| {
|
||||
if fish.entry.big_fish {
|
||||
let is_up = fish.is_up(&state.data);
|
||||
Some((fish, is_up))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
values.sort_by(|(_, aup), (_, bup)| bup.cmp(aup));
|
||||
let mut values: Vec<&CombinedFish> = meta.values().filter(|f| f.entry.big_fish).collect();
|
||||
values.sort_by(|afish, bfish| bfish.is_up.cmp(&afish.is_up));
|
||||
Ok(html! {
|
||||
h1 { "Hello! Current ET " (clock::get_current_eorzea_date().format("%H:%M")) }
|
||||
@for (fish, is_up) in values {
|
||||
@for fish in values {
|
||||
li {
|
||||
@if is_up {
|
||||
@if fish.is_up {
|
||||
"Up! "
|
||||
} @else {
|
||||
"Next uptime " (fish.next_uptime)
|
||||
}
|
||||
(fish.meta.name_en)
|
||||
details {
|
||||
|
|
Loading…
Add table
Reference in a new issue