diff --git a/config.toml b/config.toml index 6144703..4725f8a 100644 --- a/config.toml +++ b/config.toml @@ -1,4 +1,4 @@ -[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc] +[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_CONUS] from = "https://mrms.ncep.noaa.gov/data/2D/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz" needs_gzip = true valid_for = 120 @@ -13,4 +13,72 @@ Color: 70 255 255 255 128 128 128 Color: 80 128 128 128 """ missing = -99.0 +no_coverage = -999.0 + +[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_ALASKA] +from = "https://mrms.ncep.noaa.gov/data/2D/ALASKA/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz" +needs_gzip = true +valid_for = 120 +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 +""" +missing = -99.0 +no_coverage = -999.0 + +[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_CARIB] +from = "https://mrms.ncep.noaa.gov/data/2D/CARIB/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz" +needs_gzip = true +valid_for = 120 +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 +""" +missing = -99.0 +no_coverage = -999.0 + +[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_GUAM] +from = "https://mrms.ncep.noaa.gov/data/2D/GUAM/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz" +needs_gzip = true +valid_for = 120 +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 +""" +missing = -99.0 +no_coverage = -999.0 + +[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_HAWAII] +from = "https://mrms.ncep.noaa.gov/data/2D/HAWAII/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz" +needs_gzip = true +valid_for = 120 +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 +""" +missing = -99.0 no_coverage = -999.0 \ No newline at end of file diff --git a/wxbox-tiler/src/sources/grib2.rs b/wxbox-tiler/src/sources/grib2.rs index 2781aed..d3ab883 100644 --- a/wxbox-tiler/src/sources/grib2.rs +++ b/wxbox-tiler/src/sources/grib2.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, HashMap}; use std::f64::consts::PI; use std::io::{BufWriter, Cursor, Read}; -use std::ops::{Add, Mul, Sub}; +use std::ops::{Add, Div, Mul, Sub}; use std::sync::Arc; use std::time::SystemTime; use actix_web::error::UrlencodedError::ContentType; @@ -19,7 +19,7 @@ use wxbox_grib2::GribMessage; use wxbox_grib2::wgs84::LatLong; use wxbox_pal::{Color, ColorPalette, Palette}; use crate::AppState; -use tracing::debug; +use tracing::{debug, info}; use crate::config::Grib2Source; use crate::pixmap::Pixmap; @@ -75,7 +75,7 @@ pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lu const TWO_PI: f64 = PI * 2.0; const HALF_PI: f64 = PI / 2.0; -pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map: &Arc>, missing: Option, range_folded: Option, no_coverage: Option) -> Pixmap { +pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map: &Arc>, missing: Option, range_folded: Option, no_coverage: Option, options: &TileRequestOptions) -> Pixmap { let mut image: Pixmap = Pixmap::new(); let denominator = 2.0_f64.powi(z) * tilesize as f64; @@ -98,11 +98,24 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett }).map(|u| u as f64); let color = match nearest { - Some(c) if Some(c) == no_coverage => Color { red: 0, green: 0, blue: 0, alpha: 255 }, + Some(c) if Some(c) == no_coverage => Color { red: 0, green: 0, blue: 0, alpha: 30 }, Some(c) if Some(c) == missing => Color { red: 0, green: 0, blue: 0, alpha: 0 }, - Some(c) if Some(c) == range_folded => Color { red: 141, green: 0, blue: 160, alpha: 255 }, + Some(c) if Some(c) == range_folded => { + if options.show_range_folded { + let color_raw = options.range_folded_color.to_be_bytes(); + Color { red: color_raw[0], green: color_raw[1], blue: color_raw[2], alpha: color_raw[3] } + } else { + Color { red: 0, green: 0, blue: 0, alpha: 0 } + } + }, Some(value_at_pos) => { - pal.colorize(value_at_pos) + let mut c = pal.colorize(value_at_pos); + if c.red == 0 && c.green == 0 && c.blue == 0 { + c.alpha = 0; + } else { + c.alpha = coloru8(options.data_transparency); + } + c }, None => Color { red: 0, green: 0, blue: 0, alpha: 30 } }; @@ -214,22 +227,48 @@ impl Mul for ColorF64 { } } } +impl Div for ColorF64 { + type Output = ColorF64; + + fn div(self, rhs: Self) -> Self::Output { + Self { + red: self.red / rhs.red, + green: self.green / rhs.green, + blue: self.blue / rhs.blue, + alpha: self.alpha / rhs.alpha + } + } +} +impl Div for ColorF64 { + type Output = ColorF64; + + fn div(self, rhs: f64) -> Self::Output { + Self { + red: self.red / rhs, + green: self.green / rhs, + blue: self.blue / rhs, + alpha: self.alpha / rhs + } + } +} -pub fn merge(base: Pixmap, data: Pixmap, settings: &TileRequestOptions) -> Pixmap { +pub fn merge(base: Pixmap, data: Pixmap) -> Pixmap { let mut new = Pixmap::new(); for x in 0..256 { for y in 0..256 { let mut c_b: ColorF64 = base.get(x, y).into(); - let a_b = 1.0; let mut c_s: ColorF64 = data.get(x, y).into(); - let a_s = settings.data_transparency; - let mut co = c_s * a_s + c_b * a_b * (1.0 - a_s); - co.alpha = 1.0; + if c_s.red == 0.0 && c_s.green == 0.0 && c_s.blue == 0.0 && c_s.alpha == 0.0 { + new.set(x, y, c_b.into()); + } else { + let mut co = (c_s * c_s.alpha + c_b * c_b.alpha * (1.0 - c_s.alpha)) / (c_s.alpha + c_b.alpha * (1.0 - c_s.alpha)); + co.alpha = 1.0; - new.set(x, y, co.into()); + new.set(x, y, co.into()); + } } } @@ -289,7 +328,7 @@ pub async fn source(path: actix_web::web::Path<(i32, u32, u32)>, req: Query, req: Query = vec![]; // borrow checker insanity diff --git a/wxbox_client/src/lib.rs b/wxbox_client/src/lib.rs index b3db134..5d53d54 100644 --- a/wxbox_client/src/lib.rs +++ b/wxbox_client/src/lib.rs @@ -1,14 +1,20 @@ +mod toggle_switch; + use std::collections::HashMap; use std::env::var; -use egui::{Align2, Frame, Window}; +use egui::{Align2, CollapsingHeader, Color32, Frame, UiBuilder, Window}; use egui::Context; use walkers::{HttpOptions, HttpTiles, MapMemory, Position, TileId, Tiles}; use walkers::sources::{Attribution, TileSource}; use wxbox_common::TileRequestOptions; +use crate::toggle_switch::toggle; pub struct WxboxApp { provider: HttpTiles, - map_memory: MapMemory + map_memory: MapMemory, + tile_request_options: TileRequestOptions, + + rf_color: Color32 } pub struct DynamicUrlSource { @@ -45,15 +51,19 @@ impl TileSource for DynamicUrlSource { impl WxboxApp { pub fn new(ctx: Context) -> Self { egui_extras::install_image_loaders(&ctx); + + let req_options = TileRequestOptions { + baselayer: "osm".to_string(), + + data: "grib2/noaa_mrms_merged_composite_reflectivity_qc_CONUS".to_string(), + data_transparency: 0.9, + show_range_folded: false, + range_folded_color: 0x8d00a0ff, + }; Self { provider: HttpTiles::with_options( - DynamicUrlSource::new_from(&TileRequestOptions { - baselayer: "osm".to_string(), - - data: "grib2/noaa_mrms_merged_composite_reflectivity_qc".to_string(), - data_transparency: 0.9, - }), + DynamicUrlSource::new_from(&req_options), HttpOptions { cache: if cfg!(target_arch = "wasm32") { None @@ -64,7 +74,9 @@ impl WxboxApp { }, ctx.clone() ), - map_memory: MapMemory::default() + map_memory: MapMemory::default(), + tile_request_options: req_options, + rf_color: Color32::from_hex("#8d00a0ff").unwrap() } } } @@ -79,12 +91,19 @@ impl eframe::App for WxboxApp { egui::CentralPanel::default() .frame(rimless) .show(ctx, |ui| { + let Self { + provider, + map_memory, + tile_request_options, + rf_color + } = self; + let position = Position::from_lat_lon(44.967243, -103.771556); - let tiles = &mut self.provider; + let tiles = provider; let attribution = tiles.attribution(); - let map = walkers::Map::new(Some(tiles), &mut self.map_memory, position); + let map = walkers::Map::new(Some(tiles), map_memory, position); ui.add(map); @@ -100,9 +119,82 @@ impl eframe::App for WxboxApp { }); Window::new("wxbox") + .resizable(false) .show(ui.ctx(), |ui| { ui.label("Welcome to wxbox!") - }) + }); + + let mut need_to_reset_for_next_frame = false; + + Window::new("Datasource") + .resizable(false) + .show(ui.ctx(), |ui| { + ui.collapsing("NOAA", |ui| { + ui.collapsing("Multi-Radar Multi-Sensor", |ui| { + for location in ["CONUS", "ALASKA", "CARIB", "GUAM", "HAWAII"] { + ui.collapsing(location, |ui| { + ui.collapsing("Composite Reflectivity", |ui| { + if ui.radio_value(&mut tile_request_options.data, format!("grib2/noaa_mrms_merged_composite_reflectivity_qc_{}", location), "Composite Reflectivity (QCd)").changed() { + need_to_reset_for_next_frame = true; + } + }); + }); + } + }); + }); + }); + + Window::new("Datasource Settings") + .resizable(false) + .show(ui.ctx(), |ui| { + let mut ui_builder = UiBuilder::new(); + ui.scope_builder(ui_builder, |ui| { + egui::Grid::new("dsconfig_grid") + .num_columns(2) + .spacing([40.0, 4.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Data opacity"); + if ui.add(egui::Slider::new(&mut tile_request_options.data_transparency, 0.0..=1.0).suffix("%").custom_formatter( + |u, v| { + egui::emath::format_with_decimals_in_range(u * 100.0, v) + } + )).changed() { + need_to_reset_for_next_frame = true; + } + ui.end_row(); + + ui.label("Show range folded areas"); + if ui.add(toggle(&mut tile_request_options.show_range_folded)).changed() { + need_to_reset_for_next_frame = true; + } + ui.end_row(); + + ui.label("Range folded color"); + if ui.color_edit_button_srgba(rf_color).changed() { + let color = rf_color.to_array(); + let single_number = u32::from_be_bytes(color); + tile_request_options.range_folded_color = single_number; + need_to_reset_for_next_frame = true; + } + }) + }) + }); + + if need_to_reset_for_next_frame { + *tiles = HttpTiles::with_options( + DynamicUrlSource::new_from(tile_request_options), + HttpOptions { + cache: if cfg!(target_arch = "wasm32") { + None + } else { + Some(".cache".into()) + }, + user_agent: None, + }, + ctx.clone() + ); + } }); } } \ No newline at end of file diff --git a/wxbox_client/src/toggle_switch.rs b/wxbox_client/src/toggle_switch.rs new file mode 100644 index 0000000..f14b75b --- /dev/null +++ b/wxbox_client/src/toggle_switch.rs @@ -0,0 +1,111 @@ +//! Source code example of how to create your own widget. +//! This is meant to be read as a tutorial, hence the plethora of comments. + +/// iOS-style toggle switch: +/// +/// ``` text +/// _____________ +/// / /.....\ +/// | |.......| +/// \_______\_____/ +/// ``` +/// +/// ## Example: +/// ``` ignore +/// toggle_ui(ui, &mut my_bool); +/// ``` +pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { + // Widget code can be broken up in four steps: + // 1. Decide a size for the widget + // 2. Allocate space for it + // 3. Handle interactions with the widget (if any) + // 4. Paint the widget + + // 1. Deciding widget size: + // You can query the `ui` how much space is available, + // but in this example we have a fixed size widget based on the height of a standard button: + let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); + + // 2. Allocating space: + // This is where we get a region of the screen assigned. + // We also tell the Ui to sense clicks in the allocated region. + let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); + + // 3. Interact: Time to check for clicks! + if response.clicked() { + *on = !*on; + response.mark_changed(); // report back that the value changed + } + + // Attach some meta-data to the response which can be used by screen readers: + response.widget_info(|| { + egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "") + }); + + // 4. Paint! + // Make sure we need to paint: + if ui.is_rect_visible(rect) { + // Let's ask for a simple animation from egui. + // egui keeps track of changes in the boolean associated with the id and + // returns an animated value in the 0-1 range for how much "on" we are. + let how_on = ui.ctx().animate_bool_responsive(response.id, *on); + // We will follow the current style by asking + // "how should something that is being interacted with be painted?". + // This will, for instance, give us different colors when the widget is hovered or clicked. + let visuals = ui.style().interact_selectable(&response, *on); + // All coordinates are in absolute screen coordinates so we use `rect` to place the elements. + let rect = rect.expand(visuals.expansion); + let radius = 0.5 * rect.height(); + ui.painter() + .rect(rect, radius, visuals.bg_fill, visuals.bg_stroke); + // Paint the circle, animating it from left to right with `how_on`: + let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on); + let center = egui::pos2(circle_x, rect.center().y); + ui.painter() + .circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke); + } + + // All done! Return the interaction response so the user can check what happened + // (hovered, clicked, ...) and maybe show a tooltip: + response +} + +/// Here is the same code again, but a bit more compact: +#[allow(dead_code)] +fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { + let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0); + let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click()); + if response.clicked() { + *on = !*on; + response.mark_changed(); + } + response.widget_info(|| { + egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "") + }); + + if ui.is_rect_visible(rect) { + let how_on = ui.ctx().animate_bool_responsive(response.id, *on); + let visuals = ui.style().interact_selectable(&response, *on); + let rect = rect.expand(visuals.expansion); + let radius = 0.5 * rect.height(); + ui.painter() + .rect(rect, radius, visuals.bg_fill, visuals.bg_stroke); + let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on); + let center = egui::pos2(circle_x, rect.center().y); + ui.painter() + .circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke); + } + + response +} + +// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))` +/// iOS-style toggle switch. +/// +/// ## Example: +/// ``` ignore +/// ui.add(toggle(&mut my_bool)); +/// ``` +pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ { + move |ui: &mut egui::Ui| toggle_ui(ui, on) +} \ No newline at end of file diff --git a/wxbox_common/src/lib.rs b/wxbox_common/src/lib.rs index a839f83..d1e8ed0 100644 --- a/wxbox_common/src/lib.rs +++ b/wxbox_common/src/lib.rs @@ -5,5 +5,8 @@ pub struct TileRequestOptions { pub baselayer: String, pub data: String, - pub data_transparency: f64 + pub data_transparency: f64, + + pub show_range_folded: bool, + pub range_folded_color: u32 } \ No newline at end of file