diff --git a/.idea/wxbox.iml b/.idea/wxbox.iml index cab36b6..66a32a2 100644 --- a/.idea/wxbox.iml +++ b/.idea/wxbox.iml @@ -17,6 +17,8 @@ <sourceFolder url="file://$MODULE_DIR$/crates/grib2/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/crates/pal/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/crates/tiler/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/crates/ar2/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/crates/nommer/src" isTestSource="false" /> <excludeFolder url="file://$MODULE_DIR$/.tmp" /> <excludeFolder url="file://$MODULE_DIR$/temp" /> <excludeFolder url="file://$MODULE_DIR$/tmp" /> diff --git a/Cargo.lock b/Cargo.lock index ad9d6d2..ca75648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -200,6 +206,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.97" @@ -719,6 +775,26 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cacache" version = "13.1.0" @@ -820,6 +896,60 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -851,6 +981,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -2067,6 +2203,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.61.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2287,6 +2447,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -2755,6 +2921,50 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nexrad-data" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2aef96f687e5774386f0dfe4e95bbf98b531559426e4b3bdddd27ca3d38488" +dependencies = [ + "bincode", + "bzip2", + "chrono", + "clap", + "log", + "nexrad-decode", + "nexrad-model", + "reqwest", + "serde", + "thiserror 1.0.69", + "tokio", + "xml", +] + +[[package]] +name = "nexrad-decode" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab458c09a15d9a133a7935a8024022db3cd3282549c2ed000f44c4ea392213a" +dependencies = [ + "bincode", + "chrono", + "log", + "nexrad-model", + "serde", + "thiserror 1.0.69", + "uom", +] + +[[package]] +name = "nexrad-model" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13fa673733e34220daf6f2ac75051d94d66acdd3fd2127f76593b6a36d1593c" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "nix" version = "0.29.0" @@ -4316,6 +4526,12 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.3" @@ -4886,6 +5102,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "uom" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd36e5350a65d112584053ee91843955826bf9e56ec0d1351214e01f6d7cd9c" +dependencies = [ + "num-traits", + "typenum", +] + [[package]] name = "url" version = "2.5.4" @@ -4954,6 +5180,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.16.0" @@ -5977,6 +6209,16 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wxbox-ar2" +version = "0.1.0" +dependencies = [ + "nexrad-data", + "nexrad-decode", + "serde", + "toml", +] + [[package]] name = "wxbox-client" version = "0.1.0" @@ -6018,8 +6260,13 @@ dependencies = [ "png", "thiserror 2.0.12", "tracing", + "wxbox-nommer", ] +[[package]] +name = "wxbox-nommer" +version = "0.1.0" + [[package]] name = "wxbox-pal" version = "0.1.0" @@ -6043,6 +6290,7 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", + "wxbox-ar2", "wxbox-grib2", "wxbox-pal", ] @@ -6114,6 +6362,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede1c99c55b4b3ad0349018ef0eccbe954ce9c342334410707ee87177fcf2ab4" +dependencies = [ + "xml-rs", +] + [[package]] name = "xml-rs" version = "0.8.25" diff --git a/crates/ar2/Cargo.toml b/crates/ar2/Cargo.toml new file mode 100644 index 0000000..051d649 --- /dev/null +++ b/crates/ar2/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wxbox-ar2" +version = "0.1.0" +edition = "2024" + +[dependencies] +nexrad-decode = "0.1.1" +nexrad-data = "0.2" +serde = { version = "1", features = ["derive"]} +toml = "0.8" \ No newline at end of file diff --git a/crates/ar2/KCRP20170825_235733_V06 b/crates/ar2/KCRP20170825_235733_V06 new file mode 100644 index 0000000..b2e0964 Binary files /dev/null and b/crates/ar2/KCRP20170825_235733_V06 differ diff --git a/crates/ar2/src/lib.rs b/crates/ar2/src/lib.rs new file mode 100644 index 0000000..4f9d8b5 --- /dev/null +++ b/crates/ar2/src/lib.rs @@ -0,0 +1,194 @@ +use std::fmt::Debug; +use nexrad_data::volume::File; +use nexrad_decode::messages::digital_radar_data::{GenericDataBlock, RadialStatus}; +use nexrad_decode::messages::MessageContents; +use nexrad_decode::result::Error; + +pub mod sites; + +pub struct Scan { + pub coverage_pattern_number: u16, + pub sweeps: Vec<Sweep>, +} + +pub struct Sweep { + pub elevation_number: u8, + pub radials: Vec<Radial>, +} + +pub struct Radial { + pub collection_timestamp: i64, + pub azimuth_number: u16, + pub azimuth_angle_degrees: f32, + pub azimuth_spacing_degrees: f32, + pub radial_status: RadialStatus, + pub elevation_number: u8, + pub elevation_number_degrees: f32, + pub reflectivity: Option<MomentData>, + pub velocity: Option<MomentData>, + pub spectrum_width: Option<MomentData>, + pub differential_reflectivity: Option<MomentData>, + pub differential_phase: Option<MomentData>, + pub correlation_coefficient: Option<MomentData>, + pub specific_differential_phase: Option<MomentData>, +} + +#[derive(Debug)] +pub struct MomentData { + pub scale: f32, + pub offset: f32, + pub values: Vec<u8>, + pub start_range: u16, + pub sample_interval: u16 +} + +impl MomentData { + /// Values from this data moment corresponding to gates in the radial. + + pub fn values(&self) -> Vec<MomentValue> { + let copied_values = self.values.iter().copied(); + + if self.scale == 0.0 { + return copied_values + .map(|raw_value| MomentValue::Value(raw_value as f32)) + .collect(); + } + + copied_values + .map(|raw_value| match raw_value { + 0 => MomentValue::BelowThreshold, + 1 => MomentValue::RangeFolded, + _ => MomentValue::Value((raw_value as f32 - self.offset) / self.scale), + }) + .collect() + } +} + +/// The data moment value for a product in a radial's gate. The value may be a floating-point number + +/// or a special case such as "below threshold" or "range folded". + +#[derive(Debug, Clone, Copy, PartialEq)] + +pub enum MomentValue { + /// The data moment value for a gate. + Value(f32), + + /// The value for this gate was below the signal threshold. + BelowThreshold, + + /// The value for this gate exceeded the maximum unambiguous range. + RangeFolded, +} + +pub fn parse(input: Vec<u8>) -> nexrad_data::result::Result<Scan> { + let file = File::new(input); + + let mut vcp = None; + let mut radials = vec![]; + + for mut record in file.records() { + if record.compressed() { + record = record.decompress()?; + } + let messages = record.messages()?; + for message in messages { + let contents = message.into_contents(); + if let MessageContents::DigitalRadarData(radar_data_message) = contents { + if vcp.is_none() { + if let Some(volume_block) = &radar_data_message.volume_data_block { + vcp = + Some(volume_block.volume_coverage_pattern_number); + } + } + radials.push(into_radial(*radar_data_message)?); + } + } + } + + Ok(Scan { + coverage_pattern_number: vcp.ok_or(Error::DecodingError("no vcp".to_string()))?, + sweeps: Sweep::from_radials(radials), + }) +} + +fn into_radial(message: nexrad_decode::messages::digital_radar_data::Message) -> nexrad_data::result::Result<Radial> { + Ok(Radial { + collection_timestamp: message.header.date_time().ok_or(Error::MessageMissingDateError)?.timestamp_millis(), + azimuth_number: message.header.azimuth_number, + azimuth_angle_degrees: message.header.azimuth_angle, + azimuth_spacing_degrees: message.header.azimuth_resolution_spacing as f32 * 0.5, + radial_status: message.header.radial_status(), + elevation_number: message.header.elevation_number, + elevation_number_degrees: message.header.elevation_angle, + reflectivity: message.reflectivity_data_block.map(|u| into_moment_data(u)), + velocity: message.velocity_data_block.map(|u| into_moment_data(u)), + spectrum_width: message.spectrum_width_data_block.map(|u| into_moment_data(u)), + differential_reflectivity: message.differential_reflectivity_data_block.map(|u| into_moment_data(u)), + differential_phase: message.differential_phase_data_block.map(|u| into_moment_data(u)), + correlation_coefficient: message.correlation_coefficient_data_block.map(|u| into_moment_data(u)), + specific_differential_phase: message.specific_diff_phase_data_block.map(|u| into_moment_data(u)), + }) +} + +fn into_moment_data(block: GenericDataBlock) -> MomentData { + MomentData { + scale: block.header.scale, + offset: block.header.offset, + values: block.encoded_data, + start_range: block.header.data_moment_range, + sample_interval: block.header.data_moment_range_sample_interval + } +} + +impl Sweep { + pub fn new(elevation_number: u8, radials: Vec<Radial>) -> Self { + + Self { + + elevation_number, + + radials, + + } + + } + pub fn from_radials(radials: Vec<Radial>) -> Vec<Self> { + + let mut sweeps = Vec::new(); + + + let mut sweep_elevation_number = None; + + let mut sweep_radials = Vec::new(); + + + for radial in radials { + + if let Some(elevation_number) = sweep_elevation_number { + + if elevation_number != radial.elevation_number { + + sweeps.push(Sweep::new(elevation_number, sweep_radials)); + + sweep_radials = Vec::new(); + + } + + } + + + sweep_elevation_number = Some(radial.elevation_number); + + sweep_radials.push(radial); + + } + + + sweeps + + } + +} + +pub const DATA_BYTES: &[u8] = include_bytes!("../KCRP20170825_235733_V06"); \ No newline at end of file diff --git a/crates/ar2/src/main.rs b/crates/ar2/src/main.rs new file mode 100644 index 0000000..cee21d2 --- /dev/null +++ b/crates/ar2/src/main.rs @@ -0,0 +1,8 @@ +use std::fs; +use wxbox_ar2::parse; + +fn main() { + let f = fs::read("KCRP20170825_235733_V06").unwrap(); + let f = parse(f).unwrap(); + println!("{:?}", f.sweeps.get(0).unwrap().radials.get(0).unwrap().reflectivity.as_ref().unwrap()); +} \ No newline at end of file diff --git a/crates/ar2/src/sites/mod.rs b/crates/ar2/src/sites/mod.rs new file mode 100644 index 0000000..91f9f0c --- /dev/null +++ b/crates/ar2/src/sites/mod.rs @@ -0,0 +1 @@ +pub mod wsr88d; diff --git a/crates/ar2/src/sites/wsr88d.rs b/crates/ar2/src/sites/wsr88d.rs new file mode 100644 index 0000000..efc373e --- /dev/null +++ b/crates/ar2/src/sites/wsr88d.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Deserializer}; +use std::collections::HashMap; +use std::sync::{LazyLock, OnceLock}; + +#[derive(Deserialize, Debug)] +pub struct Wsr88dSite { + pub id: String, + pub name: String, + pub agency: String, + pub equipment: String, + pub city: String, + pub state: String, + pub county: String, + #[serde(deserialize_with = "from_elevation_str")] + pub elevation: f64, + #[serde(deserialize_with = "from_deg_str")] + pub lat: f64, + #[serde(deserialize_with = "from_deg_str")] + pub long: f64, +} + +#[derive(Deserialize)] +pub struct Wsr88dSites { + pub sites: HashMap<String, Wsr88dSite>, +} + +fn from_elevation_str<'de, D>(deserializer: D) -> Result<f64, D::Error> +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + + let elevation = s.split(" ").nth(0).unwrap(); + Ok(elevation.parse().unwrap()) +} + +fn from_deg_str<'de, D>(deserializer: D) -> Result<f64, D::Error> +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + + let mut split = s.split(" "); + let mut hr = split.next().unwrap(); + + let positive = hr.starts_with('+'); + if positive { + hr = hr.strip_prefix('+').unwrap(); + } else { + hr = hr.strip_prefix('-').unwrap(); + } + + let hr: f64 = hr.parse().unwrap(); + + let min = split.next().unwrap(); + + let min: f64 = min.parse().unwrap(); + + let sec = split.next().unwrap(); + + let sec: f64 = sec.parse().unwrap(); + + let mut degrees = hr + min / 60.0 + sec / 3600.0; + if !positive { + degrees *= -1.0; + } + + Ok(degrees) +} + +pub static SITES: LazyLock<Wsr88dSites> = + LazyLock::new(|| toml::from_str(include_str!("wsr88d.toml")).unwrap()); + +#[cfg(test)] +mod tests { + use crate::sites::wsr88d::SITES; + + const A: f64 = 6_378.1370 * 1000.0; // m + const B: f64 = 6_356.7523 * 1000.0; // m + + #[test] + fn azimuth_configurations() { + let radar = SITES.sites.get("KCRP").unwrap(); + let radar_theta = radar.long; + let radar_phi = radar.lat; + let radar_r = ((A.powi(2) * radar_phi.cos()).powi(2) + (B.powi(2) * radar_phi.sin()).powi(2) + / (A * radar_phi.cos()).powi(2) + (B * radar_phi.sin()).powi(2)).sqrt(); + + let radar_x = radar_r * radar_theta.cos() * radar_phi.sin(); + let radar_y = radar_r * radar_theta.sin() * radar_phi.sin(); + let radar_z = radar_r * radar_theta.cos(); + + let measurement_theta = radar_theta + 0.0001; + let measurement_phi = radar_phi; + let measurement_r = radar_r; + + + let measurement_x = measurement_r * measurement_theta.cos() * measurement_phi.sin(); + let measurement_y = measurement_r * measurement_theta.sin() * measurement_phi.sin(); + let measurement_z = measurement_r * measurement_theta.cos(); + + let radar_local_x = measurement_x - radar_x; + let radar_local_y = measurement_y - radar_y; + let radar_local_z = measurement_z - radar_z; + + let radar_local_r = (radar_local_x.powi(2) + radar_local_y.powi(2) + radar_local_z.powi(2)).sqrt(); + let radar_local_theta = (radar_local_y / radar_local_x).atan(); + let radar_local_phi = (radar_local_z / radar_local_r).acos(); + + let azimuth = radar_local_theta; + let elevation = radar_local_phi; + let distance = radar_local_r; + } +} \ No newline at end of file diff --git a/crates/ar2/src/sites/wsr88d.toml b/crates/ar2/src/sites/wsr88d.toml new file mode 100644 index 0000000..9ec2c3c --- /dev/null +++ b/crates/ar2/src/sites/wsr88d.toml @@ -0,0 +1,161 @@ +[sites] +KABR = {id = "ABR", name = "ABERDEEN", agency = "NWS", equipment = "RDA", city = "ABERDEEN", state = "SD", county = "BROWN", elevation = "396.85 m (1299.21 ft)", lat = "+45 27 21", long = "-98 24 48"} +KENX = {id = "ENX", name = "ALBANY", agency = "NWS", equipment = "RDA", city = "EAST BERNE", state = "NY", county = "ALBANY", elevation = "565 m (1853.67 ft)", lat = "+42 35 11.6", long = "-74 03 50.7"} +KABX = {id = "ABX", name = "ALBUQUERQUE", agency = "NWS", equipment = "RDA", city = "ALBUQUERQUE", state = "NM", county = "BERNALILLO", elevation = "1789.18 m (5869.42 ft)", lat = "+35 08 59", long = "-106 49 26"} +KFDR = {id = "FDR", name = "ALTUS AFB", agency = "AFWA", equipment = "RDA", city = "FREDERICK", state = "OK", county = "TILLMAN", elevation = "386.18 m (1266.4 ft)", lat = "+34 21 43.9", long = "-98 58 36"} +KAMA = {id = "AMA", name = "AMARILLO", agency = "NWS", equipment = "RDA", city = "AMARILLO", state = "TX", county = "POTTER", elevation = "1104 m (3622.05 ft)", lat = "+35 14 00", long = "-101 42 33.4"} +PAHG = {id = "AHG", name = "KENAI FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "KENAI", state = "AK", county = "N/A", elevation = "73.76 m (239.5 ft)", lat = "+60 43 33.29", long = "-151 21 05.28"} +PGUA = {id = "UAM", name = "ANDERSEN AFB", agency = "AFWA", equipment = "RDA", city = "ANDERSEN AFB", state = "GU", county = "N/A", elevation = "83 m (272.31 ft)", lat = "+13 27 21", long = "+144 48 40"} +KFFC = {id = "FFC", name = "ATLANTA", agency = "NWS", equipment = "RDA", city = "PEACHTREE CITY", state = "GA", county = "FAYETTE", elevation = "261.52 m (856.3 ft)", lat = "+33 21 48.78", long = "-84 33 57.42"} +KEWX = {id = "EWX", name = "AUSTIN/SAN ANTONIO", agency = "NWS", equipment = "RDA", city = "NEW BRAUNFELS", state = "TX", county = "COMAL", elevation = "204 m (669.29 ft)", lat = "+29 42 14.6", long = "-98 01 43"} +KBBX = {id = "BBX", name = "BEALE AFB", agency = "AFWA", equipment = "RDA", city = "OROVILLE", state = "CA", county = "BUTTE", elevation = "52.73 m (170.6 ft)", lat = "+39 29 44.3", long = "-121 37 53.8"} +PABC = {id = "ABC", name = "BETHEL FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "BETHEL", state = "AK", county = "N/A", elevation = "49.07 m (160.76 ft)", lat = "+60 47 31", long = "-161 52 35"} +KBLX = {id = "BLX", name = "BILLINGS", agency = "NWS", equipment = "RDA", city = "BILLINGS", state = "MT", county = "YELLOWSTONE", elevation = "1109 m (3638.45 ft)", lat = "+45 51 13.6", long = "-108 36 24.5"} +KBGM = {id = "BGM", name = "BINGHAMTON", agency = "NWS", equipment = "RDA", city = "BINGHAMTON", state = "NY", county = "BROOME", elevation = "489.51 m (1604.33 ft)", lat = "+42 11 58.9", long = "-75 59 05"} +KBMX = {id = "BMX", name = "BIRMINGHAM", agency = "NWS", equipment = "RDA", city = "ALABASTER", state = "AL", county = "SHELBY", elevation = "196.6 m (643.04 ft)", lat = "+33 10 20.7", long = "-86 46 12.6"} +KBIS = {id = "BIS", name = "BISMARCK", agency = "NWS", equipment = "RDA", city = "BISMARCK", state = "ND", county = "BURLEIGH", elevation = "505.36 m (1656.82 ft)", lat = "+46 46 15", long = "-100 45 38"} +KCBX = {id = "CBX", name = "BOISE", agency = "NWS", equipment = "RDA", city = "BOISE", state = "ID", county = "ADA", elevation = "942 m (3090.55 ft)", lat = "+43 29 24.78", long = "-116 14 09.72"} +KBOX = {id = "BOX", name = "BOSTON", agency = "NWS", equipment = "RDA", city = "TAUNTON", state = "MA", county = "BRISTOL", elevation = "35.97 m (114.83 ft)", lat = "+41 57 20.8", long = "-71 08 12.7"} +KOKX = {id = "OKX", name = "BROOKHAVEN", agency = "NWS", equipment = "RDA", city = "UPTON", state = "NY", county = "SUFFOLK", elevation = "25.91 m (82.02 ft)", lat = "+40 51 55.9", long = "-72 51 50.1"} +KBRO = {id = "BRO", name = "BROWNSVILLE", agency = "NWS", equipment = "RDA", city = "BROWNSVILLE", state = "TX", county = "CAMERON", elevation = "7.01 m (22.97 ft)", lat = "+25 54 57.6", long = "-97 25 08.28"} +KBUF = {id = "BUF", name = "BUFFALO", agency = "NWS", equipment = "RDA", city = "BUFFALO", state = "NY", county = "ERIE", elevation = "211.23 m (692.26 ft)", lat = "+42 56 55.64", long = "-78 44 12.41"} +KCXX = {id = "CXX", name = "BURLINGTON", agency = "NWS", equipment = "RDA", city = "COLCHESTER", state = "VT", county = "CHITTENDEN", elevation = "96.62 m (314.96 ft)", lat = "+44 30 39.6", long = "-73 09 59.15"} +RKSG = {id = "KSGR4", name = "CAMP HUMPHREYS", agency = "AFWA", equipment = "RDA", city = "CAMP HUMPHREYS", state = "KO", county = "N/A", elevation = "439 m (1440.29 ft)", lat = "+37 12 27.25", long = "+127 17 08.02"} +KFDX = {id = "FDX", name = "CANNON AFB", agency = "AFWA", equipment = "RDA", city = "FIELD", state = "NM", county = "CURRY", elevation = "1417.32 m (4648.95 ft)", lat = "+34 38 03", long = "-103 37 08"} +KCBW = {id = "CBW", name = "CARIBOU", agency = "NWS", equipment = "RDA", city = "HOULTON", state = "ME", county = "AROOSTOOK", elevation = "227.38 m (744.75 ft)", lat = "+46 02 21.30", long = "-67 48 23.15"} +KICX = {id = "ICX", name = "CEDAR CITY (RDA 1)", agency = "NWS", equipment = "RDA", city = "CEDAR CITY", state = "UT", county = "IRON", elevation = "3244 m (10643.04 ft)", lat = "+37 35 27.78", long = "-112 51 43.86"} +KCLX = {id = "CLX", name = "CHARLESTON, SC", agency = "NWS", equipment = "RDA", city = "GRAYS", state = "SC", county = "BEAUFORT", elevation = "35 m (114.83 ft)", lat = "+32 39 19.9", long = "-81 02 31.9"} +KRLX = {id = "RLX", name = "CHARLESTON, WV", agency = "NWS", equipment = "RDA", city = "CHARLESTON", state = "WV", county = "KANAWHA", elevation = "335 m (1099.08 ft)", lat = "+38 18 40", long = "-81 43 22"} +KCYS = {id = "CYS", name = "CHEYENNE", agency = "NWS", equipment = "RDA", city = "CHEYENNE", state = "WY", county = "LARAMIE", elevation = "1867.81 m (6125.33 ft)", lat = "+41 09 06.91", long = "-104 48 21.71"} +KLOT = {id = "LOT", name = "CHICAGO", agency = "NWS", equipment = "RDA", city = "ROMEOVILLE", state = "IL", county = "WILL", elevation = "202.08 m (662.73 ft)", lat = "+41 36 16", long = "-88 05 04"} +KILN = {id = "ILN", name = "CINCINNATI", agency = "NWS", equipment = "RDA", city = "WILMINGTON", state = "OH", county = "CLINTON", elevation = "321.87 m (1053.15 ft)", lat = "+39 25 13.74", long = "-83 49 17.22"} +KCLE = {id = "CLE", name = "CLEVELAND", agency = "NWS", equipment = "RDA", city = "CLEVELAND", state = "OH", county = "CUYAHOGA", elevation = "232.56 m (761.15 ft)", lat = "+41 24 47.58", long = "-81 51 35.52"} +KCAE = {id = "CAE", name = "COLUMBIA", agency = "NWS", equipment = "RDA", city = "WEST COLUMBIA", state = "SC", county = "LEXINGTON", elevation = "70.41 m (229.66 ft)", lat = "+33 56 55.4", long = "-81 07 05.8"} +KGWX = {id = "GWX", name = "COLUMBUS AFB", agency = "AFWA", equipment = "RDA", city = "GREENWOOD SPRINGS", state = "MS", county = "MONROE", elevation = "155 m (508.53 ft)", lat = "+33 53 48.9", long = "-88 19 45.1"} +KCRP = {id = "CRP", name = "CORPUS CHRISTI", agency = "NWS", equipment = "RDA", city = "CORPUS CHRISTI", state = "TX", county = "NUECES", elevation = "13.72 m (42.65 ft)", lat = "+27 47 02.46", long = "-97 30 40.5"} +KFWS = {id = "FWS", name = "DALLAS/FT WORTH", agency = "NWS", equipment = "RDA", city = "FORT WORTH", state = "TX", county = "TARRANT", elevation = "212 m (695.54 ft)", lat = "+32 34 22.8", long = "-97 18 11.34"} +KFTG = {id = "FTG", name = "DENVER", agency = "NWS", equipment = "RDA", city = "FRONT RANGE AP", state = "CO", county = "ARAPAHOE", elevation = "1675.49 m (5495.41 ft)", lat = "+39 47 11.9", long = "-104 32 44.9"} +KDMX = {id = "DMX", name = "DES MOINES", agency = "NWS", equipment = "RDA", city = "JOHNSTON", state = "IA", county = "POLK", elevation = "299.01 m (980.97 ft)", lat = "+41 43 52.32", long = "-93 43 22.33"} +KDTX = {id = "DTX", name = "DETROIT", agency = "NWS", equipment = "RDA", city = "WHITE LAKE", state = "MI", county = "OAKLAND", elevation = "336 m (1102.36 ft)", lat = "+42 42 00", long = "-83 28 18"} +KDDC = {id = "DDC", name = "DODGE CITY", agency = "NWS", equipment = "RDA", city = "DODGE CITY", state = "KS", county = "FORD", elevation = "789.43 m (2588.58 ft)", lat = "+37 45 39", long = "-99 58 08"} +KDOX = {id = "DOX", name = "DOVER AFB", agency = "AFWA", equipment = "RDA", city = "ELLENDALE STATE FOREST", state = "DE", county = "SUSSEX", elevation = "15.24 m (49.21 ft)", lat = "+38 49 32.76", long = "-75 26 24.42"} +KDLH = {id = "DLH", name = "DULUTH", agency = "NWS", equipment = "RDA", city = "DULUTH", state = "MN", county = "ST LOUIS", elevation = "435.25 m (1427.17 ft)", lat = "+46 50 13", long = "-92 12 35"} +KDYX = {id = "DYX", name = "DYESS AFB", agency = "AFWA", equipment = "RDA", city = "MORAN", state = "TX", county = "SHACKELFORD", elevation = "462.38 m (1515.75 ft)", lat = "+32 32 18.6", long = "-99 15 15.6"} +KEYX = {id = "EYX", name = "EDWARDS AFB", agency = "AFWA", equipment = "RDA", city = "BORON", state = "CA", county = "SAN BERNADINO", elevation = "846 m (2775.59 ft)", lat = "+35 05 52.26", long = "-117 33 38.7"} +KEVX = {id = "EVX", name = "EGLIN AFB", agency = "AFWA", equipment = "RDA", city = "RED BAY", state = "FL", county = "WALTON", elevation = "42.67 m (137.8 ft)", lat = "+30 33 54.12", long = "-85 55 18"} +KEPZ = {id = "EPZ", name = "EL PASO", agency = "NWS", equipment = "RDA", city = "SANTA TERESA", state = "NM", county = "DONA ANA", elevation = "1250.9 m (4101.05 ft)", lat = "+31 52 23", long = "-106 41 52.8"} +KLRX = {id = "LRX", name = "ELKO (RDA 1)", agency = "NWS", equipment = "RDA", city = "ELKO", state = "NV", county = "LANDER", elevation = "2067 m (6781.5 ft)", lat = "+40 44 22.38", long = "-116 48 09.72"} +KBHX = {id = "BHX", name = "EUREKA (BUNKER HILL)", agency = "NWS", equipment = "RDA", city = "EUREKA", state = "CA", county = "HUMBOLDT", elevation = "732.13 m (2401.57 ft)", lat = "+40 29 54.9", long = "-124 17 31.8"} +PAPD = {id = "APD", name = "FAIRBANKS FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "FAIRBANKS", state = "AK", county = "N/A", elevation = "790.35 m (2591.86 ft)", lat = "+65 02 06.41", long = "-147 30 05.15"} +KMVX = {id = "MVX", name = "FARGO/GRAND FORKS", agency = "NWS", equipment = "RDA", city = "GRAND FORKS", state = "ND", county = "TRAILL", elevation = "300.53 m (984.25 ft)", lat = "+47 31 40", long = "-97 19 32"} +KFSX = {id = "FSX", name = "FLAGSTAFF (RDA 1)", agency = "NWS", equipment = "RDA", city = "FLAGSTAFF", state = "AZ", county = "COCONINO", elevation = "2260.7 m (7414.7 ft)", lat = "+34 34 27.6", long = "-111 11 54.4"} +KHPX = {id = "HPX", name = "FT CAMPBELL", agency = "AFWA", equipment = "RDA", city = "TRENTON", state = "KY", county = "TODD", elevation = "172 m (564.3 ft)", lat = "+36 44 13.1", long = "-87 17 08.1"} +KTYX = {id = "TYX", name = "FT DRUM", agency = "AFWA", equipment = "RDA", city = "MONTAGUE", state = "NY", county = "LEWIS", elevation = "562.66 m (1843.83 ft)", lat = "+43 45 20.5", long = "-75 40 47.5"} +KGRK = {id = "GRK", name = "FT CAVAZOS", agency = "AFWA", equipment = "RDA", city = "GRANGER", state = "TX", county = "BELL", elevation = "163.98 m (534.78 ft)", lat = "+30 43 18.6", long = "-97 22 58.6"} +KPOE = {id = "POE", name = "FT JOHNSON", agency = "AFWA", equipment = "RDA", city = "FT POLK", state = "LA", county = "VERNON", elevation = "124.36 m (406.82 ft)", lat = "+31 09 19", long = "-92 58 34"} +KEOX = {id = "EOX", name = "FT NOVOSEL", agency = "AFWA", equipment = "RDA", city = "ECHO", state = "AL", county = "DALE", elevation = "144 m (472.44 ft)", lat = "+31 27 38", long = "-85 27 33.8"} +KGGW = {id = "GGW", name = "GLASGOW", agency = "NWS", equipment = "RDA", city = "GLASGOW", state = "MT", county = "VALLEY", elevation = "702 m (2303.15 ft)", lat = "+48 12 22.9", long = "-106 37 28.9"} +KGLD = {id = "GLD", name = "GOODLAND", agency = "NWS", equipment = "RDA", city = "GOODLAND", state = "KS", county = "SHERMAN", elevation = "1112.82 m (3648.29 ft)", lat = "+39 22 01", long = "-101 42 01"} +KUEX = {id = "UEX", name = "GRAND ISLAND", agency = "NWS", equipment = "RDA", city = "BLUE HILL", state = "NE", county = "WEBSTER", elevation = "602.28 m (1975.07 ft)", lat = "+40 19 15", long = "-98 26 31"} +KGJX = {id = "GJX", name = "GRAND JUNCTION (RDA 1)", agency = "NWS", equipment = "RDA", city = "GRAND JUNCTION", state = "CO", county = "MESA", elevation = "3059 m (10036.09 ft)", lat = "+39 03 43.81", long = "-108 12 49.54"} +KGRR = {id = "GRR", name = "GRAND RAPIDS", agency = "NWS", equipment = "RDA", city = "GRAND RAPIDS", state = "MI", county = "KENT", elevation = "237.13 m (777.56 ft)", lat = "+42 53 38", long = "-85 32 41.6"} +KTFX = {id = "TFX", name = "GREAT FALLS", agency = "NWS", equipment = "RDA", city = "GREAT FALLS", state = "MT", county = "CASCADE", elevation = "1140 m (3740.16 ft)", lat = "+47 27 34.5", long = "-111 23 07.2"} +KGRB = {id = "GRB", name = "GREEN BAY", agency = "NWS", equipment = "RDA", city = "GREEN BAY", state = "WI", county = "BROWN", elevation = "216 m (708.66 ft)", lat = "+44 29 55.08", long = "-88 06 40"} +KGSP = {id = "GSP", name = "GREER", agency = "NWS", equipment = "RDA", city = "GREER", state = "SC", county = "SPARTANBURG", elevation = "291 m (954.72 ft)", lat = "+34 52 59.9", long = "-82 13 11.4"} +KHDX = {id = "HDX", name = "HOLLOMAN AFB", agency = "AFWA", equipment = "RDA", city = "RUIDOSO", state = "NM", county = "DONA ANA", elevation = "1286.87 m (4219.16 ft)", lat = "+33 04 37.2", long = "-106 07 12.12"} +KHGX = {id = "HGX", name = "HOUSTON", agency = "NWS", equipment = "RDA", city = "DICKINSON", state = "TX", county = "GALVESTON", elevation = "5.49 m (16.4 ft)", lat = "+29 28 18.84", long = "-95 04 43.44"} +KIND = {id = "IND", name = "INDIANAPOLIS", agency = "NWS", equipment = "RDA", city = "INDIANAPOLIS", state = "IN", county = "MARION", elevation = "240.79 m (787.4 ft)", lat = "+39 42 27", long = "-86 16 49"} +KJKL = {id = "JKL", name = "JACKSON, KY", agency = "NWS", equipment = "RDA", city = "JACKSON", state = "KY", county = "BREATHITT", elevation = "415.75 m (1361.55 ft)", lat = "+37 35 27", long = "-83 18 47"} +KJAX = {id = "JAX", name = "JACKSONVILLE", agency = "NWS", equipment = "RDA", city = "JACKSONVILLE", state = "FL", county = "DUVAL", elevation = "19 m (62.34 ft)", lat = "+30 29 04.68", long = "-81 42 06.84"} +RODN = {id = "ODNR5", name = "KADENA AB", agency = "AFWA", equipment = "RDA", city = "KADENA AB", state = "JA", county = "N/A", elevation = "91 m (298.56 ft)", lat = "+26 18 28.08", long = "+127 54 12.49"} +PHKM = {id = "HKM", name = "KAMUELA/KOHALA APT (RDA 1)", agency = "FAA", equipment = "RDA", city = "KAMUELA", state = "HI", county = "HAWAII", elevation = "1174 m (3851.71 ft)", lat = "+20 07 31", long = "-155 46 40"} +KBYX = {id = "BYX", name = "KEY WEST", agency = "NWS", equipment = "RDA", city = "BOCA CHICA KEY", state = "FL", county = "MONROE", elevation = "2.44 m (6.56 ft)", lat = "+24 35 51", long = "-81 42 11.4"} +PAKC = {id = "AKC", name = "KING SALMON FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "KING SALMON", state = "AK", county = "N/A", elevation = "19.2 m (62.34 ft)", lat = "+58 40 46", long = "-156 37 46"} +KMRX = {id = "MRX", name = "KNOXVILLE", agency = "NWS", equipment = "RDA", city = "MORRISTOWN", state = "TN", county = "HAMBLEN", elevation = "407.52 m (1335.3 ft)", lat = "+36 10 07", long = "-83 24 07"} +RKJK = {id = "KJKR4", name = "KUNSAN AB", agency = "AFWA", equipment = "RDA", city = "KUNSAN AB", state = "KO", county = "N/A", elevation = "23.77 m (75.46 ft)", lat = "+35 55 27", long = "+126 37 20"} +KARX = {id = "ARX", name = "LA CROSSE", agency = "NWS", equipment = "RDA", city = "LA CROSSE", state = "WI", county = "LA CROSSE", elevation = "388.92 m (1272.97 ft)", lat = "+43 49 22", long = "-91 11 28"} +KLCH = {id = "LCH", name = "LAKE CHARLES", agency = "NWS", equipment = "RDA", city = "LAKE CHARLES", state = "LA", county = "CALCASIEU", elevation = "17 m (55.77 ft)", lat = "+30 07 31.1", long = "-93 12 57.2"} +KESX = {id = "ESX", name = "LAS VEGAS", agency = "NWS", equipment = "RDA", city = "LAS VEGAS", state = "NV", county = "CLARK", elevation = "1483.46 m (4865.49 ft)", lat = "+35 42 04.86", long = "-114 53 29.94"} +KDFX = {id = "DFX", name = "LAUGHLIN AFB", agency = "AFWA", equipment = "RDA", city = "BRACKETVILLE", state = "TX", county = "KINNEY", elevation = "344.73 m (1128.61 ft)", lat = "+29 16 23.3", long = "-100 16 49.2"} +KILX = {id = "ILX", name = "LINCOLN", agency = "NWS", equipment = "RDA", city = "LINCOLN", state = "IL", county = "LOGAN", elevation = "188 m (616.8 ft)", lat = "+40 09 01.8", long = "-89 20 12.45"} +KLZK = {id = "LZK", name = "LITTLE ROCK", agency = "NWS", equipment = "RDA", city = "NORTH LITTLE ROCK", state = "AR", county = "PULASKI", elevation = "173.13 m (567.59 ft)", lat = "+34 50 11.4", long = "-92 15 43.9"} +KVTX = {id = "VTX", name = "LOS ANGELES", agency = "NWS", equipment = "RDA", city = "LOS ANGELES", state = "CA", county = "VENTURA", elevation = "830.88 m (2723.1 ft)", lat = "+34 24 43.26", long = "-119 10 43.5"} +KLVX = {id = "LVX", name = "LOUISVILLE", agency = "NWS", equipment = "RDA", city = "FORT KNOX", state = "KY", county = "HARDIN", elevation = "219.15 m (718.5 ft)", lat = "+37 58 31", long = "-85 56 38"} +KLBB = {id = "LBB", name = "LUBBOCK", agency = "NWS", equipment = "RDA", city = "LUBBOCK", state = "TX", county = "LUBBOCK", elevation = "1005 m (3297.24 ft)", lat = "+33 39 14.9", long = "-101 48 51"} +KMQT = {id = "MQT", name = "MARQUETTE", agency = "NWS", equipment = "RDA", city = "NEGAUNEE", state = "MI", county = "MARQUETTE", elevation = "430.07 m (1410.76 ft)", lat = "+46 31 52", long = "-87 32 54"} +KMXX = {id = "MXX", name = "MAXWELL AFB", agency = "AFWA", equipment = "RDA", city = "CARRVILLE", state = "AL", county = "MACON", elevation = "136 m (446.19 ft)", lat = "+32 32 11.94", long = "-85 47 23.1"} +KMAX = {id = "MAX", name = "MEDFORD (RDA 1)", agency = "NWS", equipment = "RDA", city = "MEDFORD", state = "OR", county = "JACKSON", elevation = "2289.96 m (7509.84 ft)", lat = "+42 04 52.21", long = "-122 43 02.53"} +KMLB = {id = "MLB", name = "MELBOURNE", agency = "NWS", equipment = "RDA", city = "MELBOURNE", state = "FL", county = "BREVARD", elevation = "10.67 m (32.81 ft)", lat = "+28 06 47.5", long = "-80 39 14.7"} +KNQA = {id = "NQA", name = "MEMPHIS", agency = "NWS", equipment = "RDA", city = "MILLINGTON", state = "TN", county = "SHELBY", elevation = "103 m (337.93 ft)", lat = "+35 20 41", long = "-89 52 24"} +KAMX = {id = "AMX", name = "MIAMI", agency = "NWS", equipment = "RDA", city = "MIAMI", state = "FL", county = "DADE", elevation = "4.27 m (13.12 ft)", lat = "+25 36 39.9", long = "-80 24 45.6"} +PAIH = {id = "AIH", name = "MIDDLETON ISLAND (RDA 1)", agency = "FAA", equipment = "RDA", city = "MIDDLETON ISLAND", state = "AK", county = "N/A", elevation = "20.42 m (65.62 ft)", lat = "+59 27 38.76", long = "-146 18 12.41"} +KMAF = {id = "MAF", name = "MIDLAND/ODESSA", agency = "NWS", equipment = "RDA", city = "MIDLAND", state = "TX", county = "MIDLAND", elevation = "883 m (2896.98 ft)", lat = "+31 56 36.46", long = "-102 11 21.3"} +KMKX = {id = "MKX", name = "MILWAUKEE", agency = "NWS", equipment = "RDA", city = "DOUSMAN", state = "WI", county = "WAUKESHA", elevation = "292 m (958.01 ft)", lat = "+42 58 04.44", long = "-88 33 02.4"} +KMPX = {id = "MPX", name = "MINNEAPOLIS", agency = "NWS", equipment = "RDA", city = "CHANHASSEN", state = "MN", county = "CARVER", elevation = "301 m (987.53 ft)", lat = "+44 50 56", long = "-93 33 55.9"} +KMBX = {id = "MBX", name = "MINOT AFB", agency = "AFWA", equipment = "RDA", city = "DEERING", state = "ND", county = "MCHENRY", elevation = "455.07 m (1492.78 ft)", lat = "+48 23 35", long = "-100 51 52"} +KMSX = {id = "MSX", name = "MISSOULA (RDA 1)", agency = "NWS", equipment = "RDA", city = "MISSOULA", state = "MT", county = "MISSOULA", elevation = "2417 m (7929.79 ft)", lat = "+47 02 27.6", long = "-113 59 10.4"} +KMOB = {id = "MOB", name = "MOBILE", agency = "NWS", equipment = "RDA", city = "MOBILE", state = "AL", county = "MOBILE", elevation = "63.4 m (206.69 ft)", lat = "+30 40 46", long = "-88 14 24"} +PHMO = {id = "HMO", name = "MOLOKAI FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "MOLOKAI", state = "HI", county = "MOLOKAI", elevation = "415.44 m (1361.55 ft)", lat = "+21 07 58", long = "-157 10 49"} +KVAX = {id = "VAX", name = "MOODY AFB", agency = "AFWA", equipment = "RDA", city = "SOUTH STOCKTON", state = "GA", county = "LANIER", elevation = "66 m (216.54 ft)", lat = "+30 53 25", long = "-83 00 06.5"} +KMHX = {id = "MHX", name = "MOREHEAD CITY", agency = "NWS", equipment = "RDA", city = "NEWPORT", state = "NC", county = "CARTERET", elevation = "9.45 m (29.53 ft)", lat = "+34 46 33.27", long = "-76 52 34.28"} +KOHX = {id = "OHX", name = "NASHVILLE", agency = "NWS", equipment = "RDA", city = "OLD HICKORY", state = "TN", county = "WILSON", elevation = "176.48 m (577.43 ft)", lat = "+36 14 50", long = "-86 33 45"} +KAPX = {id = "APX", name = "NCL MICHIGAN", agency = "NWS", equipment = "RDA", city = "GAYLORD", state = "MI", county = "OTSEGO", elevation = "446.23 m (1463.25 ft)", lat = "+44 54 22.86", long = "-84 43 10.32"} +PAEC = {id = "AEC", name = "NOME FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "NOME", state = "AK", county = "N/A", elevation = "17.68 m (55.77 ft)", lat = "+64 30 41", long = "-165 17 42"} +KAKQ = {id = "AKQ", name = "NORFOLK", agency = "NWS", equipment = "RDA", city = "WAKEFIELD", state = "VA", county = "SUSSEX", elevation = "48 m (157.48 ft)", lat = "+36 59 02.58", long = "-77 00 26.5"} +KTLX = {id = "TLX", name = "NORMAN", agency = "NWS", equipment = "RDA", city = "OKLAHOMA CITY", state = "OK", county = "CLEVELAND", elevation = "369.72 m (1210.63 ft)", lat = "+35 20 00.10", long = "-97 16 39.94"} +KLNX = {id = "LNX", name = "NORTH PLATTE", agency = "NWS", equipment = "RDA", city = "NORTH PLATTE", state = "NE", county = "LOGAN", elevation = "919 m (3015.09 ft)", lat = "+41 57 28.6", long = "-100 34 34.4"} +KHTX = {id = "HTX", name = "NORTHEAST ALABAMA", agency = "NWS", equipment = "RDA", city = "HYTOP", state = "AL", county = "JACKSON", elevation = "537.06 m (1761.81 ft)", lat = "+34 55 50", long = "-86 05 01"} +KIWX = {id = "IWX", name = "NORTHERN INDIANA", agency = "NWS", equipment = "RDA", city = "NORTH WEBSTER", state = "IN", county = "KOSCIUSKO", elevation = "292.3 m (958.01 ft)", lat = "+41 21 31", long = "-85 42 00"} +KOUN = {id = "NORO2", name = "NSSL", agency = "NWS", equipment = "RDA", city = "NORMAN", state = "OK", county = "CLEVELAND", elevation = "370 m (1213.91 ft)", lat = "+35 14 09.81", long = "-97 27 44.46"} +KOAX = {id = "OAX", name = "OMAHA", agency = "NWS", equipment = "RDA", city = "VALLEY", state = "NE", county = "DOUGLAS", elevation = "349.91 m (1145.01 ft)", lat = "+41 19 13.33", long = "-96 22 00.55"} +KPAH = {id = "PAH", name = "PADUCAH", agency = "NWS", equipment = "RDA", city = "PADUCAH", state = "KY", county = "MCCRACKEN", elevation = "119.48 m (390.42 ft)", lat = "+37 04 06", long = "-88 46 19"} +KPDT = {id = "PDT", name = "PENDLETON", agency = "NWS", equipment = "RDA", city = "PENDLETON", state = "OR", county = "UMATILLA", elevation = "461.77 m (1512.47 ft)", lat = "+45 41 26.34", long = "-118 51 10.55"} +KDIX = {id = "DIX", name = "PHILADELPHIA", agency = "NWS", equipment = "RDA", city = "FORT DIX", state = "NJ", county = "BURLINGTON", elevation = "45.42 m (147.64 ft)", lat = "+39 56 49.52", long = "-74 24 38.63"} +KIWA = {id = "IWA", name = "PHOENIX", agency = "NWS", equipment = "RDA", city = "PHOENIX", state = "AZ", county = "MARICOPA", elevation = "415 m (1361.55 ft)", lat = "+33 17 21.24", long = "-111 40 11.7"} +KPBZ = {id = "PBZ", name = "PITTSBURGH", agency = "NWS", equipment = "RDA", city = "CORAOPOLIS", state = "PA", county = "ALLEGHENY", elevation = "361.19 m (1184.38 ft)", lat = "+40 31 54.18", long = "-80 13 04.68"} +KEAX = {id = "EAX", name = "PLEASANT HILL", agency = "NWS", equipment = "RDA", city = "PLEASANT HILL", state = "MO", county = "CASS", elevation = "303.28 m (994.09 ft)", lat = "+38 48 36.9", long = "-94 15 52.1"} +KSFX = {id = "SFX", name = "POCATELLO", agency = "NWS", equipment = "RDA", city = "SPRINGFIELD", state = "ID", county = "BINGHAM", elevation = "1363.68 m (4471.78 ft)", lat = "+43 06 20.16", long = "-112 41 10.08"} +KGYX = {id = "GYX", name = "PORTLAND, ME", agency = "NWS", equipment = "RDA", city = "GRAY", state = "ME", county = "CUMBERLAND", elevation = "124.66 m (406.82 ft)", lat = "+43 53 28.7", long = "-70 15 22.9"} +KRTX = {id = "RTX", name = "PORTLAND, OR", agency = "NWS", equipment = "RDA", city = "PORTLAND", state = "OR", county = "WASHINGTON", elevation = "492 m (1614.17 ft)", lat = "+45 42 54.14", long = "-122 57 54"} +KPUX = {id = "PUX", name = "PUEBLO", agency = "NWS", equipment = "RDA", city = "PUEBLO", state = "CO", county = "PUEBLO", elevation = "1615 m (5298.56 ft)", lat = "+38 27 34.38", long = "-104 10 52.86"} +KDVN = {id = "DVN", name = "QUAD CITIES", agency = "NWS", equipment = "RDA", city = "DAVENPORT", state = "IA", county = "SCOTT", elevation = "229.82 m (751.31 ft)", lat = "+41 36 42", long = "-90 34 51"} +KRAX = {id = "RAX", name = "RALEIGH/DURHAM", agency = "NWS", equipment = "RDA", city = "CLAYTON", state = "NC", county = "WAKE", elevation = "106.07 m (347.77 ft)", lat = "+35 39 55.87", long = "-78 29 23.10"} +KUDX = {id = "UDX", name = "RAPID CITY", agency = "NWS", equipment = "RDA", city = "NEW UNDERWOOD", state = "SD", county = "PENNINGTON", elevation = "939 m (3080.71 ft)", lat = "+44 07 29", long = "-102 49 48"} +KRGX = {id = "RGX", name = "RENO (RDA 1)", agency = "NWS", equipment = "RDA", city = "NIXON", state = "NV", county = "WASHOE", elevation = "2529.54 m (8297.24 ft)", lat = "+39 45 14.6", long = "-119 27 43.3"} +KRIW = {id = "RIW", name = "RIVERTON/LANDER", agency = "NWS", equipment = "RDA", city = "RIVERTON", state = "WY", county = "FREMONT", elevation = "1697.13 m (5567.59 ft)", lat = "+43 03 57.92", long = "-108 28 38.28"} +KFCX = {id = "FCX", name = "ROANOKE", agency = "NWS", equipment = "RDA", city = "ROANOKE", state = "VA", county = "FLOYD", elevation = "874.17 m (2867.45 ft)", lat = "+37 01 27.84", long = "-80 16 26.29"} +KJGX = {id = "JGX", name = "ROBINS AFB", agency = "AFWA", equipment = "RDA", city = "JEFFERSONVILLE", state = "GA", county = "TWIGGS", elevation = "158.8 m (518.37 ft)", lat = "+32 40 32.46", long = "-83 21 03"} +KDAX = {id = "DAX", name = "SACRAMENTO", agency = "NWS", equipment = "RDA", city = "DAVIS", state = "CA", county = "YOLO", elevation = "9.14 m (29.53 ft)", lat = "+38 30 04", long = "-121 40 40.2"} +KMTX = {id = "MTX", name = "SALT LAKE CITY (RDA 1)", agency = "NWS", equipment = "RDA", city = "SALT LAKE CITY", state = "UT", county = "SALT LAKE", elevation = "1975 m (6479.66 ft)", lat = "+41 15 46", long = "-112 26 52"} +KSJT = {id = "SJT", name = "SAN ANGELO", agency = "NWS", equipment = "RDA", city = "SAN ANGELO", state = "TX", county = "TOM GREEN", elevation = "576.07 m (1889.76 ft)", lat = "+31 22 16.6", long = "-100 29 33"} +KNKX = {id = "NKX", name = "SAN DIEGO", agency = "NWS", equipment = "RDA", city = "SAN DIEGO", state = "CA", county = "SAN DIEGO", elevation = "291.08 m (954.72 ft)", lat = "+32 55 08.46", long = "-117 02 30.48"} +KMUX = {id = "MUX", name = "SAN FRANCISCO", agency = "NWS", equipment = "RDA", city = "LOS GATOS", state = "CA", county = "SANTA CLARA", elevation = "1057.35 m (3467.85 ft)", lat = "+37 09 18.8", long = "-121 53 54.4"} +KHNX = {id = "HNX", name = "SAN JOAQUIN VALY", agency = "NWS", equipment = "RDA", city = "HANFORD", state = "CA", county = "KINGS", elevation = "74.07 m (242.78 ft)", lat = "+36 18 51.05", long = "-119 37 55.7"} +TJUA = {id = "JUA", name = "SAN JUAN FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "SAN JUAN", state = "PR", county = "N/A", elevation = "867 m (2844.49 ft)", lat = "+18 06 56.4", long = "-66 04 41.4"} +KSOX = {id = "SOX", name = "SANTA ANA MTS", agency = "NWS", equipment = "RDA", city = "SANTA ANA MOUNTAINS", state = "CA", county = "ORANGE", elevation = "927 m (3041.34 ft)", lat = "+33 49 03.84", long = "-117 38 09.6"} +KATX = {id = "ATX", name = "SEATTLE", agency = "NWS", equipment = "RDA", city = "EVERETT", state = "WA", county = "ISLAND", elevation = "161 m (528.22 ft)", lat = "+48 11 40.6", long = "-122 29 44.5"} +KSHV = {id = "SHV", name = "SHREVEPORT", agency = "NWS", equipment = "RDA", city = "SHREVEPORT", state = "LA", county = "CADDO", elevation = "83.21 m (272.31 ft)", lat = "+32 27 03", long = "-93 50 28.5"} +KFSD = {id = "FSD", name = "SIOUX FALLS", agency = "NWS", equipment = "RDA", city = "SIOUX FALLS", state = "SD", county = "MINNEHAHA", elevation = "435.86 m (1427.17 ft)", lat = "+43 35 16", long = "-96 43 46"} +PACG = {id = "ACG", name = "SITKA FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "BIORKA ISLAND", state = "AK", county = "N/A", elevation = "63.09 m (206.69 ft)", lat = "+56 51 10", long = "-135 31 45"} +PHKI = {id = "HKI", name = "SOUTH KAUAI FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "SOUTH KAUAI", state = "HI", county = "KAUAI", elevation = "69 m (226.38 ft)", lat = "+21 53 38", long = "-159 33 09"} +PHWA = {id = "HWA", name = "SOUTH SHORE FAA (RDA 1)", agency = "FAA", equipment = "RDA", city = "NAALEHU", state = "HI", county = "HAWAII", elevation = "420.62 m (1377.95 ft)", lat = "+19 05 42", long = "-155 34 08"} +KOTX = {id = "OTX", name = "SPOKANE", agency = "NWS", equipment = "RDA", city = "SPOKANE", state = "WA", county = "SPOKANE", elevation = "726.64 m (2381.89 ft)", lat = "+47 40 49.5", long = "-117 37 36.4"} +KSGF = {id = "SGF", name = "SPRINGFIELD", agency = "NWS", equipment = "RDA", city = "SPRINGFIELD", state = "MO", county = "GREENE", elevation = "389.53 m (1276.25 ft)", lat = "+37 14 06.86", long = "-93 24 01.51"} +KLSX = {id = "LSX", name = "ST LOUIS", agency = "NWS", equipment = "RDA", city = "WELDON SPRING", state = "MO", county = "ST CHARLES", elevation = "185.32 m (606.96 ft)", lat = "+38 41 55", long = "-90 40 58"} +KCCX = {id = "CCX", name = "STATE COLLEGE", agency = "NWS", equipment = "RDA", city = "STATE COLLEGE", state = "PA", county = "CENTRE", elevation = "733.04 m (2404.86 ft)", lat = "+40 55 23.4", long = "-78 00 13.4"} +KLWX = {id = "LWX", name = "STERLING", agency = "NWS", equipment = "RDA", city = "STERLING", state = "VA", county = "LOUDOUN", elevation = "88.54 m (288.71 ft)", lat = "+38 58 34", long = "-77 29 15"} +KTLH = {id = "TLH", name = "TALLAHASSEE", agency = "NWS", equipment = "RDA", city = "TALLAHASSEE", state = "FL", county = "LEON", elevation = "19.2 m (62.34 ft)", lat = "+30 23 51.3", long = "-84 19 44.2"} +KTBW = {id = "TBW", name = "TAMPA", agency = "NWS", equipment = "RDA", city = "RUSKIN", state = "FL", county = "HILLSBOROUGH", elevation = "12.5 m (39.37 ft)", lat = "+27 42 19.8", long = "-82 24 06.4"} +KTWX = {id = "TWX", name = "TOPEKA", agency = "NWS", equipment = "RDA", city = "TOPEKA", state = "KS", county = "WABAUNSEE", elevation = "416.66 m (1364.83 ft)", lat = "+38 59 49.02", long = "-96 13 57.18"} +KEMX = {id = "EMX", name = "TUCSON", agency = "NWS", equipment = "RDA", city = "TUCSON", state = "AZ", county = "PIMA", elevation = "1586.48 m (5203.41 ft)", lat = "+31 53 37.14", long = "-110 37 48.9"} +KINX = {id = "INX", name = "TULSA", agency = "NWS", equipment = "RDA", city = "INOLA", state = "OK", county = "ROGERS", elevation = "203.61 m (666.01 ft)", lat = "+36 10 30.47", long = "-95 33 50.98"} +KVNX = {id = "VNX", name = "VANCE AFB", agency = "AFWA", equipment = "RDA", city = "CHEROKEE", state = "OK", county = "ALFALFA", elevation = "368.81 m (1207.35 ft)", lat = "+36 44 26.22", long = "-98 07 39.78"} +KVBX = {id = "VBX", name = "VANDENBERG SFB", agency = "AFWA", equipment = "RDA", city = "ORCUTT", state = "CA", county = "SANTA BARBARA", elevation = "383 m (1256.56 ft)", lat = "+34 50 18.78", long = "-120 23 52.50"} +KSRX = {id = "SRX", name = "WESTERN ARKANSAS", agency = "NWS", equipment = "RDA", city = "CHAFFEE RIDGE", state = "AR", county = "SEBASTIAN", elevation = "200 m (656.17 ft)", lat = "+35 17 25.5", long = "-94 21 42.8"} +KICT = {id = "ICT", name = "WICHITA", agency = "NWS", equipment = "RDA", city = "WICHITA", state = "KS", county = "SEDGWICK", elevation = "406.91 m (1332.02 ft)", lat = "+37 39 16", long = "-97 26 35"} +KLTX = {id = "LTX", name = "WILMINGTON", agency = "NWS", equipment = "RDA", city = "SHALLOTTE", state = "NC", county = "BRUNSWICK", elevation = "19.51 m (62.34 ft)", lat = "+33 59 20.94", long = "-78 25 44.79"} +KYUX = {id = "YUX", name = "YUMA (RDA 1)", agency = "NWS", equipment = "RDA", city = "YUMA", state = "AZ", county = "PIMA", elevation = "53.04 m (173.88 ft)", lat = "+32 29 43.01", long = "-114 39 24.16"} +KVWX = {id = "VWX", name = "EVANSVILLE, IN", agency = "NWS", equipment = "RDA", city = "OWENSVILLE", state = "IN", county = "GIBSON", elevation = "155.75 m (508.53 ft)", lat = "+38 15 36.9", long = "-87 43 28.3"} +KDGX = {id = "DGX", name = "JACKSON/BRANDON, MS", agency = "NWS", equipment = "RDA", city = "BRANDON", state = "MS", county = "RANKIN", elevation = "150.92 m (492.13 ft)", lat = "+32 16 47.8", long = "-89 59 04"} +KLGX = {id = "LGX", name = "LANGLEY HILL (NW WASHINGTON)", agency = "NWS", equipment = "RDA", city = "LANGLEY HILL", state = "WA", county = "GRAYS HARBOR", elevation = "76.8 m (249.34 ft)", lat = "+47 07 01", long = "-124 06 24"} +KHDC = {id = "HDC", name = "HAMMOND", agency = "NWS", equipment = "RDA", city = "HAMMOND", state = "LA", county = "TANGIPAHOA", elevation = "13 m (42.65 ft)", lat = "+30 31 9.5", long = "-90 24 26.5"} diff --git a/crates/grib2/Cargo.toml b/crates/grib2/Cargo.toml index 440cbee..02b8813 100644 --- a/crates/grib2/Cargo.toml +++ b/crates/grib2/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" thiserror = "2" tracing = "0.1" png = "0.17" -image = "0.25" \ No newline at end of file +image = "0.25" +wxbox-nommer = { version = "0.1", path = "../nommer" } \ No newline at end of file diff --git a/crates/grib2/src/lib.rs b/crates/grib2/src/lib.rs index ba1862e..03eb77f 100644 --- a/crates/grib2/src/lib.rs +++ b/crates/grib2/src/lib.rs @@ -1,9 +1,8 @@ pub mod error; -mod nommer; pub mod wgs84; use crate::error::GribError; -use crate::nommer::NomReader; +use wxbox_nommer::NomReader; use crate::wgs84::LatLong; use crate::LatLongVectorRelativity::{EasterlyAndNortherly, IncreasingXY}; use image::{DynamicImage, ImageFormat, ImageReader}; diff --git a/crates/nommer/Cargo.toml b/crates/nommer/Cargo.toml new file mode 100644 index 0000000..96d9607 --- /dev/null +++ b/crates/nommer/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "wxbox-nommer" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/crates/grib2/src/nommer.rs b/crates/nommer/src/lib.rs similarity index 87% rename from crates/grib2/src/nommer.rs rename to crates/nommer/src/lib.rs index 7f8059b..e4c9dc5 100644 --- a/crates/grib2/src/nommer.rs +++ b/crates/nommer/src/lib.rs @@ -24,6 +24,11 @@ impl<R: Read> NomReader<R> { self.inner.read_exact(&mut buf)?; Ok(u32::from_be_bytes(buf)) } + pub fn read_i32(&mut self) -> Result<i32, std::io::Error> { + let mut buf = [0u8; 4]; + self.inner.read_exact(&mut buf)?; + Ok(i32::from_be_bytes(buf)) + } pub fn read_u64(&mut self) -> Result<u64, std::io::Error> { let mut buf = [0u8; 8]; self.inner.read_exact(&mut buf)?; diff --git a/crates/tiler/Cargo.toml b/crates/tiler/Cargo.toml index b6c8191..8ef7458 100644 --- a/crates/tiler/Cargo.toml +++ b/crates/tiler/Cargo.toml @@ -20,6 +20,7 @@ anyhow = "1" # data parsing wxbox-grib2 = { version = "0.1", path = "../grib2" } +wxbox-ar2 = { version = "0.1", path = "../ar2" } # configuration serde = { version = "1", features = ["derive"] } diff --git a/crates/tiler/config.toml b/crates/tiler/config.toml index 18c9005..e50a3ca 100644 --- a/crates/tiler/config.toml +++ b/crates/tiler/config.toml @@ -14,3 +14,16 @@ Color: 80 128 128 128 """ missing = -99.0 no_coverage = -999.0 + +[data.nexrad.kcrp_ref_test] +from = "aaaa" +palette = """ +Color: 10 164 164 255 100 100 192 +Color: 20 64 128 255 32 64 128 +Color: 30 0 255 0 0 128 0 +Color: 40 255 255 0 255 128 0 +Color: 50 255 0 0 160 0 0 +Color: 60 255 0 255 128 0 128 +Color: 70 255 255 255 128 128 128 +Color: 80 128 128 128 +""" \ No newline at end of file diff --git a/crates/tiler/src/config.rs b/crates/tiler/src/config.rs index ea226eb..cfb16d4 100644 --- a/crates/tiler/src/config.rs +++ b/crates/tiler/src/config.rs @@ -1,5 +1,6 @@ use crate::grib2::Grib2DataConfig; use serde::{Deserialize, Serialize}; +use crate::nexrad::NexradDataConfig; #[derive(Serialize, Deserialize)] pub struct Config { @@ -9,4 +10,5 @@ pub struct Config { #[derive(Serialize, Deserialize)] pub struct DataSources { pub grib2: Grib2DataConfig, + pub nexrad: NexradDataConfig, } diff --git a/crates/tiler/src/main.rs b/crates/tiler/src/main.rs index d27ad3b..2eec30c 100644 --- a/crates/tiler/src/main.rs +++ b/crates/tiler/src/main.rs @@ -2,6 +2,7 @@ mod config; mod error; mod grib2; mod tiles; +mod nexrad; use crate::config::Config; use crate::grib2::{Grib2DataCache, Grib2TileCache, grib2_handler}; @@ -15,11 +16,16 @@ use std::sync::Arc; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::util::SubscriberInitExt; use wxbox_grib2::GribMessage; +use crate::nexrad::{nexrad_handler, NexradDataCache, NexradTileCache}; #[derive(Clone)] pub struct AppState { grib2_data_cache: Grib2DataCache, grib2_tile_cache: Grib2TileCache, + + nexrad_data_cache: NexradDataCache, + nexrad_tile_cache: NexradTileCache, + config: Arc<Config>, } @@ -41,15 +47,20 @@ async fn main() -> anyhow::Result<()> { let grib2_data_cache: Grib2DataCache = Cache::new(10_000); let grib2_tile_cache: Grib2TileCache = Cache::new(10_000); + let nexrad_data_cache: NexradDataCache = Cache::new(10_000); + let nexrad_tile_cache: NexradTileCache = Cache::new(10_000); let state = AppState { grib2_tile_cache, grib2_data_cache, + nexrad_tile_cache, + nexrad_data_cache, config: Arc::new(config), }; let app = Router::new() .route("/grib2/{source}/{z}/{x}/{y}", get(grib2_handler)) + .route("/nexrad/{source}/{z}/{x}/{y}", get(nexrad_handler)) .with_state(state); let listener = tokio::net::TcpListener::bind("[::]:3000").await?; diff --git a/crates/tiler/src/nexrad.rs b/crates/tiler/src/nexrad.rs new file mode 100644 index 0000000..14954c6 --- /dev/null +++ b/crates/tiler/src/nexrad.rs @@ -0,0 +1,234 @@ +use crate::AppState; +use crate::error::AppError; +use crate::tiles::{DataId, TileId}; +use anyhow::{anyhow, bail}; +use axum::extract::{Path, State}; +use axum::http::{StatusCode, header}; +use axum::response::IntoResponse; +use flate2::read::GzDecoder; +use image::codecs::png::PngEncoder; +use image::{Rgba, RgbaImage}; +use moka::future::Cache; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::f64::consts::PI; +use std::fmt::Debug; +use std::io; +use std::io::{Cursor, ErrorKind}; +use std::num::TryFromIntError; +use std::sync::Arc; +use tokio::io::AsyncReadExt; +use tracing::{debug, info_span}; +use wxbox_ar2::{parse, MomentValue, Scan, DATA_BYTES}; +use wxbox_ar2::sites::wsr88d::SITES; +use wxbox_grib2::GribMessage; +use wxbox_grib2::wgs84::LatLong; +use wxbox_pal::{ColorPalette, Palette}; + +pub type NexradDataCache = Cache<DataId, Arc<wxbox_ar2::Scan>>; +pub type NexradTileCache = Cache<TileId, Arc<Vec<u8>>>; + +pub type NexradDataConfig = HashMap<String, NexradDataSource>; + +#[derive(Serialize, Deserialize, Clone)] +pub struct NexradDataSource { + pub from: String, + pub palette: String, +} +impl Debug for NexradDataSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "<nexrad data source>") + } +} + +#[tracing::instrument(level = "info")] +pub async fn nexrad_handler( + Path((source, z, x, y)): Path<(String, usize, usize, String)>, + State(state): State<AppState>, +) -> Result<impl IntoResponse, AppError> { + let mut y = y + .strip_suffix(".png") + .ok_or(io::Error::new(ErrorKind::InvalidInput, "invalid"))?; + let mut size = 256; + if y.ends_with("@2x") { + size = 512; + y = y.strip_suffix("@2x").unwrap(); + } + let y: usize = y.parse()?; + let tile_id = TileId { + source, + z, + x, + y, + size, + }; + + // do we have a pre-prepared tile? if so, return it immediately + if let Some(tile) = state.nexrad_tile_cache.get(&tile_id).await { + return Ok(([(header::CONTENT_TYPE, "image/png")], tile.as_ref().clone())); + } + + // is this even a valid data source? + let data_id = tile_id.data_id(); + let Some(ds) = state.config.data.nexrad.get(&data_id.source) else { + return Err(anyhow!("invalid/unknown nexrad state").into()); + }; + + // ok, so we don't have a tile image yet + // this means we are going to have to kick off a task to put that in the cache + // lets check if we have the raw data + let data = if !state.nexrad_data_cache.contains_key(&data_id) { + // we don't, so let's start by starting a task for that + load_nexrad_data(state.nexrad_data_cache, data_id, ds.clone()).await? + } else { + state.nexrad_data_cache.get(&data_id).await.unwrap() + }; + + // we know we need to build the tile, so let's do that now + // it also returns it, so we can conveniently return it right now + let pixel_data = + render_to_png(state.nexrad_tile_cache.clone(), data, tile_id, ds.clone()).await?; + + Ok(( + [(header::CONTENT_TYPE, "image/png")], + pixel_data.as_ref().clone(), + )) +} + +async fn load_nexrad_data( + cache: NexradDataCache, + data_id: DataId, + data_source: NexradDataSource, +) -> anyhow::Result<Arc<Scan>> { + /* + let client = reqwest::Client::new(); + let r = client.get(data_source.from.as_str()).send().await?; + + if !r.status().is_success() { + bail!("nexrad data failed to load: {}", r.status()); + }*/ + + let bytes = DATA_BYTES.to_vec(); + + let data = Arc::new(parse(bytes)?); + + cache.insert(data_id, data.clone()).await; + + Ok(data) +} + +const TWO_PI: f64 = PI * 2.0; +const HALF_PI: f64 = PI / 2.0; + +const A: f64 = 6_378.1370 * 1000.0; // m +const B: f64 = 6_356.7523 * 1000.0; // m + + +async fn render_to_png( + cache: NexradTileCache, + data: Arc<Scan>, + tile_id: TileId, + data_source: NexradDataSource, +) -> anyhow::Result<Arc<Vec<u8>>> { + let span = info_span!("render_to_png"); + let span = span.enter(); + let mut image = RgbaImage::new(tile_id.size as u32, tile_id.size as u32); + + let n = 2_usize.pow(tile_id.z as u32) as f64 * tile_id.size as f64; + let tile_x_times_tilesize = tile_id.x as f64 * tile_id.size as f64; + let tile_y_times_tilesize = tile_id.y as f64 * tile_id.size as f64; + + for x in 0..tile_id.size { + for y in 0..tile_id.size { + let x_cartesian = (tile_x_times_tilesize + x as f64) / n; + let y_cartesian = (tile_y_times_tilesize + y as f64) / n; + + + + let radar = SITES.sites.get("KCRP").unwrap(); + let radar_theta = radar.long; + let radar_phi = radar.lat; + let radar_r = ((A.powi(2) * radar_phi.cos()).powi(2) + (B.powi(2) * radar_phi.sin()).powi(2) + / (A * radar_phi.cos()).powi(2) + (B * radar_phi.sin()).powi(2)).sqrt(); + + let radar_x = radar_r * radar_theta.cos() * radar_phi.sin(); + let radar_y = radar_r * radar_theta.sin() * radar_phi.sin(); + let radar_z = radar_r * radar_theta.cos(); + + let measurement_theta = (TWO_PI * x_cartesian - PI).to_degrees(); + let measurement_phi = ((PI - TWO_PI * y_cartesian).exp().atan() * 2.0_f64 - HALF_PI).to_degrees(); + let measurement_r = ((A.powi(2) * measurement_phi.cos()).powi(2) + (B.powi(2) * measurement_phi.sin()).powi(2) + / (A * measurement_phi.cos()).powi(2) + (B * measurement_phi.sin()).powi(2)).sqrt(); + + + let measurement_x = measurement_r * measurement_theta.cos() * measurement_phi.sin(); + let measurement_y = measurement_r * measurement_theta.sin() * measurement_phi.sin(); + let measurement_z = measurement_r * measurement_theta.cos(); + + let radar_local_x = measurement_x - radar_x; + let radar_local_y = measurement_y - radar_y; + let radar_local_z = measurement_z - radar_z; + + let radar_local_r = (radar_local_x.powi(2) + radar_local_y.powi(2) + radar_local_z.powi(2)).sqrt(); + let radar_local_theta = (radar_local_y / radar_local_x).atan(); + let radar_local_phi = (radar_local_z / radar_local_r).acos(); + + let azimuth = radar_local_theta; + let _elevation = radar_local_phi; + let distance = radar_local_r; + + let data = data.sweeps.get(0).unwrap(); + let first_radial = data.radials.get(0).unwrap(); + let azimuth_spacing = first_radial.azimuth_spacing_degrees; + let azimuth_number = (azimuth / azimuth_spacing as f64).floor() as usize; + + let radial = data.radials.get(azimuth_number).unwrap(); + let data = radial.reflectivity.as_ref().unwrap(); + + let distance_minus_offset = distance - data.start_range as f64; + let distance_gate = (distance_minus_offset / data.sample_interval as f64).floor() as usize; + + let values = data.values(); + println!("{}", distance_gate); + let value = values.get(distance_gate).unwrap(); + + let color = colorize(Some(value), &data_source)?; + + image.put_pixel(x as u32, y as u32, color); + } + } + + // encode to png bytes and return + let mut result = vec![]; + let encoder = PngEncoder::new(&mut result); + image.write_with_encoder(encoder)?; + + let output = Arc::new(result); + + drop(span); + + cache.insert(tile_id, output.clone()).await; + + Ok(output) +} + +fn colorize(value: Option<&MomentValue>, data_source: &NexradDataSource) -> anyhow::Result<Rgba<u8>> { + Ok(match value { + Some(MomentValue::BelowThreshold) => Rgba([0, 0, 0, 0]), + Some(MomentValue::RangeFolded) => Rgba([119, 0, 125, 255]), + Some(MomentValue::Value(value)) => { + let color = wxbox_pal::parser::parse(&data_source.palette)?.colorize(*value as f64); + Rgba([ + color.red, + color.green, + color.blue, + if color.red == 0 && color.green == 0 && color.blue == 0 { + 0 + } else { + 255 + }, + ]) + } + None => Rgba([0, 0, 0, 30]), + }) +}