This commit is contained in:
core 2023-11-09 13:34:05 -05:00
parent 95618c5b5c
commit f709d1fb88
Signed by: core
GPG Key ID: FDBF740DADDCEECF
22 changed files with 276 additions and 9 deletions

View File

@ -6,6 +6,7 @@
<sourceFolder url="file://$MODULE_DIR$/nexrad2/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/nexrad2/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/nxar2/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/nxar2/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rtwx/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/rtwx/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rtwx-desktop/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

View File

@ -3,7 +3,8 @@ members = [
"nexrad2", "nexrad2",
"rtwx", "rtwx",
"nexrad-browser", "nexrad-browser",
"nxar2" "nxar2",
"rtwx-desktop"
] ]
resolver = "2" resolver = "2"

View File

@ -33,10 +33,18 @@ To open RTWX, open [rtwx.e3t.cc](https://rtwx.e3t.cc) in your browser. After a b
## The Scope ## 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. The scope is broken down into 5 main parts.
![Marked Up Scope](scope_inop_markedup.png)
### Status Information Area ### Status Information Area
In the top left of the scope is the *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 ### 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 ### 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 ### Radar Scope

BIN
docs/elevations_filled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/elevations_inop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
docs/empty_scope.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
docs/mode_filled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
docs/mode_inop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
docs/mode_partial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
docs/scope_inop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -27,6 +27,7 @@ wgpu = { version = "0.18", features = ["webgl"] }
raw-window-handle = "0.5" raw-window-handle = "0.5"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
bytemuck = { version = "1", features = ["derive"]} bytemuck = { version = "1", features = ["derive"]}
profiling = "1"
# Text rendering # # Text rendering #
wgpu_glyph = { git = "https://github.com/hecrj/wgpu_glyph" } wgpu_glyph = { git = "https://github.com/hecrj/wgpu_glyph" }

View File

@ -122,7 +122,7 @@ pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiProxy {
if event.logical_key == Key::Named(NamedKey::Enter) { if event.logical_key == Key::Named(NamedKey::Enter) {
let command = scope_state.command_buf.clone(); let command = scope_state.command_buf.clone();
scope_state.command_buf = String::new(); scope_state.command_buf = String::new();
exec(&mut scope_state, command); exec(&mut scope_state, command.replace("\n", " "));
return; return;
} }

View File

@ -4,6 +4,7 @@ use nexrad2::Nexrad2Chunk;
use std::iter; use std::iter;
use chrono::Utc; use chrono::Utc;
use itertools::Itertools;
use log::info; use log::info;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -22,9 +23,12 @@ use wgpu_glyph::{GlyphBrush, GlyphBrushBuilder, HorizontalAlign, Layout, Section
use winit::dpi::PhysicalSize; use winit::dpi::PhysicalSize;
use winit::event::WindowEvent; use winit::event::WindowEvent;
use winit::window::Window; use winit::window::Window;
use crate::mode::Mode::{RadarInoperative, Reflectivity};
use crate::rendering::colors::rgb_to_srgb; use crate::rendering::colors::rgb_to_srgb;
use crate::rendering::time::zulu; use crate::rendering::time::zulu;
use crate::rendering::vertex::{VERTICES}; use crate::rendering::vertex::{VERTICES};
use crate::utils::parse_date;
use crate::vcp::vcp_string;
pub struct ScopeState { pub struct ScopeState {
pub ar2: Option<Nexrad2Chunk>, pub ar2: Option<Nexrad2Chunk>,
@ -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() { for (line_no, line) in self.command_buf.split('\n').enumerate() {
self.wgpu.glyph_brush.queue(Section { 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), bounds: (self.wgpu.surface_config.width as f32, self.wgpu.surface_config.height as f32),
text: vec![Text::new(line) text: vec![Text::new(line)
.with_color(rgb_to_srgb(0x32cd32, 1.0)) .with_color(rgb_to_srgb(0x32cd32, 1.0))
@ -340,6 +344,191 @@ impl ScopeState {
.with_scale(self.prefs.fcs)], .with_scale(self.prefs.fcs)],
..Section::default() ..Section::default()
}.with_layout(Layout::default().h_align(HorizontalAlign::Right))); }.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 self.wgpu.glyph_brush

View File

@ -18,5 +18,5 @@ fn vs_main(
@fragment @fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(0.3, 0.2, 0.1, 1.0); return vec4<f32>(0.0, 0.0, 0.0, 1.0);
} }

View File

@ -10,6 +10,7 @@ edition = "2021"
bzip2-rs = { version = "0.1", optional = true } bzip2-rs = { version = "0.1", optional = true }
bzip2 = { version = "0.4", optional = true } bzip2 = { version = "0.4", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
profiling = "1"
log = "0.4" log = "0.4"

View File

@ -102,7 +102,7 @@ impl Error for NexradParseError {}
pub const NEXRAD2_META_CHUNK_FIXED_LENGTH: usize = 325888; pub const NEXRAD2_META_CHUNK_FIXED_LENGTH: usize = 325888;
#[profiling::function]
pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result<Nexrad2Chunk, NexradParseError> { pub fn parse_nx2_chunk(cursor: &mut (impl Read + Seek)) -> Result<Nexrad2Chunk, NexradParseError> {
let mut volume_header = [0u8; 24]; let mut volume_header = [0u8; 24];
let read = cursor.read(&mut volume_header).map_err(|e| NexradParseError::HeaderReadError(e))?; let read = cursor.read(&mut volume_header).map_err(|e| NexradParseError::HeaderReadError(e))?;

View File

@ -121,6 +121,7 @@ pub struct Message31 {
} }
impl DataMoment { impl DataMoment {
#[profiling::function]
pub fn scaled_data(&self) -> Vec<f32> { pub fn scaled_data(&self) -> Vec<f32> {
let mut gates = vec![0u16; self.gdm.data_moment_gate_count as usize]; 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 { fn scale_uint(gate: u16, offset: f32, scale: f32) -> f32 {
if scale == 0.0 { if scale == 0.0 {
gate as f32 gate as f32

12
rtwx-desktop/Cargo.toml Normal file
View File

@ -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"

3
rtwx-desktop/src/main.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
simple_logger::init_with_env().unwrap();
}