Initial commit
This commit is contained in:
commit
d052cb8152
8 changed files with 1907 additions and 0 deletions
6
.editorconfig
Normal file
6
.editorconfig
Normal file
|
@ -0,0 +1,6 @@
|
|||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{rs}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1039
Cargo.lock
generated
Normal file
1039
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "l2decodeV5"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.4"
|
||||
bzip2 = "0.5.1"
|
||||
image = "0.25.5"
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# l2decodeV5
|
||||
|
||||
This is a prototype WSR-88D Level-II (NEXRAD weather radar) data decoder and image generator written in Rust. I've never really used Rust before, so I decided to rewrite one of my old C routines to get a feel for how things work. This code probably isn't the best, but it's a start. I've come to like Rust a lot in the process of writing this over the past couple of days!
|
||||
|
||||
Notes:
|
||||
- Only processes reflectivity data for the first elevation scan. The code is structured such that it's possible to get other products out of it though.
|
||||
- Expected input is an Archive-II file assuming RDA build 19 and later. It probably wouldn't take much to get it to support data from sites running older builds of the RDA software.
|
||||
|
||||
## Output Example
|
||||
|
||||

|
BIN
output.png
Normal file
BIN
output.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 910 KiB |
841
src/main.rs
Normal file
841
src/main.rs
Normal file
|
@ -0,0 +1,841 @@
|
|||
//
|
||||
// NEXRAD Level-II data decoder and image generator with WSR-88D Build
|
||||
// 10 Message 31 support
|
||||
//
|
||||
use std::io::{self, Read, Seek};
|
||||
use std::fs::File;
|
||||
use std::mem;
|
||||
use std::str;
|
||||
use std::cmp;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use bzip2::read::{BzDecoder};
|
||||
use image::{ImageBuffer, Rgba};
|
||||
|
||||
const MSG_LENGTH: usize = 2432;
|
||||
const MOMENT_LENGTH: usize = 2400;
|
||||
|
||||
#[repr(u8)]
|
||||
enum RadialStatus {
|
||||
StartElevationScan,
|
||||
InterRadial,
|
||||
EndElevationScan,
|
||||
StartVolumeScan,
|
||||
EndVolumeScan
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
enum DataBlockType {
|
||||
Volume,
|
||||
Elevation,
|
||||
Radial,
|
||||
Reflectivity,
|
||||
Velocity,
|
||||
SpectrumWidth,
|
||||
Zdr, // Differential reflectivity
|
||||
Phi, // Differential phase
|
||||
Rho, // Correlation coefficient
|
||||
}
|
||||
|
||||
// --------
|
||||
// ARCHIVE II STRUCTS
|
||||
// These are implemented based on what's written in the ICD from NOAA:
|
||||
// https://www.roc.noaa.gov/public-documents/icds/2620002Y.pdf
|
||||
// --------
|
||||
|
||||
// At the start of every Archive II file is a 24-byte record
|
||||
// describing certain attributes of the volume scan.
|
||||
#[repr(C, packed)]
|
||||
struct VolumeHeader {
|
||||
ver: [u8; 2],
|
||||
period: [u8; 1],
|
||||
ext: [u8; 3],
|
||||
modified_julian_date: i32,
|
||||
modified_time_millis: i32,
|
||||
icao_id: [u8; 4]
|
||||
}
|
||||
|
||||
// Level II data packet -- used for all message types
|
||||
#[repr(C, packed)]
|
||||
struct Packet {
|
||||
ctm: [i16; 6],
|
||||
size: u16,
|
||||
id: u8,
|
||||
message_type: u8,
|
||||
seq: u16,
|
||||
gen_julian_date: u16,
|
||||
gen_time_millis: u32,
|
||||
num_message_segments: u16,
|
||||
message_segment: u16,
|
||||
data: [u8; 2404]
|
||||
}
|
||||
|
||||
// Header data for message 31 (digital radar data)
|
||||
#[repr(C, packed)]
|
||||
#[derive(Copy, Clone)]
|
||||
struct Message31Header {
|
||||
site_id: [u8; 4],
|
||||
collection_time: u32,
|
||||
modified_julian_date: u16,
|
||||
radial_idx: u16,
|
||||
radial_azimuth: f32,
|
||||
compression_indicator: u8,
|
||||
spare: u8,
|
||||
radial_length: u16,
|
||||
azimuth_res_spacing_code: u8,
|
||||
radial_status: u8,
|
||||
elevation_number: u8,
|
||||
cut_sector_number: u8,
|
||||
elevation_angle: f32,
|
||||
radial_spot_blanking_status: u8,
|
||||
azimuth_indexing_mode: u8,
|
||||
data_block_count: u16,
|
||||
data_block_pointers: [u32; 9]
|
||||
}
|
||||
|
||||
// Data block (descriptor of generic data moment type)
|
||||
#[repr(C, packed)]
|
||||
#[derive(Copy, Clone)]
|
||||
struct DataBlock {
|
||||
block_type: u8,
|
||||
moment_name: [u8; 3]
|
||||
}
|
||||
|
||||
impl Default for DataBlock {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block_type: 0,
|
||||
moment_name: [0; 3]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct VolumeData {
|
||||
data_block: DataBlock,
|
||||
size: u16,
|
||||
ver_major: u8,
|
||||
ver_minor: u8,
|
||||
lat: f32,
|
||||
lon: f32,
|
||||
site_height: i16,
|
||||
feedhorn_height: u16,
|
||||
calibration_constant: f32,
|
||||
hor_tx_power: f32,
|
||||
ver_tx_power: f32,
|
||||
system_diff_refl: f32,
|
||||
initial_system_diff_refl: f32,
|
||||
vcp_num: u16,
|
||||
processing_status: u16
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct ElevationData {
|
||||
data_block: DataBlock,
|
||||
size: u16,
|
||||
atmos: [u8; 2],
|
||||
calibration_constant: f32
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct RadialData {
|
||||
data_block: DataBlock,
|
||||
size: u16,
|
||||
unambiguous_range: u16,
|
||||
noise_level_hor: f32,
|
||||
noise_level_ver: f32,
|
||||
nyquist_velocity: u16,
|
||||
spare: [u8; 2],
|
||||
calibration_constant_channel_hor: f32,
|
||||
calibration_constant_channel_ver: f32
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Default, Copy, Clone)]
|
||||
struct GenericDataMoment {
|
||||
data_block: DataBlock,
|
||||
reserved: u32,
|
||||
num_gates: u16,
|
||||
range: u16,
|
||||
range_sample_interval: u16,
|
||||
threshold: u16,
|
||||
snr_threshold: u16,
|
||||
control_flags: i8,
|
||||
word_size: u8,
|
||||
scale: f32,
|
||||
offset: f32
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Copy, Clone)]
|
||||
struct DataMoment {
|
||||
generic_data_moment: GenericDataMoment,
|
||||
data: [u16; MOMENT_LENGTH]
|
||||
}
|
||||
|
||||
impl Default for DataMoment {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
generic_data_moment: GenericDataMoment::default(),
|
||||
data: [69; MOMENT_LENGTH]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------
|
||||
// PROCESSED STRUCTS
|
||||
// These aren't set to 1-byte alignment as these are *separate* from
|
||||
// structs meant to be written to from raw data
|
||||
// --------
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct Message31 {
|
||||
header: Message31Header,
|
||||
volume_data: VolumeData,
|
||||
elevation_data: ElevationData,
|
||||
radial_data: RadialData,
|
||||
reflectivity_data: DataMoment,
|
||||
velocity_data: DataMoment,
|
||||
spectrum_width_data: DataMoment,
|
||||
zdr_data: DataMoment,
|
||||
phi_data: DataMoment,
|
||||
rho_data: DataMoment,
|
||||
cfp_data: DataMoment
|
||||
}
|
||||
|
||||
impl Message31 {
|
||||
fn new(header: Message31Header) -> Self {
|
||||
Self {
|
||||
header,
|
||||
volume_data: VolumeData::default(),
|
||||
elevation_data: ElevationData::default(),
|
||||
radial_data: RadialData::default(),
|
||||
reflectivity_data: DataMoment::default(),
|
||||
velocity_data: DataMoment::default(),
|
||||
spectrum_width_data: DataMoment::default(),
|
||||
zdr_data: DataMoment::default(),
|
||||
phi_data: DataMoment::default(),
|
||||
rho_data: DataMoment::default(),
|
||||
cfp_data: DataMoment::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Structure to hold radial data (radials), which contains momentary data (bins)
|
||||
struct ElevationScan {
|
||||
radials: Vec<Message31>,
|
||||
azimuth: [f32; 800], // Expecting 720 radials, but
|
||||
// overscan/underscan can occur...
|
||||
first_radial_idx: u16,
|
||||
first_radial_az: f32,
|
||||
num_radials: i32,
|
||||
num_gates: i32,
|
||||
mean_elev_angle: f32,
|
||||
accum_elev_angle: f32,
|
||||
gate_size: f32,
|
||||
first_gate: f32
|
||||
}
|
||||
|
||||
impl ElevationScan {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
radials: Vec::new(),
|
||||
azimuth: [0.0; 800],
|
||||
first_radial_idx: 0,
|
||||
first_radial_az: 0.0,
|
||||
num_radials: 0,
|
||||
num_gates: 0,
|
||||
mean_elev_angle: 0.0,
|
||||
accum_elev_angle: 0.0,
|
||||
gate_size: 0.0,
|
||||
first_gate: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate structure for any and all data coming from message packets
|
||||
struct VolumeScan {
|
||||
volume_header: VolumeHeader,
|
||||
elevation_scans: Vec<ElevationScan>
|
||||
}
|
||||
|
||||
impl VolumeScan {
|
||||
fn new(volume_header: VolumeHeader) -> Self {
|
||||
Self { volume_header, elevation_scans: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_volume_scan<R: Read + Seek>(mut stream: R) -> Option<VolumeScan> {
|
||||
let magic_str = b"AR2V00";
|
||||
let mut magic_pos = 0;
|
||||
let mut buf = [0u8; 1];
|
||||
|
||||
while magic_pos < magic_str.len() {
|
||||
if stream.read_exact(&mut buf).is_err() {
|
||||
println!("Stream error; EOF?");
|
||||
return None;
|
||||
}
|
||||
if buf[0] == magic_str[magic_pos] {
|
||||
magic_pos += 1;
|
||||
} else {
|
||||
magic_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Found AR2V00xx identifier");
|
||||
|
||||
Some(process_volume_scan(stream))
|
||||
}
|
||||
|
||||
fn process_volume_scan<R: Read>(mut stream: R) -> VolumeScan {
|
||||
// Read volume header
|
||||
// ------------------
|
||||
let mut header_buffer = [0u8; mem::size_of::<VolumeHeader>()];
|
||||
stream.read_exact(&mut header_buffer);
|
||||
let volume_header: VolumeHeader = unsafe {
|
||||
std::ptr::read(header_buffer.as_ptr() as *const _)
|
||||
};
|
||||
// Test the ICAO ID from the volume header real quick...
|
||||
// TODO: We should include some kind of check against an expected
|
||||
// ICAO ID via command line arguments
|
||||
let icao = str::from_utf8(&volume_header.icao_id).unwrap();
|
||||
println!("Detected ICAO ID: {}", icao);
|
||||
|
||||
// Read rest of the data
|
||||
// ---------------------
|
||||
let mut volume_scan = VolumeScan::new(volume_header);
|
||||
let mut control_word: i32 = 0;
|
||||
let mut verify_control_word: i32 = 0;
|
||||
|
||||
let mut bail = false;
|
||||
while !bail {
|
||||
let mut raw_buffer = Vec::new();
|
||||
let mut raw_buffer_size = 0;
|
||||
let mut decompressed_data = Vec::new();
|
||||
// Read in the control word (size) of the LDM record. There's
|
||||
// also a chance this is a new volume scan, so we need to
|
||||
// check for the "magic string" we encountered earlier.
|
||||
let mut control_word: i32 = 0;
|
||||
match stream.read_i32::<BigEndian>() {
|
||||
Ok(res) => {
|
||||
control_word = res;
|
||||
},
|
||||
Err(e) => {
|
||||
bail = true;
|
||||
}
|
||||
}
|
||||
if control_word == 0x56325241 /* "AR2V" but big endian */ {
|
||||
// We've got another volume scan. We're bailing here, but
|
||||
// really we ought to just start the decoding process over
|
||||
// again from this byte.
|
||||
println!("Encountered magic string twice!");
|
||||
bail = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decompress the data
|
||||
// -------------------
|
||||
// TODO: check if it's actually bzip2-compressed first...?
|
||||
if control_word >= raw_buffer_size {
|
||||
raw_buffer.resize(control_word as usize, 0);
|
||||
raw_buffer_size = control_word;
|
||||
}
|
||||
stream.read_exact(&mut raw_buffer);
|
||||
let mut decoder = BzDecoder::new(&raw_buffer[..]);
|
||||
let mut decoding = true;
|
||||
let mut buffer = [0u8; 8192];
|
||||
while decoding {
|
||||
match decoder.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
if bytes_read == 0 {
|
||||
decoding = false;
|
||||
break;
|
||||
}
|
||||
decompressed_data.extend_from_slice(&buffer[..bytes_read]);
|
||||
},
|
||||
Err(error) => match error.kind() {
|
||||
DataMagic => {
|
||||
// If we've made it here then the bzip2 magic
|
||||
// string is missing. In this case, we'll
|
||||
// assume it's uncompressed data, although I
|
||||
// feel like it'd probably be better to check
|
||||
// for the string ahead of time prior to
|
||||
// decompressing it. Doing it this way allows
|
||||
// us to deal with Archive II files with data
|
||||
// blocks that may or may not be
|
||||
// bzip2-compressed though...
|
||||
decompressed_data.extend_from_slice(&raw_buffer[..raw_buffer_size as usize]);
|
||||
},
|
||||
_ => panic!("Problem decompressing bzip2 data: {error:?}")
|
||||
}
|
||||
}
|
||||
// If we have no data left, we can assume we're done.
|
||||
if decompressed_data.len() == 0 {
|
||||
decoding = false;
|
||||
bail = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process decompressed data
|
||||
// -------------------------
|
||||
let mut msg_length: usize = MSG_LENGTH;
|
||||
let mut i: usize = 0;
|
||||
while i < decompressed_data.len() {
|
||||
let slice = &decompressed_data[i..i+mem::size_of::<Packet>()];
|
||||
let packet: Packet = unsafe {
|
||||
std::ptr::read(slice.as_ptr() as *const _)
|
||||
};
|
||||
|
||||
// Message 31 packets are variable length; we must
|
||||
// readjust. If this isn't a message 31 packet though,
|
||||
// then return to standard length.
|
||||
if packet.message_type == 31 {
|
||||
msg_length = u16::from_be(packet.size) as usize * 2 + 12;
|
||||
} else {
|
||||
msg_length = MSG_LENGTH;
|
||||
}
|
||||
|
||||
if packet.message_type != 0 {
|
||||
// Handle messages accordingly. We'll break out
|
||||
// message processing into separate routines since
|
||||
// some of them can be pretty involved (e.g. 1, 31)
|
||||
match packet.message_type {
|
||||
31 => {
|
||||
// println!("Processing message 31... length: {}", msg_length);
|
||||
let raw_data = &decompressed_data[i + mem::size_of::<Packet>() + mem::size_of::<Message31Header>() - packet.data.len()..];
|
||||
bail |= process_message_31(&mut volume_scan, packet, raw_data);
|
||||
if bail {
|
||||
// Bail as soon as the elevation (not
|
||||
// volume!) scan is done.
|
||||
// This works for my purposes, but isn't
|
||||
// isn't ideal for loading a whole archive
|
||||
// at once.
|
||||
render_image(&mut volume_scan);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
println!("Message {} functionality unimplemented...", packet.message_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i += msg_length;
|
||||
}
|
||||
}
|
||||
|
||||
return volume_scan;
|
||||
}
|
||||
|
||||
fn convert_float(float: f32) -> f32 {
|
||||
return f32::from_bits(float.to_bits().swap_bytes());
|
||||
}
|
||||
|
||||
fn process_message_31(volume_scan: &mut VolumeScan, packet: Packet, raw_data: &[u8]) -> bool {
|
||||
let slice = &packet.data[..mem::size_of::<Message31Header>()];
|
||||
let header: Message31Header = unsafe {
|
||||
std::ptr::read(slice.as_ptr() as *const _)
|
||||
};
|
||||
|
||||
// Set up our new elevation scan (really not doing this the best
|
||||
// way...)
|
||||
let elevation_number: usize = header.elevation_number as usize;
|
||||
if volume_scan.elevation_scans.len() < elevation_number {
|
||||
volume_scan.elevation_scans.push(ElevationScan::new());
|
||||
}
|
||||
let elevation_scan: &mut ElevationScan = &mut volume_scan.elevation_scans[elevation_number - 1];
|
||||
|
||||
// Bump up the radial counter. This should contain the # of
|
||||
// radials when we're done.
|
||||
elevation_scan.num_radials += 1;
|
||||
|
||||
// Add the elevation angle of this radial (it varies) to the
|
||||
// elevation scan structure
|
||||
elevation_scan.accum_elev_angle += convert_float(header.elevation_angle);
|
||||
elevation_scan.mean_elev_angle = elevation_scan.accum_elev_angle / elevation_scan.num_radials as f32;
|
||||
|
||||
// Note that NEXRAD radials are numbered starting at 1, but for
|
||||
// our purposes we will number them starting at zero.
|
||||
let radial: usize = u16::from_be(header.radial_idx) as usize - 1;
|
||||
|
||||
// Record the azimuth of the start of this radial. 'angle' is
|
||||
// actually the center of the radial so to get the start we will
|
||||
// subtract off 1/2 of the radial spacing (which will currently
|
||||
// either be 1 or 0.5 degrees).
|
||||
elevation_scan.azimuth[radial] = match header.azimuth_res_spacing_code {
|
||||
1 => convert_float(header.radial_azimuth) - 0.25,
|
||||
2 => convert_float(header.radial_azimuth) - 0.5,
|
||||
_ => convert_float(header.radial_azimuth)
|
||||
};
|
||||
|
||||
// If we go negative from the above operation, then correct
|
||||
if elevation_scan.azimuth[radial] < 0.0 {
|
||||
elevation_scan.azimuth[radial] = 360.0 + elevation_scan.azimuth[radial];
|
||||
}
|
||||
|
||||
// Set our azimuth and index of the northernmost radial here.
|
||||
// We'll also check if the azimuth of this radial is further
|
||||
// clockwise than the ones we've already recorded. It's nice to
|
||||
// know where the first radial is.
|
||||
let wrap_around = elevation_scan.azimuth[radial] < elevation_scan.first_radial_az;
|
||||
if radial == 0 || wrap_around {
|
||||
elevation_scan.first_radial_az = elevation_scan.azimuth[radial];
|
||||
elevation_scan.first_radial_idx = radial as u16;
|
||||
}
|
||||
|
||||
// If we've wrapped around (i.e. crossed 0deg north) and the
|
||||
// azimuth of this radial is further clockwise than the first one
|
||||
// we've processed, then this indicates an overscan condition. In
|
||||
// other words, we have scanned past 360 degrees. In this case,
|
||||
// we'll effectively nullify this and all subsequent radials by
|
||||
// decrementing the number of radials, making these not count.
|
||||
if wrap_around && elevation_scan.azimuth[radial] > elevation_scan.azimuth[0] {
|
||||
elevation_scan.num_radials -= 1;
|
||||
}
|
||||
|
||||
// Build 19 added CFP (clutter filter power removed) data, an
|
||||
// ABI-breaking change. Since our Message31Header structure is
|
||||
// still set up to have only 9 data blocks, we'll need to check
|
||||
// the RDA software version and bump up our byte index should this
|
||||
// data be present. For now though, we'll hardcode it (cool)
|
||||
let cfp_present_hack: usize = 4;
|
||||
|
||||
// Build 20 introduced another ABI-breaking change with the "RPG
|
||||
// weighted mean ZDR bias estimate" field. It's 2 bytes w/ 6
|
||||
// leftover as spare memory. We'll address it here too.
|
||||
let rpg_weighted_mean_zdr_bias_present_hack = 0;
|
||||
|
||||
// Copy over the data to our block structure
|
||||
let mut byte_idx = cfp_present_hack;
|
||||
for i in 0..u16::from_be(header.data_block_count) {
|
||||
let slice = &raw_data[byte_idx..byte_idx + mem::size_of::<DataBlock>()];
|
||||
let data_block: DataBlock = unsafe { std::ptr::read(slice.as_ptr() as *const _) };
|
||||
let name = str::from_utf8(&data_block.moment_name).unwrap();
|
||||
if radial >= elevation_scan.radials.len() {
|
||||
// This will be where the read data goes. We'll insert this into
|
||||
// the elevation scan's radial data vector once we're done.
|
||||
elevation_scan.radials.push(Message31::new(header));
|
||||
}
|
||||
match name {
|
||||
"VOL" => {
|
||||
byte_idx += mem::size_of::<VolumeData>();
|
||||
},
|
||||
"ELV" => {
|
||||
byte_idx += mem::size_of::<ElevationData>();
|
||||
},
|
||||
"RAD" => {
|
||||
byte_idx += mem::size_of::<RadialData>();
|
||||
},
|
||||
"REF"|"VEL"|"SW "|"ZDR"|"PHI"|"RHO"|"CFP" => {
|
||||
let slice = &raw_data[byte_idx..byte_idx + mem::size_of::<GenericDataMoment>()];
|
||||
let generic_moment: GenericDataMoment = unsafe {
|
||||
std::ptr::read(slice.as_ptr() as *const _)
|
||||
};
|
||||
byte_idx += mem::size_of::<GenericDataMoment>();
|
||||
|
||||
// Per the ICD:
|
||||
//
|
||||
// LDM is the amount of space in bytes required for a
|
||||
// data moment array and equals ((NG * DWS) / 8) where
|
||||
// NG is the number of gates at the gate spacing
|
||||
// resolution specified and DWS is the number of bits
|
||||
// stored for each gate (DWS is always a multiple of
|
||||
// 8).
|
||||
//
|
||||
// Means "Length of Data Moment"; no relation to
|
||||
// UCAR's related LDM software :P
|
||||
let num_gates = u16::from_be(generic_moment.num_gates);
|
||||
let ldm = {num_gates * generic_moment.word_size as u16 / 8} as usize;
|
||||
|
||||
// Record adjusted gate information
|
||||
elevation_scan.num_gates = cmp::max(elevation_scan.num_gates, num_gates as i32);
|
||||
elevation_scan.gate_size = u16::from_be(generic_moment.range_sample_interval) as f32;
|
||||
elevation_scan.first_gate = u16::from_be(generic_moment.range) as f32;
|
||||
|
||||
// Record radial data (gross nested match here but whatever)
|
||||
let mut radial_block: &mut Message31 = &mut elevation_scan.radials[radial];
|
||||
let mut moment: DataMoment = DataMoment::default();
|
||||
let radar = &raw_data[byte_idx..];
|
||||
for (i, chunk) in radar.chunks_exact(1).enumerate().take(MOMENT_LENGTH) {
|
||||
moment.data[i] = chunk[0] as u16;
|
||||
// For 16-bit data types (e.g. ZDR) we'd probably want this instead:
|
||||
// moment.data[i] = u16::from_le_bytes([chunk[0], chunk[1]]);
|
||||
}
|
||||
match name {
|
||||
"REF" => { radial_block.reflectivity_data = moment; },
|
||||
"VEL" => { radial_block.velocity_data = moment; },
|
||||
"SW " => { radial_block.spectrum_width_data = moment; },
|
||||
"ZDR" => { radial_block.zdr_data = moment; },
|
||||
"PHI" => { radial_block.phi_data = moment; },
|
||||
"RHO" => { radial_block.rho_data = moment; },
|
||||
"CFP" => { radial_block.cfp_data = moment; }
|
||||
_ => {}
|
||||
}
|
||||
byte_idx += ldm;
|
||||
|
||||
}
|
||||
_ => {
|
||||
// panic!("Unexpected block type!");
|
||||
}
|
||||
}
|
||||
|
||||
// Debug:
|
||||
// println!("Block name: {}", name);
|
||||
}
|
||||
|
||||
// For our purposes we only want to process one elevation scan for
|
||||
// now... if we're at the end of the elevation (not volume) scan,
|
||||
// we're done.
|
||||
if header.radial_status == RadialStatus::EndElevationScan as u8 {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn get_dbz(r: f32, theta: f32, elevation_scan: &ElevationScan, smooth: bool) -> f32 {
|
||||
let num_radials: i32 = elevation_scan.num_radials as i32;
|
||||
let last_radial: i32 = num_radials - 1;
|
||||
|
||||
let native_scale_r = elevation_scan.num_gates as f32 / 458.0; // Gates per km
|
||||
let native_scale_theta = elevation_scan.num_radials as f32 / 360.0; // Radials per deg
|
||||
|
||||
// Determine the range to the first gate that contains data (this
|
||||
// is in the metadata from the radar). There is no data for the
|
||||
// first km or two close to the radar site. It's the so-called
|
||||
// "cone of silence". If we're asked for data in this region we'll
|
||||
// return 0 dBZ.
|
||||
let range_to_first_gate_km = elevation_scan.first_gate / 1000.0 - elevation_scan.gate_size / 1000.0 / 2.0;
|
||||
if r <= range_to_first_gate_km {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Translate r (which is in km) to a gate number by subtracting
|
||||
// off the range to the first gate data (which is also in km),
|
||||
// then multiplying by the native_scale_r value (which is in
|
||||
// gates/km). We need two gate numbers, one 'below' the actual r
|
||||
// (i.e. closer to the radar site) and one 'above' the actual r
|
||||
// (i.e. further from the radar site). These top and bottom gates
|
||||
// will be used for interpolation.
|
||||
let bottom = ((r - range_to_first_gate_km) * native_scale_r) as usize;
|
||||
let top = (bottom + 1) as usize;
|
||||
|
||||
// Not only do we need the gate numbers for the top and bottom,
|
||||
// but we also need the relative distance of r from the top and
|
||||
// bottom gates. This distance is used as an interpolation weight,
|
||||
// or to determine the nearest neighbor. For now we're only doing
|
||||
// nearest neighbor interpolation.
|
||||
let dist_bottom: f32 = (r - range_to_first_gate_km) * native_scale_r - bottom as f32;
|
||||
let dist_top: f32 = 1.0 - dist_bottom;
|
||||
|
||||
// Identify the radial immediately preceeding the northernmost radial. It crosses the 0 degree threshold
|
||||
let mut trans_radial = elevation_scan.first_radial_idx as i32;
|
||||
if trans_radial < 0 {
|
||||
trans_radial = last_radial;
|
||||
}
|
||||
|
||||
// Here's a cool trick. Estimate the proper radial num (0 -
|
||||
// lastradial) by subtracting the azimuth of the northernmost
|
||||
// radial then taking the integer portion of the remainder. This
|
||||
// will provide an index, relative to the northernmost radial, of
|
||||
// the radial that we need. Once we have an index, we have to
|
||||
// resolve it to an actual radial number by adding it to the
|
||||
// radial number of the northernmost radial. If we exceed
|
||||
// lastradial, then we have simply wrapped around.
|
||||
let mut radial_idx: i32 = ((theta - elevation_scan.first_radial_az) * native_scale_theta + elevation_scan.first_radial_idx as f32) as i32;
|
||||
if radial_idx > last_radial {
|
||||
radial_idx -= num_radials;
|
||||
}
|
||||
|
||||
// Check to see if our estimated radial index was correct. Usually
|
||||
// it will be, however in some cases it might not. We check for
|
||||
// overestimates and underestimates and starting at the estimate
|
||||
// we work back and forth trying to find the true value. This was
|
||||
// done to improve interpolation accuracy for some sites that have
|
||||
// very uneven spacing between radial azimuths. A safety check is
|
||||
// provided to prevent hangs. Also, the transition radial (the one
|
||||
// right before 0 degrees) is exempted from this check for obvious
|
||||
// reasons. This could be made better, but meh.
|
||||
let mut safety = 10;
|
||||
while theta < elevation_scan.azimuth[radial_idx as usize] && radial_idx != trans_radial && safety != 0 {
|
||||
safety -= 1;
|
||||
if radial_idx > 0 {
|
||||
radial_idx -= 1;
|
||||
}
|
||||
if radial_idx < 0 {
|
||||
radial_idx = last_radial;
|
||||
}
|
||||
}
|
||||
safety = 10;
|
||||
while theta > elevation_scan.azimuth[radial_idx as usize + 1] && radial_idx != trans_radial && safety != 0 {
|
||||
safety -= 1;
|
||||
radial_idx += 1;
|
||||
if radial_idx > last_radial {
|
||||
radial_idx -= num_radials;
|
||||
}
|
||||
}
|
||||
if safety == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Determine the left and right radial numbers to use for interpolation.
|
||||
let mut left: usize = 0;
|
||||
let mut right: usize = 0;
|
||||
let mut dist_left: f32 = 0.0;
|
||||
let mut dist_right: f32 = 0.0;
|
||||
if radial_idx == trans_radial {
|
||||
// We're processing the radial immediately before the northernmost one
|
||||
left = trans_radial as usize;
|
||||
right = elevation_scan.first_radial_idx as usize;
|
||||
if theta < 180.0 {
|
||||
dist_left = theta + 360.0 - elevation_scan.azimuth[left];
|
||||
dist_right = elevation_scan.azimuth[right] - theta;
|
||||
} else {
|
||||
dist_left = theta - elevation_scan.azimuth[left];
|
||||
dist_right = elevation_scan.azimuth[right] + 360.0 - theta;
|
||||
}
|
||||
} else {
|
||||
left = radial_idx as usize;
|
||||
right = left + 1;
|
||||
if right > last_radial as usize {
|
||||
right = 0;
|
||||
}
|
||||
dist_left = theta - elevation_scan.azimuth[left];
|
||||
dist_right = elevation_scan.azimuth[right] - theta;
|
||||
}
|
||||
|
||||
// Grab the dBZ values for all 4 neighboring points
|
||||
let mut dbz_ll = elevation_scan.radials[left].reflectivity_data.data[bottom] as f32;
|
||||
let mut dbz_ul = elevation_scan.radials[left].reflectivity_data.data[top] as f32;
|
||||
let mut dbz_lr = elevation_scan.radials[right].reflectivity_data.data[bottom] as f32;
|
||||
let mut dbz_ur = elevation_scan.radials[right].reflectivity_data.data[top] as f32;
|
||||
|
||||
// Apply a dBZ threshold (don't want anything below 0 for reasons)
|
||||
dbz_ll = f32::max(dbz_ll, 66.0);
|
||||
dbz_ul = f32::max(dbz_ul, 66.0);
|
||||
dbz_lr = f32::max(dbz_lr, 66.0);
|
||||
dbz_ur = f32::max(dbz_ur, 66.0);
|
||||
|
||||
// Perform interpolation
|
||||
let mut dbz_top: f32 = 0.0;
|
||||
let mut dbz_bottom: f32 = 0.0;
|
||||
let mut dbz: f32 = 0.0;
|
||||
if smooth {
|
||||
dbz_top = ((dbz_ul * dist_right) + (dbz_ur * dist_left)) / (dist_left + dist_right);
|
||||
dbz_bottom = ((dbz_ll * dist_right) + (dbz_lr * dist_left)) / (dist_left + dist_right);
|
||||
dbz = ((dbz_top * dist_bottom) + (dbz_bottom * dist_top)) / (dist_top + dist_bottom);
|
||||
} else {
|
||||
if dist_left < dist_right {
|
||||
dbz_top = dbz_ul;
|
||||
dbz_bottom = dbz_ll;
|
||||
} else {
|
||||
dbz_top = dbz_ur;
|
||||
dbz_bottom = dbz_lr;
|
||||
}
|
||||
if dist_top < dist_bottom {
|
||||
dbz = dbz_top;
|
||||
} else {
|
||||
dbz = dbz_bottom;
|
||||
}
|
||||
}
|
||||
|
||||
return (dbz - 2.0) / 2.0 - 32.0;
|
||||
}
|
||||
|
||||
const NUMINTERVALS: usize = 8;
|
||||
|
||||
fn render_image(volume_scan: &mut VolumeScan) {
|
||||
let elevation_scan: &ElevationScan = &volume_scan.elevation_scans[0];
|
||||
|
||||
// We *could* use a 16-color scale, but this 128-color interpolated one is cooler
|
||||
let mut color_scale: [[u8; 3]; 16 * NUMINTERVALS] = [[0; 3]; 16 * NUMINTERVALS];
|
||||
build_smooth_color_scale(&mut color_scale);
|
||||
|
||||
// Render :D
|
||||
let width = 1920;
|
||||
let height = 1920;
|
||||
let mut img: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height);
|
||||
let mut x1: f32 = 0.0;
|
||||
let mut y1: f32 = 0.0;
|
||||
for (x, y, pixel) in img.enumerate_pixels_mut() {
|
||||
x1 = x as f32 - width as f32 / 2.0;
|
||||
y1 = height as f32 / 2.0 - y as f32;
|
||||
let r = f32::sqrt(x1 * x1 + y1 * y1) * 0.25;
|
||||
let mut theta = (1.570795 - f32::atan2(y1, x1)) * 57.29583; // (90deg-atan2(y1,x1))*180/pi)
|
||||
if theta < 0.0 {
|
||||
theta = 360.0 + theta;
|
||||
}
|
||||
let mut dbz = get_dbz(r, theta, elevation_scan, false);
|
||||
let color_idx: usize = (dbz * NUMINTERVALS as f32 / 5.0 + 0.5) as usize;
|
||||
if color_idx >= 16 * NUMINTERVALS || dbz <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let out_r = color_scale[color_idx][0];
|
||||
let out_g = color_scale[color_idx][1];
|
||||
let out_b = color_scale[color_idx][2];
|
||||
*pixel = Rgba([out_r, out_g, out_b, 0xFF]);
|
||||
}
|
||||
img.save("output.png").expect("Failed to save image");
|
||||
}
|
||||
|
||||
fn build_smooth_color_scale(scolorscale: &mut [[u8; 3]; 16 * NUMINTERVALS]) {
|
||||
let colorscale: [[u8; 3]; 16] = [
|
||||
[0, 0, 0],
|
||||
[0, 72, 144],
|
||||
[0, 96, 240],
|
||||
[128, 224, 80],
|
||||
[100, 184, 64],
|
||||
[72, 144, 48],
|
||||
[44, 104, 32],
|
||||
[16, 64, 16],
|
||||
[240, 160, 16],
|
||||
[240, 118, 32],
|
||||
[240, 16, 32],
|
||||
[144, 0, 0],
|
||||
[176, 32, 128],
|
||||
[202, 64, 160],
|
||||
[229, 96, 192],
|
||||
[225, 128, 224],
|
||||
];
|
||||
let numintervals = NUMINTERVALS as f32;
|
||||
let mut step = 0;
|
||||
while step < 15 {
|
||||
let (curr, curg, curb) = (colorscale[step][0], colorscale[step][1], colorscale[step][2]);
|
||||
let (nextr, nextg, nextb) = (colorscale[step + 1][0], colorscale[step + 1][1], colorscale[step + 1][2]);
|
||||
|
||||
// Compute the deltas between the colors
|
||||
let deltar = (nextr as f32 - curr as f32) / numintervals;
|
||||
let deltag = (nextg as f32 - curg as f32) / numintervals;
|
||||
let deltab = (nextb as f32 - curb as f32) / numintervals;
|
||||
|
||||
for t in 0..numintervals as usize {
|
||||
scolorscale[step * numintervals as usize + t][0] = (curr as f32 + deltar * t as f32).round() as u8;
|
||||
scolorscale[step * numintervals as usize + t][1] = (curg as f32 + deltag * t as f32).round() as u8;
|
||||
scolorscale[step * numintervals as usize + t][2] = (curb as f32 + deltab * t as f32).round() as u8;
|
||||
}
|
||||
|
||||
step += 1;
|
||||
}
|
||||
scolorscale[step * numintervals as usize][0] = colorscale[step][0];
|
||||
scolorscale[step * numintervals as usize][1] = colorscale[step][1];
|
||||
scolorscale[step * numintervals as usize][2] = colorscale[step][2];
|
||||
|
||||
// Last bit of color scale is all white
|
||||
for t in 1..numintervals as usize {
|
||||
scolorscale[step * numintervals as usize + t][0] = (248 + t) as u8;
|
||||
scolorscale[step * numintervals as usize + t][1] = (248 + t) as u8;
|
||||
scolorscale[step * numintervals as usize + t][2] = (248 + t) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
println!("Processing file...");
|
||||
let file = File::open("test/KRAX.dat")?;
|
||||
if let Some(scan) = check_for_volume_scan(file) {
|
||||
println!("Successfully processed volume scan");
|
||||
} else {
|
||||
println!("Failed to find volume scan data");
|
||||
}
|
||||
Ok(())
|
||||
}
|
BIN
test/KRAX.dat
Normal file
BIN
test/KRAX.dat
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue