From 2a2f46a5ffa6544f732d1cc5aecfcd23edcc600d Mon Sep 17 00:00:00 2001 From: core Date: Tue, 4 Mar 2025 20:51:33 -0500 Subject: [PATCH] ci workflow --- .forgejo/workflows/ci.yml | 26 +++ .forgejo/workflows/demo.yml | 7 - wxbox-grib2/src/error.rs | 6 +- wxbox-grib2/src/lib.rs | 271 +++++++++++++++------------- wxbox-grib2/src/nommer.rs | 8 +- wxbox-grib2/src/wgs84.rs | 4 +- wxbox-pal/src/default_palettes.rs | 2 +- wxbox-pal/src/lib.rs | 25 ++- wxbox-pal/src/parser.rs | 95 +++++----- wxbox-tiler/src/config.rs | 10 +- wxbox-tiler/src/main.rs | 22 +-- wxbox-tiler/src/pixmap.rs | 13 +- wxbox-tiler/src/sources/grib2.rs | 222 ++++++++++++++++------- wxbox-tiler/src/sources/mod.rs | 2 +- wxbox-tiler/src/sources/noaa/mod.rs | 2 +- wxbox_client/src/lib.rs | 31 ++-- wxbox_client/src/toggle_switch.rs | 2 +- wxbox_client_native/src/main.rs | 4 +- wxbox_client_wasm/src/main.rs | 6 +- wxbox_common/src/lib.rs | 4 +- 20 files changed, 453 insertions(+), 309 deletions(-) create mode 100644 .forgejo/workflows/ci.yml delete mode 100644 .forgejo/workflows/demo.yml diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..816b32e --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,26 @@ +name: build and test + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + name: wxbox - latest + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v4 + - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + - run: cargo build + - run: cargo test + - run: cargo clippy + - run: cargo fmt --check \ No newline at end of file diff --git a/.forgejo/workflows/demo.yml b/.forgejo/workflows/demo.yml deleted file mode 100644 index bda508a..0000000 --- a/.forgejo/workflows/demo.yml +++ /dev/null @@ -1,7 +0,0 @@ -on: [push] - -jobs: - test: - runs-on: docker - steps: - - run: echo All Good \ No newline at end of file diff --git a/wxbox-grib2/src/error.rs b/wxbox-grib2/src/error.rs index b98d005..79cd878 100644 --- a/wxbox-grib2/src/error.rs +++ b/wxbox-grib2/src/error.rs @@ -1,6 +1,6 @@ -use std::io; use image::ImageError; use png::DecodingError; +use std::io; use thiserror::Error; #[derive(Error, Debug)] @@ -38,5 +38,5 @@ pub enum GribError { #[error("image error")] PNGImageError(#[from] DecodingError), #[error("image error")] - ImageError(#[from] ImageError) -} \ No newline at end of file + ImageError(#[from] ImageError), +} diff --git a/wxbox-grib2/src/lib.rs b/wxbox-grib2/src/lib.rs index 2f3daac..694623d 100644 --- a/wxbox-grib2/src/lib.rs +++ b/wxbox-grib2/src/lib.rs @@ -2,16 +2,18 @@ pub mod error; mod nommer; pub mod wgs84; +use crate::error::GribError; +use crate::nommer::NomReader; +use crate::wgs84::LatLong; +use crate::LatLongVectorRelativity::{EasterlyAndNortherly, IncreasingXY}; +use image::codecs::png::PngDecoder; +use image::{ + DynamicImage, GenericImageView, ImageBuffer, ImageDecoder, ImageFormat, ImageReader, Luma, +}; use std::fmt::{Debug, Formatter}; use std::io::{Cursor, Read}; use std::time::Instant; -use image::codecs::png::PngDecoder; -use image::{DynamicImage, GenericImageView, ImageBuffer, ImageDecoder, ImageFormat, ImageReader, Luma}; use tracing::{debug, warn}; -use crate::error::GribError; -use crate::LatLongVectorRelativity::{EasterlyAndNortherly, IncreasingXY}; -use crate::nommer::NomReader; -use crate::wgs84::LatLong; pub const INDICATOR: u32 = u32::from_be_bytes(*b"GRIB"); pub const EDITION: u8 = 2; @@ -59,9 +61,11 @@ impl GribMessage { loop { let length = match nommer.read_u32() { Ok(l) => l, - Err(e) => return match e.kind() { - std::io::ErrorKind::UnexpectedEof => break, - _ => return Err(e.into()) + Err(e) => { + return match e.kind() { + std::io::ErrorKind::UnexpectedEof => break, + _ => return Err(e.into()), + } } }; @@ -74,22 +78,22 @@ impl GribMessage { match section_number { 1 => { identification = Some(IdentificationSection::parse(length, &mut nommer)?); - }, + } 3 => { grid_definition = Some(GridDefinitionSection::parse(length, &mut nommer)?); - }, + } 4 => { - product_definition = Some(ProductDefinitionSection::parse(length, &mut nommer)?); - }, + product_definition = + Some(ProductDefinitionSection::parse(length, &mut nommer)?); + } 5 => { - data_representation = Some(DataRepresentationSection::parse(length, &mut nommer)?); - }, + data_representation = + Some(DataRepresentationSection::parse(length, &mut nommer)?); + } 6 => { bitmap = Some(BitmapSection::parse(length, &mut nommer)?); - }, - 7 => { - data = Some(DataSection::parse(length, &mut nommer)?) - }, + } + 7 => data = Some(DataSection::parse(length, &mut nommer)?), _ => { let _ = nommer.read_n((length - 5) as usize); warn!("unimplemented section # {} len {}", section_number, length) @@ -100,12 +104,13 @@ impl GribMessage { let identification = identification.ok_or(GribError::MissingIdentification)?; let grid_definition = grid_definition.ok_or(GribError::MissingGridDefinition)?; let product_definition = product_definition.ok_or(GribError::MissingProductDefinition)?; - let mut data_representation = data_representation.ok_or(GribError::MissingDataRepresentation)?; + let mut data_representation = + data_representation.ok_or(GribError::MissingDataRepresentation)?; let bitmap = bitmap.ok_or(GribError::MissingBitmap)?; let data = data.ok_or(GribError::MissingData)?; debug!("{:?}", data_representation); - + data_representation.load_data(data.data.clone())?; Ok(Self { @@ -119,14 +124,16 @@ impl GribMessage { product_definition, data_representation, bitmap, - data + data, }) } pub fn value_for(&self, coord: LatLong) -> Option { match self.grid_definition.image_coordinates(coord) { - Some((i, j)) => self.data_representation.get_image_coordinate(i as u32, j as u32), - None => None + Some((i, j)) => self + .data_representation + .get_image_coordinate(i as u32, j as u32), + None => None, } } } @@ -146,7 +153,7 @@ pub struct IdentificationSection { pub second: u8, pub production_status: u8, pub processed_data_type: u8, - pub reserved: Vec + pub reserved: Vec, } impl IdentificationSection { fn parse(length: u32, nommer: &mut NomReader) -> Result { @@ -178,12 +185,11 @@ impl IdentificationSection { second, production_status, processed_data_type, - reserved + reserved, }) } } - #[derive(Debug)] pub struct GridDefinitionSection { pub source: u8, @@ -191,7 +197,7 @@ pub struct GridDefinitionSection { pub length_of_optional_number_list: u8, pub interpretation_of_number_list: u8, pub grid_definition_template_number: u16, - pub grid_definition: GridDefinition + pub grid_definition: GridDefinition, } impl GridDefinitionSection { fn parse(length: u32, nommer: &mut NomReader) -> Result { @@ -211,7 +217,6 @@ impl GridDefinitionSection { let interpretation_of_number_list = nommer.read_u8()?; let grid_definition_template_number = nommer.read_u16()?; - let grid_definition = match grid_definition_template_number { 0 => GridDefinition::LatitudeLongitude(LatLongGrid::parse(length, nommer)?), _ => return Err(GribError::UnsupportedGrid(grid_definition_template_number)), @@ -223,7 +228,7 @@ impl GridDefinitionSection { length_of_optional_number_list, interpretation_of_number_list, grid_definition_template_number, - grid_definition + grid_definition, }) } @@ -243,20 +248,25 @@ impl ProductDefinitionSection { let number_of_coordinate_values_after_template = nommer.read_u16()?; let product_definition_template_number = nommer.read_u16()?; - if number_of_coordinate_values_after_template != 0 { return Err(GribError::ListOfNumbersNotSupported); } let product_definition = match product_definition_template_number { - 0 => ProductDefinition::HorizontalLayerAtPointInTime(HorizontalLayerInstantaneousProductDef::parse(length, nommer)?), - _ => return Err(GribError::UnsupportedProductDefTmpl(product_definition_template_number)) + 0 => ProductDefinition::HorizontalLayerAtPointInTime( + HorizontalLayerInstantaneousProductDef::parse(length, nommer)?, + ), + _ => { + return Err(GribError::UnsupportedProductDefTmpl( + product_definition_template_number, + )) + } }; Ok(Self { number_of_coordinate_values_after_template, product_definition_template_number, - product_definition + product_definition, }) } } @@ -265,7 +275,7 @@ impl ProductDefinitionSection { pub struct DataRepresentationSection { pub number_of_data_points: u32, pub data_representation_template_number: u16, - pub data_representation_template: DataRepresentationTemplate + pub data_representation_template: DataRepresentationTemplate, } impl DataRepresentationSection { fn parse(length: u32, nommer: &mut NomReader) -> Result { @@ -273,14 +283,20 @@ impl DataRepresentationSection { let data_representation_template_number = nommer.read_u16()?; let data_representation_template = match data_representation_template_number { - 41 => DataRepresentationTemplate::GridpointPNG(GridpointPNGDataRepresentation::parse(length, nommer)?), - _ => return Err(GribError::UnsupportedDataRepresentationTemplate(data_representation_template_number)) + 41 => DataRepresentationTemplate::GridpointPNG(GridpointPNGDataRepresentation::parse( + length, nommer, + )?), + _ => { + return Err(GribError::UnsupportedDataRepresentationTemplate( + data_representation_template_number, + )) + } }; Ok(Self { number_of_data_points, data_representation_template_number, - data_representation_template + data_representation_template, }) } @@ -294,17 +310,17 @@ impl DataRepresentationSection { #[derive(Debug)] pub enum DataRepresentationTemplate { - GridpointPNG(GridpointPNGDataRepresentation) + GridpointPNG(GridpointPNGDataRepresentation), } impl DataRepresentationTemplate { fn load_data(&mut self, data: Vec) -> Result<(), GribError> { match self { - Self::GridpointPNG(png) => png.load_data(data) + Self::GridpointPNG(png) => png.load_data(data), } } fn get_image_coordinate(&self, x: u32, y: u32) -> Option { match self { - Self::GridpointPNG(png) => png.get_image_coordinate(x, y) + Self::GridpointPNG(png) => png.get_image_coordinate(x, y), } } } @@ -317,7 +333,7 @@ pub struct GridpointPNGDataRepresentation { pub depth: u8, pub type_of_values: u8, - pub image: Option + pub image: Option, } impl GridpointPNGDataRepresentation { fn parse(_length: u32, nommer: &mut NomReader) -> Result { @@ -332,7 +348,7 @@ impl GridpointPNGDataRepresentation { decimal_scale_factor, depth, type_of_values, - image: None + image: None, }) } fn load_data(&mut self, data: Vec) -> Result<(), GribError> { @@ -348,51 +364,54 @@ impl GridpointPNGDataRepresentation { fn get_image_coordinate(&self, x: u32, y: u32) -> Option { match &self.image { - Some(i) => { - match self.depth { - 1 | 2 | 4 | 8 | 16 => { - if x >= i.width() || y >= i.height() { - None - } else { - let datapoint = i.as_luma16().unwrap().get_pixel(x, y).0[0] as f32; - let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32); - let dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32)); - let value = (self.reference_value + diff) * dig_factor; - Some(value) - } - }, - 24 => { - if x >= i.width() || y >= i.height() { - None - } else { - let datapoint_channels = i.as_rgb8().unwrap().get_pixel(x, y).0; - - let datapoint = u32::from_be_bytes([0, datapoint_channels[0], datapoint_channels[1], datapoint_channels[2]]) as f32; - - let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32); - let dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32)); - let value = (self.reference_value + diff) * dig_factor; - Some(value) - } - }, - 32 => { - if x >= i.width() || y >= i.height() { - None - } else { - let datapoint_channels = i.as_rgba8().unwrap().get_pixel(x, y).0; - - let datapoint = u32::from_be_bytes(datapoint_channels) as f32; - - let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32); - let dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32)); - let value = (self.reference_value + diff) * dig_factor; - Some(value) - } - }, - _ => panic!("unsupported bit depth") + Some(i) => match self.depth { + 1 | 2 | 4 | 8 | 16 => { + if x >= i.width() || y >= i.height() { + None + } else { + let datapoint = i.as_luma16().unwrap().get_pixel(x, y).0[0] as f32; + let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32); + let dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32)); + let value = (self.reference_value + diff) * dig_factor; + Some(value) + } } + 24 => { + if x >= i.width() || y >= i.height() { + None + } else { + let datapoint_channels = i.as_rgb8().unwrap().get_pixel(x, y).0; + + let datapoint = u32::from_be_bytes([ + 0, + datapoint_channels[0], + datapoint_channels[1], + datapoint_channels[2], + ]) as f32; + + let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32); + let dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32)); + let value = (self.reference_value + diff) * dig_factor; + Some(value) + } + } + 32 => { + if x >= i.width() || y >= i.height() { + None + } else { + let datapoint_channels = i.as_rgba8().unwrap().get_pixel(x, y).0; + + let datapoint = u32::from_be_bytes(datapoint_channels) as f32; + + let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32); + let dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32)); + let value = (self.reference_value + diff) * dig_factor; + Some(value) + } + } + _ => panic!("unsupported bit depth"), }, - None => None + None => None, } } } @@ -400,7 +419,7 @@ impl GridpointPNGDataRepresentation { #[derive(Debug)] pub struct BitmapSection { pub indicator: u8, - pub bitmap: Vec + pub bitmap: Vec, } impl BitmapSection { fn parse(length: u32, nommer: &mut NomReader) -> Result { @@ -410,22 +429,17 @@ impl BitmapSection { } let bitmap = nommer.read_n((length - 6) as usize)?; - Ok(Self { - indicator, - bitmap - }) + Ok(Self { indicator, bitmap }) } } pub struct DataSection { - pub data: Vec + pub data: Vec, } impl DataSection { fn parse(length: u32, nommer: &mut NomReader) -> Result { let data = nommer.read_n((length - 5) as usize)?; - Ok(Self { - data - }) + Ok(Self { data }) } } impl Debug for DataSection { @@ -454,7 +468,7 @@ pub struct HorizontalLayerInstantaneousProductDef { pub scaled_value_first_fixed_surface: u32, pub second_fixed_surface_type: u8, pub scale_factor_second_fixed_surface: u8, - pub scaled_value_second_fixed_surface: u32 + pub scaled_value_second_fixed_surface: u32, } impl HorizontalLayerInstantaneousProductDef { fn parse(_length: u32, nommer: &mut NomReader) -> Result { @@ -495,12 +509,12 @@ impl HorizontalLayerInstantaneousProductDef { #[derive(Debug)] pub enum GridDefinition { - LatitudeLongitude(LatLongGrid) + LatitudeLongitude(LatLongGrid), } impl GridDefinition { fn image_coordinates(&self, at: LatLong) -> Option<(u64, u64)> { match self { - GridDefinition::LatitudeLongitude(grid) => grid.image_coordinates(at) + GridDefinition::LatitudeLongitude(grid) => grid.image_coordinates(at), } } } @@ -532,37 +546,41 @@ pub struct LatLongGrid { #[derive(Debug)] pub enum LatLongVectorRelativity { EasterlyAndNortherly, - IncreasingXY + IncreasingXY, } impl LatLongGrid { fn parse(length: u32, nommer: &mut NomReader) -> Result { if length != 72 { return Err(GribError::ListOfNumbersNotSupported); } - let shape_of_the_earth= nommer.read_u8()?; - let scale_factor_of_radius_of_spherical_earth= nommer.read_u8()?; - let scale_value_of_radius_of_spherical_earth= nommer.read_u32()?; - let scale_factor_of_major_axis_of_oblate_spheroid_earth= nommer.read_u8()?; - let scaled_value_of_major_axis_of_oblate_spheroid_earth= nommer.read_u32()?; - let scale_factor_of_minor_axis_of_oblate_spheroid_earth= nommer.read_u8()?; - let scaled_value_of_minor_axis_of_oblate_spheroid_earth= nommer.read_u32()?; - let ni= nommer.read_u32()?; // number of points along a parallel - let nj= nommer.read_u32()?; // number of points along a meridian - let basic_angle_of_the_initial_production_domain= nommer.read_u32()?; - let subdivisions_of_basic_angle= nommer.read_u32()?; - let la1= nommer.read_u32()? as f64 * 10.0_f64.powi(-6); - let lo1= convert_longitude(nommer.read_u32()? as f64 * 10.0_f64.powi(-6)); - let resolution_and_component_flags= nommer.read_u8()?; + let shape_of_the_earth = nommer.read_u8()?; + let scale_factor_of_radius_of_spherical_earth = nommer.read_u8()?; + let scale_value_of_radius_of_spherical_earth = nommer.read_u32()?; + let scale_factor_of_major_axis_of_oblate_spheroid_earth = nommer.read_u8()?; + let scaled_value_of_major_axis_of_oblate_spheroid_earth = nommer.read_u32()?; + let scale_factor_of_minor_axis_of_oblate_spheroid_earth = nommer.read_u8()?; + let scaled_value_of_minor_axis_of_oblate_spheroid_earth = nommer.read_u32()?; + let ni = nommer.read_u32()?; // number of points along a parallel + let nj = nommer.read_u32()?; // number of points along a meridian + let basic_angle_of_the_initial_production_domain = nommer.read_u32()?; + let subdivisions_of_basic_angle = nommer.read_u32()?; + let la1 = nommer.read_u32()? as f64 * 10.0_f64.powi(-6); + let lo1 = convert_longitude(nommer.read_u32()? as f64 * 10.0_f64.powi(-6)); + let resolution_and_component_flags = nommer.read_u8()?; let i_direction_increments_given = resolution_and_component_flags & (1 << 5) != 0; let j_direction_increments_given = resolution_and_component_flags & (1 << 4) != 0; - let vector_relativity = if resolution_and_component_flags & (1 << 3) != 0 { IncreasingXY } else { EasterlyAndNortherly }; + let vector_relativity = if resolution_and_component_flags & (1 << 3) != 0 { + IncreasingXY + } else { + EasterlyAndNortherly + }; - let la2= nommer.read_u32()? as f64 * 10.0_f64.powi(-6); - let lo2= convert_longitude(nommer.read_u32()? as f64 * 10.0_f64.powi(-6)); - let di= nommer.read_u32()? as f64 * 10.0_f64.powi(-6); - let dj= nommer.read_u32()? as f64 * 10.0_f64.powi(-6); - let scanning_mode_flags= nommer.read_u8()?; + let la2 = nommer.read_u32()? as f64 * 10.0_f64.powi(-6); + let lo2 = convert_longitude(nommer.read_u32()? as f64 * 10.0_f64.powi(-6)); + let di = nommer.read_u32()? as f64 * 10.0_f64.powi(-6); + let dj = nommer.read_u32()? as f64 * 10.0_f64.powi(-6); + let scanning_mode_flags = nommer.read_u8()?; Ok(Self { shape_of_the_earth, @@ -590,10 +608,14 @@ impl LatLongGrid { } fn image_coordinates(&self, at: LatLong) -> Option<(u64, u64)> { - if at.lat > self.la1 || at.lat < self.la2 { return None; } - if at.long < self.lo1 || at.long > self.lo2 { return None; } + if at.lat > self.la1 || at.lat < self.la2 { + return None; + } + if at.long < self.lo1 || at.long > self.lo2 { + return None; + } - let lat = ((at.lat - self.la1) / self.di ).round() * self.di + self.la1; + let lat = ((at.lat - self.la1) / self.di).round() * self.di + self.la1; let long = ((at.long - self.lo1) / self.dj).round() * self.dj + self.lo1; if (lat - at.lat).abs() > self.di || (long - at.long).abs() > self.dj { @@ -610,13 +632,10 @@ impl LatLongGrid { } } - - - fn convert_longitude(longitude: f64) -> f64 { if longitude > 180.0 { longitude - 360.0 } else { longitude } -} \ No newline at end of file +} diff --git a/wxbox-grib2/src/nommer.rs b/wxbox-grib2/src/nommer.rs index 872894d..7f8059b 100644 --- a/wxbox-grib2/src/nommer.rs +++ b/wxbox-grib2/src/nommer.rs @@ -2,13 +2,11 @@ use std::io::Read; #[derive(Debug)] pub struct NomReader { - inner: R + inner: R, } impl NomReader { pub fn new(inner: R) -> NomReader { - Self { - inner - } + Self { inner } } pub fn read_u8(&mut self) -> Result { @@ -41,4 +39,4 @@ impl NomReader { self.inner.read_exact(&mut buf)?; Ok(f32::from_be_bytes(buf)) } -} \ No newline at end of file +} diff --git a/wxbox-grib2/src/wgs84.rs b/wxbox-grib2/src/wgs84.rs index 92b1e18..e0bcf9b 100644 --- a/wxbox-grib2/src/wgs84.rs +++ b/wxbox-grib2/src/wgs84.rs @@ -1,5 +1,5 @@ #[derive(Debug, Copy, Clone)] pub struct LatLong { pub lat: f64, - pub long: f64 -} \ No newline at end of file + pub long: f64, +} diff --git a/wxbox-pal/src/default_palettes.rs b/wxbox-pal/src/default_palettes.rs index d0de313..2c042c1 100644 --- a/wxbox-pal/src/default_palettes.rs +++ b/wxbox-pal/src/default_palettes.rs @@ -6,4 +6,4 @@ 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/wxbox-pal/src/lib.rs b/wxbox-pal/src/lib.rs index cdb6305..42c8f95 100644 --- a/wxbox-pal/src/lib.rs +++ b/wxbox-pal/src/lib.rs @@ -1,5 +1,5 @@ -pub mod parser; pub mod default_palettes; +pub mod parser; pub use ordered_float::OrderedFloat; @@ -8,7 +8,7 @@ pub struct Color { pub red: u8, pub green: u8, pub blue: u8, - pub alpha: u8 + pub alpha: u8, } pub type Palette = Vec<((OrderedFloat, OrderedFloat), (Color, Color))>; @@ -16,10 +16,20 @@ pub type Palette = Vec<((OrderedFloat, OrderedFloat), (Color, Color))> #[macro_export] macro_rules! c { ($r:expr,$g:expr,$b:expr) => { - crate::Color { red: $r, green: $g, blue: $b, alpha: 255 } + crate::Color { + red: $r, + green: $g, + blue: $b, + alpha: 255, + } }; ($r:expr,$g:expr,$b:expr,$a:expr) => { - crate::Color { red: $r, green: $g, blue: $b, alpha: $a } + crate::Color { + red: $r, + green: $g, + blue: $b, + alpha: $a, + } }; } @@ -36,9 +46,8 @@ pub trait ColorPalette { impl ColorPalette for Palette { fn colorize(&self, at: f64) -> Color { for (raw_range, color_range) in self { - let range = *raw_range.0 .. *raw_range.1; + let range = *raw_range.0..*raw_range.1; if range.contains(&at) { - let mapped_end = *raw_range.1 - *raw_range.0; let mapped_point = at - *raw_range.0; let t = mapped_point / mapped_end; @@ -53,7 +62,7 @@ impl ColorPalette for Palette { red: 0, green: 0, blue: 0, - alpha: 0 + alpha: 0, } } } @@ -66,6 +75,6 @@ fn lerp_color(c0: Color, c1: Color, t: f64) -> Color { red: lerp(c0.red as f64, c1.red as f64, t) as u8, green: lerp(c0.green as f64, c1.green as f64, t) as u8, blue: lerp(c0.blue as f64, c1.blue as f64, t) as u8, - alpha: lerp(c0.alpha as f64, c1.alpha as f64, t) as u8 + alpha: lerp(c0.alpha as f64, c1.alpha as f64, t) as u8, } } diff --git a/wxbox-pal/src/parser.rs b/wxbox-pal/src/parser.rs index 9a2d07f..1e11284 100644 --- a/wxbox-pal/src/parser.rs +++ b/wxbox-pal/src/parser.rs @@ -1,7 +1,7 @@ +use crate::{c, r, Palette}; use std::num::{ParseFloatError, ParseIntError}; use std::str::FromStr; use thiserror::Error; -use crate::{c, Palette, r}; #[derive(Error, Debug, Clone)] pub enum PaletteParseError { @@ -16,7 +16,7 @@ pub enum PaletteParseError { #[error("invalid int")] InvalidInt(#[from] ParseIntError), #[error("palette is empty")] - PaletteEmpty + PaletteEmpty, } pub fn parse(pal_str: &str) -> Result { @@ -52,7 +52,7 @@ pub fn parse(pal_str: &str) -> Result { } else { return Err(PaletteParseError::IncorrectColorCommandLength(tokens.len())); } - }, + } "color4:" => { if tokens.len() == 6 { // color4: val r g b a (5 tkns + command = 6 total) @@ -77,54 +77,54 @@ pub fn parse(pal_str: &str) -> Result { } else { return Err(PaletteParseError::IncorrectColorCommandLength(tokens.len())); } - }, - "product:" => {}, // valid but ignored - "units:" => {}, // valid but ignored - "step:" => {}, // valid but ignored - unknown => return Err(PaletteParseError::UnknownCommand(unknown.to_string())) + } + "product:" => {} // valid but ignored + "units:" => {} // valid but ignored + "step:" => {} // valid but ignored + unknown => return Err(PaletteParseError::UnknownCommand(unknown.to_string())), } } if parsed_data.is_empty() { - return Err(PaletteParseError::PaletteEmpty) + return Err(PaletteParseError::PaletteEmpty); } let mut intermediate_with_infinities_added: Vec<(f64, u8, u8, u8, u8, u8, u8, u8, u8)> = vec![]; - intermediate_with_infinities_added.push( - ( - f64::NEG_INFINITY, - parsed_data[0].1, - parsed_data[0].2, - parsed_data[0].3, - parsed_data[0].4, - parsed_data[0].1, - parsed_data[0].2, - parsed_data[0].3, - parsed_data[0].4, - ) - ); + intermediate_with_infinities_added.push(( + f64::NEG_INFINITY, + parsed_data[0].1, + parsed_data[0].2, + parsed_data[0].3, + parsed_data[0].4, + parsed_data[0].1, + parsed_data[0].2, + parsed_data[0].3, + parsed_data[0].4, + )); intermediate_with_infinities_added.append(&mut parsed_data); - intermediate_with_infinities_added.push( - ( - f64::INFINITY, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].1, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].2, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].3, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].4, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].1, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].2, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].3, - intermediate_with_infinities_added[intermediate_with_infinities_added.len()-1].4, - ) - ); + intermediate_with_infinities_added.push(( + f64::INFINITY, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].1, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].2, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].3, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].4, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].1, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].2, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].3, + intermediate_with_infinities_added[intermediate_with_infinities_added.len() - 1].4, + )); let mut output = vec![]; for range in intermediate_with_infinities_added.windows(2) { - output.push( - (r!(range[0].0, range[1].0), (c!(range[0].1, range[0].2, range[0].3, range[0].4), c!(range[0].5, range[0].6, range[0].7, range[0].8))) - ) + output.push(( + r!(range[0].0, range[1].0), + ( + c!(range[0].1, range[0].2, range[0].3, range[0].4), + c!(range[0].5, range[0].6, range[0].7, range[0].8), + ), + )) } Ok(output) @@ -138,7 +138,8 @@ mod tests { #[test] fn almanydesigns_radaromega_nws_default() { assert_eq!( - parse(r#"color: -30 165 165 165 8 230 230 + parse( + r#"color: -30 165 165 165 8 230 230 color: 10 0 165 255 0 8 197 color: 20 16 255 8 10 126 3 color: 35 251 238 0 210 112 2 @@ -146,9 +147,14 @@ color: 50 255 0 0 171 0 1 color: 65 247 1 249 136 63 174 color: 75 255 255 255 184 184 184 color: 85 184 184 184 -color: 95 184 184 184"#).unwrap(), +color: 95 184 184 184"# + ) + .unwrap(), vec![ - (r!(f64::NEG_INFINITY, -30.0), (c!(165, 165, 165), c!(165, 165, 165))), + ( + r!(f64::NEG_INFINITY, -30.0), + (c!(165, 165, 165), c!(165, 165, 165)) + ), (r!(-30.0, 10.0), (c!(165, 165, 165), c!(8, 230, 230))), (r!(10.0, 20.0), (c!(0, 165, 255), c!(0, 8, 197))), (r!(20.0, 35.0), (c!(16, 255, 8), c!(10, 126, 3))), @@ -157,8 +163,11 @@ color: 95 184 184 184"#).unwrap(), (r!(65.0, 75.0), (c!(247, 1, 249), c!(136, 63, 174))), (r!(75.0, 85.0), (c!(255, 255, 255), c!(184, 184, 184))), (r!(85.0, 95.0), (c!(184, 184, 184), c!(184, 184, 184))), - (r!(95.0, f64::INFINITY), (c!(184, 184, 184), c!(184, 184, 184))) + ( + r!(95.0, f64::INFINITY), + (c!(184, 184, 184), c!(184, 184, 184)) + ) ] ) } -} \ No newline at end of file +} diff --git a/wxbox-tiler/src/config.rs b/wxbox-tiler/src/config.rs index 978a7bc..f1f3239 100644 --- a/wxbox-tiler/src/config.rs +++ b/wxbox-tiler/src/config.rs @@ -1,14 +1,14 @@ -use std::collections::HashMap; use serde::Deserialize; +use std::collections::HashMap; #[derive(Deserialize)] pub struct Config { - pub sources: Sources + pub sources: Sources, } #[derive(Deserialize)] pub struct Sources { - pub grib2: HashMap + pub grib2: HashMap, } #[derive(Deserialize)] @@ -19,5 +19,5 @@ pub struct Grib2Source { pub palette: String, pub missing: Option, pub range_folded: Option, - pub no_coverage: Option -} \ No newline at end of file + pub no_coverage: Option, +} diff --git a/wxbox-tiler/src/main.rs b/wxbox-tiler/src/main.rs index cc038f9..76ccd4d 100644 --- a/wxbox-tiler/src/main.rs +++ b/wxbox-tiler/src/main.rs @@ -1,24 +1,24 @@ pub(crate) mod sources; -mod pixmap; mod config; +mod pixmap; +use crate::config::Config; +use actix_web::web::Data; +use actix_web::{App, HttpServer}; use std::collections::{BTreeMap, HashMap}; use std::env::args; use std::fmt::Debug; use std::fs; use std::sync::Arc; -use tokio::sync::RwLock; use std::time::SystemTime; -use actix_web::{App, HttpServer}; -use actix_web::web::Data; +use tokio::sync::RwLock; use wxbox_grib2::GribMessage; -use crate::config::Config; pub struct AppState { grib2_cache: RwLock>>>, grib2_cache_timestamps: RwLock>, - config: Config + config: Config, } #[actix_web::main] @@ -32,16 +32,14 @@ async fn main() -> std::io::Result<()> { let data = Data::new(AppState { grib2_cache: RwLock::new(HashMap::new()), grib2_cache_timestamps: RwLock::new(HashMap::new()), - config + config, }); HttpServer::new(move || { App::new() .service(sources::grib2::source) .app_data(data.clone()) }) - .bind(("::", 8080))? - .run() - .await - + .bind(("::", 8080))? + .run() + .await } - diff --git a/wxbox-tiler/src/pixmap.rs b/wxbox-tiler/src/pixmap.rs index d5131f5..37c49a1 100644 --- a/wxbox-tiler/src/pixmap.rs +++ b/wxbox-tiler/src/pixmap.rs @@ -1,15 +1,18 @@ - - use wxbox_pal::Color; pub struct Pixmap { - data: [Color; 256 * 256] + data: [Color; 256 * 256], } impl Pixmap { #[inline] pub fn new() -> Self { Self { - data: [Color { red: 0, green: 0, blue: 0, alpha: 0 }; 256 * 256] + data: [Color { + red: 0, + green: 0, + blue: 0, + alpha: 0, + }; 256 * 256], } } #[inline] @@ -34,4 +37,4 @@ impl Pixmap { output } -} \ No newline at end of file +} diff --git a/wxbox-tiler/src/sources/grib2.rs b/wxbox-tiler/src/sources/grib2.rs index d3ab883..21d0ef9 100644 --- a/wxbox-tiler/src/sources/grib2.rs +++ b/wxbox-tiler/src/sources/grib2.rs @@ -1,33 +1,40 @@ +use crate::config::Grib2Source; +use crate::pixmap::Pixmap; +use crate::AppState; +use actix_web::error::UrlencodedError::ContentType; +use actix_web::http::StatusCode; +use actix_web::web::{Data, Query}; +use actix_web::HttpResponse; +use flate2::read::GzDecoder; +use image::{ImageFormat, ImageReader}; +use png::{BitDepth, ColorType, Encoder}; +use reqwest::ClientBuilder; +use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use std::f64::consts::PI; use std::io::{BufWriter, Cursor, Read}; use std::ops::{Add, Div, Mul, Sub}; use std::sync::Arc; use std::time::SystemTime; -use actix_web::error::UrlencodedError::ContentType; -use actix_web::http::StatusCode; -use actix_web::HttpResponse; -use actix_web::web::{Data, Query}; -use flate2::read::GzDecoder; -use png::{BitDepth, ColorType, Encoder}; -use serde::Deserialize; use tokio::sync::RwLock; -use wxbox_common::TileRequestOptions; -use image::{ImageFormat, ImageReader}; -use reqwest::ClientBuilder; -use wxbox_grib2::GribMessage; -use wxbox_grib2::wgs84::LatLong; -use wxbox_pal::{Color, ColorPalette, Palette}; -use crate::AppState; use tracing::{debug, info}; -use crate::config::Grib2Source; -use crate::pixmap::Pixmap; +use wxbox_common::TileRequestOptions; +use wxbox_grib2::wgs84::LatLong; +use wxbox_grib2::GribMessage; +use wxbox_pal::{Color, ColorPalette, Palette}; -pub async fn needs_reload(lct: &RwLock>, lutkey: &String, valid_for: u64) -> bool { +pub async fn needs_reload( + lct: &RwLock>, + lutkey: &String, + valid_for: u64, +) -> bool { let lct_reader = lct.read().await; if let Some(t) = lct_reader.get(lutkey) { - let dur = SystemTime::now().duration_since(*t).expect("time went backwards").as_secs(); + let dur = SystemTime::now() + .duration_since(*t) + .expect("time went backwards") + .as_secs(); if dur > valid_for { return true; } @@ -59,15 +66,24 @@ pub async fn load(url: &str, is_gzipped: bool) -> Vec { out } - -pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lut_key: &String, lct_cache: &RwLock>, lut_cache: &RwLock>>>) { +pub async fn reload_if_required( + from: &str, + needs_gzip: bool, + valid_for: u64, + lut_key: &String, + lct_cache: &RwLock>, + lut_cache: &RwLock>>>, +) { if needs_reload(&lct_cache, lut_key, valid_for).await { let mut lct_writer = lct_cache.write().await; let message = load(from, needs_gzip).await; let grib = GribMessage::new(Cursor::new(message)).unwrap(); - lut_cache.write().await.insert(lut_key.clone(), Arc::new(RwLock::new(grib))); + lut_cache + .write() + .await + .insert(lut_key.clone(), Arc::new(RwLock::new(grib))); lct_writer.insert(lut_key.clone(), SystemTime::now()); } } @@ -75,7 +91,18 @@ 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, options: &TileRequestOptions) -> 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; @@ -92,22 +119,41 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett let lon = (TWO_PI * tx - PI).to_degrees(); let lat = ((PI - TWO_PI * ty).exp().atan() * 2.0_f64 - HALF_PI).to_degrees(); - let nearest = message.value_for(LatLong { - lat, - long: lon - }).map(|u| u as f64); + let nearest = message + .value_for(LatLong { lat, long: lon }) + .map(|u| u as f64); let color = match nearest { - 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) == 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 => { 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] } + 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 } + Color { + red: 0, + green: 0, + blue: 0, + alpha: 0, + } } - }, + } Some(value_at_pos) => { let mut c = pal.colorize(value_at_pos); if c.red == 0 && c.green == 0 && c.blue == 0 { @@ -116,8 +162,13 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett c.alpha = coloru8(options.data_transparency); } c + } + None => Color { + red: 0, + green: 0, + blue: 0, + alpha: 30, }, - None => Color { red: 0, green: 0, blue: 0, alpha: 30 } }; image.set(y, x, color); @@ -130,7 +181,7 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett #[derive(Deserialize)] struct QueryReq { - settings: String + settings: String, } fn colorf64(i: u8) -> f64 { @@ -145,7 +196,7 @@ pub struct ColorF64 { pub red: f64, pub green: f64, pub blue: f64, - pub alpha: f64 + pub alpha: f64, } impl From for ColorF64 { fn from(value: Color) -> Self { @@ -153,7 +204,7 @@ impl From for ColorF64 { red: colorf64(value.red), green: colorf64(value.green), blue: colorf64(value.blue), - alpha: colorf64(value.alpha) + alpha: colorf64(value.alpha), } } } @@ -163,7 +214,7 @@ impl From for Color { red: coloru8(value.red), green: coloru8(value.green), blue: coloru8(value.blue), - alpha: coloru8(value.alpha) + alpha: coloru8(value.alpha), } } } @@ -175,7 +226,7 @@ impl Add for ColorF64 { red: self.red + rhs.red, green: self.green + rhs.green, blue: self.blue + rhs.blue, - alpha: self.alpha + rhs.alpha + alpha: self.alpha + rhs.alpha, } } } @@ -187,7 +238,7 @@ impl Sub for ColorF64 { red: self.red - rhs.red, green: self.green - rhs.green, blue: self.blue - rhs.blue, - alpha: self.alpha - rhs.alpha + alpha: self.alpha - rhs.alpha, } } } @@ -199,7 +250,7 @@ impl Sub for f64 { red: rhs.red - self, green: rhs.green - self, blue: rhs.blue - self, - alpha: rhs.alpha - self + alpha: rhs.alpha - self, } } } @@ -211,7 +262,7 @@ impl Mul for ColorF64 { red: self.red * rhs, blue: self.blue * rhs, green: self.green * rhs, - alpha: self.alpha * rhs + alpha: self.alpha * rhs, } } } @@ -223,7 +274,7 @@ impl Mul for ColorF64 { red: self.red * rhs.red, green: self.green * rhs.green, blue: self.blue * rhs.blue, - alpha: self.alpha * rhs.alpha + alpha: self.alpha * rhs.alpha, } } } @@ -235,7 +286,7 @@ impl Div for ColorF64 { red: self.red / rhs.red, green: self.green / rhs.green, blue: self.blue / rhs.blue, - alpha: self.alpha / rhs.alpha + alpha: self.alpha / rhs.alpha, } } } @@ -247,12 +298,11 @@ impl Div for ColorF64 { red: self.red / rhs, green: self.green / rhs, blue: self.blue / rhs, - alpha: self.alpha / rhs + alpha: self.alpha / rhs, } } } - pub fn merge(base: Pixmap, data: Pixmap) -> Pixmap { let mut new = Pixmap::new(); @@ -264,7 +314,8 @@ pub fn merge(base: Pixmap, data: Pixmap) -> Pixmap { 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)); + 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()); @@ -276,7 +327,11 @@ pub fn merge(base: Pixmap, data: Pixmap) -> Pixmap { } #[actix_web::get("/{z}/{x}/{y}.png")] -pub async fn source(path: actix_web::web::Path<(i32, u32, u32)>, req: Query, data: Data) -> HttpResponse { +pub async fn source( + path: actix_web::web::Path<(i32, u32, u32)>, + req: Query, + data: Data, +) -> HttpResponse { let settings: TileRequestOptions = serde_json::from_str(&req.settings).unwrap(); // todo: load the base layer from external source @@ -285,31 +340,44 @@ pub async fn source(path: actix_web::web::Path<(i32, u32, u32)>, req: Query, req: Query, req: Query Self { Self { - url_query: serde_json::to_string(options).unwrap() + url_query: serde_json::to_string(options).unwrap(), } } } @@ -32,7 +32,9 @@ impl TileSource for DynamicUrlSource { format!( "{}/{}/{}/{}.png?settings={}", env!("TILER_BASE_URL"), - tile_id.zoom, tile_id.x, tile_id.y, + tile_id.zoom, + tile_id.x, + tile_id.y, self.url_query ) } @@ -42,12 +44,11 @@ impl TileSource for DynamicUrlSource { text: "OpenStreetMap contributors, NOAA, wxbox", url: "https://copyright.wxbox.e3t.cc", logo_light: None, - logo_dark: None + logo_dark: None, } } } - impl WxboxApp { pub fn new(ctx: Context) -> Self { egui_extras::install_image_loaders(&ctx); @@ -60,7 +61,7 @@ impl WxboxApp { show_range_folded: false, range_folded_color: 0x8d00a0ff, }; - + Self { provider: HttpTiles::with_options( DynamicUrlSource::new_from(&req_options), @@ -72,11 +73,11 @@ impl WxboxApp { }, user_agent: None, }, - ctx.clone() + ctx.clone(), ), map_memory: MapMemory::default(), tile_request_options: req_options, - rf_color: Color32::from_hex("#8d00a0ff").unwrap() + rf_color: Color32::from_hex("#8d00a0ff").unwrap(), } } } @@ -149,7 +150,7 @@ impl eframe::App for WxboxApp { need_to_reset_for_next_frame = true; } }); - + if location == "CONUS" { ui.collapsing("RhoHV (Correlation Coefficient)", |ui| { if ui.radio_value(&mut tile_request_options.data, format!("grib2/noaa_mrms_merged_rhohv_3km_{}", location), "RhoHV @ 3.00 KM").changed() { @@ -209,4 +210,4 @@ impl eframe::App for WxboxApp { } }); } -} \ No newline at end of file +} diff --git a/wxbox_client/src/toggle_switch.rs b/wxbox_client/src/toggle_switch.rs index f14b75b..4aa07ad 100644 --- a/wxbox_client/src/toggle_switch.rs +++ b/wxbox_client/src/toggle_switch.rs @@ -108,4 +108,4 @@ fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response { /// ``` 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_client_native/src/main.rs b/wxbox_client_native/src/main.rs index 6724612..2b5bb2e 100644 --- a/wxbox_client_native/src/main.rs +++ b/wxbox_client_native/src/main.rs @@ -5,6 +5,6 @@ fn main() -> Result<(), eframe::Error> { eframe::run_native( "wxbox", Default::default(), - Box::new(|cc| Ok(Box::new(WxboxApp::new(cc.egui_ctx.clone())))) + Box::new(|cc| Ok(Box::new(WxboxApp::new(cc.egui_ctx.clone())))), ) -} \ No newline at end of file +} diff --git a/wxbox_client_wasm/src/main.rs b/wxbox_client_wasm/src/main.rs index 382e0aa..7148406 100644 --- a/wxbox_client_wasm/src/main.rs +++ b/wxbox_client_wasm/src/main.rs @@ -1,5 +1,5 @@ -use wxbox_client::WxboxApp; use web_sys::wasm_bindgen::JsCast; +use wxbox_client::WxboxApp; fn main() { console_error_panic_hook::set_once(); @@ -22,9 +22,9 @@ fn main() { .start( canvas, web_options, - Box::new(|cc| Ok(Box::new(WxboxApp::new(cc.egui_ctx.clone())))) + Box::new(|cc| Ok(Box::new(WxboxApp::new(cc.egui_ctx.clone())))), ) .await .expect("failed to start eframe") }) -} \ No newline at end of file +} diff --git a/wxbox_common/src/lib.rs b/wxbox_common/src/lib.rs index d1e8ed0..bb1ff82 100644 --- a/wxbox_common/src/lib.rs +++ b/wxbox_common/src/lib.rs @@ -8,5 +8,5 @@ pub struct TileRequestOptions { pub data_transparency: f64, pub show_range_folded: bool, - pub range_folded_color: u32 -} \ No newline at end of file + pub range_folded_color: u32, +}