diff --git a/.idea/rtwx.iml b/.idea/rtwx.iml
index 361925b..550a483 100644
--- a/.idea/rtwx.iml
+++ b/.idea/rtwx.iml
@@ -7,6 +7,7 @@
+
diff --git a/Cargo.toml b/Cargo.toml
index 8bdd420..5678377 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,8 @@ members = [
"rtwx",
"nexrad-browser",
"nxar2",
- "rtwx-desktop"
+ "rtwx-desktop",
+ "rtwx-render"
]
resolver = "2"
diff --git a/KATX20231111_172241_V06 b/KATX20231111_172241_V06
new file mode 100644
index 0000000..8ea0b8e
Binary files /dev/null and b/KATX20231111_172241_V06 differ
diff --git a/nexrad-browser/src/command.rs b/nexrad-browser/src/command.rs
index bf8d8c5..2ef8431 100644
--- a/nexrad-browser/src/command.rs
+++ b/nexrad-browser/src/command.rs
@@ -4,7 +4,7 @@ use crate::rendering::scope::ScopeState;
pub fn should_newline(state: &mut ScopeState) -> bool {
- state.command_buf.starts_with("MODE SET") || state.command_buf.starts_with("ELEVATION SET") || state.command_buf.starts_with("FCS SET")
+ state.command_buf.starts_with("MODE SET") || state.command_buf.starts_with("ELEVATION SET") || state.command_buf.starts_with("FCS SET") || state.command_buf.starts_with("RANGE SET")
}
pub fn exec(state: &mut ScopeState, command: String) {
@@ -19,6 +19,10 @@ pub fn exec(state: &mut ScopeState, command: String) {
state.command_buf = state.prefs.fcs.to_string();
state.command_buf_response_mode = true;
return;
+ } else if command == "RANGE" {
+ state.command_buf = state.prefs.range.to_string();
+ state.command_buf_response_mode = true;
+ return;
} else if command.starts_with("FCS SET") {
if tokens.len() < 3 {
state.command_buf = "ARGUMENT INVALID".to_string();
@@ -35,6 +39,23 @@ pub fn exec(state: &mut ScopeState, command: String) {
};
state.prefs.fcs = new_val;
return;
+ } else if command.starts_with("RANGE SET") {
+ if tokens.len() < 3 {
+ state.command_buf = "ARGUMENT INVALID".to_string();
+ state.command_buf_response_mode = true;
+ return;
+ }
+ let new_val: f32 = match tokens[2].parse() {
+ Ok(v) => v,
+ Err(_) => {
+ state.command_buf = "ARGUMENT INVALID".to_string();
+ state.command_buf_response_mode = true;
+ return;
+ }
+ };
+ state.prefs.range = new_val;
+ state.recalculate_scope_display();
+ return;
} else if command.starts_with("MODE SET") {
if tokens.len() < 3 {
state.command_buf = "ARGUMENT INVALID".to_string();
@@ -62,6 +83,8 @@ pub fn exec(state: &mut ScopeState, command: String) {
"CFP" => Mode::ClutterFilterPowerRemoved,
_ => unreachable!(),
};
+
+ state.recalculate_scope_display();
} else {
state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string();
state.command_buf_response_mode = true;
@@ -90,6 +113,7 @@ pub fn exec(state: &mut ScopeState, command: String) {
// reset scope back to reflectivity which is included in every elevation
state.scope_mode = Mode::Reflectivity;
state.selected_elevation = number;
+ state.recalculate_scope_display();
} else {
state.command_buf = "CANNOT SET MODE\nRADAR INOPERATIVE".to_string();
state.command_buf_response_mode = true;
diff --git a/nexrad-browser/src/lib.rs b/nexrad-browser/src/lib.rs
index f54821b..72b92b9 100644
--- a/nexrad-browser/src/lib.rs
+++ b/nexrad-browser/src/lib.rs
@@ -99,6 +99,7 @@ pub async fn __nxrd_browser_init(w: u32, h: u32) -> AbiProxy {
scope_state.command_buf_response_mode = true;
scope_state.command_buf = "NEW DATA LOADED SUCCESSFULLY".to_string();
scope_state.selected_elevation = 1;
+ scope_state.recalculate_scope_display();
}
},
Event::WindowEvent {
diff --git a/nexrad-browser/src/rendering/colors.rs b/nexrad-browser/src/rendering/colors.rs
index f21bd84..693c97a 100644
--- a/nexrad-browser/src/rendering/colors.rs
+++ b/nexrad-browser/src/rendering/colors.rs
@@ -1,5 +1,5 @@
use crate::mode::Mode;
-use nexrad2::message31::MOMENT_DATA_FOLDED;
+use nexrad2::message31::{MOMENT_DATA_BELOW_THRESHOLD, MOMENT_DATA_FOLDED};
pub fn correlation_coefficient(val: f32) -> [f32; 3] {
let gradient = [
@@ -199,6 +199,9 @@ pub fn scale_int(val: i32, o_max: i32, o_min: i32, n_max: i32, n_min: i32) -> i3
}
pub fn color_scheme(product: Mode, value: f32) -> [f32; 3] {
+ if value == MOMENT_DATA_BELOW_THRESHOLD {
+ return [rgb_to_srgb_float_individual_unscaled(0x69 as f32), rgb_to_srgb_float_individual_unscaled(0x1a as f32), rgb_to_srgb_float_individual_unscaled(0xc1 as f32)];
+ }
let rgb = match product {
Mode::Reflectivity => dbz_noaa(value),
Mode::Velocity => velocity(value),
@@ -207,7 +210,7 @@ pub fn color_scheme(product: Mode, value: f32) -> [f32; 3] {
Mode::DifferentialPhase => correlation_coefficient(value),
Mode::CorrelationCoefficient => correlation_coefficient(value),
Mode::ClutterFilterPowerRemoved => dbz_noaa(value),
- Mode::RadarInoperative => [255.0, 255.0, 255.0],
+ Mode::RadarInoperative => [0x69 as f32, 0x1a as f32, 0xc1 as f32],
};
[rgb_to_srgb_float_individual_unscaled(rgb[0]), rgb_to_srgb_float_individual_unscaled(rgb[1]), rgb_to_srgb_float_individual_unscaled(rgb[2])]
diff --git a/nexrad-browser/src/rendering/scope.rs b/nexrad-browser/src/rendering/scope.rs
index 44c0f27..6b8c6af 100644
--- a/nexrad-browser/src/rendering/scope.rs
+++ b/nexrad-browser/src/rendering/scope.rs
@@ -1,4 +1,4 @@
-use std::f64::consts::PI;
+use std::f32::consts::PI;
use crate::mode::Mode;
use nexrad2::Nexrad2Chunk;
@@ -6,7 +6,7 @@ use nexrad2::Nexrad2Chunk;
use std::iter;
use chrono::Utc;
use itertools::Itertools;
-use log::info;
+use log::{debug, info};
use wasm_bindgen::JsCast;
@@ -58,7 +58,10 @@ pub struct WgpuState {
pub index_buffer: wgpu::Buffer,
pub pipeline: RenderPipeline,
pub staging_belt: StagingBelt,
- pub glyph_brush: GlyphBrush<()>
+ pub glyph_brush: GlyphBrush<()>,
+ pub verticies: Vec,
+ pub indicies: Vec,
+ pub buffers_need_reinitialization: bool
}
impl ScopeState {
@@ -190,7 +193,10 @@ impl ScopeState {
pipeline: render_pipeline,
staging_belt,
glyph_brush,
- index_buffer
+ index_buffer,
+ verticies: vec![],
+ indicies: vec![],
+ buffers_need_reinitialization: false
};
info!("initializing the scope");
@@ -216,7 +222,7 @@ impl ScopeState {
file_input: file,
lat: 30.48500, // jacksonville
long: -81.70200, // jacksonville
- prefs: Preferences { fcs: 24.0 },
+ prefs: Preferences { fcs: 24.0, range: 50.0 },
command_buf: String::new(),
command_buf_response_mode: false,
new_data_available: false,
@@ -247,6 +253,160 @@ impl ScopeState {
pub fn update(&mut self) {}
+ pub fn recalculate_scope_display(&mut self) {
+ self.wgpu.buffers_need_reinitialization = true;
+ self.wgpu.verticies = vec![];
+ self.wgpu.indicies = vec![];
+ let ar2 = self.ar2.as_ref().unwrap();
+ let px_per_km = 1.0 / (self.prefs.range * 2.0);
+
+ let radials = ar2.elevations.get(&self.selected_elevation).unwrap();
+
+ let first_gate_px = radials[0].available_data.get(self.scope_mode.rname()).unwrap().gdm.data_moment_range as f32 / 1000.0 * px_per_km;
+ let gate_interval_km = radials[0].available_data.get(self.scope_mode.rname()).unwrap().gdm.data_moment_range_sample_interval as f32 / 1000.0;
+ let gate_width_px = gate_interval_km * px_per_km;
+
+ for radial in radials {
+ // radial rounding
+ let mut azimuth_angle = radial.header.azimuth_angle - 90.0;
+ if azimuth_angle < 0.0 {
+ azimuth_angle = 360.0 + azimuth_angle;
+ }
+ let azimuth_spacing = match radial.header.azimuth_resolution_spacing {
+ 1 => 0.5,
+ _ => 1.0
+ };
+ let mut azimuth = azimuth_angle.floor();
+ if (azimuth_angle + azimuth_spacing).floor() > azimuth {
+ azimuth += azimuth_spacing;
+ }
+ let start_angle = azimuth * (PI / 180.0);
+ let end_angle = (azimuth + azimuth_spacing) * (PI / 180.0);
+
+ let mut distance = first_gate_px;
+
+ let gates = radial.available_data.get(self.scope_mode.rname()).unwrap().scaled_data();
+
+ for (gate_num, gate) in gates.iter().enumerate() {
+ if *gate != MOMENT_DATA_BELOW_THRESHOLD {
+ let a = [(distance * start_angle.cos()) as f32, (distance * start_angle.sin()) as f32, 0.0];
+ let b = [(distance * end_angle.cos()) as f32, (distance * end_angle.sin()) as f32, 0.0];
+ let c = [(distance + gate_width_px * start_angle.cos()) as f32, (distance + gate_width_px * start_angle.sin()) as f32, 0.0];
+ let d = [(distance + gate_width_px * end_angle.cos()) as f32, (distance + gate_width_px * end_angle.sin()) as f32, 0.0];
+ let vertex_a = Vertex { position: a, color: color_scheme(self.scope_mode, *gate) };
+ let vertex_b = Vertex { position: b, color: color_scheme(self.scope_mode, *gate) };
+ let vertex_c = Vertex { position: c, color: color_scheme(self.scope_mode, *gate) };
+ let vertex_d = Vertex { position: d, color: color_scheme(self.scope_mode, *gate) };
+
+ let vindex_a = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_a);
+ let vindex_b = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_b);
+ let vindex_c = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_c);
+ let vindex_d = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_d);
+
+ // ABC <-> BCD
+
+ self.wgpu.indicies.extend_from_slice(&[vindex_a as u16, vindex_b as u16, vindex_c as u16]);
+ self.wgpu.indicies.extend_from_slice(&[vindex_b as u16, vindex_c as u16, vindex_d as u16]);
+
+ }
+ distance += gate_width_px;
+ azimuth += azimuth_spacing;
+ }
+ }
+ info!("Tesselated, {}/{}", self.wgpu.verticies.len(), self.wgpu.indicies.len());
+ info!("{:?} {:?} {:?} {:?}", self.wgpu.verticies[0], self.wgpu.verticies[1], self.wgpu.verticies[2], self.wgpu.verticies[3]);
+ }
+
+/*
+ pub fn recalculate_scope_display(&mut self) {
+ self.wgpu.buffers_need_reinitialization = true;
+ self.wgpu.verticies = vec![];
+ self.wgpu.indicies = vec![];
+ let ar2 = self.ar2.as_ref().unwrap();
+ let px_per_km = 1.0 / (self.prefs.range * 2.0);
+
+ let radials = ar2.elevations.get(&self.selected_elevation).unwrap();
+
+ info!("Tesselating {} radials", radials.len());
+
+ let distance_firstgate = ((radials[0].available_data.get(self.scope_mode.rname()).expect("selected mode is missing!").gdm.data_moment_range as f64) / 1000.0) * px_per_km;
+ let distance_gatespacing = ((radials[0].available_data.get(self.scope_mode.rname()).expect("selected mode is missing!").gdm.data_moment_range_sample_interval as f64) / 1000.0) * px_per_km;
+
+ let mut distance_gate = distance_firstgate;
+
+ for radial in radials {
+ let mut azimuth = radial.header.azimuth_angle as f64;
+
+ let azimuth_spacing = match radial.header.azimuth_resolution_spacing {
+ 1 => 0.5,
+ _ => 1.0,
+ };
+
+ // line width
+ // line cap
+
+ let gates = radial
+ .available_data
+ .get(self.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 {
+ let az = azimuth * (PI / 180.0);
+ let bz = (azimuth + azimuth_spacing) * (PI / 180.0);
+
+ let d_gateend = distance_gate + distance_gatespacing;
+
+ let a = [(distance_gate * az.cos()) as f32, (distance_gate * az.sin()) as f32, 0.0];
+ let b = [(distance_gate * bz.cos()) as f32, (distance_gate * bz.sin()) as f32, 0.0];
+ let c = [(d_gateend * az.cos()) as f32, (d_gateend * az.sin()) as f32, 0.0];
+ let d = [(d_gateend * bz.cos()) as f32, (d_gateend * bz.sin()) as f32, 0.0];
+
+ // C--D
+ // | |
+ // A--B
+ /*
+ (x0, y1) -- (x1, y1)
+ | |
+ (x0, y0) -- (x1, y0)
+
+ */
+
+ let vertex_a = Vertex { position: a, color: color_scheme(self.scope_mode, *value) };
+ let vertex_b = Vertex { position: b, color: color_scheme(self.scope_mode, *value) };
+ let vertex_c = Vertex { position: c, color: color_scheme(self.scope_mode, *value) };
+ let vertex_d = Vertex { position: d, color: color_scheme(self.scope_mode, *value) };
+
+ let vindex_a = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_a);
+ let vindex_b = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_b);
+ let vindex_c = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_c);
+ let vindex_d = self.wgpu.verticies.len();
+ self.wgpu.verticies.push(vertex_d);
+
+ // ABC <-> BCD
+
+ self.wgpu.indicies.extend_from_slice(&[vindex_a as u16, vindex_b as u16, vindex_c as u16]);
+ //self.wgpu.indicies.extend_from_slice(&[vindex_a as u16, vindex_d as u16, vindex_b as u16]);
+ }
+ azimuth += azimuth_spacing;
+ }
+ distance_gate += distance_gatespacing;
+ }
+ info!("Radar data triangled - {} verticies {} indicies", self.wgpu.verticies.len(), self.wgpu.indicies.len());
+ }
+
+
+ */
pub fn render(&mut self) -> Result<(), SurfaceError> {
let output = self.wgpu.surface.get_current_texture()?;
let view = output
@@ -258,160 +418,29 @@ impl ScopeState {
label: Some("Render Encoder"),
});
- let physical_width = (self.wgpu.surface_config.width as f64 * self.wgpu.window.scale_factor()) as f32;
- let physical_height =
- (self.wgpu.surface_config.height as f64 * self.wgpu.window.scale_factor()) as f32;
-
- let mut verticies = vec![];
- let mut indicies = vec![];
-
- // ACTUAL DATA RENDERING
- if let Some(ar2) = &self.ar2 {
- let px_per_km = 1.0 / (250.0 * 2.0);
-
- let radials = ar2.elevations.get(&self.selected_elevation).unwrap();
-
- for radial in radials {
- let distance_firstgate_km = (radial.available_data.get(self.scope_mode.rname()).expect("selected mode is missing!").gdm.data_moment_range as f64) / 1000.0;
- let distance_gatespacing_km = (radials[0].available_data.get(self.scope_mode.rname()).expect("selected mode is missing!").gdm.data_moment_range_sample_interval as f64) / 1000.0;
-
- let mut azimuth = radial.header.azimuth_angle as f64;
-
- let azimuth_spacing = match radial.header.azimuth_resolution_spacing {
- 1 => 0.5,
- _ => 1.0,
- };
-
- // line width
- // line cap
-
- let gates = radial
- .available_data
- .get(self.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 {
- let distance_gate_km = distance_firstgate_km + (distance_gatespacing_km * num as f64);
- let az = azimuth * (PI / 180.0);
- let bz = (azimuth + azimuth_spacing) * (PI / 180.0);
-
- let d_gateend = distance_gate_km + distance_gatespacing_km;
-
- let a = [(distance_gate_km * az.cos()) as f32 * px_per_km, (distance_gate_km * az.sin()) as f32 * px_per_km, 0.0];
- let b = [(distance_gate_km * bz.cos()) as f32 * px_per_km, (distance_gate_km * bz.sin()) as f32 * px_per_km, 0.0];
- let c = [(d_gateend * az.cos()) as f32 * px_per_km, (d_gateend * az.sin()) as f32 * px_per_km, 0.0];
- let d = [(d_gateend * bz.cos()) as f32 * px_per_km, (d_gateend * bz.sin()) as f32 * px_per_km, 0.0];
-
- // C--D
- // | |
- // A--B
- /*
- (x0, y1) -- (x1, y1)
- | |
- (x0, y0) -- (x1, y0)
-
- */
-
- let vertex_a = Vertex { position: a, color: color_scheme(self.scope_mode, *value) };
- let vertex_b = Vertex { position: b, color: color_scheme(self.scope_mode, *value) };
- let vertex_c = Vertex { position: c, color: color_scheme(self.scope_mode, *value) };
- let vertex_d = Vertex { position: d, color: color_scheme(self.scope_mode, *value) };
-
- let vindex_a = verticies.len();
- verticies.push(vertex_a);
- let vindex_b = verticies.len();
- verticies.push(vertex_b);
- let vindex_c = verticies.len();
- verticies.push(vertex_c);
- let vindex_d = verticies.len();
- verticies.push(vertex_d);
-
- // ABC <-> BCD
-
- indicies.extend_from_slice(&[vindex_a, vindex_b, vindex_c]);
- indicies.extend_from_slice(&[vindex_b, vindex_c, vindex_d]);
-
- /*
- 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();
-
- */
- }
-
- azimuth += azimuth_spacing;
+ if self.wgpu.buffers_need_reinitialization {
+ self.wgpu.buffers_need_reinitialization = false;
+ self.wgpu.vertex_buffer.destroy();
+ let new_vertex_buffer = self.wgpu.device.create_buffer_init(
+ &wgpu::util::BufferInitDescriptor {
+ label: Some("Vertex Buffer"),
+ contents: bytemuck::cast_slice(self.wgpu.verticies.as_slice()),
+ usage: wgpu::BufferUsages::VERTEX,
}
- }
+ );
+ self.wgpu.vertex_buffer = new_vertex_buffer;
+
+ self.wgpu.index_buffer.destroy();
+ let new_index_buffer = self.wgpu.device.create_buffer_init(
+ &wgpu::util::BufferInitDescriptor {
+ label: Some("Index Buffer"),
+ contents: bytemuck::cast_slice(self.wgpu.indicies.as_slice()),
+ usage: wgpu::BufferUsages::INDEX,
+ }
+ );
+ self.wgpu.index_buffer = new_index_buffer;
}
- info!("Radar data tesselated - {} verticies {} indicies", verticies.len(), indicies.len());
- info!("{:?}", verticies.get(0).map(|u| u.position).unwrap_or([0.0, 0.0, 0.0]));
-
- self.wgpu.vertex_buffer.destroy();
- let new_vertex_buffer = self.wgpu.device.create_buffer_init(
- &wgpu::util::BufferInitDescriptor {
- label: Some("Vertex Buffer"),
- contents: bytemuck::cast_slice(verticies.as_slice()),
- usage: wgpu::BufferUsages::VERTEX,
- }
- );
- self.wgpu.vertex_buffer = new_vertex_buffer;
-
- self.wgpu.index_buffer.destroy();
- let new_index_buffer = self.wgpu.device.create_buffer_init(
- &wgpu::util::BufferInitDescriptor {
- label: Some("Index Buffer"),
- contents: bytemuck::cast_slice(indicies.as_slice()),
- usage: wgpu::BufferUsages::INDEX,
- }
- );
- self.wgpu.index_buffer = new_index_buffer;
// LOCK BLOCK
{
@@ -438,7 +467,7 @@ impl ScopeState {
render_pass.set_pipeline(&self.wgpu.pipeline);
render_pass.set_vertex_buffer(0, self.wgpu.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.wgpu.index_buffer.slice(..), wgpu::IndexFormat::Uint16); // 1.
- render_pass.draw_indexed(0..(indicies.len() as u32), 0, 0..1);
+ render_pass.draw_indexed(0..(self.wgpu.indicies.len() as u32), 0, 0..1);
}
@@ -718,4 +747,5 @@ impl ScopeState {
pub struct Preferences {
pub fcs: f32,
+ pub range: f32
}
diff --git a/nxar2/Cargo.toml b/nxar2/Cargo.toml
index 8a81f7e..3ccb7a6 100644
--- a/nxar2/Cargo.toml
+++ b/nxar2/Cargo.toml
@@ -7,4 +7,6 @@ edition = "2021"
[dependencies]
simple_logger = "4"
-nexrad2 = { version = "0.1", path = "../nexrad2", default-features = false, features = ["bzip-impl-libbzip2"] }
\ No newline at end of file
+nexrad2 = { version = "0.1", path = "../nexrad2", default-features = false, features = ["bzip-impl-libbzip2"] }
+rtwx-render = { version = "0.1", path = "../rtwx-render" }
+image = "0.24"
\ No newline at end of file
diff --git a/nxar2/out.png b/nxar2/out.png
new file mode 100644
index 0000000..a9ae50d
Binary files /dev/null and b/nxar2/out.png differ
diff --git a/nxar2/src/main.rs b/nxar2/src/main.rs
index d35d48d..fd38f4f 100644
--- a/nxar2/src/main.rs
+++ b/nxar2/src/main.rs
@@ -1,12 +1,33 @@
use std::fs;
use std::fs::File;
+use std::io::Write;
+use image::{ImageBuffer, RgbImage};
use nexrad2::parse_nx2_chunk;
+use rtwx_render::NexradRenderExt;
fn main() {
- simple_logger::init().unwrap();
- let test = std::env::args().nth(1).unwrap();
+ simple_logger::init_with_env().unwrap();
+ let file = std::env::args().nth(1).unwrap();
- let mut data = File::open(test).unwrap();
+ let mut data = File::open(file).unwrap();
- parse_nx2_chunk(&mut data).unwrap();
+ let chunk = parse_nx2_chunk(&mut data).unwrap();
+
+ let elevation: usize = std::env::args().nth(2).unwrap().parse().unwrap();
+ let mode = std::env::args().nth(3).unwrap();
+ let img_size: usize = std::env::args().nth(4).unwrap().parse().unwrap();
+
+ let img = chunk.render(elevation, mode.as_str(), img_size).unwrap();
+
+ let out_file = std::env::args().nth(5).unwrap();
+
+ let mut img_rgb: RgbImage = ImageBuffer::new(img_size as u32, img_size as u32);
+ for x in 0..img_size {
+ for y in 0..img_size {
+ let pixel = img[y * img_size + x];
+ img_rgb.put_pixel(x as u32, y as u32, image::Rgb([pixel.0, pixel.1, pixel.2]));
+ }
+ }
+
+ img_rgb.save(out_file).unwrap();
}
\ No newline at end of file
diff --git a/rtwx-render/Cargo.toml b/rtwx-render/Cargo.toml
new file mode 100644
index 0000000..a6454ac
--- /dev/null
+++ b/rtwx-render/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "rtwx-render"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+nexrad2 = { version = "0.1", path = "../nexrad2" }
\ No newline at end of file
diff --git a/rtwx-render/src/colors.rs b/rtwx-render/src/colors.rs
new file mode 100644
index 0000000..42991bf
--- /dev/null
+++ b/rtwx-render/src/colors.rs
@@ -0,0 +1,233 @@
+use nexrad2::message31::{MOMENT_DATA_BELOW_THRESHOLD, MOMENT_DATA_FOLDED};
+
+pub fn correlation_coefficient(val: f32) -> [f32; 3] {
+ let gradient = [
+ (0.275, [0.0, 0.0, 0.0]),
+ (0.35, [169.0, 169.0, 169.0]),
+ (0.4, [128.0, 128.0, 128.0]),
+ (0.5, [192.0, 192.0, 192.0]),
+ (0.6, [25.0, 25.0, 112.0]),
+ (0.7, [0.0, 0.0, 139.0]),
+ (0.8, [0.0, 0.0, 255.0]),
+ (0.91, [0.0, 128.0, 0.0]),
+ (0.92, [154.0, 205.0, 50.0]),
+ (0.93, [107.0, 142.0, 35.0]),
+ (0.94, [255.0, 255.0, 0.0]),
+ (0.95, [255.0, 215.0, 0.0]),
+ (0.96, [255.0, 165.0, 0.0]),
+ (0.97, [255.0, 69.0, 0.0]),
+ (0.98, [255.0, 0.0, 0.0]),
+ (0.99, [178.0, 34.0, 34.0]),
+ (1.0, [128.0, 0.0, 0.0]),
+ (1.01, [139.0, 0.0, 139.0]),
+ (1.02, [128.0, 0.0, 128.0]),
+ (1.03, [199.0, 21.0, 133.0]),
+ (1.045, [255.0, 192.0, 203.0]),
+ (1.05, [255.0, 240.0, 245.0]),
+ ];
+
+ for (threshold, color) in gradient {
+ if val < threshold {
+ return color;
+ }
+ }
+
+ [255.0, 255.0, 255.0]
+}
+
+pub fn spectrum_width(val: f32) -> [f32; 3] {
+ let gradient = [
+ (1.0, [0.0, 0.0, 0.0]),
+ (2.0, [0x22 as f32, 0x22 as f32, 0x22 as f32]),
+ (3.0, [0x33 as f32, 0x33 as f32, 0x33 as f32]),
+ (4.0, [0x44 as f32, 0x44 as f32, 0x44 as f32]),
+ (5.0, [0x55 as f32, 0x55 as f32, 0x55 as f32]),
+ (6.0, [0x66 as f32, 0x66 as f32, 0x66 as f32]),
+ (7.0, [0x77 as f32, 0x77 as f32, 0x77 as f32]),
+ (8.0, [0x88 as f32, 0x88 as f32, 0x88 as f32]),
+ (9.0, [0x99 as f32, 0x99 as f32, 0x99 as f32]),
+ (10.0, [222.0, 184.0, 135.0]),
+ (11.0, [244.0, 164.0, 96.0]),
+ (12.0, [255.0, 215.0, 0.0]),
+ (13.0, [255.0, 165.0, 0.0]),
+ (15.0, [255.0, 69.0, 0.0]),
+ (17.0, [255.0, 0.0, 0.0]),
+ (19.0, [178.0, 34.0, 34.0]),
+ (23.0, [139.0, 0.0, 0.0]),
+ (25.0, [255.0, 105.0, 180.0]),
+ (27.0, [255.0, 0.0, 255.0]),
+ (30.0, [230.0, 230.0, 250.0]),
+ (32.0, [255.0, 255.0, 255.0]),
+ (35.0, [255.0, 255.0, 0.0]),
+ (60.0, [0.0, 255.0, 0.0]),
+ (1000.0, [128.0, 0.0, 128.0]),
+ ];
+
+ for (threshold, color) in gradient {
+ if val < threshold {
+ return color;
+ }
+ }
+
+ [255.0, 255.0, 255.0]
+}
+
+pub fn differential_reflectivity(val: f32) -> [f32; 3] {
+ let gradient = [
+ (-3.0, [0.0, 0.0, 0.0]),
+ (-1.0, [0x33 as f32, 0x33 as f32, 0x33 as f32]),
+ (-0.5, [0x66 as f32, 0x66 as f32, 0x66 as f32]),
+ (0.1, [0x99 as f32, 0x99 as f32, 0x99 as f32]),
+ (0.0, [0xcc as f32, 0xcc as f32, 0xcc as f32]),
+ (0.1, [255.0, 255.0, 255.0]),
+ (0.25, [0.0, 0.0, 128.0]),
+ (0.5, [0.0, 0.0, 255.0]),
+ (0.75, [0.0, 191.0, 255.0]),
+ (1.0, [0.0, 255.0, 255.0]),
+ (1.25, [102.0, 205.0, 170.0]),
+ (1.5, [0.0, 255.0, 0.0]),
+ (1.75, [154.0, 205.0, 50.0]),
+ (2.0, [255.0, 255.0, 0.0]),
+ (2.5, [255.0, 215.0, 0.0]),
+ (3.0, [255.0, 165.0, 0.0]),
+ (4.0, [255.0, 69.0, 0.0]),
+ (5.0, [255.0, 0.0, 0.0]),
+ (6.0, [128.0, 0.0, 0.0]),
+ (7.0, [255.0, 105.0, 180.0]),
+ (10.0, [255.0, 192.0, 203.0]),
+ (999.0, [255.0, 255.0, 255.0]),
+ ];
+
+ for (threshold, color) in gradient {
+ if val < threshold {
+ return color;
+ }
+ }
+
+ [255.0, 255.0, 255.0]
+}
+
+pub fn dbz_noaa(dbz: f32) -> [f32; 3] {
+ if dbz < 5.0 || dbz == MOMENT_DATA_FOLDED {
+ [0x00 as f32, 0x00 as f32, 0x00 as f32]
+ } else if dbz >= 5.0 && dbz < 10.0 {
+ [0x40 as f32, 0xe8 as f32, 0xe3 as f32]
+ } else if dbz >= 10.0 && dbz < 15.0 {
+ [0x26 as f32, 0xa4 as f32, 0xfa as f32]
+ } else if dbz >= 15.0 && dbz < 20.0 {
+ [0x00 as f32, 0x30 as f32, 0xed as f32]
+ } else if dbz >= 20.0 && dbz < 25.0 {
+ [0x49 as f32, 0xfb as f32, 0x3e as f32]
+ } else if dbz >= 25.0 && dbz < 30.0 {
+ [0x36 as f32, 0xc2 as f32, 0x2e as f32]
+ } else if dbz >= 30.0 && dbz < 35.0 {
+ [0x27 as f32, 0x8c as f32, 0x1e as f32]
+ } else if dbz >= 35.0 && dbz < 40.0 {
+ [0xfe as f32, 0xf5 as f32, 0x43 as f32]
+ } else if dbz >= 40.0 && dbz < 45.0 {
+ [0xeb as f32, 0xb4 as f32, 0x33 as f32]
+ } else if dbz >= 45.0 && dbz < 50.0 {
+ [0xf6 as f32, 0x95 as f32, 0x2e as f32]
+ } else if dbz >= 50.0 && dbz < 55.0 {
+ [0xf8 as f32, 0x0a as f32, 0x26 as f32]
+ } else if dbz >= 55.0 && dbz < 60.0 {
+ [0xcb as f32, 0x05 as f32, 0x16 as f32]
+ } else if dbz >= 60.0 && dbz < 65.0 {
+ [0xa9 as f32, 0x08 as f32, 0x13 as f32]
+ } else if dbz >= 65.0 && dbz < 70.0 {
+ [0xee as f32, 0x34 as f32, 0xfa as f32]
+ } else if dbz >= 79.0 && dbz < 75.0 {
+ [0x91 as f32, 0x61 as f32, 0xc4 as f32]
+ } else {
+ [255.0, 255.0, 255.0]
+ }
+}
+
+pub fn velocity(vel: f32) -> [f32; 3] {
+ if vel == MOMENT_DATA_FOLDED {
+ return [0x69 as f32, 0x1a as f32, 0xc1 as f32];
+ }
+
+ let colors = [
+ [0xf9, 0x14, 0x73],
+ [0xaa, 0x10, 0x79],
+ [0x6e, 0x0e, 0x80],
+ [0x2e, 0x0e, 0x84],
+ [0x15, 0x1f, 0x93],
+ [0x23, 0x6f, 0xb3],
+ [0x41, 0xda, 0xdb],
+ [0x66, 0xe1, 0xe2],
+ [0x9e, 0xe8, 0xea],
+ [0x58, 0xfa, 0x63],
+ [0x31, 0xe3, 0x2b],
+ // "#21BE0A", // 35
+ [0x24, 0xaa, 0x1f],
+ [0x19, 0x76, 0x13],
+ [0x45, 0x67, 0x42],
+ [0x63, 0x4f, 0x50],
+ [0x6e, 0x2e, 0x39],
+ [0x7f, 0x03, 0x0c],
+ [0xb6, 0x07, 0x16],
+ // "#C5000D", // 35
+ [0xf3, 0x22, 0x45],
+ [0xf6, 0x50, 0x8a],
+ [0xfb, 0x8b, 0xbf],
+ [0xfd, 0xde, 0x93],
+ [0xfc, 0xb4, 0x70],
+ [0xfa, 0x81, 0x4b],
+ [0xdd, 0x60, 0x3c],
+ [0xb7, 0x45, 0x2d],
+ [0x93, 0x2c, 0x20],
+ [0x71, 0x16, 0x14],
+ [0x52, 0x01, 0x06],
+ ];
+
+ let i = scale_int(
+ (vel.floor()) as i32,
+ 140,
+ -140,
+ (colors.len() - 1) as i32,
+ 0,
+ );
+
+ [colors[i as usize][0] as f32, colors[i as usize][1] as f32, colors[i as usize][2] as f32]
+}
+
+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: &str, value: f32) -> [f32; 3] {
+ if value == MOMENT_DATA_BELOW_THRESHOLD {
+ return [rgb_to_srgb_float_individual_unscaled(0x69 as f32), rgb_to_srgb_float_individual_unscaled(0x1a as f32), rgb_to_srgb_float_individual_unscaled(0xc1 as f32)];
+ }
+ let rgb = match product {
+ "REF" => dbz_noaa(value),
+ "VEL" => velocity(value),
+ "SW" => spectrum_width(value),
+ "ZDR" => differential_reflectivity(value),
+ "PHI" => correlation_coefficient(value),
+ "RHO" => correlation_coefficient(value),
+ "CFP" => dbz_noaa(value),
+ _ => panic!()
+ };
+
+ rgb
+ //[rgb_to_srgb_float_individual_unscaled(rgb[0]), rgb_to_srgb_float_individual_unscaled(rgb[1]), rgb_to_srgb_float_individual_unscaled(rgb[2])]
+}
+
+pub fn rgb_to_srgb_float_individual_unscaled(rgb_color: f32) -> f32 {
+ ((rgb_color / 255.0 + 0.055) / 1.055).powf(2.4)
+}
+
+pub fn rgb_to_srgb(c: u32, a: f32) -> [f32; 4] {
+ let f = |xu: u32| {
+ let x = (xu & 0xFF) as f32 / 255.0;
+ if x > 0.04045 {
+ ((x + 0.055) / 1.055).powf(2.4)
+ } else {
+ x / 12.92
+ }
+ };
+ [f(c >> 16), f(c >> 8), f(c), a]
+}
\ No newline at end of file
diff --git a/rtwx-render/src/lib.rs b/rtwx-render/src/lib.rs
new file mode 100644
index 0000000..d9af909
--- /dev/null
+++ b/rtwx-render/src/lib.rs
@@ -0,0 +1,117 @@
+use std::error::Error;
+use std::f32::consts::PI;
+use std::fmt::{Display, Formatter};
+use nexrad2::message31::MOMENT_DATA_BELOW_THRESHOLD;
+use crate::colors::color_scheme;
+
+pub mod colors;
+
+pub trait NexradRenderExt {
+ fn render(&self, elevation: usize, product: &str, image_size: usize) -> Result, RenderError>;
+}
+
+impl NexradRenderExt for nexrad2::Nexrad2Chunk {
+ fn render(&self, elevation: usize, product: &str, image_size: usize) -> Result, RenderError> {
+ let mut pixel_data = vec![[0.0, 0.0, 0.0]; image_size * image_size];
+
+ let center = (image_size as f64) / 2.0;
+ let px_per_km = (image_size as f64) / 2.0 / 920.0;
+
+ let radials = self.elevations.get(&elevation).ok_or(RenderError::ElevationMissing)?;
+
+ let moment_range = radials[0].available_data.get("REF").unwrap().gdm.data_moment_range;
+ let first_gate_px = moment_range as f32 / 1000.0 * px_per_km as f32;
+ let gate_interval_km = radials[0].available_data.get("REF").unwrap().gdm.data_moment_range_sample_interval as f64 / 1000.0;
+ println!("{}", gate_interval_km);
+ let gate_width_px = gate_interval_km * px_per_km as f64;
+ println!("{} {}", px_per_km, gate_width_px);
+
+ for radial in radials {
+ let mut azimuth_angle = radial.header.azimuth_angle - 90.0;
+ if azimuth_angle < 0.0 {
+ azimuth_angle += 360.0;
+ }
+
+ let azimuth_spacing = if radial.header.azimuth_resolution_spacing == 1 { 0.5 } else { 1.0 };
+
+ let mut azimuth = azimuth_angle.floor();
+
+ if azimuth < (azimuth_angle + azimuth_spacing).floor() {
+ azimuth += azimuth_spacing;
+ }
+
+ println!("azimuth: {}", azimuth);
+
+ let start_angle = azimuth * (PI / 180.0);
+
+ println!("azimuth, radians: {}", start_angle);
+
+ let mut distance = first_gate_px;
+
+ let data_moment = radials[0].available_data.get(product).ok_or(RenderError::ProductMissing)?;
+
+ let gates = data_moment.scaled_data();
+
+ for gate in gates {
+ if gate != MOMENT_DATA_BELOW_THRESHOLD {
+ let mut pixel_x = (center as f32 + start_angle.cos() * distance).round() as usize;
+ let mut pixel_y = (center as f32 + start_angle.sin() * distance).round() as usize;
+
+ if pixel_x >= image_size {
+ pixel_x = image_size-1;
+ }
+ if pixel_y >= image_size {
+ pixel_y = image_size-1;
+ }
+
+ if pixel_y * image_size + pixel_x > (image_size * image_size) {
+ println!("{} {}", pixel_x, pixel_y);
+ }
+
+ pixel_data[pixel_y * image_size + pixel_x] = color_scheme(product, gate);
+ //println!("{} {} {}", pixel_x, pixel_y, gate);
+ //println!("not below threshold! {} {}", pixel_x, pixel_y);
+ //println!("{}", gate_width_px);
+ } else {
+ let mut pixel_x = (center as f32 + start_angle.cos() * distance).round() as usize;
+ let mut pixel_y = (center as f32 + start_angle.sin() * distance).round() as usize;
+
+ if pixel_x >= image_size {
+ pixel_x = image_size-1;
+ }
+ if pixel_y >= image_size {
+ pixel_y = image_size-1;
+ }
+
+ if pixel_y * image_size + pixel_x > (image_size * image_size) {
+ println!("{} {}", pixel_x, pixel_y);
+ }
+
+ pixel_data[pixel_y * image_size + pixel_x] = [0.3, 0.2, 0.1];
+ //println!("not below threshold! {} {}", pixel_x, pixel_y);
+ //println!("{}", gate_width_px);
+ }
+
+ distance += gate_width_px as f32;
+ azimuth += azimuth_spacing;
+ }
+ }
+
+ Ok(pixel_data.iter().map(|u| ((u[0] * 255.0).round() as u8, (u[1] * 255.0).round() as u8, (u[2] * 255.0).round() as u8)).collect())
+ }
+}
+
+#[derive(Debug)]
+pub enum RenderError {
+ ElevationMissing,
+ ProductMissing
+}
+impl Display for RenderError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::ElevationMissing => write!(f, "elevation missing"),
+ Self::ProductMissing => write!(f, "product missing")
+ }
+ }
+}
+impl Error for RenderError {}
\ No newline at end of file