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