use std::fmt;
use web_sys::{wasm_bindgen::JsCast, window, CanvasRenderingContext2d, HtmlCanvasElement};
#[derive(Debug, Clone, PartialEq)]
pub struct Segment {
pub start_hour: f32,
pub end_hour: f32,
pub status: DutyStatus,
pub location: String,
pub note: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DutyStatus {
OffDuty,
Sleeper,
Driving,
OnDuty,
PersonalConveyance,
YardMove,
}
impl fmt::Display for DutyStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status_str = match *self {
DutyStatus::OffDuty => "OffDuty",
DutyStatus::Sleeper => "Sleeper",
DutyStatus::Driving => "Driving",
DutyStatus::OnDuty => "OnDuty",
DutyStatus::PersonalConveyance => "PersonalConveyance",
DutyStatus::YardMove => "YardMove",
};
write!(f, "{}", status_str)
}
}
#[derive(PartialEq, Clone)]
pub struct ChartProps {
pub width: u32,
pub height: u32,
pub background_color: &'static str,
pub grid_color: &'static str,
pub font: &'static str,
pub label_color: &'static str,
pub off_duty_color: &'static str,
pub sleeper_color: &'static str,
pub driving_color: &'static str,
pub on_duty_color: &'static str,
}
pub fn draw_chart<'a>(
segments: &'a [Segment],
props: &'a ChartProps,
) -> Result<&'a ChartProps, String> {
let canvas = get_canvas("eld-canvas")?;
let context = get_canvas_context(&canvas)?;
let (width, height) = (canvas.width() as f64, canvas.height() as f64);
if grid_already_drawn()? {
draw_segments(&context, segments, width, height, props);
return Ok(props);
}
draw_grid(&context, width, height, props);
draw_segments(&context, segments, width, height, props);
mark_grid_as_drawn()?;
Ok(props)
}
pub fn clear_chart() -> Result<(), String> {
let canvas = get_canvas("eld-canvas")?;
let context = get_canvas_context(&canvas)?;
context.clear_rect(0.0, 0.0, canvas.width() as f64, canvas.height() as f64);
let document = window()
.ok_or("No Window found".to_string())?
.document()
.ok_or("No Document found".to_string())?;
if let Some(existing_grid) = document.get_element_by_id("grid-drawn") {
existing_grid
.set_attribute("data-drawn", "falase")
.map_err(|_| "Failed to set attribute".to_string())?;
}
Ok(())
}
fn get_canvas(id: &str) -> Result<HtmlCanvasElement, String> {
window()
.ok_or("No Window found".to_string())?
.document()
.ok_or("No Document found".to_string())?
.get_element_by_id(id)
.ok_or_else(|| format!("Canvas with id '{}' not found", id))?
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| "Failed to cast element to Canvas".to_string())
}
fn get_canvas_context(canvas: &HtmlCanvasElement) -> Result<CanvasRenderingContext2d, String> {
canvas
.get_context("2d")
.map_err(|_| "Failed to get 2D context".to_string())?
.ok_or_else(|| "2D context is unavailable".to_string())?
.dyn_into::<CanvasRenderingContext2d>()
.map_err(|_| "Failed to cast context to CanvasRenderingContext2d".to_string())
}
fn grid_already_drawn() -> Result<bool, String> {
let document = window()
.ok_or("No Window found".to_string())?
.document()
.ok_or("No Document found".to_string())?;
if let Some(existing_grid) = document.get_element_by_id("grid-drawn") {
return Ok(existing_grid.get_attribute("data-drawn") == Some("true".to_string()));
}
Ok(false)
}
fn mark_grid_as_drawn() -> Result<(), String> {
let document = window()
.ok_or("No Window found".to_string())?
.document()
.ok_or("No Document found".to_string())?;
let grid_marker = document
.create_element("div")
.map_err(|_| "Failed to create grid marker".to_string())?;
grid_marker.set_id("grid-drawn");
grid_marker
.set_attribute("data-drawn", "true")
.map_err(|_| "Failed to set attribute".to_string())?;
document
.body()
.ok_or("No body found".to_string())?
.append_child(&grid_marker)
.map_err(|_| "Failed to append child".to_string())?;
Ok(())
}
fn draw_grid(context: &CanvasRenderingContext2d, width: f64, height: f64, props: &ChartProps) {
context.clear_rect(0.0, 0.0, width, height);
let padding_x = 70.0;
let padding_y = 40.0;
let row_height = (height - 2.0 * padding_y) / 4.0;
let col_width = (width - 2.0 * padding_x) / 24.0;
let statuses = ["Off Duty", "Sleeper", "Driving", "On Duty"];
let hours = generate_hour_labels();
context.set_stroke_style_str(props.grid_color);
context.set_fill_style_str(props.label_color);
context.set_font(props.font);
for i in 0..=4 {
let y = padding_y + i as f64 * row_height;
context.begin_path();
context.move_to(padding_x, y);
context.line_to(width, y);
context.stroke();
if i < 4 {
context
.fill_text(statuses[i], 10.0, y + row_height / 2.0)
.unwrap_or_else(|_| log::warn!("Failed to draw text"));
}
}
context.set_font("12px Arial");
for i in 0..25 {
let x = padding_x + i as f64 * col_width;
context.begin_path();
context.move_to(x, padding_y);
context.line_to(x, height);
context.set_stroke_style_str(props.grid_color);
context.stroke();
if i % 2 == 0 {
context
.fill_text(&hours[i], x - 10.0, height - 10.0)
.unwrap_or_else(|_| log::warn!("Failed to draw text"));
}
}
}
fn draw_segments(
context: &CanvasRenderingContext2d,
segments: &[Segment],
width: f64,
height: f64,
props: &ChartProps,
) {
let padding_x = 70.0;
let padding_y = 40.0;
let row_height = (height - 2.0 * padding_y) / 4.0;
let col_width = (width - 2.0 * padding_x) / 24.0;
context.set_line_width(4.0);
for segment in segments {
let y_index = match segment.status {
DutyStatus::OffDuty => 0,
DutyStatus::Sleeper => 1,
DutyStatus::Driving => 2,
DutyStatus::OnDuty => 3,
DutyStatus::PersonalConveyance | DutyStatus::YardMove => 999, };
let y_val = padding_y + (y_index as f64) * row_height + (row_height / 2.0);
let x_start = padding_x + (segment.start_hour as f64) * col_width;
let x_end = padding_x + (segment.end_hour as f64) * col_width;
let color = match segment.status {
DutyStatus::OffDuty => props.off_duty_color,
DutyStatus::Sleeper => props.sleeper_color,
DutyStatus::Driving => props.driving_color,
DutyStatus::OnDuty => props.on_duty_color,
DutyStatus::PersonalConveyance | DutyStatus::YardMove => "",
};
context.set_stroke_style_str(color);
context.begin_path();
context.move_to(x_start, y_val);
context.line_to(x_end, y_val);
context.stroke();
}
}
fn generate_hour_labels() -> Vec<String> {
let mut hours: Vec<String> = (0..24)
.map(|h| {
format!(
"{} {}",
if h == 0 || h == 12 { 12 } else { h % 12 },
if h < 12 { "AM" } else { "PM" }
)
})
.collect();
hours.push("12 AM".to_string());
hours
}