aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html2
-rw-r--r--src/history.rs1
-rw-r--r--src/lib.rs190
-rw-r--r--src/ocr.rs7
4 files changed, 131 insertions, 69 deletions
diff --git a/index.html b/index.html
index cb5aa8c..bc7a760 100644
--- a/index.html
+++ b/index.html
@@ -27,7 +27,7 @@
<canvas id="canvas">
</canvas>
<main>
- <h1>queue go brrr<span id="eta"></span></h1>
+ <h1 id="title">queue go brrr</h1>
<h2>For tracking your Login Queue (Extreme) raid progression.</h2>
<p>
Share your FFXIV window, and then this tool will try to calculate how much longer you probably have to wait.
diff --git a/src/history.rs b/src/history.rs
index 6031499..a579a8c 100644
--- a/src/history.rs
+++ b/src/history.rs
@@ -1,7 +1,6 @@
// god is dead and we have killed him and consequently std::time::Instant doesn't work on wasm
use chrono::prelude::*;
-use chrono::Duration;
use wasm_bindgen::prelude::*;
struct Reading {
diff --git a/src/lib.rs b/src/lib.rs
index b0c7353..572b533 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-use image::{DynamicImage, ImageFormat};
+use image::{DynamicImage, GrayImage, ImageFormat};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
@@ -40,6 +40,14 @@ fn get_queue_size(image: DynamicImage, corner: &(u32, u32)) -> DynamicImage {
useful_region
}
+fn is_connection_error(image: &GrayImage) -> bool {
+ // prior experiments show that the 80th percentile is like 20 and the 100th percentile is 51 for the error state
+ let pct_80 = imageproc::stats::percentile(image, 80);
+ let pct_100 = imageproc::stats::percentile(image, 100);
+
+ pct_80 < 25 && pct_100 < 60
+}
+
fn update_target_marker(
target_marker: &HtmlElement,
width: &Rc<Cell<u32>>,
@@ -96,6 +104,120 @@ impl QueuePosition {
}
}
+enum Status {
+ ETAPending,
+ HasETA(js_sys::Date),
+ ETAFucked,
+ ConnectionError,
+}
+
+impl Status {
+ fn render_title(&self) -> String {
+ match self {
+ Status::ETAPending => format!("queue go brrr - ETA pending"),
+ Status::ETAFucked => format!("queue go brrr - ETA fucked (try reloading i guess?)"),
+ Status::HasETA(date) => {
+ let empty_array = js_sys::Array::new();
+ let options = {
+ let kv_pair = js_sys::Array::new();
+ kv_pair.push(&JsValue::from("timeStyle"));
+ kv_pair.push(&JsValue::from("short"));
+ let kv_pairs = js_sys::Array::new();
+ kv_pairs.push(&kv_pair);
+ js_sys::Object::from_entries(&kv_pairs).unwrap()
+ };
+ let format = js_sys::Intl::DateTimeFormat::new(&empty_array, &options);
+ let format = format.format();
+ let locale_string = format.call1(&JsValue::UNDEFINED, &date).unwrap();
+ locale_string.as_string().unwrap()
+ }
+ Status::ConnectionError => format!("queue go brrr - connection error?"),
+ }
+ }
+}
+
+#[derive(Clone)]
+struct State {
+ history: Rc<RefCell<History>>,
+ status: Rc<RefCell<Status>>,
+ queue_position: QueuePosition,
+ preview_image: Element,
+ current_queue_position: HtmlElement,
+ title_h1: HtmlElement,
+ title_element: HtmlElement,
+}
+
+impl State {
+ fn new(queue_position: &QueuePosition) -> Self {
+ let document = gloo::utils::document();
+ let preview_image = document.get_element_by_id("photo").unwrap();
+ let current_queue_position = document
+ .get_element_by_id("current-queue-position")
+ .unwrap()
+ .dyn_into()
+ .unwrap();
+ let title_h1 = document
+ .get_element_by_id("title")
+ .unwrap()
+ .dyn_into()
+ .unwrap();
+ let title_element = document
+ .query_selector("title")
+ .unwrap()
+ .unwrap()
+ .dyn_into()
+ .unwrap();
+ Self {
+ history: Rc::new(RefCell::new(History::default())),
+ status: Rc::new(RefCell::new(Status::ETAPending)),
+ queue_position: queue_position.clone(),
+ preview_image,
+ current_queue_position,
+ title_h1,
+ title_element,
+ }
+ }
+
+ async fn update(&self, data: Blob) {
+ let image = blob_to_image(data).await;
+ let useful_region = get_queue_size(image, &self.queue_position.get());
+ let data_uri = image_to_data_uri(&useful_region);
+ self.preview_image.set_attribute("src", &data_uri).unwrap();
+ let useful_region = image::imageops::grayscale(&useful_region);
+ let new_status = if is_connection_error(&useful_region) {
+ Status::ConnectionError
+ } else {
+ let queue_size = ocr::ocr(&useful_region);
+ match queue_size {
+ Some(queue_size) => {
+ self.current_queue_position
+ .set_inner_text(&format!("{}", queue_size));
+ match self.history.try_borrow_mut() {
+ Ok(mut history) => {
+ history.record(queue_size);
+ let estimated_finish = history.completion_time();
+ match estimated_finish {
+ Ok(finish) => Status::HasETA(finish),
+ Err(NoETA::Pending) => Status::ETAPending,
+ Err(NoETA::Fuck) => Status::ETAFucked,
+ }
+ }
+ Err(_) => return,
+ }
+ }
+ None => {
+ self.current_queue_position
+ .set_inner_text("something i can't read");
+ return;
+ }
+ }
+ };
+ let title = new_status.render_title();
+ self.title_h1.set_inner_text(&title);
+ self.title_element.set_inner_text(&title);
+ }
+}
+
async fn do_boot() {
let width = Rc::new(Cell::new(0));
let height = Rc::new(Cell::new(0));
@@ -106,7 +228,7 @@ async fn do_boot() {
let document = gloo::utils::document();
- let history = Rc::new(RefCell::new(History::default()));
+ let state = State::new(&queue_position);
let (manual_update_tx, manual_update_rx) = mpsc::channel::<()>(5);
@@ -198,8 +320,7 @@ async fn do_boot() {
manual_update_rx,
);
let update_future = update_stream.for_each(move |_| {
- let history = history.clone();
- let queue_position = queue_position.clone();
+ let state = state.clone();
if !streaming.get() {
return futures::future::ready(());
}
@@ -220,66 +341,9 @@ async fn do_boot() {
canvas
.to_blob(
Closure::once_into_js(move |data: Blob| {
- let history = history.clone();
- let queue_position = queue_position.clone();
- // lifetime bullshit here
- let document = gloo::utils::document();
- let photo = document.get_element_by_id("photo").unwrap();
- let current_queue_position: HtmlElement = document
- .get_element_by_id("current-queue-position")
- .unwrap()
- .dyn_into()
- .unwrap();
- let eta: HtmlElement = document
- .get_element_by_id("eta")
- .unwrap()
- .dyn_into()
- .unwrap();
+ let state = state.clone();
spawn_local(async move {
- let image = blob_to_image(data).await;
- let useful_region = get_queue_size(image, &queue_position.get());
- let data_uri = image_to_data_uri(&useful_region);
- photo.set_attribute("src", &data_uri).unwrap();
- let queue_size = ocr::ocr(useful_region);
- match queue_size {
- Some(queue_size) => {
- current_queue_position.set_inner_text(&format!("{}", queue_size));
- if let Ok(mut history) = history.try_borrow_mut() {
- history.record(queue_size);
- let estimated_finish = history.completion_time();
- let eta_text = match estimated_finish {
- Ok(finish) => {
- let empty_array = js_sys::Array::new();
- let options = {
- let kv_pair = js_sys::Array::new();
- kv_pair.push(&JsValue::from("timeStyle"));
- kv_pair.push(&JsValue::from("short"));
- let kv_pairs = js_sys::Array::new();
- kv_pairs.push(&kv_pair);
- js_sys::Object::from_entries(&kv_pairs).unwrap()
- };
- let format = js_sys::Intl::DateTimeFormat::new(
- &empty_array,
- &options,
- );
- let format = format.format();
- let locale_string =
- format.call1(&JsValue::UNDEFINED, &finish).unwrap();
- locale_string.as_string().unwrap()
- }
- Err(NoETA::Pending) => format!("pending"),
- Err(NoETA::Fuck) => {
- format!("fucked (try reloading i guess?)")
- }
- };
- let label = format!(" - ETA {}", eta_text);
- eta.set_inner_text(&label);
- }
- }
- None => {
- current_queue_position.set_inner_text("something i can't read");
- }
- }
+ state.update(data).await;
});
})
.dyn_ref()
diff --git a/src/ocr.rs b/src/ocr.rs
index 14021cc..52e0020 100644
--- a/src/ocr.rs
+++ b/src/ocr.rs
@@ -1,4 +1,4 @@
-use image::{DynamicImage, GrayImage, ImageFormat};
+use image::{GrayImage, ImageFormat};
use imageproc::template_matching::MatchTemplateMethod;
use lazy_static::lazy_static;
@@ -36,11 +36,10 @@ fn x_matches(image: &GrayImage, template: &GrayImage) -> Vec<u32> {
.collect()
}
-pub fn ocr(image: DynamicImage) -> Option<u32> {
- let grayscale_image = image::imageops::grayscale(&image);
+pub fn ocr(image: &GrayImage) -> Option<u32> {
let mut digit_x_positions: Vec<(u8, u32)> = (0..10)
.flat_map(|i| {
- x_matches(&grayscale_image, &REFERENCES[i as usize])
+ x_matches(image, &REFERENCES[i as usize])
.into_iter()
.map(move |x| (i, x))
})