looks like i am going to rewrite the entire rendering engine again
This commit is contained in:
parent
4b7ec8c058
commit
313dca0483
|
@ -0,0 +1,206 @@
|
|||
use nexrad2::message31::MOMENT_DATA_FOLDED;
|
||||
use crate::mode::Mode;
|
||||
|
||||
pub fn correlation_coefficient(val: f32) -> &'static str {
|
||||
let gradient = [
|
||||
(0.275, "black"),
|
||||
(0.35, "darkgrey"),
|
||||
(0.4, "gray"),
|
||||
(0.5, "silver"),
|
||||
(0.6, "midnightblue"),
|
||||
(0.7, "darkblue"),
|
||||
(0.8, "blue"),
|
||||
(0.91, "green"),
|
||||
(0.92, "yellowgreen"),
|
||||
(0.93, "olivedrab"),
|
||||
(0.94, "yellow"),
|
||||
(0.95, "gold"),
|
||||
(0.96, "orange"),
|
||||
(0.97, "orangered"),
|
||||
(0.98, "red"),
|
||||
(0.99, "firebrick"),
|
||||
(1.0, "maroon"),
|
||||
(1.01, "darkmagenta"),
|
||||
(1.02, "purple"),
|
||||
(1.03, "mediumvioletred"),
|
||||
(1.045, "pink"),
|
||||
(1.05, "lavenderblush")
|
||||
];
|
||||
|
||||
for (threshold, color) in gradient {
|
||||
if val < threshold {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
"white"
|
||||
}
|
||||
|
||||
pub fn spectrum_width(val: f32) -> &'static str {
|
||||
let gradient = [
|
||||
(1.0, "black"),
|
||||
(2.0, "#222222"),
|
||||
(3.0, "#333333"),
|
||||
(4.0, "#444444"),
|
||||
(5.0, "#555555"),
|
||||
(6.0, "#666666"),
|
||||
(7.0, "#777777"),
|
||||
(8.0, "#888888"),
|
||||
(9.0, "#999999"),
|
||||
(10.0, "burlywood"),
|
||||
(11.0, "sandybrown"),
|
||||
(12.0, "gold"),
|
||||
(13.0, "orange"),
|
||||
(15.0, "orangered"),
|
||||
(17.0, "red"),
|
||||
(19.0, "firebrick"),
|
||||
(23.0, "darkred"),
|
||||
(25.0, "hotpink"),
|
||||
(27.0, "fuchsia"),
|
||||
(30.0, "lavender"),
|
||||
(32.0, "white"),
|
||||
(35.0, "yellow"),
|
||||
(60.0, "lime"),
|
||||
(1000.0, "purple")
|
||||
];
|
||||
|
||||
for (threshold, color) in gradient {
|
||||
if val < threshold {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
"white"
|
||||
}
|
||||
|
||||
pub fn differential_reflectivity(val: f32) -> &'static str {
|
||||
let gradient = [
|
||||
(-3.0, "black"),
|
||||
(-1.0, "#333333"),
|
||||
(-0.5, "#666666"),
|
||||
(0.1, "#999999"),
|
||||
(0.0, "#cccccc"),
|
||||
(0.1, "white"),
|
||||
(0.25, "navy"),
|
||||
(0.5, "blue"),
|
||||
(0.75, "deepskyblue"),
|
||||
(1.0, "cyan"),
|
||||
(1.25, "mediumaquamarine"),
|
||||
(1.5, "lime"),
|
||||
(1.75, "yellowgreen"),
|
||||
(2.0, "yellow"),
|
||||
(2.5, "gold"),
|
||||
(3.0, "orange"),
|
||||
(4.0, "orangered"),
|
||||
(5.0, "red"),
|
||||
(6.0, "maroon"),
|
||||
(7.0, "hotpink"),
|
||||
(10.0, "pink"),
|
||||
(999.0, "white")
|
||||
];
|
||||
|
||||
for (threshold, color) in gradient {
|
||||
if val < threshold {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
"white"
|
||||
}
|
||||
|
||||
pub fn dbz_noaa(dbz: f32) -> &'static str {
|
||||
if dbz < 5.0 || dbz == MOMENT_DATA_FOLDED {
|
||||
"#000000"
|
||||
} else if dbz >= 5.0 && dbz < 10.0 {
|
||||
"#40e8e3"
|
||||
} else if dbz >= 10.0 && dbz < 15.0 {
|
||||
"#26a4fa"
|
||||
} else if dbz >= 15.0 && dbz < 20.0 {
|
||||
"#0030ed"
|
||||
} else if dbz >= 20.0 && dbz < 25.0 {
|
||||
"#49fb3e"
|
||||
} else if dbz >= 25.0 && dbz < 30.0 {
|
||||
"#36c22e"
|
||||
} else if dbz >= 30.0 && dbz < 35.0 {
|
||||
"#278c1e"
|
||||
} else if dbz >= 35.0 && dbz < 40.0 {
|
||||
"#fef543"
|
||||
} else if dbz >= 40.0 && dbz < 45.0 {
|
||||
"#ebb433"
|
||||
} else if dbz >= 45.0 && dbz < 50.0 {
|
||||
"#f6952e"
|
||||
} else if dbz >= 50.0 && dbz < 55.0 {
|
||||
"#f80a26"
|
||||
} else if dbz >= 55.0 && dbz < 60.0 {
|
||||
"#cb0516"
|
||||
} else if dbz >= 60.0 && dbz < 65.0 {
|
||||
"#a90813"
|
||||
} else if dbz >= 65.0 && dbz < 70.0 {
|
||||
"#ee34fa"
|
||||
} else if dbz >= 79.0 && dbz < 75.0 {
|
||||
"#9161c4"
|
||||
} else {
|
||||
"#ffffff"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn velocity(vel: f32) -> &'static str {
|
||||
if vel == MOMENT_DATA_FOLDED {
|
||||
return "#691ac1";
|
||||
}
|
||||
|
||||
let colors = [
|
||||
"#F91473", // 140
|
||||
"#AA1079", // 130
|
||||
"#6E0E80", // 120
|
||||
"#2E0E84", // 110
|
||||
"#151F93", // 100
|
||||
"#236FB3", // 90
|
||||
"#41DADB", // 80
|
||||
"#66E1E2", // 70
|
||||
"#9EE8EA", // 60
|
||||
"#57FA63", // 50
|
||||
"#31E32B", // 40
|
||||
// "#21BE0A", // 35
|
||||
"#24AA1F", // 30
|
||||
"#197613", // 20
|
||||
"#456742", // -10
|
||||
"#634F50", // 0
|
||||
"#6e2e39", // 10
|
||||
"#7F030C", // 20
|
||||
"#B60716", // 30
|
||||
// "#C5000D", // 35
|
||||
"#F32245", // 40
|
||||
"#F6508A", // 50
|
||||
"#FB8BBF", // 60
|
||||
"#FDDE93", // 70
|
||||
"#FCB470", // 80
|
||||
"#FA814B", // 90
|
||||
"#DD603C", // 100
|
||||
"#B7452D", // 110
|
||||
"#932C20", // 120
|
||||
"#711614", // 130
|
||||
"#520106", // 140
|
||||
];
|
||||
|
||||
let i = scale_int((vel.floor()) as i32, 140, -140, (colors.len() - 1) as i32, 0);
|
||||
|
||||
colors[i as usize]
|
||||
}
|
||||
|
||||
pub fn scale_int(val: i32, o_max: i32, o_min: i32, n_max: i32, n_min: i32) -> i32 {
|
||||
(((val - o_min) * n_max - n_min) / o_max - o_min) + n_min
|
||||
}
|
||||
|
||||
pub fn color_scheme(product: Mode, value: f32) -> &'static str {
|
||||
match product {
|
||||
Mode::Reflectivity => dbz_noaa(value),
|
||||
Mode::Velocity => velocity(value),
|
||||
Mode::SpectrumWidth => spectrum_width(value),
|
||||
Mode::DifferentialReflectivity => differential_reflectivity(value),
|
||||
Mode::DifferentialPhase => correlation_coefficient(value),
|
||||
Mode::CorrelationCoefficient => correlation_coefficient(value),
|
||||
Mode::ClutterFilterPowerRemoved => dbz_noaa(value),
|
||||
Mode::RadarInoperative => "#ff0000"
|
||||
}
|
||||
}
|
|
@ -79,6 +79,8 @@ pub fn exec(state: &mut ScopeState, command: String) {
|
|||
return
|
||||
}
|
||||
}
|
||||
} else if command == "`T" {
|
||||
state.show_ui = !state.show_ui;
|
||||
} else {
|
||||
state.command_buf = "UNRECOGNIZED COMMAND".to_string();
|
||||
state.command_buf_response_mode = true;
|
||||
|
|
|
@ -6,6 +6,7 @@ pub mod equirectangular;
|
|||
pub mod sites;
|
||||
pub mod command;
|
||||
pub mod vcp;
|
||||
pub mod colors;
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
@ -63,7 +64,8 @@ pub fn __nxrd_browser_init() -> AbiScopeState {
|
|||
command_buf: String::new(),
|
||||
command_buf_response_mode: false,
|
||||
new_data_available: false,
|
||||
selected_elevation: 1
|
||||
selected_elevation: 1,
|
||||
show_ui: false
|
||||
};
|
||||
|
||||
info!("nexrad-browser initialized successfully");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Mode {
|
||||
/// REF
|
||||
Reflectivity,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use std::f64::consts::PI;
|
||||
use chrono::{DateTime, Timelike, Utc};
|
||||
use itertools::Itertools;
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
use wasm_bindgen::JsValue;
|
||||
use nexrad2::message31::MOMENT_DATA_BELOW_THRESHOLD;
|
||||
use crate::colors::color_scheme;
|
||||
use crate::mode::Mode;
|
||||
use crate::mode::Mode::{ClutterFilterPowerRemoved, CorrelationCoefficient, DifferentialPhase, DifferentialReflectivity, RadarInoperative, Reflectivity, SpectrumWidth, Velocity};
|
||||
use crate::rescaleCanvas;
|
||||
|
@ -48,8 +51,8 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_GREEN));
|
||||
|
||||
// render out the scope id line
|
||||
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)?;
|
||||
|
||||
if state.ar2.is_none() {
|
||||
// inop alert
|
||||
ctx.set_fill_style(&JsValue::from_str(TEXT_COLOR_RED));
|
||||
|
@ -62,6 +65,7 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// render the command buffer
|
||||
for (line_no, line) in state.command_buf.split('\n').enumerate() {
|
||||
|
@ -69,6 +73,7 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
}
|
||||
|
||||
// render the radar site info
|
||||
if state.show_ui {
|
||||
ctx.set_text_align("right");
|
||||
ctx.fill_text("RADAR SITE", (canvas.width() - 50) as f64, 50.0)?;
|
||||
if let Some(ar2) = &state.ar2 {
|
||||
|
@ -78,7 +83,7 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
|
||||
let seconds = delay.num_seconds() % 60;
|
||||
let minutes = (delay.num_seconds() / 60) % 60;
|
||||
let hours = (delay.num_seconds() / 60) / 60;
|
||||
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))?;
|
||||
} else {
|
||||
|
@ -105,7 +110,6 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
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);
|
||||
|
@ -140,26 +144,96 @@ pub fn render(state: &mut ScopeState) -> Result<(), JsValue> {
|
|||
|
||||
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| {
|
||||
let mut line_no = 0;
|
||||
|
||||
for chunk in &ar2.elevations.iter().sorted_by(|a, b| {
|
||||
Ord::cmp(&a.0, &b.0)
|
||||
}).enumerate() {
|
||||
}).enumerate().chunks(4) {
|
||||
for (no_in_line, (elevation_no, scans)) in chunk {
|
||||
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)?;
|
||||
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 {
|
||||
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.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));
|
||||
}
|
||||
}
|
||||
line_no += 1;
|
||||
}
|
||||
} 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 px_per_km = (canvas.height() / RANGE as u32) as f32;
|
||||
let xc = canvas.width() / 2;
|
||||
let yc = canvas.height() / 2;
|
||||
|
||||
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 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;
|
||||
|
||||
for radial in radials {
|
||||
/* weird rounding stolen from go-nexrad */
|
||||
let mut azimuth_angle = (radial.header.azimuth_angle as f64) - 90.0;
|
||||
if azimuth_angle < 0.0 {
|
||||
azimuth_angle = 360.0 + azimuth_angle;
|
||||
}
|
||||
|
||||
let mut azimuth = azimuth_angle.floor();
|
||||
|
||||
let azimuth_spacing = match radial.header.azimuth_resolution_spacing {
|
||||
1 => 0.5,
|
||||
_ => 1.0
|
||||
};
|
||||
if (azimuth_angle + azimuth_spacing).floor() > azimuth {
|
||||
azimuth += azimuth_spacing;
|
||||
}
|
||||
/* conclude the weird rounding stolen from go-nexrad */
|
||||
|
||||
// Angles specified clockwise in radians
|
||||
let start_angle = azimuth * (PI / 180.0);
|
||||
let end_angle = azimuth_spacing * (PI / 180.0);
|
||||
|
||||
let mut distance_x = first_gate_px;
|
||||
let mut distance_y = first_gate_px;
|
||||
|
||||
// line width
|
||||
// line cap
|
||||
|
||||
let gates = radial.available_data.get(state.scope_mode.rname()).expect("selected unavailable product").scaled_data();
|
||||
|
||||
let num_gates = gates.len();
|
||||
|
||||
for (num, value) in gates.iter().enumerate() {
|
||||
if *value != MOMENT_DATA_BELOW_THRESHOLD {
|
||||
|
||||
ctx.move_to(xc as f64 + start_angle.cos() * distance_x as f64, yc as f64 + start_angle.sin() * distance_y as f64);
|
||||
|
||||
ctx.begin_path();
|
||||
|
||||
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)?;
|
||||
} 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)?;
|
||||
} 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.set_stroke_style(&JsValue::from_str(&format!("{}px {}", gate_width_px + 1.0, color_scheme(state.scope_mode, *value))));
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
distance_x += gate_width_px;
|
||||
distance_y += gate_width_px;
|
||||
|
||||
azimuth += azimuth_spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -13,7 +13,8 @@ pub struct ScopeState {
|
|||
pub command_buf: String,
|
||||
pub command_buf_response_mode: bool,
|
||||
pub new_data_available: bool,
|
||||
pub selected_elevation: usize
|
||||
pub selected_elevation: usize,
|
||||
pub show_ui: bool
|
||||
}
|
||||
|
||||
pub struct RenderState {
|
||||
|
|
|
@ -14,6 +14,8 @@ pub fn set_panic_hook() {
|
|||
pub const MILLIS_PER_DAY: u128 = 86_400_000;
|
||||
|
||||
pub fn parse_date(julian: u32, millis: u32) -> DateTime<Utc> {
|
||||
let unix_time_millis = (julian as u128) * MILLIS_PER_DAY + millis as u128;
|
||||
DateTime::UNIX_EPOCH + Duration::milliseconds(unix_time_millis as i64)
|
||||
let mut date = DateTime::UNIX_EPOCH;
|
||||
date += Duration::days(julian as i64 - 1);
|
||||
date += Duration::milliseconds(millis as i64);
|
||||
date
|
||||
}
|
|
@ -3,7 +3,8 @@ use std::fmt::{Debug, Formatter};
|
|||
use std::io::{Cursor, Read};
|
||||
|
||||
pub const MSG31_HEADER_LENGTH: usize = 4 + 4 + 2 + 2 + 4 + 1 + 1 + 2 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 2;
|
||||
|
||||
pub const MOMENT_DATA_BELOW_THRESHOLD: f32 = 999.0;
|
||||
pub const MOMENT_DATA_FOLDED: f32 = 998.0;
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde_derive", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Message31Header {
|
||||
|
@ -138,10 +139,10 @@ impl DataMoment {
|
|||
for gate in &gates {
|
||||
if *gate == 0 {
|
||||
// below threshold
|
||||
scaled_data.push(999.0);
|
||||
scaled_data.push(MOMENT_DATA_BELOW_THRESHOLD);
|
||||
} else if *gate == 1 {
|
||||
// folded
|
||||
scaled_data.push(998.0);
|
||||
scaled_data.push(MOMENT_DATA_FOLDED);
|
||||
} else {
|
||||
scaled_data.push(scale_uint(*gate, self.gdm.offset, self.gdm.scale))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue