lots of rendering work & the font pain begins

This commit is contained in:
core 2023-11-07 20:23:08 -05:00
parent 95f27171f8
commit 7df3f8b3a8
Signed by: core
GPG Key ID: FDBF740DADDCEECF
16 changed files with 2121 additions and 339 deletions

View File

@ -26,6 +26,7 @@ winit = { version = "0.29", features = ["rwh_05"], default-features = false }
wgpu = { version = "0.18", features = ["webgl"] } 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"
glyphon = { git = "https://github.com/grovesNL/glyphon" }
[dependencies.js-sys] [dependencies.js-sys]
version = "0.3" version = "0.3"

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
use nexrad2::message31::MOMENT_DATA_FOLDED;
use crate::mode::Mode; use crate::mode::Mode;
use nexrad2::message31::MOMENT_DATA_FOLDED;
pub fn correlation_coefficient(val: f32) -> &'static str { pub fn correlation_coefficient(val: f32) -> &'static str {
let gradient = [ let gradient = [
@ -24,7 +24,7 @@ pub fn correlation_coefficient(val: f32) -> &'static str {
(1.02, "purple"), (1.02, "purple"),
(1.03, "mediumvioletred"), (1.03, "mediumvioletred"),
(1.045, "pink"), (1.045, "pink"),
(1.05, "lavenderblush") (1.05, "lavenderblush"),
]; ];
for (threshold, color) in gradient { for (threshold, color) in gradient {
@ -61,7 +61,7 @@ pub fn spectrum_width(val: f32) -> &'static str {
(32.0, "white"), (32.0, "white"),
(35.0, "yellow"), (35.0, "yellow"),
(60.0, "lime"), (60.0, "lime"),
(1000.0, "purple") (1000.0, "purple"),
]; ];
for (threshold, color) in gradient { for (threshold, color) in gradient {
@ -96,7 +96,7 @@ pub fn differential_reflectivity(val: f32) -> &'static str {
(6.0, "maroon"), (6.0, "maroon"),
(7.0, "hotpink"), (7.0, "hotpink"),
(10.0, "pink"), (10.0, "pink"),
(999.0, "white") (999.0, "white"),
]; ];
for (threshold, color) in gradient { for (threshold, color) in gradient {
@ -183,7 +183,13 @@ pub fn velocity(vel: f32) -> &'static str {
"#520106", // 140 "#520106", // 140
]; ];
let i = scale_int((vel.floor()) as i32, 140, -140, (colors.len() - 1) as i32, 0); let i = scale_int(
(vel.floor()) as i32,
140,
-140,
(colors.len() - 1) as i32,
0,
);
colors[i as usize] colors[i as usize]
} }
@ -201,6 +207,6 @@ pub fn color_scheme(product: Mode, value: f32) -> &'static str {
Mode::DifferentialPhase => correlation_coefficient(value), Mode::DifferentialPhase => correlation_coefficient(value),
Mode::CorrelationCoefficient => correlation_coefficient(value), Mode::CorrelationCoefficient => correlation_coefficient(value),
Mode::ClutterFilterPowerRemoved => dbz_noaa(value), Mode::ClutterFilterPowerRemoved => dbz_noaa(value),
Mode::RadarInoperative => "#ff0000" Mode::RadarInoperative => "#ff0000",
} }
} }

View File

