diff --git a/nexrad-browser/Cargo.toml b/nexrad-browser/Cargo.toml index 03d0cf6..389d2fa 100644 --- a/nexrad-browser/Cargo.toml +++ b/nexrad-browser/Cargo.toml @@ -21,6 +21,7 @@ log = "0.4" wasm-logger = "0.2" serde-wasm-bindgen = "0.6" chrono = "0.4" +itertools = "0.11" [dependencies.js-sys] version = "0.3" diff --git a/nexrad-browser/src/command.rs b/nexrad-browser/src/command.rs index e915614..7d26bb2 100644 --- a/nexrad-browser/src/command.rs +++ b/nexrad-browser/src/command.rs @@ -1,13 +1,16 @@ use web_sys::FileReader; use crate::loadar2; +use crate::mode::Mode; use crate::rendering::render; use crate::scope::ScopeState; pub fn should_newline(state: &mut ScopeState) -> bool { - state.command_buf.starts_with("SET MODE") + state.command_buf.starts_with("MODE SET") + || state.command_buf.starts_with("ELEVATION SET") } pub fn exec(state: &mut ScopeState, command: String) { + let tokens = command.split(' ').collect::>(); if command == "CLF OV" { state.file_input.click(); } else if command == "CLF RELOAD" { @@ -15,6 +18,67 @@ pub fn exec(state: &mut ScopeState, command: String) { state.command_buf_response_mode = true; render(state).expect("rerender failed"); loadar2("file"); + } else if command.starts_with("MODE SET") { + if tokens.len() < 3 { + state.command_buf = "ARGUMENT INVALID".to_string(); + state.command_buf_response_mode = true; + return + } else { + let mode = tokens[2]; + if let Some(ar2) = &state.ar2 { + let types = ar2.elevations.get(&state.selected_elevation).unwrap(); + let valid_modes: Vec<_> = types[0].available_data.keys().collect(); + + if !valid_modes.contains(&&mode.to_string()) { + state.command_buf = "ARGUMENT INVALID".to_string(); + state.command_buf_response_mode = true; + return + } + + state.scope_mode = match mode { + "REF" => Mode::Reflectivity, + "VEL" => Mode::Velocity, + "SW" => Mode::SpectrumWidth, + "ZDR" => Mode::DifferentialReflectivity, + "PHI" => Mode::DifferentialPhase, + "RHO" => Mode::CorrelationCoefficient, + "CFP" => Mode::ClutterFilterPowerRemoved, + _ => unreachable!() + }; + } else { + state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string(); + state.command_buf_response_mode = true; + return + } + } + } else if command.starts_with("ELEVATION SET") { + if tokens.len() < 3 { + state.command_buf = "ARGUMENT INVALID".to_string(); + state.command_buf_response_mode = true; + return + } else { + let elev = tokens[2].to_string(); + if let Some(ar2) = &state.ar2 { + let mut valid_elevs = vec![]; + for elev in ar2.elevations.keys() { + valid_elevs.push(format!("{:0>3}", elev)); + } + if !valid_elevs.contains(&elev) { + state.command_buf = "ARGUMENT INVALID".to_string(); + state.command_buf_response_mode = true; + return + } + let number: usize = elev.parse().unwrap(); + + // reset scope back to reflectivity which is included in every elevation + state.scope_mode = Mode::Reflectivity; + state.selected_elevation = number; + } else { + state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string(); + state.command_buf_response_mode = true; + return + } + } } else { state.command_buf = "UNRECOGNIZED COMMAND".to_string(); state.command_buf_response_mode = true; diff --git a/nexrad-browser/src/lib.rs b/nexrad-browser/src/lib.rs index 05dea0f..895b5ee 100644 --- a/nexrad-browser/src/lib.rs +++ b/nexrad-browser/src/lib.rs @@ -15,6 +15,7 @@ use wasm_bindgen::prelude::*; use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; use crate::command::{exec, should_newline}; use crate::mode::Mode; +use crate::mode::Mode::Reflectivity; use crate::scope::{Preferences, RenderState, ScopeState}; #[wasm_bindgen] @@ -61,7 +62,8 @@ pub fn __nxrd_browser_init() -> AbiScopeState { }, command_buf: String::new(), command_buf_response_mode: false, - new_data_available: false + new_data_available: false, + selected_elevation: 1 }; info!("nexrad-browser initialized successfully"); @@ -83,6 +85,8 @@ pub fn load_ar2(buf: &JsValue, scope: &mut AbiScopeState) { info!("new data chunk loaded"); scope.0.ar2 = Some(loaded); scope.0.new_data_available = false; + scope.0.scope_mode = Reflectivity; + scope.0.command_buf = "NEW DATA LOADED SUCCESSFULLY".to_string(); } #[wasm_bindgen] @@ -92,7 +96,8 @@ pub fn new_file_available(scope: &mut AbiScopeState) { #[wasm_bindgen] pub fn keydown(state: &mut AbiScopeState, key: String) { - if state.0.command_buf_response_mode { + if state.0.command_buf_response_mode && (key == "Backspace" || key.len() == 1) { + info!("resetting, key {} buf {}", key, state.0.command_buf); state.0.command_buf = String::new(); state.0.command_buf_response_mode = false; } @@ -101,8 +106,8 @@ pub fn keydown(state: &mut AbiScopeState, key: String) { state.0.command_buf = String::new(); } else if key == "Enter" { let cmd = state.0.command_buf.clone(); - exec(&mut state.0, cmd.replace('\n', " ")); state.0.command_buf = String::new(); + exec(&mut state.0, cmd.replace('\n', " ")); } else if key == " " && should_newline(&mut state.0) { state.0.command_buf += "\n"; } else if key == "Backspace" { diff --git a/nexrad-browser/src/mode.rs b/nexrad-browser/src/mode.rs index 18dd4bc..3f77059 100644 --- a/nexrad-browser/src/mode.rs +++ b/nexrad-browser/src/mode.rs @@ -1,4 +1,4 @@ -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Mode { /// REF Reflectivity, @@ -16,4 +16,43 @@ pub enum Mode { ClutterFilterPowerRemoved, /// RADR INOP RadarInoperative +} +impl Mode { + pub fn unselected(&self) -> &str { + match self { + Self::Reflectivity => " REF ", + Self::Velocity => " VEL ", + Self::SpectrumWidth => " SW ", + Self::DifferentialReflectivity => " ZDR ", + Self::DifferentialPhase => " PHI ", + Self::CorrelationCoefficient => " RHO ", + Self::ClutterFilterPowerRemoved => " CFP ", + Self::RadarInoperative => " RADR INOP" + } + } + pub fn selected(&self) -> &str { + match self { + Self::Reflectivity => ">REF<", + Self::Velocity => ">VEL<", + Self::SpectrumWidth => ">SW <", + Self::DifferentialReflectivity => ">ZDR<", + Self::DifferentialPhase => ">PHI<", + Self::CorrelationCoefficient => ">RHO<", + Self::ClutterFilterPowerRemoved => ">CFP<", + Self::RadarInoperative => ">RADR INOP<" + } + } + + pub fn rname(&self) -> &str { + match self { + Self::Reflectivity => "REF", + Self::Velocity => "VEL", + Self::SpectrumWidth => "SW ", + Self::DifferentialReflectivity => "ZDR", + Self::DifferentialPhase => "PHI", + Self::CorrelationCoefficient => "RHO", + Self::ClutterFilterPowerRemoved => "CFP", + Self::RadarInoperative => "NOP" + } + } } \ No newline at end of file diff --git a/nexrad-browser/src/rendering.rs b/nexrad-browser/src/rendering.rs index 7fa493f..af3c95c 100644 --- a/nexrad-browser/src/rendering.rs +++ b/nexrad-browser/src/rendering.rs @@ -1,5 +1,9 @@ use chrono::{DateTime, Timelike, Utc}; +use itertools::Itertools; +use log::info; use wasm_bindgen::JsValue; +use crate::mode::Mode; +use crate::mode::Mode::{ClutterFilterPowerRemoved, CorrelationCoefficient, DifferentialPhase, DifferentialReflectivity, RadarInoperative, Reflectivity, SpectrumWidth, Velocity}; use crate::rescaleCanvas; use crate::scope::ScopeState; use crate::utils::parse_date; @@ -9,6 +13,8 @@ pub const TEXT_COLOR_RED: &str = "#ef0000"; pub const TEXT_COLOR_GREEN: &str = "#4af626"; pub const TEXT_COLOR_WHITE: &str = "#dedede"; +pub const RANGE: usize = 50; + pub fn time(h: usize, m: usize, s: usize, tag_end: &str) -> String { format!("{:0>2}:{:0>2}:{:0>2}{}", h, m, s, tag_end) } @@ -69,12 +75,92 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> { ctx.fill_text(&format!("{} VCP {} {}", ar2.volume_header_record.icao, ar2.meta_rda_status_data.vcp, vcp_string(ar2.meta_rda_status_data.vcp)), (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs as f64))?; let recorded = parse_date(ar2.volume_header_record.date, ar2.volume_header_record.time); let delay = time - recorded; - ctx.fill_text(&format!("SITE DELAY {}:{}:{}", delay.num_hours(), delay.num_minutes() - (delay.num_hours() * 60), delay.num_seconds() - (delay.num_minutes() - (delay.num_hours() * 60) * 60)), (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs as f64 * 2.0))?; + + let seconds = delay.num_seconds() % 60; + let minutes = (delay.num_seconds() / 60) % 60; + let hours = (delay.num_seconds() / 60) / 60; + + ctx.fill_text(&format!("SITE DELAY {:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds), (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs as f64 * 2.0))?; } else { ctx.fill_text("SITE INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, 50.0 + state.prefs.fcs as f64)?; ctx.fill_text("DELAY INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs * 2) as f64)?; } + // mode info + ctx.fill_text("MODE", (canvas.width() - 50) as f64, (canvas.height() / 3) as f64)?; + let modelines = [ + [Reflectivity, Velocity, SpectrumWidth].as_slice(), + [DifferentialReflectivity, DifferentialPhase, CorrelationCoefficient].as_slice(), + [ClutterFilterPowerRemoved, RadarInoperative].as_slice() + ]; + + let mut available_modes = vec![]; + + if let Some(ar2) = &state.ar2 { + let types = ar2.elevations.get(&state.selected_elevation).unwrap(); + available_modes = types[0].available_data.keys().collect(); + } + + for (line_no, line) in modelines.iter().enumerate() { + let x = (canvas.width() - 50) as f64; + let y = (canvas.height() / 3) as f64 + (state.prefs.fcs * (line_no + 1)) as f64; + for (item_no, item) in line.iter().enumerate() { + + let pad_start = item_no; + let pad_end = 2 - item_no; + let pad_start = " ".repeat(pad_start * 6); + let pad_end = " ".repeat(pad_end * 6); + let middle = if state.scope_mode == *item { item.selected() } else { item.unselected() }; + + if state.scope_mode == *item { + // display selected color + if *item == RadarInoperative { + ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED)); + ctx.fill_text(">RADR INOP<", x, y)?; + ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN)); + continue; + } else { + ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_WHITE)); + } + } else if *item == RadarInoperative { + continue; + } + + + if available_modes.contains(&&item.rname().to_string()) { + ctx.fill_text(&format!(" {}{}{}", pad_start, middle, pad_end), x, y)?; + } + + if state.scope_mode == *item { + // reset + ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN)); + } + } + } + + ctx.fill_text("ELEVATIONS", (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64)?; + if let Some(ar2) = &state.ar2 { + for (line_no, (elevation_no, scans)) in ar2.elevations.iter().sorted_by(|a, b| { + Ord::cmp(&a.0, &b.0) + }).enumerate() { + if state.selected_elevation != *elevation_no { + ctx.fill_text(&format!(" {:0>3} ", elevation_no), (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + (state.prefs.fcs * (line_no + 1)) as f64)?; + } else { + ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_WHITE)); + ctx.fill_text(&format!(">{:0>3}<", elevation_no), (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + (state.prefs.fcs * (line_no + 1)) as f64)?; + ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN)); + } + } + } else { + ctx.fill_text("ELEVATION INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + state.prefs.fcs as f64)?; + } + + // ACTUAL DATA RENDERING + if let Some(ar2) = &state.ar2 { + let px_per_km = canvas.height() / RANGE as u32; + let xc = canvas.width() / 2; + let yc = canvas.height() / 2; + } Ok(()) } \ No newline at end of file diff --git a/nexrad-browser/src/scope.rs b/nexrad-browser/src/scope.rs index cce09a4..ec6ba94 100644 --- a/nexrad-browser/src/scope.rs +++ b/nexrad-browser/src/scope.rs @@ -12,7 +12,8 @@ pub struct ScopeState { pub prefs: Preferences, pub command_buf: String, pub command_buf_response_mode: bool, - pub new_data_available: bool + pub new_data_available: bool, + pub selected_elevation: usize } pub struct RenderState { diff --git a/nexrad2/src/lib.rs b/nexrad2/src/lib.rs index b45cd12..7be542e 100644 --- a/nexrad2/src/lib.rs +++ b/nexrad2/src/lib.rs @@ -23,7 +23,8 @@ pub mod message31; pub struct Nexrad2Chunk { pub volume_header_record: VolumeHeaderRecord, pub chunks: Vec>, - pub meta_rda_status_data: Msg02RDAStatusData + pub meta_rda_status_data: Msg02RDAStatusData, + pub elevations: HashMap> } #[derive(Debug, Clone, PartialEq, Eq)] @@ -130,6 +131,7 @@ pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result> = vec![]; + let mut elevations: HashMap> = HashMap::new(); loop { // LDM records @@ -344,17 +346,26 @@ pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result Result Result, pub radial_info: Option, pub available_data: HashMap +} + +impl DataMoment { + pub fn scaled_data(&self) -> Vec { + let mut gates = vec![0u16; self.gdm.data_moment_gate_count as usize]; + + if self.gdm.data_word_size == 8 { + for byte in &self.data { + gates.push(*byte as u16); + } + } else if self.gdm.data_word_size == 16 { + for chunk in self.data.chunks(2) { + gates.push(u16::from_be_bytes(chunk.try_into().unwrap())); + } + } + + let mut scaled_data = vec![]; + + for gate in &gates { + if *gate == 0 { + // below threshold + scaled_data.push(999.0); + } else if *gate == 1 { + // folded + scaled_data.push(998.0); + } else { + scaled_data.push(scale_uint(*gate, self.gdm.offset, self.gdm.scale)) + } + } + + scaled_data + } +} + +fn scale_uint(gate: u16, offset: f32, scale: f32) -> f32 { + if scale == 0.0 { + gate as f32 + } else { + (gate as f32 - offset) / scale + } } \ No newline at end of file