ci workflow
Some checks are pending
build and test / wxbox - latest (push) Waiting to run

This commit is contained in:
core 2025-03-04 20:51:33 -05:00
parent 2927361680
commit 2a2f46a5ff
Signed by: core
GPG key ID: FDBF740DADDCEECF
20 changed files with 453 additions and 309 deletions

26
.forgejo/workflows/ci.yml Normal file
View file

@ -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

View file

@ -1,7 +0,0 @@
on: [push]
jobs:
test:
runs-on: docker
steps:
- run: echo All Good

View file

@ -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)
ImageError(#[from] ImageError),
}

View file

@ -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,7 +104,8 @@ 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)?;
@ -119,14 +124,16 @@ impl GribMessage {
product_definition,
data_representation,
bitmap,
data
data,
})
}
pub fn value_for(&self, coord: LatLong) -> Option<f32> {
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<u8>
pub reserved: Vec<u8>,
}
impl IdentificationSection {
fn parse<R: Read>(length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
@ -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<R: Read>(length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
@ -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<R: Read>(length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
@ -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<u8>) -> 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<f32> {
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<DynamicImage>
pub image: Option<DynamicImage>,
}
impl GridpointPNGDataRepresentation {
fn parse<R: Read>(_length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
@ -332,7 +348,7 @@ impl GridpointPNGDataRepresentation {
decimal_scale_factor,
depth,
type_of_values,
image: None
image: None,
})
}
fn load_data(&mut self, data: Vec<u8>) -> Result<(), GribError> {
@ -348,51 +364,54 @@ impl GridpointPNGDataRepresentation {
fn get_image_coordinate(&self, x: u32, y: u32) -> Option<f32> {
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<u8>
pub bitmap: Vec<u8>,
}
impl BitmapSection {
fn parse<R: Read>(length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
@ -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<u8>
pub data: Vec<u8>,
}
impl DataSection {
fn parse<R: Read>(length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
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<R: Read>(_length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
@ -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<R: Read>(length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
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,9 +632,6 @@ impl LatLongGrid {
}
}
fn convert_longitude(longitude: f64) -> f64 {
if longitude > 180.0 {
longitude - 360.0

View file

@ -2,13 +2,11 @@ use std::io::Read;
#[derive(Debug)]
pub struct NomReader<R: Read> {
inner: R
inner: R,
}
impl<R: Read> NomReader<R> {
pub fn new(inner: R) -> NomReader<R> {
Self {
inner
}
Self { inner }
}
pub fn read_u8(&mut self) -> Result<u8, std::io::Error> {

View file

@ -1,5 +1,5 @@
#[derive(Debug, Copy, Clone)]
pub struct LatLong {
pub lat: f64,
pub long: f64
pub long: f64,
}

View file

@ -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<f64>, OrderedFloat<f64>), (Color, Color))>;
@ -16,10 +16,20 @@ pub type Palette = Vec<((OrderedFloat<f64>, OrderedFloat<f64>), (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,
}
}

View file

@ -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<Palette, PaletteParseError> {
@ -52,7 +52,7 @@ pub fn parse(pal_str: &str) -> Result<Palette, PaletteParseError> {
} 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<Palette, PaletteParseError> {
} 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,7 +163,10 @@ 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))
)
]
)
}

View file

@ -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<String, Grib2Source>
pub grib2: HashMap<String, Grib2Source>,
}
#[derive(Deserialize)]
@ -19,5 +19,5 @@ pub struct Grib2Source {
pub palette: String,
pub missing: Option<f64>,
pub range_folded: Option<f64>,
pub no_coverage: Option<f64>
pub no_coverage: Option<f64>,
}

View file

@ -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<HashMap<String, Arc<RwLock<GribMessage>>>>,
grib2_cache_timestamps: RwLock<HashMap<String, SystemTime>>,
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
}

View file

@ -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]

View file

@ -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<HashMap<String, SystemTime>>, lutkey: &String, valid_for: u64) -> bool {
pub async fn needs_reload(
lct: &RwLock<HashMap<String, SystemTime>>,
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<u8> {
out
}
pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lut_key: &String, lct_cache: &RwLock<HashMap<String, SystemTime>>, lut_cache: &RwLock<HashMap<String, Arc<RwLock<GribMessage>>>>) {
pub async fn reload_if_required(
from: &str,
needs_gzip: bool,
valid_for: u64,
lut_key: &String,
lct_cache: &RwLock<HashMap<String, SystemTime>>,
lut_cache: &RwLock<HashMap<String, Arc<RwLock<GribMessage>>>>,
) {
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<RwLock<GribMessage>>, missing: Option<f64>, range_folded: Option<f64>, no_coverage: Option<f64>, options: &TileRequestOptions) -> Pixmap {
pub async fn render(
xtile: f64,
ytile: f64,
z: i32,
tilesize: usize,
pal: Palette,
map: &Arc<RwLock<GribMessage>>,
missing: Option<f64>,
range_folded: Option<f64>,
no_coverage: Option<f64>,
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<Color> for ColorF64 {
fn from(value: Color) -> Self {
@ -153,7 +204,7 @@ impl From<Color> 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<ColorF64> 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<ColorF64> 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<f64> 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<f64> 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<QueryReq>, data: Data<AppState>) -> HttpResponse {
pub async fn source(
path: actix_web::web::Path<(i32, u32, u32)>,
req: Query<QueryReq>,
data: Data<AppState>,
) -> 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<Quer
let base_layer: Pixmap = if settings.baselayer == "osm" {
let client = ClientBuilder::new()
.user_agent(format!("wxbox-tiler/{}", env!("CARGO_PKG_VERSION")))
.build().unwrap();
let body = client.get(format!("https://tile.openstreetmap.org/{}/{}/{}.png", path.0, path.1, path.2))
.build()
.unwrap();
let body = client
.get(format!(
"https://tile.openstreetmap.org/{}/{}/{}.png",
path.0, path.1, path.2
))
.send()
.await.unwrap().bytes().await.unwrap();
.await
.unwrap()
.bytes()
.await
.unwrap();
let mut img = ImageReader::new(Cursor::new(body.to_vec()));
img.set_format(ImageFormat::Png);
let img = img.decode().unwrap();
let img = img.decode().unwrap();
let rgb = img.into_rgba8();
// copy it into a pixmap
let mut map = Pixmap::new();
for x in 0..256_usize {
for y in 0..256_usize {
let pix = rgb.get_pixel(y as u32, x as u32);
map.set(x, y, Color {
red: pix[0],
green: pix[1],
blue: pix[2],
alpha: pix[3]
});
map.set(
x,
y,
Color {
red: pix[0],
green: pix[1],
blue: pix[2],
alpha: pix[3],
},
);
}
}
map
} else {
debug!("not found baselayer");
return HttpResponse::new(StatusCode::NOT_FOUND)
return HttpResponse::new(StatusCode::NOT_FOUND);
};
// data layer
@ -317,29 +385,47 @@ pub async fn source(path: actix_web::web::Path<(i32, u32, u32)>, req: Query<Quer
// - grib2/
let data_layer: Pixmap = if settings.data.starts_with("grib2/") {
if let Some(known_source) = data.config.sources.grib2.get(settings.data.strip_prefix("grib2/").unwrap()) {
if let Some(known_source) = data
.config
.sources
.grib2
.get(settings.data.strip_prefix("grib2/").unwrap())
{
reload_if_required(
&known_source.from,
known_source.needs_gzip,
known_source.valid_for.into(),
&settings.data,
&data.grib2_cache_timestamps,
&data.grib2_cache
).await;
&data.grib2_cache,
)
.await;
let lct_reader = data.grib2_cache_timestamps.read().await;
if let Some(grib2) = data.grib2_cache.read().await.get(&settings.data) {
crate::sources::grib2::render(path.1 as f64, path.2 as f64, path.0, 256, wxbox_pal::parser::parse(&known_source.palette).unwrap(), grib2, known_source.missing, known_source.range_folded, known_source.no_coverage, &settings).await
crate::sources::grib2::render(
path.1 as f64,
path.2 as f64,
path.0,
256,
wxbox_pal::parser::parse(&known_source.palette).unwrap(),
grib2,
known_source.missing,
known_source.range_folded,
known_source.no_coverage,
&settings,
)
.await
} else {
debug!("not found grib2 after reload in base cache");
return HttpResponse::new(StatusCode::NOT_FOUND)
return HttpResponse::new(StatusCode::NOT_FOUND);
}
} else {
debug!("not found grib2 in configuration");
return HttpResponse::new(StatusCode::NOT_FOUND)
return HttpResponse::new(StatusCode::NOT_FOUND);
}
} else {
debug!("not found datalayer registry");
return HttpResponse::new(StatusCode::NOT_FOUND)
return HttpResponse::new(StatusCode::NOT_FOUND);
};
let image = merge(base_layer, data_layer);
@ -358,11 +444,13 @@ pub async fn source(path: actix_web::web::Path<(i32, u32, u32)>, req: Query<Quer
(0.31270, 0.32900),
(0.64000, 0.33000),
(0.30000, 0.60000),
(0.15000, 0.06000)
(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
.write_image_data(&image.to_raw())
.expect("failed to encode png");
writer.finish().unwrap();
}

View file

@ -1,29 +1,29 @@
mod toggle_switch;
use crate::toggle_switch::toggle;
use egui::Context;
use egui::{Align2, CollapsingHeader, Color32, Frame, UiBuilder, Window};
use std::collections::HashMap;
use std::env::var;
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 walkers::{HttpOptions, HttpTiles, MapMemory, Position, TileId, Tiles};
use wxbox_common::TileRequestOptions;
use crate::toggle_switch::toggle;
pub struct WxboxApp {
provider: HttpTiles,
map_memory: MapMemory,
tile_request_options: TileRequestOptions,
rf_color: Color32
rf_color: Color32,
}
pub struct DynamicUrlSource {
pub url_query: String
pub url_query: String,
}
impl DynamicUrlSource {
pub fn new_from(options: &TileRequestOptions) -> 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);
@ -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(),
}
}
}

View file

@ -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())))),
)
}

View file

@ -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,7 +22,7 @@ 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")

View file

@ -8,5 +8,5 @@ pub struct TileRequestOptions {
pub data_transparency: f64,
pub show_range_folded: bool,
pub range_folded_color: u32
pub range_folded_color: u32,
}