@ -1,11 +1,10 @@
use web_sys::FileReader;
use crate::loadar2; use crate::loadar2;
use crate::mode::Mode; use crate::mode::Mode;
use crate::scope::ScopeState; use crate::scope::ScopeState;
use web_sys::FileReader;
pub fn should_newline(state: &mut ScopeState) -> bool { pub fn should_newline(state: &mut ScopeState) -> bool {
state.command_buf.starts_with("MODE SET") state.command_buf.starts_with("MODE SET") || state.command_buf.starts_with("ELEVATION SET")
|| state.command_buf.starts_with("ELEVATION SET")
} }
pub fn exec(state: &mut ScopeState, command: String) { pub fn exec(state: &mut ScopeState, command: String) {
@ -20,7 +19,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
if tokens.len() < 3 { if tokens.len() < 3 {
state.command_buf = "ARGUMENT INVALID".to_string(); state.command_buf = "ARGUMENT INVALID".to_string();
state.command_buf_response_mode = true; state.command_buf_response_mode = true;
return return;
} else { } else {
let mode = tokens[2]; let mode = tokens[2];
if let Some(ar2) = &state.ar2 { if let Some(ar2) = &state.ar2 {
@ -30,7 +29,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
if !valid_modes.contains(&&mode.to_string()) { if !valid_modes.contains(&&mode.to_string()) {
state.command_buf = "ARGUMENT INVALID".to_string(); state.command_buf = "ARGUMENT INVALID".to_string();
state.command_buf_response_mode = true; state.command_buf_response_mode = true;
return return;
} }
state.scope_mode = match mode { state.scope_mode = match mode {
@ -41,19 +40,19 @@ pub fn exec(state: &mut ScopeState, command: String) {
"PHI" => Mode::DifferentialPhase, "PHI" => Mode::DifferentialPhase,
"RHO" => Mode::CorrelationCoefficient, "RHO" => Mode::CorrelationCoefficient,
"CFP" => Mode::ClutterFilterPowerRemoved, "CFP" => Mode::ClutterFilterPowerRemoved,
_ => unreachable!() _ => unreachable!(),
}; };
} else { } else {
state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string(); state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string();
state.command_buf_response_mode = true; state.command_buf_response_mode = true;
return return;
} }
} }
} else if command.starts_with("ELEVATION SET") { } else if command.starts_with("ELEVATION SET") {
if tokens.len() < 3 { if tokens.len() < 3 {
state.command_buf = "ARGUMENT INVALID".to_string(); state.command_buf = "ARGUMENT INVALID".to_string();
state.command_buf_response_mode = true; state.command_buf_response_mode = true;
return return;
} else { } else {
let elev = tokens[2].to_string(); let elev = tokens[2].to_string();
if let Some(ar2) = &state.ar2 { if let Some(ar2) = &state.ar2 {
@ -64,7 +63,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
if !valid_elevs.contains(&elev) { if !valid_elevs.contains(&elev) {
state.command_buf = "ARGUMENT INVALID".to_string(); state.command_buf = "ARGUMENT INVALID".to_string();
state.command_buf_response_mode = true; state.command_buf_response_mode = true;
return return;
} }
let number: usize = elev.parse().unwrap(); let number: usize = elev.parse().unwrap();
@ -74,7 +73,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
} else { } else {
state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string(); state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string();
state.command_buf_response_mode = true; state.command_buf_response_mode = true;
return return;
} }
} }
} else if command == "`T" { } else if command == "`T" {

View File

@ -3,5 +3,8 @@ pub const EARTH_RADIUS_MILES: f64 = 3958.8;
pub fn latlong_to_xy(lat: f64, long: f64) -> (f64, f64) { pub fn latlong_to_xy(lat: f64, long: f64) -> (f64, f64) {
// x = r λ cos(φ0) // x = r λ cos(φ0)
// y = r φ // y = r φ
(EARTH_RADIUS_MILES * long * lat.cos(), EARTH_RADIUS_MILES * lat) (
EARTH_RADIUS_MILES * long * lat.cos(),
EARTH_RADIUS_MILES * lat,
)
} }

View File

@ -1,31 +1,32 @@
pub mod utils; pub mod colors;
pub mod command;
pub mod equirectangular;
pub mod mode; pub mod mode;
pub mod scope; pub mod scope;
pub mod equirectangular;
pub mod sites; pub mod sites;
pub mod command; pub mod text;
pub mod utils;
pub mod vcp; pub mod vcp;
pub mod colors;
use std::io::Cursor;
use std::ops::{Deref, DerefMut};
use js_sys::Uint8Array;
use log::{debug, error, info};
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use wgpu::SurfaceError;
use winit::dpi::PhysicalSize;
use winit::event::{ElementState, Event, WindowEvent};
use winit::event::WindowEvent::KeyboardInput;
use winit::event_loop::{EventLoop, EventLoopWindowTarget};
use winit::keyboard::KeyCode;
use winit::window::WindowBuilder;
use winit::platform::web::{WindowExtWebSys, WindowBuilderExtWebSys, EventLoopExtWebSys};
use crate::command::{exec, should_newline}; use crate::command::{exec, should_newline};
use crate::mode::Mode; use crate::mode::Mode;
use crate::mode::Mode::Reflectivity; use crate::mode::Mode::Reflectivity;
use crate::scope::{Preferences, ScopeState, WgpuState}; use crate::scope::{Preferences, ScopeState, WgpuState};
use glyphon::{Metrics, TextBounds};
use js_sys::Uint8Array;
use log::{debug, error, info};
use std::io::Cursor;
use std::ops::{Deref, DerefMut};
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use wgpu::SurfaceError;
use winit::dpi::PhysicalSize;
use winit::event::WindowEvent::KeyboardInput;
use winit::event::{ElementState, Event, WindowEvent};
use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget};
use winit::keyboard::KeyCode;
use winit::platform::web::{EventLoopExtWebSys, WindowBuilderExtWebSys, WindowExtWebSys};
use winit::window::WindowBuilder;
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
@ -33,22 +34,31 @@ extern "C" {
fn loadar2(c: &str); fn loadar2(c: &str);
} }
#[wasm_bindgen] #[derive(Debug, Clone, PartialEq)]
pub struct AbiScopeState(ScopeState); pub enum RenderMessage {
Resize { new_w: u32, new_h: u32 },
}
#[wasm_bindgen] #[wasm_bindgen]
pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiScopeState { pub struct AbiProxy(EventLoopProxy<RenderMessage>);
#[wasm_bindgen]
pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiProxy {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
utils::set_panic_hook(); utils::set_panic_hook();
info!("wgpu setup"); info!("wgpu setup");
let event_loop = EventLoop::new().expect("event loop creation failed"); let event_loop = EventLoopBuilder::<RenderMessage>::with_user_event()
let window = WindowBuilder::new().with_prevent_default(false).build(&event_loop).unwrap(); .build()
.expect("event loop creation failed");
let window = WindowBuilder::new()
.with_prevent_default(false)
.build(&event_loop)
.unwrap();
window.set_min_inner_size(Some(PhysicalSize::new(w, h))); window.set_min_inner_size(Some(PhysicalSize::new(w, h)));
web_sys::window() web_sys::window()
.and_then(|win| win.document()) .and_then(|win| win.document())
.and_then(|doc| { .and_then(|doc| {
@ -62,70 +72,115 @@ pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiScopeState {
let mut render_state = WgpuState::new(window, PhysicalSize::new(w, h)).await; let mut render_state = WgpuState::new(window, PhysicalSize::new(w, h)).await;
// If you see an error here, your IDE is not compiling for webassembly
event_loop.spawn(move |event: Event<_>, window: &EventLoopWindowTarget<_>| {
match event {
Event::WindowEvent { ref event, window_id} => {
match event {
KeyboardInput { event, .. } => {
debug!("{:?}", event.physical_key);
},
WindowEvent::CloseRequested => { window.exit(); },
WindowEvent::Resized(physical_size) => {
render_state.reconfigure(*physical_size);
},
WindowEvent::RedrawRequested => {
render_state.update();
match render_state.render() {
Ok(_) => {},
Err(SurfaceError::Lost) => render_state.reconfigure(render_state.size),
Err(SurfaceError::OutOfMemory) => {
error!("out of memory!");
window.exit();
},
Err(e) => error!("transient rendering error: {}", e),
}
},
_ => {}
}
},
Event::AboutToWait => {
render_state.window().request_redraw();
},
_ => {}
}
});
info!("initializing the scope"); info!("initializing the scope");
let document = web_sys::window().expect("window should exist").document().expect("document should exist"); let document = web_sys::window()
.expect("window should exist")
.document()
.expect("document should exist");
info!("finding form input element"); info!("finding form input element");
let file = document.get_element_by_id("file").expect("file element should exist"); let file = document
let file: web_sys::HtmlInputElement = file.dyn_into::<web_sys::HtmlInputElement>().map_err(|_| ()).expect("file input is not an input"); .get_element_by_id("file")
.expect("file element should exist");
let file: web_sys::HtmlInputElement = file
.dyn_into::<web_sys::HtmlInputElement>()
.map_err(|_| ())
.expect("file input is not an input");
let scope_state = ScopeState { let mut scope_state = ScopeState {
ar2: None, ar2: None,
scope_mode: Mode::RadarInoperative, scope_mode: Mode::RadarInoperative,
file_input: file, file_input: file,
lat: 30.48500, // jacksonville lat: 30.48500, // jacksonville
long: -81.70200, // jacksonville long: -81.70200, // jacksonville
prefs: Preferences { prefs: Preferences { fcs: 20 },
fcs: 20
},
command_buf: String::new(), command_buf: String::new(),
command_buf_response_mode: false, command_buf_response_mode: false,
new_data_available: false, new_data_available: false,
selected_elevation: 1, selected_elevation: 1,
show_ui: false show_ui: false,
wgpu: render_state,
}; };
scope_state.wgpu.add_textarea(
"status_bar".to_string(),
Metrics::new(30.0, 42.0),
10.0,
10.0,
1.0,
TextBounds {
left: 0,
top: 0,
right: 600,
bottom: 160,
},
glyphon::Color::rgb(0x4a, 0xf6, 0x26),
"NEXRAD INOP 00:00:00Z",
);
let event_proxy: EventLoopProxy<RenderMessage> = event_loop.create_proxy();
// If you see an error here, your IDE is not compiling for webassembly
event_loop.spawn(
move |event: Event<_>, window: &EventLoopWindowTarget<_>| match event {
Event::UserEvent(e) => match e {
RenderMessage::Resize { new_w, new_h } => {
scope_state
.wgpu
.reconfigure(PhysicalSize::new(new_w, new_h));
}
},
Event::WindowEvent {
ref event,
window_id,
} => match event {
KeyboardInput { event, .. } => {
debug!("{:?}", event.physical_key);
}
WindowEvent::CloseRequested => {
window.exit();
}
WindowEvent::Resized(physical_size) => {
scope_state.wgpu.reconfigure(*physical_size);
}
WindowEvent::RedrawRequested => {
scope_state.wgpu.update();
match scope_state.wgpu.render() {
Ok(_) => {}
Err(SurfaceError::Lost) => {
scope_state.wgpu.reconfigure(scope_state.wgpu.size)
}
Err(SurfaceError::OutOfMemory) => {
error!("out of memory!");
window.exit();
}
Err(e) => error!("transient rendering error: {}", e),
}
}
_ => {}
},
Event::AboutToWait => {
scope_state.wgpu.window().request_redraw();
}
_ => {}
},
);
info!("nexrad-browser initialized successfully"); info!("nexrad-browser initialized successfully");
AbiScopeState(scope_state) AbiProxy(event_proxy)
} }
#[wasm_bindgen]
pub fn resize_abi(w: u32, h: u32, proxy: &mut AbiProxy) {
proxy
.0
.send_event(RenderMessage::Resize { new_w: w, new_h: h })
.unwrap();
}
/* /*
#[wasm_bindgen] #[wasm_bindgen]
pub fn render_abi(state: &mut AbiScopeState) { pub fn render_abi(state: &mut AbiScopeState) {
@ -133,7 +188,7 @@ pub fn render_abi(state: &mut AbiScopeState) {
} }
*/ */
/*
#[wasm_bindgen] #[wasm_bindgen]
pub fn load_ar2(buf: &JsValue, scope: &mut AbiScopeState) { pub fn load_ar2(buf: &JsValue, scope: &mut AbiScopeState) {
let array = Uint8Array::new(buf); let array = Uint8Array::new(buf);
@ -175,3 +230,5 @@ pub fn keydown(state: &mut AbiScopeState, key: String) {
state.0.command_buf += &key.to_uppercase(); state.0.command_buf += &key.to_uppercase();
} }
} }
*/

View File

@ -15,7 +15,7 @@ pub enum Mode {
/// CFP /// CFP
ClutterFilterPowerRemoved, ClutterFilterPowerRemoved,
/// RADR INOP /// RADR INOP
RadarInoperative RadarInoperative,
} }
impl Mode { impl Mode {
pub fn unselected(&self) -> &str { pub fn unselected(&self) -> &str {
@ -27,7 +27,7 @@ impl Mode {
Self::DifferentialPhase => " PHI ", Self::DifferentialPhase => " PHI ",
Self::CorrelationCoefficient => " RHO ", Self::CorrelationCoefficient => " RHO ",
Self::ClutterFilterPowerRemoved => " CFP ", Self::ClutterFilterPowerRemoved => " CFP ",
Self::RadarInoperative => " RADR INOP" Self::RadarInoperative => " RADR INOP",
} }
} }
pub fn selected(&self) -> &str { pub fn selected(&self) -> &str {
@ -39,7 +39,7 @@ impl Mode {
Self::DifferentialPhase => ">PHI<", Self::DifferentialPhase => ">PHI<",
Self::CorrelationCoefficient => ">RHO<", Self::CorrelationCoefficient => ">RHO<",
Self::ClutterFilterPowerRemoved => ">CFP<", Self::ClutterFilterPowerRemoved => ">CFP<",
Self::RadarInoperative => ">RADR INOP<" Self::RadarInoperative => ">RADR INOP<",
} }
} }
@ -52,7 +52,7 @@ impl Mode {
Self::DifferentialPhase => "PHI", Self::DifferentialPhase => "PHI",
Self::CorrelationCoefficient => "RHO", Self::CorrelationCoefficient => "RHO",
Self::ClutterFilterPowerRemoved => "CFP", Self::ClutterFilterPowerRemoved => "CFP",
Self::RadarInoperative => "NOP" Self::RadarInoperative => "NOP",
} }
} }
} }

