diff --git a/.idea/rtwx.iml b/.idea/rtwx.iml index bbe0211..361925b 100644 --- a/.idea/rtwx.iml +++ b/.idea/rtwx.iml @@ -6,6 +6,7 @@ + diff --git a/Cargo.toml b/Cargo.toml index 7ee8d5d..8bdd420 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "nexrad2", "rtwx", "nexrad-browser", - "nxar2" + "nxar2", + "rtwx-desktop" ] resolver = "2" diff --git a/docs/README.md b/docs/README.md index b10f6b3..19eb7ac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,10 +33,18 @@ To open RTWX, open [rtwx.e3t.cc](https://rtwx.e3t.cc) in your browser. After a b ## The Scope -After you initially load RTWX, the scope will appear blank. This is because text is hidden by default. To show it, enter the command `` `T `` to toggle `` ` `` text `T`. +After you initially load RTWX, the scope will appear blank. + +![Empty Scope](empty_scope.png) + +This is because text is hidden by default. To show it, enter the command `` `T `` to toggle `` ` `` text `T`. + +![Inoperative Scope](scope_inop.png) The scope is broken down into 5 main parts. +![Marked Up Scope](scope_inop_markedup.png) + ### Status Information Area In the top left of the scope is the *Status Information Area.* @@ -69,11 +77,60 @@ The command buffer will contain any commands you are entering and responses to t ### Site Information Area -*Reserved* +In the top right of the scope is the Site Information Area, which contains information about the currently loaded radar file. + +When inoperative, the Site Information Area will report that all information is "unavailable". + +![Site Information Area: Inoperative](site_information_area_inop.png) + +Once loaded, it will show the following data: + +![Site Information Area: Filled KOTX](site_information_area_kotx.png) + +- the ICAO of the current radar site +- the VCP number in use +- a plain-language description of the VCP +- the site delay: how long ago was the current radar file captured (HH:MM:SS) ### Mode Selector -*Reserved* +The Mode Selector is located on the midright of the scope. It contains information about the current Mode and Elevation that the scope is displaying. + +The Mode Selector is broken down into two parts: + +#### Radar Product Mode + +The Radar Product Mode display shows which radar product is currently being displayed. While the scope is inoperative, it will display `>RADR INOP<` in place of mode options: + +![Radar Product Mode: Inoperative](mode_inop.png) + +Once radar data is being received, it will show a list of the products available for the currently selected elevation: + +![Radar Product Mode: Filled](mode_filled.png) + +The currently selected product is shown highlighted in white, with angle brackets around it. +Typically, `REF` (Reflectivity) will be selected by default. + +Not all products are available for every elevation. For example, most commonly Velocity and Spectrum Width will not be present: + +![Radar Product Mode: Partial](mode_partial.png) + +To change what mode is being displayed, use the `MODE SET` command. + +#### Radar Elevation + +The Radar Elevation display, below the Radar Product Mode display, shows which elevations are available in the current radar data. While the scope is inoperative, it will display `ELEVATION INFO UNAVAILABLE` in place of elevations: + +![Radar Elevation: Inoperative](elevations_inop.png) + +Once radar data is being received, it will show a list of the elevations available: + +![Radar Elevation: Filled](elevations_filled.png) + +The currently selected elevation will be highlighted in white, and have angle brackets around it. +Typically, elevation `001` will be selected by default. + +To change which elevation is being displayed, use the `ELEVATION SET` command. Changing elevation will automatically reset the scope to `REF` (Reflectivity) mode. ### Radar Scope diff --git a/docs/elevations_filled.png b/docs/elevations_filled.png new file mode 100644 index 0000000..75a31c2 Binary files /dev/null and b/docs/elevations_filled.png differ diff --git a/docs/elevations_inop.png b/docs/elevations_inop.png new file mode 100644 index 0000000..71cc524 Binary files /dev/null and b/docs/elevations_inop.png differ diff --git a/docs/empty_scope.png b/docs/empty_scope.png new file mode 100644 index 0000000..c26c75b Binary files /dev/null and b/docs/empty_scope.png differ diff --git a/docs/mode_filled.png b/docs/mode_filled.png new file mode 100644 index 0000000..01b6cfa Binary files /dev/null and b/docs/mode_filled.png differ diff --git a/docs/mode_inop.png b/docs/mode_inop.png new file mode 100644 index 0000000..a27d3c0 Binary files /dev/null and b/docs/mode_inop.png differ diff --git a/docs/mode_partial.png b/docs/mode_partial.png new file mode 100644 index 0000000..b3de89f Binary files /dev/null and b/docs/mode_partial.png differ diff --git a/docs/scope_inop.png b/docs/scope_inop.png new file mode 100644 index 0000000..f83271a Binary files /dev/null and b/docs/scope_inop.png differ diff --git a/docs/scope_inop_markedup.png b/docs/scope_inop_markedup.png new file mode 100644 index 0000000..ae33041 Binary files /dev/null and b/docs/scope_inop_markedup.png differ diff --git a/docs/site_information_area_inop.png b/docs/site_information_area_inop.png new file mode 100644 index 0000000..785b946 Binary files /dev/null and b/docs/site_information_area_inop.png differ diff --git a/docs/site_information_area_kotx.png b/docs/site_information_area_kotx.png new file mode 100644 index 0000000..5675361 Binary files /dev/null and b/docs/site_information_area_kotx.png differ diff --git a/nexrad-browser/Cargo.toml b/nexrad-browser/Cargo.toml index c9906cf..b1f399a 100644 --- a/nexrad-browser/Cargo.toml +++ b/nexrad-browser/Cargo.toml @@ -27,6 +27,7 @@ wgpu = { version = "0.18", features = ["webgl"] } raw-window-handle = "0.5" wasm-bindgen-futures = "0.4" bytemuck = { version = "1", features = ["derive"]} +profiling = "1" # Text rendering # wgpu_glyph = { git = "https://github.com/hecrj/wgpu_glyph" } diff --git a/nexrad-browser/src/lib.rs b/nexrad-browser/src/lib.rs index e472120..f54821b 100644 --- a/nexrad-browser/src/lib.rs +++ b/nexrad-browser/src/lib.rs @@ -122,7 +122,7 @@ pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiProxy { if event.logical_key == Key::Named(NamedKey::Enter) { let command = scope_state.command_buf.clone(); scope_state.command_buf = String::new(); - exec(&mut scope_state, command); + exec(&mut scope_state, command.replace("\n", " ")); return; } diff --git a/nexrad-browser/src/rendering/scope.rs b/nexrad-browser/src/rendering/scope.rs index c790a81..50cd516 100644 --- a/nexrad-browser/src/rendering/scope.rs +++ b/nexrad-browser/src/rendering/scope.rs @@ -4,6 +4,7 @@ use nexrad2::Nexrad2Chunk; use std::iter; use chrono::Utc; +use itertools::Itertools; use log::info; use wasm_bindgen::JsCast; @@ -22,9 +23,12 @@ use wgpu_glyph::{GlyphBrush, GlyphBrushBuilder, HorizontalAlign, Layout, Section use winit::dpi::PhysicalSize; use winit::event::WindowEvent; use winit::window::Window; +use crate::mode::Mode::{RadarInoperative, Reflectivity}; use crate::rendering::colors::rgb_to_srgb; use crate::rendering::time::zulu; use crate::rendering::vertex::{VERTICES}; +use crate::utils::parse_date; +use crate::vcp::vcp_string; pub struct ScopeState { pub ar2: Option, @@ -318,11 +322,11 @@ impl ScopeState { } } - let command_base = (self.wgpu.surface_config.height / 3) as f32; + let one_third = (self.wgpu.surface_config.height / 3) as f32; for (line_no, line) in self.command_buf.split('\n').enumerate() { self.wgpu.glyph_brush.queue(Section { - screen_position: (10.0, command_base + (line_height * line_no as f32)), + screen_position: (10.0, one_third + (line_height * line_no as f32)), bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), text: vec![Text::new(line) .with_color(rgb_to_srgb(0x32cd32, 1.0)) @@ -340,6 +344,191 @@ impl ScopeState { .with_scale(self.prefs.fcs)], ..Section::default() }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + + if let Some(ar2) = &self.ar2 { + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, 8.0 + line_height), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new(&format!("{} VCP {} {}", ar2.volume_header_record.icao, + ar2.meta_rda_status_data.vcp, + vcp_string(ar2.meta_rda_status_data.vcp))) + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + + let recorded = parse_date(ar2.volume_header_record.date, ar2.volume_header_record.time); + let delay = time - recorded; + + let seconds = delay.num_seconds() % 60; + let minutes = (delay.num_seconds() / 60) % 60; + let hours = delay.num_seconds() / 3600; + + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, 8.0 + (line_height*2.0)), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new(&format!("SITE DELAY {:0>2}:{:0>2}:{:0>2}", hours, minutes, seconds)) + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + } else { + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, 8.0 + line_height), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new("SITE INFORMATION UNAVAILABLE") + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, 8.0 + (line_height * 2.0)), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new("DELAY INFORMATION UNAVAILABLE") + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + } + + // mode info + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, one_third), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new("MODE") + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + + let modelines = [ + [Mode::Reflectivity, Mode::Velocity, Mode::SpectrumWidth].as_slice(), + [ + Mode::DifferentialReflectivity, + Mode::DifferentialPhase, + Mode::CorrelationCoefficient, + ] + .as_slice(), + [Mode::ClutterFilterPowerRemoved, Mode::RadarInoperative].as_slice(), + ]; + + let mut available_modes = vec![]; + + if let Some(ar2) = &self.ar2 { + let types = ar2.elevations.get(&self.selected_elevation).unwrap(); + available_modes = types[0].available_data.keys().collect(); + } + + for (line_no, line) in modelines.iter().enumerate() { + let x = (self.wgpu.surface_config.width as f32 - 10.0); + let y = one_third + line_height * (line_no as f32 + 1.0); + 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 self.scope_mode == *item { + item.selected() + } else { + item.unselected() + }; + + if self.scope_mode == *item { + // display selected color + if *item == RadarInoperative { + self.wgpu.glyph_brush.queue(Section { + screen_position: (x, y), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new(">RADR INOP<") + .with_color(rgb_to_srgb(0xff0000, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + continue; + } + } else if *item == RadarInoperative { + continue; + } + + if available_modes.contains(&&item.rname().to_string()) { + self.wgpu.glyph_brush.queue(Section { + screen_position: (x, y), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new(&format!(" {}{}{}", pad_start, middle, pad_end)) + .with_color(rgb_to_srgb(if self.scope_mode == *item { 0xdedede } else { 0x32cd32 }, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + } + } + } + + let four_sevenths = ((self.wgpu.surface_config.height / 7) * 4) as f32; + + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, four_sevenths), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new("ELEVATIONS") + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + + if let Some(ar2) = &self.ar2 { + let mut line_no = 0; + + for chunk in &ar2 + .elevations + .iter() + .sorted_by(|a, b| Ord::cmp(&a.0, &b.0)) + .enumerate() + .chunks(4) + { + for (no_in_line, (elevation_no, scans)) in chunk { + if self.selected_elevation != *elevation_no { + let s = &format!( + " {:0>3} {}", + elevation_no, + " ".repeat(5 * (3 - (no_in_line % 4))) + ); + + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, four_sevenths + line_height * (line_no as f32 + 1.0)), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new(s) + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + } else { + let s = &format!( + ">{:0>3}<{}", + elevation_no, + " ".repeat(5 * (3 - (no_in_line % 4))) + ); + + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, four_sevenths + line_height * (line_no as f32 + 1.0)), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new(s) + .with_color(rgb_to_srgb(0xdedede, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + } + } + line_no += 1; + } + } else { + self.wgpu.glyph_brush.queue(Section { + screen_position: (self.wgpu.surface_config.width as f32 - 10.0, four_sevenths + line_height), + bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32), + text: vec![Text::new("ELEVATION INFO UNAVAILABLE") + .with_color(rgb_to_srgb(0x32cd32, 1.0)) + .with_scale(self.prefs.fcs)], + ..Section::default() + }.with_layout(Layout::default().h_align(HorizontalAlign::Right))); + } } self.wgpu.glyph_brush diff --git a/nexrad-browser/src/shader.wgsl b/nexrad-browser/src/shader.wgsl index 83b6747..19b12ad 100644 --- a/nexrad-browser/src/shader.wgsl +++ b/nexrad-browser/src/shader.wgsl @@ -18,5 +18,5 @@ fn vs_main( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return vec4(0.3, 0.2, 0.1, 1.0); + return vec4(0.0, 0.0, 0.0, 1.0); } \ No newline at end of file diff --git a/nexrad2/Cargo.toml b/nexrad2/Cargo.toml index bf04889..a365f9d 100644 --- a/nexrad2/Cargo.toml +++ b/nexrad2/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" bzip2-rs = { version = "0.1", optional = true } bzip2 = { version = "0.4", optional = true } serde = { version = "1", features = ["derive"], optional = true } +profiling = "1" log = "0.4" diff --git a/nexrad2/src/lib.rs b/nexrad2/src/lib.rs index deeb043..8377dd9 100644 --- a/nexrad2/src/lib.rs +++ b/nexrad2/src/lib.rs @@ -102,7 +102,7 @@ impl Error for NexradParseError {} pub const NEXRAD2_META_CHUNK_FIXED_LENGTH: usize = 325888; - +#[profiling::function] pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result { let mut volume_header = [0u8; 24]; let read = cursor.read(&mut volume_header).map_err(|e| NexradParseError::HeaderReadError(e))?; diff --git a/nexrad2/src/message31.rs b/nexrad2/src/message31.rs index 003539e..c57fd67 100644 --- a/nexrad2/src/message31.rs +++ b/nexrad2/src/message31.rs @@ -121,6 +121,7 @@ pub struct Message31 { } impl DataMoment { + #[profiling::function] pub fn scaled_data(&self) -> Vec { let mut gates = vec![0u16; self.gdm.data_moment_gate_count as usize]; @@ -152,6 +153,7 @@ impl DataMoment { } } +#[profiling::function] fn scale_uint(gate: u16, offset: f32, scale: f32) -> f32 { if scale == 0.0 { gate as f32 diff --git a/rtwx-desktop/Cargo.toml b/rtwx-desktop/Cargo.toml new file mode 100644 index 0000000..419efc2 --- /dev/null +++ b/rtwx-desktop/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rtwx-desktop" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +winit = "0.29" +simple_logger = "4" +log = "0.4" +wgpu = "0.18" \ No newline at end of file diff --git a/rtwx-desktop/src/main.rs b/rtwx-desktop/src/main.rs new file mode 100644 index 0000000..0666a9c --- /dev/null +++ b/rtwx-desktop/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + simple_logger::init_with_env().unwrap(); +}