This commit is contained in:
core 2023-11-13 16:58:03 -05:00
parent ad0f0d444c
commit a2153d8219
Signed by: core
GPG Key ID: FDBF740DADDCEECF
13 changed files with 608 additions and 166 deletions

View File

@ -7,6 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/nxar2/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rtwx/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rtwx-desktop/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rtwx-render/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />

View File

@ -4,7 +4,8 @@ members = [
"rtwx",
"nexrad-browser",
"nxar2",
"rtwx-desktop"
"rtwx-desktop",
"rtwx-render"
]
resolver = "2"

BIN
KATX20231111_172241_V06 Normal file

Binary file not shown.

View File

@ -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;

View File

@ -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 {

View File

@ -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])]

View File

@ -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<Vertex>,
pub indicies: Vec<u16>,
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,34 +253,92 @@ impl ScopeState {
pub fn update(&mut self) {}
pub fn render(&mut self) -> Result<(), SurfaceError> {
let output = self.wgpu.surface.get_current_texture()?;
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
let mut encoder = self
.wgpu.device
.create_command_encoder(&CommandEncoderDescriptor {
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);
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();
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 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 {
@ -295,16 +359,15 @@ impl ScopeState {
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 d_gateend = distance_gate + distance_gatespacing;
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];
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
// | |
@ -321,83 +384,47 @@ impl ScopeState {
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);
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
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,
)?;
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]);
}
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;
}
distance_gate += distance_gatespacing;
}
info!("Radar data triangled - {} verticies {} indicies", self.wgpu.verticies.len(), self.wgpu.indicies.len());
}
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]));
*/
pub fn render(&mut self) -> Result<(), SurfaceError> {
let output = self.wgpu.surface.get_current_texture()?;
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
let mut encoder = self
.wgpu.device
.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
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(verticies.as_slice()),
contents: bytemuck::cast_slice(self.wgpu.verticies.as_slice()),
usage: wgpu::BufferUsages::VERTEX,
}
);
@ -407,11 +434,13 @@ impl ScopeState {
let new_index_buffer = self.wgpu.device.create_buffer_init(
&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(indicies.as_slice()),
contents: bytemuck::cast_slice(self.wgpu.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
}

View File

@ -8,3 +8,5 @@ edition = "2021"
[dependencies]
simple_logger = "4"
nexrad2 = { version = "0.1", path = "../nexrad2", default-features = false, features = ["bzip-impl-libbzip2"] }
rtwx-render = { version = "0.1", path = "../rtwx-render" }
image = "0.24"

BIN
nxar2/out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

View File

@ -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();
}

9
rtwx-render/Cargo.toml Normal file
View File

@ -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" }

233
rtwx-render/src/colors.rs Normal file
View File

@ -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]
}

117
rtwx-render/src/lib.rs Normal file
View File

@ -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<Vec<(u8, u8, u8)>, RenderError>;
}
impl NexradRenderExt for nexrad2::Nexrad2Chunk {
fn render(&self, elevation: usize, product: &str, image_size: usize) -> Result<Vec<(u8, u8, u8)>, 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 {}