View File

@ -1,16 +1,19 @@
use std::f64::consts::PI;
use chrono::{DateTime, Timelike, Utc};
use itertools::Itertools;
use log::{debug, info};
use wasm_bindgen::JsValue;
use nexrad2::message31::MOMENT_DATA_BELOW_THRESHOLD;
use crate::colors::color_scheme; use crate::colors::color_scheme;
use crate::mode::Mode; use crate::mode::Mode;
use crate::mode::Mode::{ClutterFilterPowerRemoved, CorrelationCoefficient, DifferentialPhase, DifferentialReflectivity, RadarInoperative, Reflectivity, SpectrumWidth, Velocity}; use crate::mode::Mode::{
ClutterFilterPowerRemoved, CorrelationCoefficient, DifferentialPhase, DifferentialReflectivity,
RadarInoperative, Reflectivity, SpectrumWidth, Velocity,
};
use crate::rescaleCanvas; use crate::rescaleCanvas;
use crate::scope::ScopeState; use crate::scope::ScopeState;
use crate::utils::parse_date; use crate::utils::parse_date;
use crate::vcp::vcp_string; use crate::vcp::vcp_string;
use chrono::{DateTime, Timelike, Utc};
use itertools::Itertools;
use log::{debug, info};
use nexrad2::message31::MOMENT_DATA_BELOW_THRESHOLD;
use std::f64::consts::PI;
use wasm_bindgen::JsValue;
pub const TEXT_COLOR_RED: &str = "#ef0000"; pub const TEXT_COLOR_RED: &str = "#ef0000";
pub const TEXT_COLOR_GREEN: &str = "#4af626"; pub const TEXT_COLOR_GREEN: &str = "#4af626";
@ -52,24 +55,48 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
// render out the scope id line // render out the scope id line
if state.show_ui { if state.show_ui {
ctx.fill_text(&format!("NEXRAD {} {}", state.ar2.as_ref().map(|u| u.volume_header_record.icao.as_str()).unwrap_or("INOP"), zulu(time)), 50.0, 50.0)?; ctx.fill_text(
&format!(
"NEXRAD {} {}",
state
.ar2
.as_ref()
.map(|u| u.volume_header_record.icao.as_str())
.unwrap_or("INOP"),
zulu(time)
),
50.0,
50.0,
)?;
if state.ar2.is_none() { if state.ar2.is_none() {
// inop alert // inop alert
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED)); ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED));
ctx.fill_text("RADAR INOPERATIVE NO DATA LOADED", 50.0, 50.0 + (state.prefs.fcs as f64))?; ctx.fill_text(
"RADAR INOPERATIVE NO DATA LOADED",
50.0,
50.0 + (state.prefs.fcs as f64),
)?;
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN)); ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
} }
if state.new_data_available { if state.new_data_available {
// inop alert // inop alert
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED)); ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED));
ctx.fill_text("NEW DATA AVAIL RLD RQD", 50.0, 50.0 + (state.prefs.fcs as f64) * 2.0)?; ctx.fill_text(
"NEW DATA AVAIL RLD RQD",
50.0,
50.0 + (state.prefs.fcs as f64) * 2.0,
)?;
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN)); ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
} }
} }
// render the command buffer // render the command buffer
for (line_no, line) in state.command_buf.split('\n').enumerate() { for (line_no, line) in state.command_buf.split('\n').enumerate() {
ctx.fill_text(line, 50.0, (canvas.height() / 3) as f64 + (state.prefs.fcs * line_no) as f64)?; ctx.fill_text(
line,
50.0,
(canvas.height() / 3) as f64 + (state.prefs.fcs * line_no) as f64,
)?;
} }
// render the radar site info // render the radar site info
@ -77,7 +104,16 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
ctx.set_text_align("right"); ctx.set_text_align("right");
ctx.fill_text("RADAR SITE", (canvas.width() - 50) as f64, 50.0)?; ctx.fill_text("RADAR SITE", (canvas.width() - 50) as f64, 50.0)?;
if let Some(ar2) = &state.ar2 { if let Some(ar2) = &state.ar2 {
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))?; 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 recorded = parse_date(ar2.volume_header_record.date, ar2.volume_header_record.time);
let delay = time - recorded; let delay = time - recorded;
@ -85,18 +121,39 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
let minutes = (delay.num_seconds() / 60) % 60; let minutes = (delay.num_seconds() / 60) % 60;
let hours = delay.num_seconds() / 3600; let hours = delay.num_seconds() / 3600;
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))?; 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 { } else {
ctx.fill_text("SITE INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, 50.0 + state.prefs.fcs as f64)?; ctx.fill_text(
ctx.fill_text("DELAY INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, 50.0 + (state.prefs.fcs * 2) as f64)?; "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 // mode info
ctx.fill_text("MODE", (canvas.width() - 50) as f64, (canvas.height() / 3) as f64)?; ctx.fill_text(
"MODE",
(canvas.width() - 50) as f64,
(canvas.height() / 3) as f64,
)?;
let modelines = [ let modelines = [
[Reflectivity, Velocity, SpectrumWidth].as_slice(), [Reflectivity, Velocity, SpectrumWidth].as_slice(),
[DifferentialReflectivity, DifferentialPhase, CorrelationCoefficient].as_slice(), [
[ClutterFilterPowerRemoved, RadarInoperative].as_slice() DifferentialReflectivity,
DifferentialPhase,
CorrelationCoefficient,
]
.as_slice(),
[ClutterFilterPowerRemoved, RadarInoperative].as_slice(),
]; ];
let mut available_modes = vec![]; let mut available_modes = vec![];
@ -114,7 +171,11 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
let pad_end = 2 - item_no; let pad_end = 2 - item_no;
let pad_start = " ".repeat(pad_start * 6); let pad_start = " ".repeat(pad_start * 6);
let pad_end = " ".repeat(pad_end * 6); let pad_end = " ".repeat(pad_end * 6);
let middle = if state.scope_mode == *item { item.selected() } else { item.unselected() }; let middle = if state.scope_mode == *item {
item.selected()
} else {
item.unselected()
};
if state.scope_mode == *item { if state.scope_mode == *item {
// display selected color // display selected color
@ -130,7 +191,6 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
continue; continue;
} }
if available_modes.contains(&&item.rname().to_string()) { if available_modes.contains(&&item.rname().to_string()) {
ctx.fill_text(&format!(" {}{}{}", pad_start, middle, pad_end), x, y)?; ctx.fill_text(&format!(" {}{}{}", pad_start, middle, pad_end), x, y)?;
} }
@ -142,26 +202,56 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
} }
} }
ctx.fill_text("ELEVATIONS", (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64)?; ctx.fill_text(
"ELEVATIONS",
(canvas.width() - 50) as f64,
((canvas.height() / 7) * 4) as f64,
)?;
if let Some(ar2) = &state.ar2 { if let Some(ar2) = &state.ar2 {
let mut line_no = 0; let mut line_no = 0;
for chunk in &ar2.elevations.iter().sorted_by(|a, b| { for chunk in &ar2
Ord::cmp(&a.0, &b.0) .elevations
}).enumerate().chunks(4) { .iter()
.sorted_by(|a, b| Ord::cmp(&a.0, &b.0))
.enumerate()
.chunks(4)
{
for (no_in_line, (elevation_no, scans)) in chunk { for (no_in_line, (elevation_no, scans)) in chunk {
if state.selected_elevation != *elevation_no { if state.selected_elevation != *elevation_no {
ctx.fill_text(&format!(" {:0>3} {}", elevation_no, " ".repeat(5 * (3 - (no_in_line % 4)))), (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + (state.prefs.fcs * (line_no + 1)) as f64)?; ctx.fill_text(
&format!(
" {:0>3} {}",
elevation_no,
" ".repeat(5 * (3 - (no_in_line % 4)))
),
(canvas.width() - 50) as f64,
((canvas.height() / 7) * 4) as f64
+ (state.prefs.fcs * (line_no + 1)) as f64,
)?;
} else { } else {
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_WHITE)); ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_WHITE));
ctx.fill_text(&format!(">{:0>3}<{}", elevation_no, " ".repeat(5 * (3 - (no_in_line % 4)))), (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + (state.prefs.fcs * (line_no + 1)) as f64)?; ctx.fill_text(
&format!(
">{:0>3}<{}",
elevation_no,
" ".repeat(5 * (3 - (no_in_line % 4)))
),
(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)); ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
} }
} }
line_no += 1; line_no += 1;
} }
} else { } else {
ctx.fill_text("ELEVATION INFORMATION UNAVAILABLE", (canvas.width() - 50) as f64, ((canvas.height() / 7) * 4) as f64 + state.prefs.fcs as f64)?; ctx.fill_text(
"ELEVATION INFORMATION UNAVAILABLE",
(canvas.width() - 50) as f64,
((canvas.height() / 7) * 4) as f64 + state.prefs.fcs as f64,
)?;
} }
} }
@ -173,8 +263,21 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
let radials = ar2.elevations.get(&state.selected_elevation).unwrap(); let radials = ar2.elevations.get(&state.selected_elevation).unwrap();
let first_gate_px = (radials[0].available_data.get("REF").expect("reflectivity is missing!").gdm.data_moment_range) as f32 / 1000.0 * px_per_km; let first_gate_px = (radials[0]
let gate_interval_km = (radials[0].available_data.get("REF").expect("reflectivity is missing!").gdm.data_moment_range_sample_interval) as f32 / 1000.0; .available_data
.get("REF")
.expect("reflectivity is missing!")
.gdm
.data_moment_range) as f32
/ 1000.0
* px_per_km;
let gate_interval_km = (radials[0]
.available_data
.get("REF")
.expect("reflectivity is missing!")
.gdm
.data_moment_range_sample_interval) as f32
/ 1000.0;
let gate_width_px = gate_interval_km * px_per_km; let gate_width_px = gate_interval_km * px_per_km;
for radial in radials { for radial in radials {
@ -188,7 +291,7 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
let azimuth_spacing = match radial.header.azimuth_resolution_spacing { let azimuth_spacing = match radial.header.azimuth_resolution_spacing {
1 => 0.5, 1 => 0.5,
_ => 1.0 _ => 1.0,
}; };
if (azimuth_angle + azimuth_spacing).floor() > azimuth { if (azimuth_angle + azimuth_spacing).floor() > azimuth {
azimuth += azimuth_spacing; azimuth += azimuth_spacing;
@ -205,26 +308,60 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
// line width // line width
// line cap // line cap
let gates = radial.available_data.get(state.scope_mode.rname()).expect("selected unavailable product").scaled_data(); let gates = radial
.available_data
.get(state.scope_mode.rname())
.expect("selected unavailable product")
.scaled_data();
let num_gates = gates.len(); let num_gates = gates.len();
for (num, value) in gates.iter().enumerate() { for (num, value) in gates.iter().enumerate() {
if *value != MOMENT_DATA_BELOW_THRESHOLD { if *value != MOMENT_DATA_BELOW_THRESHOLD {
ctx.move_to(
ctx.move_to(xc as f64 + start_angle.cos() * distance_x as f64, yc as f64 + start_angle.sin() * distance_y as f64); xc as f64 + start_angle.cos() * distance_x as f64,
yc as f64 + start_angle.sin() * distance_y as f64,
);
ctx.begin_path(); ctx.begin_path();
if num == 0 { if num == 0 {
ctx.ellipse(xc as f64, yc as f64, distance_x as f64, distance_y as f64, 0.0, start_angle - 0.001, end_angle + 0.001)?; ctx.ellipse(
xc as f64,
yc as f64,
distance_x as f64,
distance_y as f64,
0.0,
start_angle - 0.001,
end_angle + 0.001,
)?;
} else if num == num_gates - 1 { } else if num == num_gates - 1 {
ctx.ellipse(xc as f64, yc as f64, distance_x as f64, distance_y as f64, 0.0, start_angle, end_angle)?; ctx.ellipse(
xc as f64,
yc as f64,
distance_x as f64,
distance_y as f64,
0.0,
start_angle,
end_angle,
)?;
} else { } else {
ctx.ellipse(xc as f64, yc as f64, distance_x as f64, distance_y as f64, 0.0, start_angle, end_angle + 0.001)?; ctx.ellipse(
xc as f64,
yc as f64,
distance_x as f64,
distance_y as f64,
0.0,
start_angle,
end_angle + 0.001,
)?;
} }
ctx.set_stroke_style(&JsValue::from_str(&format!("{}px {}", gate_width_px + 1.0, color_scheme(state.scope_mode, *value)))); ctx.set_stroke_style(&JsValue::from_str(&format!(
"{}px {}",
gate_width_px + 1.0,
color_scheme(state.scope_mode, *value)
)));
ctx.stroke(); ctx.stroke();
} }

View File

@ -1,13 +1,26 @@
use crate::mode::Mode;
use crate::text::OTextArea;
use glyphon::{
Attrs, Buffer, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache, TextArea,
TextAtlas, TextBounds, TextRenderer,
};
use nexrad2::Nexrad2Chunk;
use raw_window_handle::HasRawDisplayHandle;
use std::collections::HashMap;
use std::iter; use std::iter;
use std::sync::Arc;
use glyphon::fontdb::Source;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlInputElement}; use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlInputElement};
use wgpu::{Backends, Color, CommandEncoderDescriptor, Device, Features, Instance, InstanceDescriptor, Limits, LoadOp, Operations, Queue, RenderPassColorAttachment, RenderPassDescriptor, StoreOp, Surface, SurfaceConfiguration, SurfaceError, TextureUsages, TextureViewDescriptor}; use wgpu::DeviceDescriptor;
use wgpu::{
Backends, Color, CommandEncoderDescriptor, Device, Features, Instance, InstanceDescriptor,
Limits, LoadOp, MultisampleState, Operations, Queue, RenderPassColorAttachment,
RenderPassDescriptor, StoreOp, Surface, SurfaceConfiguration, SurfaceError, TextureUsages,
TextureViewDescriptor,
};
use winit::dpi::PhysicalSize; use winit::dpi::PhysicalSize;
use winit::event::WindowEvent; use winit::event::WindowEvent;
use raw_window_handle::HasRawDisplayHandle;
use winit::window::Window; use winit::window::Window;
use nexrad2::Nexrad2Chunk;
use crate::mode::Mode;
use wgpu::DeviceDescriptor;
pub struct ScopeState { pub struct ScopeState {
pub ar2: Option<Nexrad2Chunk>, pub ar2: Option<Nexrad2Chunk>,
@ -20,7 +33,8 @@ pub struct ScopeState {
pub command_buf_response_mode: bool, pub command_buf_response_mode: bool,
pub new_data_available: bool, pub new_data_available: bool,
pub selected_elevation: usize, pub selected_elevation: usize,
pub show_ui: bool pub show_ui: bool,
pub wgpu: WgpuState,
} }
pub struct WgpuState { pub struct WgpuState {
@ -29,7 +43,12 @@ pub struct WgpuState {
pub queue: Queue, pub queue: Queue,
pub surface_config: SurfaceConfiguration, pub surface_config: SurfaceConfiguration,
pub size: PhysicalSize<u32>, pub size: PhysicalSize<u32>,
pub window: Window pub window: Window,
pub font_system: FontSystem,
pub font_cache: SwashCache,
pub atlas: TextAtlas,
pub text_renderer: TextRenderer,
pub text_buffers: HashMap<String, OTextArea>,
} }
impl WgpuState { impl WgpuState {
@ -40,30 +59,34 @@ impl WgpuState {
dx12_shader_compiler: Default::default(), dx12_shader_compiler: Default::default(),
gles_minor_version: Default::default(), gles_minor_version: Default::default(),
}); });
let surface = unsafe { let surface = unsafe { instance.create_surface(&window).unwrap() };
instance.create_surface(&window).unwrap()
};
let adapter = instance.request_adapter( let adapter = instance
&wgpu::RequestAdapterOptions { .request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(), power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface), compatible_surface: Some(&surface),
force_fallback_adapter: false, force_fallback_adapter: false,
}, })
).await.unwrap(); .await
.unwrap();
let (device, queue) = adapter.request_device( let (device, queue) = adapter
&DeviceDescriptor { .request_device(
features: Features::empty(), &DeviceDescriptor {
limits: Limits::downlevel_webgl2_defaults(), features: Features::empty(),
label: None, limits: Limits::downlevel_webgl2_defaults(),
}, label: None,
None, // Trace path },
).await.unwrap(); None, // Trace path
)
.await
.unwrap();
let surface_capabilities = surface.get_capabilities(&adapter); let surface_capabilities = surface.get_capabilities(&adapter);
let surface_format = surface_capabilities.formats.iter() let surface_format = surface_capabilities
.formats
.iter()
.copied() .copied()
.find(|f| f.is_srgb()) .find(|f| f.is_srgb())
.unwrap_or(surface_capabilities.formats[0]); .unwrap_or(surface_capabilities.formats[0]);
@ -80,13 +103,27 @@ impl WgpuState {
surface.configure(&device, &surface_config); surface.configure(&device, &surface_config);
let mut fonts = vec![];
fonts.push(Source::Binary(Arc::new(include_bytes!("./VT323-Regular.ttf"))));
let mut font_system = FontSystem::new_with_fonts(fonts.into_iter());
let mut font_cache = SwashCache::new();
let mut atlas = TextAtlas::new(&device, &queue, surface_format);
let mut text_renderer =
TextRenderer::new(&mut atlas, &device, MultisampleState::default(), None);
Self { Self {
window, window,
surface, surface,
device, device,
queue, queue,
surface_config, surface_config,
size size,
font_system,
font_cache,
atlas,
text_renderer,
text_buffers: HashMap::new(),
} }
} }
@ -107,29 +144,70 @@ impl WgpuState {
false false
} }
pub fn update(&mut self) { pub fn update(&mut self) {}
}
pub fn render(&mut self) -> Result<(), SurfaceError> { pub fn render(&mut self) -> Result<(), SurfaceError> {
let output = self.surface.get_current_texture()?; let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&TextureViewDescriptor::default()); let view = output
let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { .texture
label: Some("Render Encoder"), .create_view(&TextureViewDescriptor::default());
}); let mut encoder = self
.device
.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
let physical_width = (self.surface_config.width as f64 * self.window.scale_factor()) as f32;
let physical_height =
(self.surface_config.height as f64 * self.window.scale_factor()) as f32;
for textarea in self.text_buffers.values_mut() {
// resize the buf
textarea
.buffer
.set_size(&mut self.font_system, physical_width, physical_height);
}
let textareas = self
.text_buffers
.values()
.map(|u| TextArea {
buffer: &u.buffer,
left: u.left.clone() as f32,
top: u.top.clone() as f32,
scale: u.scale.clone() as f32,
bounds: u.bounds.clone(),
default_color: u.default_color.clone(),
})
.collect::<Vec<_>>();
self.text_renderer
.prepare(
&self.device,
&self.queue,
&mut self.font_system,
&mut self.atlas,
Resolution {
width: self.surface_config.width,
height: self.surface_config.height,
},
textareas,
&mut self.font_cache,
)
.unwrap();
// LOCK BLOCK // LOCK BLOCK
{ {
let render_pass = encoder.begin_render_pass(&RenderPassDescriptor { let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Render Pass"), label: Some("Render Pass"),
color_attachments: &[Some(RenderPassColorAttachment { color_attachments: &[Some(RenderPassColorAttachment {
view: &view, view: &view,
resolve_target: None, resolve_target: None,
ops: Operations { ops: Operations {
load: LoadOp::Clear(Color { load: LoadOp::Clear(Color {
r: 0.1, r: 0.0,
g: 0.2, g: 0.0,
b: 0.3, b: 0.0,
a: 1.0, a: 1.0,
}), }),
store: StoreOp::Store, store: StoreOp::Store,
@ -139,16 +217,61 @@ impl WgpuState {
timestamp_writes: None, timestamp_writes: None,
occlusion_query_set: None, occlusion_query_set: None,
}); });
self.text_renderer
.render(&self.atlas, &mut render_pass)
.unwrap();
} }
self.queue.submit(iter::once(encoder.finish())); self.queue.submit(iter::once(encoder.finish()));
output.present(); output.present();
self.atlas.trim();
Ok(()) Ok(())
} }
pub fn add_textarea(
&mut self,
id: String,
metrics: Metrics,
left: f64,
top: f64,
scale: f64,
bounds: TextBounds,
default_color: glyphon::Color,
start_text: &str,
) -> &mut OTextArea {
let mut buffer = Buffer::new(&mut self.font_system, metrics);
let physical_width = (self.surface_config.width as f64 * self.window.scale_factor()) as f32;
let physical_height =
(self.surface_config.height as f64 * self.window.scale_factor()) as f32;
buffer.set_size(&mut self.font_system, physical_width, physical_height);
buffer.set_text(
&mut self.font_system,
start_text,
Attrs::new().family(Family::Monospace),
Shaping::Basic,
);
buffer.shape_until_scroll(&mut self.font_system);
let textarea = OTextArea {
left,
top,
scale,
bounds,
default_color,
buffer,
};
self.text_buffers.insert(id.clone(), textarea);
self.text_buffers.get_mut(&id).unwrap()
}
} }
pub struct Preferences { pub struct Preferences {
pub fcs: usize pub fcs: usize,
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
use glyphon::{Buffer, Color, TextArea, TextBounds};
pub struct OTextArea {
pub left: f64,
pub top: f64,
pub scale: f64,
pub bounds: TextBounds,
pub default_color: Color,
pub buffer: Buffer,
}

View File

@ -1,6 +1,12 @@
import * as wasm from "./wasm/nexrad_browser.js"; import * as wasm from "./wasm/nexrad_browser.js";
await wasm.default(); await wasm.default();
let global_context = wasm.__nxrd_browser_init(window.innerWidth, window.innerHeight); let abi_proxy = await wasm.__nxrd_browser_init(window.innerWidth, window.innerHeight);
window.onresize = async () => {
console.log("window resized!");
await wasm.resize_abi(window.innerWidth, window.innerHeight, abi_proxy);
}
/* /*
function rescaleCanvas(canvas, ctx) { function rescaleCanvas(canvas, ctx) {
var dpr = window.devicePixelRatio || 1; var dpr = window.devicePixelRatio || 1;