From f42a4a739a95cb209db7b2ef4c56515f699d95a3 Mon Sep 17 00:00:00 2001 From: core Date: Sun, 27 Oct 2024 21:19:36 -0400 Subject: [PATCH] cool things --- wxbox-pal/src/default_palettes.rs | 9 + wxbox-pal/src/lib.rs | 30 ++-- wxbox-pal/src/parser.rs | 164 ++++++++++++++++++ wxbox-tiler/src/main.rs | 1 - wxbox-tiler/src/sources/grib2.rs | 26 ++- wxbox-tiler/src/sources/noaa/mod.rs | 6 +- wxbox-tiler/src/tile.rs | 184 --------------------- wxbox-web/src/lib/color.ts | 12 ++ wxbox-web/src/lib/global.css | 55 ++++++ wxbox-web/src/lib/map/Map.svelte | 96 +++++++++-- wxbox-web/src/lib/menubar/ButtonBar.svelte | 16 ++ wxbox-web/src/routes/+layout.svelte | 3 +- wxbox-web/src/routes/+page.svelte | 56 +++++-- 13 files changed, 422 insertions(+), 236 deletions(-) create mode 100644 wxbox-pal/src/default_palettes.rs create mode 100644 wxbox-pal/src/parser.rs delete mode 100644 wxbox-tiler/src/tile.rs create mode 100644 wxbox-web/src/lib/color.ts create mode 100644 wxbox-web/src/lib/global.css diff --git a/wxbox-pal/src/default_palettes.rs b/wxbox-pal/src/default_palettes.rs new file mode 100644 index 0000000..d0de313 --- /dev/null +++ b/wxbox-pal/src/default_palettes.rs @@ -0,0 +1,9 @@ +pub const DEFAULT_REFLECTIVITY_PALETTE: &str = r#"Color: 10 164 164 255 100 100 192 +Color: 20 64 128 255 32 64 128 +Color: 30 0 255 0 0 128 0 +Color: 40 255 255 0 255 128 0 +Color: 50 255 0 0 160 0 0 +Color: 60 255 0 255 128 0 128 +Color: 70 255 255 255 128 128 128 +Color: 80 128 128 128 +"#; \ No newline at end of file diff --git a/wxbox-pal/src/lib.rs b/wxbox-pal/src/lib.rs index 1658bc3..cdb6305 100644 --- a/wxbox-pal/src/lib.rs +++ b/wxbox-pal/src/lib.rs @@ -1,7 +1,9 @@ +pub mod parser; +pub mod default_palettes; -use ordered_float::OrderedFloat; +pub use ordered_float::OrderedFloat; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct Color { pub red: u8, pub green: u8, @@ -11,35 +13,23 @@ pub struct Color { pub type Palette = Vec<((OrderedFloat, OrderedFloat), (Color, Color))>; +#[macro_export] macro_rules! c { ($r:expr,$g:expr,$b:expr) => { - 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) => { - Color { red: $r, green: $g, blue: $b, alpha: $a } + crate::Color { red: $r, green: $g, blue: $b, alpha: $a } }; } + +#[macro_export] macro_rules! r { ($f:expr,$t:expr) => { - (OrderedFloat($f), OrderedFloat($t)) + (crate::OrderedFloat($f), crate::OrderedFloat($t)) }; } -pub fn create_test_palette() -> Palette { - let mut out = vec![]; - - out.push((r!(f64::NEG_INFINITY, 10.0), (c!(164, 164, 255), c!(164, 164, 255)))); - out.push((r!(10.0, 20.0), (c!(64, 128, 255, 128), c!(32, 64, 128, 192)))); - out.push((r!(20.0, 30.0), (c!(0, 255, 0, 192), c!(0, 128, 0, 192)))); - out.push((r!(30.0, 45.0), (c!(255, 255, 0, 192), c!(255, 128, 0, 192)))); - out.push((r!(45.0, 60.0), (c!(255, 0, 0, 192), c!(160, 0, 0, 192)))); - out.push((r!(60.0, 70.0), (c!(255, 0, 255, 192), c!(128, 0, 128, 192)))); - out.push((r!(70.0, 80.0), (c!(255, 255, 255, 192), c!(128, 128, 128, 192)))); - out.push((r!(80.0, f64::INFINITY), (c!(128, 128, 128, 192), c!(128, 128, 128, 192)))); - - out -} - pub trait ColorPalette { fn colorize(&self, at: f64) -> Color; } diff --git a/wxbox-pal/src/parser.rs b/wxbox-pal/src/parser.rs new file mode 100644 index 0000000..9a2d07f --- /dev/null +++ b/wxbox-pal/src/parser.rs @@ -0,0 +1,164 @@ +use std::num::{ParseFloatError, ParseIntError}; +use std::str::FromStr; +use thiserror::Error; +use crate::{c, Palette, r}; + +#[derive(Error, Debug, Clone)] +pub enum PaletteParseError { + #[error("unknown command `{0}`")] + UnknownCommand(String), + #[error("invalid `color:` command, expected 5 or 8 tokens but found `{0}`")] + IncorrectColorCommandLength(usize), + #[error("invalid `color4:` command, expected 6 or 10 tokens but found `{0}`")] + IncorrectColor4CommandLength(usize), + #[error("invalid float")] + InvalidFloat(#[from] ParseFloatError), + #[error("invalid int")] + InvalidInt(#[from] ParseIntError), + #[error("palette is empty")] + PaletteEmpty +} + +pub fn parse(pal_str: &str) -> Result { + // to lowercase + let input_lowercase = pal_str.to_lowercase(); + // iterate over each line + + // will be used to build the ranged palette + // value r1 g1 b1 a1 r2 g2 b2 a2 + let mut parsed_data: Vec<(f64, u8, u8, u8, u8, u8, u8, u8, u8)> = vec![]; + + for line in input_lowercase.lines() { + let tokens = line.split_whitespace().collect::>(); + match tokens[0] { + "color:" => { + if tokens.len() == 5 { + // color: val r g b (4 tkns + command = 5 total) + let value = f64::from_str(tokens[1])?; + let r = u8::from_str(tokens[2])?; + let g = u8::from_str(tokens[3])?; + let b = u8::from_str(tokens[4])?; + parsed_data.push((value, r, g, b, 255, r, g, b, 255)); + } else if tokens.len() == 8 { + // color: val r1 g1 b1 r2 b2 g2 (7 tkns + command = 8 total) + let value = f64::from_str(tokens[1])?; + let r1 = u8::from_str(tokens[2])?; + let g1 = u8::from_str(tokens[3])?; + let b1 = u8::from_str(tokens[4])?; + let r2 = u8::from_str(tokens[5])?; + let g2 = u8::from_str(tokens[6])?; + let b2 = u8::from_str(tokens[7])?; + parsed_data.push((value, r1, g1, b1, 255, r2, g2, b2, 255)); + } else { + return Err(PaletteParseError::IncorrectColorCommandLength(tokens.len())); + } + }, + "color4:" => { + if tokens.len() == 6 { + // color4: val r g b a (5 tkns + command = 6 total) + let value = f64::from_str(tokens[1])?; + let r = u8::from_str(tokens[2])?; + let g = u8::from_str(tokens[3])?; + let b = u8::from_str(tokens[4])?; + let a = u8::from_str(tokens[5])?; + parsed_data.push((value, r, g, b, a, r, g, b, a)); + } else if tokens.len() == 10 { + // color4: val r1 g1 b1 a1 r2 b2 g2 a2 (9 tkns + command = 10 total) + let value = f64::from_str(tokens[1])?; + let r1 = u8::from_str(tokens[2])?; + let g1 = u8::from_str(tokens[3])?; + let b1 = u8::from_str(tokens[4])?; + let a1 = u8::from_str(tokens[5])?; + let r2 = u8::from_str(tokens[6])?; + let g2 = u8::from_str(tokens[7])?; + let b2 = u8::from_str(tokens[8])?; + let a2 = u8::from_str(tokens[9])?; + parsed_data.push((value, r1, g1, b1, a1, r2, g2, b2, a2)); + } 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())) + } + } + + if parsed_data.is_empty() { + 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.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, + ) + ); + + 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))) + ) + } + + Ok(output) +} + +#[cfg(test)] +mod tests { + use crate::parser::parse; + use crate::{c, r}; + + #[test] + fn almanydesigns_radaromega_nws_default() { + assert_eq!( + 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 +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(), + vec![ + (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))), + (r!(35.0, 50.0), (c!(251, 238, 0), c!(210, 112, 2))), + (r!(50.0, 65.0), (c!(255, 0, 0), c!(171, 0, 1))), + (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))) + ] + ) + } +} \ No newline at end of file diff --git a/wxbox-tiler/src/main.rs b/wxbox-tiler/src/main.rs index b91fed1..74c0038 100644 --- a/wxbox-tiler/src/main.rs +++ b/wxbox-tiler/src/main.rs @@ -2,7 +2,6 @@ pub(crate) mod sources; mod grib2; mod pixmap; -mod tile; use std::collections::{BTreeMap}; use std::fmt::Debug; diff --git a/wxbox-tiler/src/sources/grib2.rs b/wxbox-tiler/src/sources/grib2.rs index a47cd90..00826b6 100644 --- a/wxbox-tiler/src/sources/grib2.rs +++ b/wxbox-tiler/src/sources/grib2.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::f64::consts::PI; use std::io::{BufWriter, Cursor, Read}; -use std::time::SystemTime; +use std::time::{SystemTime, UNIX_EPOCH}; use eccodes::{CodesHandle, FallibleStreamingIterator, ProductKind}; use flate2::read::GzDecoder; use ndarray::Zip; @@ -14,7 +14,9 @@ use crate::LutKey; use crate::pixmap::Pixmap; pub async fn needs_reload(lct: &RwLock>, lutkey: &LutKey, valid_for: u64) -> bool { + eprintln!("debug: try lock needs_reload() lut_cache_timestamps start"); let lct_reader = lct.read().await; + eprintln!("debug: try lock needs_reload() lut_cache_timestamps locked"); if let Some(t) = lct_reader.get(&lutkey) { let dur = SystemTime::now().duration_since(*t).expect("time went backwards").as_secs(); @@ -86,11 +88,15 @@ pub fn eccodes_remap(data: Vec) -> LookupTable2D { pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lut_key: LutKey, lct_cache: &RwLock>, lut_cache: &RwLock>) { if needs_reload(&lct_cache, &lut_key, valid_for).await { + eprintln!("debug: try lock reload_if_required() lut_cache_timestamps start WRITE"); let mut lct_writer = lct_cache.write().await; + eprintln!("debug: try lock reload_if_required() lut_cache_timestamps locked WRITE"); let map = eccodes_remap(load(from, needs_gzip).await); + eprintln!("debug: try lock reload_if_required() lut_cache start"); lut_cache.write().await.insert(lut_key, map); + eprintln!("debug: try lock reload_if_required() lut_cache locked"); lct_writer.insert(lut_key, SystemTime::now()); } } @@ -98,7 +104,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 fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map: &LookupTable2D) -> Vec { +pub fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map: &LookupTable2D, missing: Option, range_folded: Option, no_coverage: Option) -> Vec { let mut image: Pixmap = Pixmap::new(); let denominator = 2.0_f64.powi(z) * tilesize as f64; @@ -132,8 +138,9 @@ pub fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map } let color = match value { - -999.0 => Color { red: 0, green: 0, blue: 0, alpha: 30 }, - -99.0 => Color { red: 0, green: 0, blue: 0, alpha: 0 }, + c if Some(*c) == no_coverage => Color { red: 0, green: 0, blue: 0, alpha: 30 }, + c if Some(*c) == missing => Color { red: 0, green: 0, blue: 0, alpha: 0 }, + c if Some(*c) == range_folded => Color { red: 141, green: 0, blue: 160, alpha: 0 }, value_at_pos => { pal.colorize(*value_at_pos) } @@ -169,7 +176,7 @@ pub fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map #[macro_export] macro_rules! grib2_handler { - (mount $f:ident, at: $path:expr, from: $from:expr, needs_gzip: $needs_gzip:expr, valid_for: $valid_for:expr, lut_with: $lut_key:expr, palette: $pal:expr) => { + (mount $f:ident, at: $path:expr, from: $from:expr, needs_gzip: $needs_gzip:expr, valid_for: $valid_for:expr, lut_with: $lut_key:expr, palette: $pal:expr, missing: $missing:expr, range_folded: $rf:expr, no_coverage: $nc:expr) => { #[::actix_web::get($path)] async fn $f(path: ::actix_web::web::Path<(i32,u32,u32)>, data: ::actix_web::web::Data) -> ::actix_web::HttpResponse { crate::sources::grib2::reload_if_required( @@ -180,10 +187,17 @@ macro_rules! grib2_handler { &data.lut_cache_timestamps, &data.lut_cache ).await; + eprintln!("debug: try lock handler lut_cache_timestamps start"); + let lct_reader = data.lut_cache_timestamps.read().await; + eprintln!("debug: try lock handler lut_cache_timestamps locked"); if let Some(map) = data.lut_cache.read().await.get(&$lut_key) { ::actix_web::HttpResponse::Ok() .insert_header(::actix_web::http::header::ContentType(::mime::IMAGE_PNG)) - .body(crate::sources::grib2::render(path.1 as f64, path.2 as f64, path.0, 256, $pal, map)) + .insert_header(("x-wxbox-tiler-data-valid-time", lct_reader.get(&$lut_key).expect("impossible").duration_since(::std::time::UNIX_EPOCH).expect("time went backwards").as_secs().to_string())) + .insert_header(("Access-Control-Allow-Origin", "*")) + .insert_header(("Access-Control-Expose-Headers", "*")) + .insert_header(("Access-Control-Allow-Headers", "*")) + .body(crate::sources::grib2::render(path.1 as f64, path.2 as f64, path.0, 256, $pal, map, $missing, $rf, $nc)) } else { ::actix_web::HttpResponse::new(::actix_web::http::StatusCode::NOT_FOUND) } diff --git a/wxbox-tiler/src/sources/noaa/mod.rs b/wxbox-tiler/src/sources/noaa/mod.rs index f7371e9..077d513 100644 --- a/wxbox-tiler/src/sources/noaa/mod.rs +++ b/wxbox-tiler/src/sources/noaa/mod.rs @@ -1,4 +1,3 @@ -use wxbox_pal::create_test_palette; use crate::{grib2_handler, LutKey}; grib2_handler! { @@ -8,5 +7,8 @@ grib2_handler! { needs_gzip: true, valid_for: 120, lut_with: LutKey::NoaaMrmsMergedCompositeReflectivityQc, - palette: create_test_palette() + palette: wxbox_pal::parser::parse(wxbox_pal::default_palettes::DEFAULT_REFLECTIVITY_PALETTE).unwrap(), + missing: Some(-99.0), + range_folded: None, + no_coverage: Some(-999.0) } \ No newline at end of file diff --git a/wxbox-tiler/src/tile.rs b/wxbox-tiler/src/tile.rs deleted file mode 100644 index db8d61d..0000000 --- a/wxbox-tiler/src/tile.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Tilemath adapted from maplibre/martin - -use std::f64::consts::PI; - -pub const EARTH_CIRCUMFERENCE: f64 = 40_075_016.685_578_5; -pub const EARTH_RADIUS: f64 = EARTH_CIRCUMFERENCE / 2.0 / PI; - -pub const MAX_ZOOM: u8 = 30; - -/// Convert longitude and latitude to a tile (x,y) coordinates for a given zoom -#[must_use] -#[allow(clippy::cast_possible_truncation)] -#[allow(clippy::cast_sign_loss)] -pub fn tile_index(lng: f64, lat: f64, zoom: u8) -> (u32, u32) { - let tile_size = EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom); - let (x, y) = wgs84_to_webmercator(lng, lat); - let col = (((x - (EARTH_CIRCUMFERENCE * -0.5)).abs() / tile_size) as u32).min((1 << zoom) - 1); - let row = ((((EARTH_CIRCUMFERENCE * 0.5) - y).abs() / tile_size) as u32).min((1 << zoom) - 1); - (col, row) -} - -/// Convert min/max XYZ tile coordinates to a bounding box values. -/// The result is `[min_lng, min_lat, max_lng, max_lat]` -#[must_use] -pub fn xyz_to_bbox(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> [f64; 4] { - assert!(zoom <= MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); - - let tile_length = EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom); - - let left_down_bbox = tile_bbox(min_x, max_y, tile_length); - let right_top_bbox = tile_bbox(max_x, min_y, tile_length); - - let (min_lng, min_lat) = webmercator_to_wgs84(left_down_bbox[0], left_down_bbox[1]); - let (max_lng, max_lat) = webmercator_to_wgs84(right_top_bbox[2], right_top_bbox[3]); - [min_lng, min_lat, max_lng, max_lat] -} - -#[allow(clippy::cast_lossless)] -fn tile_bbox(x: u32, y: u32, tile_length: f64) -> [f64; 4] { - let min_x = EARTH_CIRCUMFERENCE * -0.5 + x as f64 * tile_length; - let max_y = EARTH_CIRCUMFERENCE * 0.5 - y as f64 * tile_length; - - [min_x, max_y - tile_length, min_x + tile_length, max_y] -} - -/// Convert bounding box to a tile box `(min_x, min_y, max_x, max_y)` for a given zoom -#[must_use] -pub fn bbox_to_xyz(left: f64, bottom: f64, right: f64, top: f64, zoom: u8) -> (u32, u32, u32, u32) { - let (min_col, min_row) = tile_index(left, top, zoom); - let (max_col, max_row) = tile_index(right, bottom, zoom); - (min_col, min_row, max_col, max_row) -} - -/// Compute precision of a zoom level, i.e. how many decimal digits of the longitude and latitude are relevant -#[must_use] -#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] -pub fn get_zoom_precision(zoom: u8) -> usize { - assert!(zoom < MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); - let lng_delta = webmercator_to_wgs84(EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom), 0.0).0; - let log = lng_delta.log10() - 0.5; - if log > 0.0 { - 0 - } else { - -log.ceil() as usize - } -} - -#[must_use] -pub fn webmercator_to_wgs84(x: f64, y: f64) -> (f64, f64) { - let lng = (x / EARTH_RADIUS).to_degrees(); - let lat = f64::atan(f64::sinh(y / EARTH_RADIUS)).to_degrees(); - (lng, lat) -} - -/// transform WGS84 to `WebMercator` -// from https://github.com/Esri/arcgis-osm-editor/blob/e4b9905c264aa22f8eeb657efd52b12cdebea69a/src/OSMWeb10_1/Utils/WebMercator.cs -#[must_use] -pub fn wgs84_to_webmercator(lon: f64, lat: f64) -> (f64, f64) { - let x = lon * PI / 180.0 * EARTH_RADIUS; - - let rad = lat * PI / 180.0; - let sin = rad.sin(); - let y = EARTH_RADIUS / 2.0 * ((1.0 + sin) / (1.0 - sin)).ln(); - - (x, y) -} - -#[cfg(test)] -mod tests { - #![allow(clippy::unreadable_literal)] - - use std::fs::read; - - use approx::assert_relative_eq; - use insta::assert_snapshot; - - use super::*; - - #[test] - fn test_xyz_to_bbox() { - // you could easily get test cases from maptiler: https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection/#4/-118.82/71.02 - let bbox = xyz_to_bbox(0, 0, 0, 0, 0); - assert_relative_eq!(bbox[0], -180.0, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[1], -85.0511287798066, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[2], 180.0, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[3], 85.0511287798066, epsilon = f64::EPSILON * 2.0); - - let bbox = xyz_to_bbox(1, 0, 0, 0, 0); - assert_relative_eq!(bbox[0], -180.0, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[1], 0.0, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[2], 0.0, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[3], 85.0511287798066, epsilon = f64::EPSILON * 2.0); - - let bbox = xyz_to_bbox(5, 1, 1, 2, 2); - assert_relative_eq!(bbox[0], -168.75, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[1], 81.09321385260837, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[2], -146.25, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[3], 83.97925949886205, epsilon = f64::EPSILON * 2.0); - - let bbox = xyz_to_bbox(5, 1, 3, 2, 5); - assert_relative_eq!(bbox[0], -168.75, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[1], 74.01954331150226, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[2], -146.25, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(bbox[3], 81.09321385260837, epsilon = f64::EPSILON * 2.0); - } - - #[test] - fn test_box_to_xyz() { - fn tst(left: f64, bottom: f64, right: f64, top: f64, zoom: u8) -> String { - let (x0, y0, x1, y1) = bbox_to_xyz(left, bottom, right, top, zoom); - format!("({x0}, {y0}, {x1}, {y1})") - } - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 0), @"(0, 0, 0, 0)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 1), @"(0, 1, 0, 1)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 2), @"(0, 3, 0, 3)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 3), @"(0, 7, 0, 7)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 4), @"(0, 14, 1, 15)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 5), @"(0, 29, 2, 31)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 6), @"(0, 58, 5, 63)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 7), @"(0, 116, 11, 126)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 8), @"(0, 233, 23, 253)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 9), @"(0, 466, 47, 507)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 10), @"(1, 933, 94, 1014)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 11), @"(3, 1866, 188, 2029)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 12), @"(6, 3732, 377, 4059)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 13), @"(12, 7465, 755, 8119)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 14), @"(25, 14931, 1510, 16239)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 15), @"(51, 29863, 3020, 32479)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 16), @"(102, 59727, 6041, 64958)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 17), @"(204, 119455, 12083, 129917)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 18), @"(409, 238911, 24166, 259834)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 19), @"(819, 477823, 48332, 519669)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 20), @"(1638, 955647, 96665, 1039339)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 21), @"(3276, 1911295, 193331, 2078678)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 22), @"(6553, 3822590, 386662, 4157356)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 23), @"(13107, 7645181, 773324, 8314713)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 24), @"(26214, 15290363, 1546649, 16629427)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 25), @"(52428, 30580726, 3093299, 33258855)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 26), @"(104857, 61161453, 6186598, 66517711)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 27), @"(209715, 122322907, 12373196, 133035423)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 28), @"(419430, 244645814, 24746393, 266070846)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 29), @"(838860, 489291628, 49492787, 532141692)"); - assert_snapshot!(tst(-179.43749999999955,-84.76987877980656,-146.8124999999996,-81.37446385260833, 30), @"(1677721, 978583256, 98985574, 1064283385)"); - } - - #[test] - fn meter_to_lng_lat() { - let (lng, lat) = webmercator_to_wgs84(-20037508.34, -20037508.34); - assert_relative_eq!(lng, -179.9999999749437, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(lat, -85.05112877764508, epsilon = f64::EPSILON * 2.0); - - let (lng, lat) = webmercator_to_wgs84(20037508.34, 20037508.34); - assert_relative_eq!(lng, 179.9999999749437, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(lat, 85.05112877764508, epsilon = f64::EPSILON * 2.0); - - let (lng, lat) = webmercator_to_wgs84(0.0, 0.0); - assert_relative_eq!(lng, 0.0, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(lat, 0.0, epsilon = f64::EPSILON * 2.0); - - let (lng, lat) = webmercator_to_wgs84(3000.0, 9000.0); - assert_relative_eq!(lng, 0.026949458523585632, epsilon = f64::EPSILON * 2.0); - assert_relative_eq!(lat, 0.08084834874097367, epsilon = f64::EPSILON * 2.0); - } -} \ No newline at end of file diff --git a/wxbox-web/src/lib/color.ts b/wxbox-web/src/lib/color.ts new file mode 100644 index 0000000..b134021 --- /dev/null +++ b/wxbox-web/src/lib/color.ts @@ -0,0 +1,12 @@ +export function strToColor(str: string): string { + let hash = 0; + str.split('').forEach(char => { + hash = char.charCodeAt(0) + ((hash << 5) - hash) + }) + let colour = '#' + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff + colour += value.toString(16).padStart(2, '0') + } + return colour +} \ No newline at end of file diff --git a/wxbox-web/src/lib/global.css b/wxbox-web/src/lib/global.css new file mode 100644 index 0000000..9e95488 --- /dev/null +++ b/wxbox-web/src/lib/global.css @@ -0,0 +1,55 @@ +:root { + --size-0: 0px; + --size-px: 1px; + --size-0_5: 0.125rem; + --size-1: 0.25rem; + --size-1_5: 0.375rem; + --size-2: 0.5rem; + --size-2_5: 0.625rem; + --size-3: 0.75rem; + --size-3_5: 0.875rem; + --size-4: 1rem; + --size-5: 1.25rem; + --size-6: 1.5rem; + --size-7: 1.75rem; + --size-8: 2rem; + --size-9: 2.25rem; + --size-10: 2.5rem; + --size-11: 2.75rem; + --size-12: 3rem; + --size-14: 3.5rem; + --size-16: 4rem; + --size-20: 5rem; + --size-24: 6rem; + --size-28: 7rem; + --size-32: 8rem; + --size-36: 9rem; + --size-40: 10rem; + --size-44: 11rem; + --size-48: 12rem; + --size-52: 13rem; + --size-56: 14rem; + --size-60: 15rem; + --size-64: 16rem; + --size-72: 18rem; + --size-80: 20rem; + --size-96: 24rem; +} + +html { + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + line-height: 1.5rem; + margin: 0; +} + +body { + margin: 0; + background-color: #000; + color: #fff; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} \ No newline at end of file diff --git a/wxbox-web/src/lib/map/Map.svelte b/wxbox-web/src/lib/map/Map.svelte index dce82a7..2277770 100644 --- a/wxbox-web/src/lib/map/Map.svelte +++ b/wxbox-web/src/lib/map/Map.svelte @@ -1,17 +1,20 @@ -{@render children()} +{@render children()} \ No newline at end of file diff --git a/wxbox-web/src/routes/+page.svelte b/wxbox-web/src/routes/+page.svelte index 4c8120b..4b8c947 100644 --- a/wxbox-web/src/routes/+page.svelte +++ b/wxbox-web/src/routes/+page.svelte @@ -6,6 +6,7 @@ import type { View } from '$lib/map'; import type { Map as LMap } from 'leaflet'; import { syncMaps } from '$lib/map/sync.svelte.js'; + import {strToColor} from "$lib/color"; let map1: LMap | null = $state(null); let map2: LMap | null = $state(null); @@ -298,13 +299,37 @@ let status: string = $derived.by(() => { return mode + ' ' + pane; }); + + let reload1: boolean = $state(false); + let reload2: boolean = $state(false); + let reload3: boolean = $state(false); + let reload4: boolean = $state(false); + let timeUntilReload: number = $state(60); + + let reloadInterval: ReturnType | null = null; + + $effect(() => { + if (!reloadInterval) { + reloadInterval = setInterval(() => { + timeUntilReload--; + if (timeUntilReload === 0) { + timeUntilReload = 60; + reload1 = true; + reload2 = true; + reload3 = true; + reload4 = true; + } + }, 1000); + } + })

wxbox

- {status} + {status} + data reload in {timeUntilReload}s
{#if view === 'two' || view === 'four'} {/if}
@@ -332,6 +359,7 @@ baseLayer={baseLayer.map3} dataLayer={dataLayer.map3} overlayLayers={overlayLayers.map3} + bind:reload={reload3} />
{/if} -