diff --git a/.idea/wxbox.iml b/.idea/wxbox.iml index 83cd15c..125c7cc 100644 --- a/.idea/wxbox.iml +++ b/.idea/wxbox.iml @@ -2,6 +2,7 @@ + diff --git a/10.png b/10.png new file mode 100644 index 0000000..b8c2a65 Binary files /dev/null and b/10.png differ diff --git a/Cargo.lock b/Cargo.lock index 2a63de7..a9fd1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2194,6 +2194,14 @@ dependencies = [ "memchr", ] +[[package]] +name = "wxbox-pal" +version = "0.1.0" +dependencies = [ + "ordered-float", + "thiserror", +] + [[package]] name = "wxbox-tiler" version = "0.1.0" @@ -2201,11 +2209,14 @@ dependencies = [ "actix-web", "flate2", "grib", + "mime", "ordered-float", + "png", "reqwest", "thiserror", "tikv-jemallocator", "tokio", + "wxbox-pal", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fe5ee95..0ac0f55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["wxbox-tiler"] +members = [ "wxbox-pal","wxbox-tiler"] [profile.release] codegen-units = 1 -lto = "fat" \ No newline at end of file +lto = "fat" diff --git a/MRMS_ReflectivityAtLowestAltitude.latest.grib2 b/MRMS_ReflectivityAtLowestAltitude.latest.grib2 new file mode 100644 index 0000000..53f11b3 Binary files /dev/null and b/MRMS_ReflectivityAtLowestAltitude.latest.grib2 differ diff --git a/MRMS_ReflectivityAtLowestAltitude.latest.grib2.gz b/MRMS_ReflectivityAtLowestAltitude.latest.grib2.gz deleted file mode 100644 index 712e9a2..0000000 Binary files a/MRMS_ReflectivityAtLowestAltitude.latest.grib2.gz and /dev/null differ diff --git a/wxbox-pal/Cargo.toml b/wxbox-pal/Cargo.toml new file mode 100644 index 0000000..2947c56 --- /dev/null +++ b/wxbox-pal/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "wxbox-pal" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = "1" +ordered-float = "4" \ No newline at end of file diff --git a/wxbox-pal/src/lib.rs b/wxbox-pal/src/lib.rs new file mode 100644 index 0000000..f0b2b19 --- /dev/null +++ b/wxbox-pal/src/lib.rs @@ -0,0 +1,40 @@ +use std::collections::BTreeMap; +use ordered_float::OrderedFloat; + +pub struct Palette { + pub colors: BTreeMap, Color> +} +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Color { + pub red: u8, + pub green: u8, + pub blue: u8, + pub alpha: u8 +} + +impl Color { + pub fn average(self, other: Color) -> Color { + Color { + red: (self.red + other.red) / 2, + green: (self.green + other.green) / 2, + blue: (self.blue + other.blue) / 2, + alpha: (self.alpha + other.alpha) / 2 + } + } +} + +impl Palette { + pub fn color_for(&self, val: f64) -> Color { + if self.colors.is_empty() { + panic!("color_for() on empty palette"); + } else { + let mut best_v: Option = None; + for (k, v) in &self.colors { + if val < **k { + best_v = Some(*v); + } + } + best_v.expect("color_for() on empty palette?") + } + } +} \ No newline at end of file diff --git a/wxbox-tiler/Cargo.toml b/wxbox-tiler/Cargo.toml index 488c0b8..ef3bc42 100644 --- a/wxbox-tiler/Cargo.toml +++ b/wxbox-tiler/Cargo.toml @@ -13,4 +13,7 @@ ordered-float = "4" tikv-jemallocator = "0.6" reqwest = "0.12" flate2 = "1" -tokio = "1" \ No newline at end of file +tokio = "1" +wxbox-pal = { version = "0.1", path = "../wxbox-pal" } +png = "0.17" +mime = "0.3.17" \ No newline at end of file diff --git a/wxbox-tiler/src/grib2.rs b/wxbox-tiler/src/grib2.rs index 8dd8cd9..8f66157 100644 --- a/wxbox-tiler/src/grib2.rs +++ b/wxbox-tiler/src/grib2.rs @@ -10,23 +10,26 @@ use thiserror::Error; pub fn closest_key(map: &BTreeMap, V>, val: f64) -> Option> { let mut r1 = map.range(OrderedFloat(val)..); - let mut r2 = map.range(..OrderedFloat(val)); + let mut r2 = map.range(..=OrderedFloat(val)); let o1 = r1.next(); - let o2 = r2.next(); + let o2 = r2.last(); match (o1, o2) { - (None, None) => None, - (Some(i), None) => Some(*i.0), - (None, Some(i)) => Some(*i.0), + (None, None) => { + None + }, + (Some(i), None) => { + Some(*i.0) + }, + (None, Some(i)) => { + Some(*i.0) + }, (Some(i1), Some(i2)) => { // abs(f - i) - let i1_dist = (i1.0 - val).abs(); let i2_dist = (i2.0 - val).abs(); return Some(if i1_dist < i2_dist { - println!("closest_key() {} -> ({}, {}), ({}, {}) => {}", val, i1.0, i2.0, i1_dist, i2_dist, i1.0); *i1.0 } else { - println!("closest_key() {} -> ({}, {}), ({}, {}) => {}", val, i1.0, i2.0, i1_dist, i2_dist, i2.0); *i2.0 }) } @@ -178,7 +181,11 @@ pub async fn map_grib2(i: Response) -> Result // prepare the map for ((lat, long), value) in values { let lat = OrderedFloat(lat as f64); - let long = OrderedFloat(long as f64); + let mut long = long as f64; + if long > 180.0 { + long -= 360.0 + } + let long = OrderedFloat(long); let value = value as f64; if !map.contains_key(&lat) { map.insert(lat, BTreeMap::new()); diff --git a/wxbox-tiler/src/main.rs b/wxbox-tiler/src/main.rs index 810273e..b300cd7 100644 --- a/wxbox-tiler/src/main.rs +++ b/wxbox-tiler/src/main.rs @@ -1,6 +1,7 @@ pub(crate) mod sources; pub(crate) mod coords; mod grib2; +mod pixmap; use std::borrow::Cow; use std::collections::{BTreeMap, HashMap}; diff --git a/wxbox-tiler/src/pixmap.rs b/wxbox-tiler/src/pixmap.rs new file mode 100644 index 0000000..d5131f5 --- /dev/null +++ b/wxbox-tiler/src/pixmap.rs @@ -0,0 +1,37 @@ + + +use wxbox_pal::Color; + +pub struct Pixmap { + data: [Color; 256 * 256] +} +impl Pixmap { + #[inline] + pub fn new() -> Self { + Self { + data: [Color { red: 0, green: 0, blue: 0, alpha: 0 }; 256 * 256] + } + } + #[inline] + pub fn set(&mut self, x: usize, y: usize, color: Color) { + self.data[256 * x + y] = color; + } + + #[inline] + pub fn get(&self, x: usize, y: usize) -> Color { + self.data[256 * x + y] + } + + pub fn to_raw(self) -> [u8; 256 * 256 * 4] { + let mut output = [0u8; 256 * 256 * 4]; + + for (idx, color) in self.data.iter().enumerate() { + output[4 * idx] = color.red; + output[4 * idx + 1] = color.green; + output[4 * idx + 2] = color.blue; + output[4 * idx + 3] = color.alpha; + } + + output + } +} \ No newline at end of file diff --git a/wxbox-tiler/src/sources/noaa/mod.rs b/wxbox-tiler/src/sources/noaa/mod.rs index 376aad9..b792978 100644 --- a/wxbox-tiler/src/sources/noaa/mod.rs +++ b/wxbox-tiler/src/sources/noaa/mod.rs @@ -1,17 +1,26 @@ use std::borrow::Cow; use std::collections::BTreeMap; -use std::io::{BufReader, Cursor, Read}; +use std::io::{BufReader, BufWriter, Cursor, Read}; use std::time::{SystemTime, UNIX_EPOCH}; use actix_web::{get, HttpResponse, web}; -use actix_web::http::StatusCode; +use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; +use actix_web::http::{header, StatusCode}; use actix_web::web::Data; use flate2::read::GzDecoder; use grib::{GribError, SectionBody}; use grib::codetables::{CodeTable4_2, CodeTable4_3, Lookup}; use ordered_float::OrderedFloat; +use png::{BitDepth, ColorType, Encoder}; +use wxbox_pal::{Color, Palette}; use crate::{AppState, LutKey}; use crate::coords::bounds; use crate::grib2::{closest_key, lookup, map_grib2}; +use crate::pixmap::Pixmap; + +macro_rules! color { + ($v:expr,$r:expr,$g:expr,$b:expr) => { (OrderedFloat(f64::from($v)), Color { red: $r, green: $g, blue: $b, alpha: 255 }) }; + ($v:expr,$r:expr,$g:expr,$b:expr,$a:expr) => { (OrderedFloat(f64::from($v)), Color { red: $r, green: $g, blue: $b, alpha: $a }) }; +} #[get("/mrms_cref/{z}/{x}/{y}.png")] async fn mrms_cref(path: web::Path<(i32, i32, i32)>, data: Data) -> HttpResponse { @@ -22,26 +31,21 @@ async fn mrms_cref(path: web::Path<(i32, i32, i32)>, data: Data) -> Ht let tile = crate::coords::Tile::new(x, y, z); let bbox = bounds(tile); - - println!("{:?}", bbox); let mut needs_reload = false; - println!("{:?}", data.lut_cache_timestamps); let lct_reader = data.lut_cache_timestamps.read().await; if let Some(t) = lct_reader.get(&LutKey::NoaaMrmsCref) { let dur = SystemTime::now().duration_since(*t).expect("time went backwards").as_secs(); if dur > 120 { - println!("cache busted, dur = {}", dur); needs_reload = true; } } else { - println!("nothing in timestamp cache, redownloading"); needs_reload = true; } - std::mem::drop(lct_reader); + drop(lct_reader); if needs_reload { let mut lct_writer = data.lut_cache_timestamps.write().await; @@ -50,26 +54,97 @@ async fn mrms_cref(path: web::Path<(i32, i32, i32)>, data: Data) -> Ht let map = map_grib2(f).await.unwrap(); lc_writer.insert(LutKey::NoaaMrmsCref, map); lct_writer.insert(LutKey::NoaaMrmsCref, SystemTime::now()); - println!("{:?}", data.lut_cache_timestamps); } if let Some(map) = data.lut_cache.read().await.get(&LutKey::NoaaMrmsCref) { - let rows = map.range(closest_key(map, bbox.south).unwrap()..=closest_key(map, bbox.north).unwrap()); - println!("{}", map.len()); - println!("{:#?}", map.keys()); - println!("N {}/{:?} S {}/{:?}", bbox.south, closest_key(map, bbox.south), bbox.north, closest_key(map, bbox.north)); - let mut rowcount = 0; - let mut colcount = 0; - for row in rows { - rowcount += 1; - colcount = 0; - let cols = row.1.range(closest_key(row.1, bbox.west).unwrap()..=closest_key(row.1, bbox.east).unwrap()); - for col in cols { - colcount += 1; + let closest_south = match closest_key(&map, bbox.south) { + Some(c) => c, + None => { eprintln!("gridded LUT is empty?"); return HttpResponse::new(StatusCode::NOT_FOUND); } + }; + let closest_north = match closest_key(&map, bbox.north) { + Some(c) => c, + None => { eprintln!("gridded LUT is empty?"); return HttpResponse::new(StatusCode::NOT_FOUND); } + }; + + let pal = Palette { + colors: BTreeMap::from([ + color!(-30, 165, 165, 165, 0), + color!(10, 0, 165, 255), + color!(20, 16, 255, 8), + color!(35, 251, 238, 0), + color!(50, 255, 0, 0), + color!(65, 247, 1, 249), + color!(75, 255, 255, 255), + color!(85, 184, 184, 184), + color!(95, 184, 184, 184) + ]), + }; + + let mut image: Pixmap = Pixmap::new(); + + for (lat, row) in map.range(closest_south..=closest_north) { + let closest_west = match closest_key(&row, bbox.west) { + Some(c) => c, + None => { eprintln!("row LUT is empty?"); return HttpResponse::new(StatusCode::NOT_FOUND); } + }; + let closest_east = match closest_key(&row, bbox.east) { + Some(c) => c, + None => { eprintln!("row LUT is empty?"); return HttpResponse::new(StatusCode::NOT_FOUND); } + }; + for (long, gridpoint) in row.range(closest_west..=closest_east) { + let epsg_lon = **long; + let epsg_lat = **lat; + let wm_x = epsg_lon; + let wm_y = epsg_lat.tan().asinh(); + let x = 0.5 + wm_x / 360.0; + let y = 0.5 - wm_y / (2.0 * std::f64::consts::PI); + let zoom = z; + let n = 2.0_f64.powi(zoom); + let x_tile = n * x; + let y_tile = n * y; + + let x_pixel = x_tile.fract() * 256.0; + let y_pixel = y_tile.fract() * 256.0; + + let x = x_pixel.trunc() as usize; + let y = y_pixel.trunc() as usize; + + let mut color = pal.color_for(*gridpoint); + + let existing = image.get(x, y); + if existing != (Color { red: 0, green: 0, blue: 0, alpha: 0 }) { + color = color.average(existing); + } + + image.set(x, y, color); } } - println!("{}x{}", rowcount, colcount); - return HttpResponse::new(StatusCode::NOT_FOUND); + + let mut buf: Vec = vec![]; + // borrow checker insanity + { + let mut cur: Cursor<_> = Cursor::new(&mut buf); + let ref mut w = BufWriter::new(&mut cur); + let mut encoder = Encoder::new(w, 256, 256); + encoder.set_color(ColorType::Rgba); + encoder.set_depth(BitDepth::Eight); + encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455)); + encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); + let source_chromaticities = png::SourceChromaticities::new( + (0.31270, 0.32900), + (0.64000, 0.33000), + (0.30000, 0.60000), + (0.15000, 0.06000) + ); + encoder.set_source_chromaticities(source_chromaticities); + let mut writer = encoder.write_header().unwrap(); + writer.write_image_data(&image.to_raw()).expect("failed to encode png"); + writer.finish().unwrap(); + } + + HttpResponse::Ok() + .insert_header(header::ContentType(mime::IMAGE_PNG)) + .body(buf) } else { println!("gridded LUT not available for LutKey::NoaaMrmsCref while handling /mrms_cref/{z}/{x}/{y} tile request"); return HttpResponse::new(StatusCode::NOT_FOUND); diff --git a/wxbox-web/package.json b/wxbox-web/package.json index 5722488..94fa637 100644 --- a/wxbox-web/package.json +++ b/wxbox-web/package.json @@ -30,5 +30,6 @@ "typescript-eslint": "^8.0.0", "vite": "^5.0.3" }, - "type": "module" + "type": "module", + "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" }