work
|
@ -6,6 +6,7 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/nexrad2/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-desktop/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
|
|
@ -3,7 +3,8 @@ members = [
|
|||
"nexrad2",
|
||||
"rtwx",
|
||||
"nexrad-browser",
|
||||
"nxar2"
|
||||
"nxar2",
|
||||
"rtwx-desktop"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 17 KiB |
|
@ -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" }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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() {
|
||||
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
|
||||
|
|
|
@ -18,5 +18,5 @@ fn vs_main(
|
|||
|
||||
@fragment
|
||||
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);
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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<Nexrad2Chunk, NexradParseError> {
|
||||
let mut volume_header = [0u8; 24];
|
||||
let read = cursor.read(&mut volume_header).map_err(|e| NexradParseError::HeaderReadError(e))?;
|
||||
|
|
|
@ -121,6 +121,7 @@ pub struct Message31 {
|
|||
}
|
||||
|
||||
impl DataMoment {
|
||||
#[profiling::function]
|
||||
pub fn scaled_data(&self) -> Vec<f32> {
|
||||
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
|
||||
|
|
|
@ -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"
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
simple_logger::init_with_env().unwrap();
|
||||
}
|