cool things
This commit is contained in:
parent
bff5de2e05
commit
f42a4a739a
|
@ -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
|
||||||
|
"#;
|
|
@ -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 struct Color {
|
||||||
pub red: u8,
|
pub red: u8,
|
||||||
pub green: u8,
|
pub green: u8,
|
||||||
|
@ -11,35 +13,23 @@ pub struct Color {
|
||||||
|
|
||||||
pub type Palette = Vec<((OrderedFloat<f64>, OrderedFloat<f64>), (Color, Color))>;
|
pub type Palette = Vec<((OrderedFloat<f64>, OrderedFloat<f64>), (Color, Color))>;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! c {
|
macro_rules! c {
|
||||||
($r:expr,$g:expr,$b:expr) => {
|
($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) => {
|
($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 {
|
macro_rules! r {
|
||||||
($f:expr,$t:expr) => {
|
($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 {
|
pub trait ColorPalette {
|
||||||
fn colorize(&self, at: f64) -> Color;
|
fn colorize(&self, at: f64) -> Color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Palette, PaletteParseError> {
|
||||||
|
// 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::<Vec<_>>();
|
||||||
|
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)))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ pub(crate) mod sources;
|
||||||
|
|
||||||
mod grib2;
|
mod grib2;
|
||||||
mod pixmap;
|
mod pixmap;
|
||||||
mod tile;
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap};
|
use std::collections::{BTreeMap};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
use std::io::{BufWriter, Cursor, Read};
|
use std::io::{BufWriter, Cursor, Read};
|
||||||
use std::time::SystemTime;
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use eccodes::{CodesHandle, FallibleStreamingIterator, ProductKind};
|
use eccodes::{CodesHandle, FallibleStreamingIterator, ProductKind};
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use ndarray::Zip;
|
use ndarray::Zip;
|
||||||
|
@ -14,7 +14,9 @@ use crate::LutKey;
|
||||||
use crate::pixmap::Pixmap;
|
use crate::pixmap::Pixmap;
|
||||||
|
|
||||||
pub async fn needs_reload(lct: &RwLock<BTreeMap<LutKey, SystemTime>>, lutkey: &LutKey, valid_for: u64) -> bool {
|
pub async fn needs_reload(lct: &RwLock<BTreeMap<LutKey, SystemTime>>, lutkey: &LutKey, valid_for: u64) -> bool {
|
||||||
|
eprintln!("debug: try lock needs_reload() lut_cache_timestamps start");
|
||||||
let lct_reader = lct.read().await;
|
let lct_reader = lct.read().await;
|
||||||
|
eprintln!("debug: try lock needs_reload() lut_cache_timestamps locked");
|
||||||
|
|
||||||
if let Some(t) = lct_reader.get(&lutkey) {
|
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();
|
||||||
|
@ -86,11 +88,15 @@ pub fn eccodes_remap(data: Vec<u8>) -> LookupTable2D {
|
||||||
|
|
||||||
pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lut_key: LutKey, lct_cache: &RwLock<BTreeMap<LutKey, SystemTime>>, lut_cache: &RwLock<BTreeMap<LutKey, LookupTable2D>>) {
|
pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lut_key: LutKey, lct_cache: &RwLock<BTreeMap<LutKey, SystemTime>>, lut_cache: &RwLock<BTreeMap<LutKey, LookupTable2D>>) {
|
||||||
if needs_reload(&lct_cache, &lut_key, valid_for).await {
|
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;
|
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);
|
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);
|
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());
|
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 TWO_PI: f64 = PI * 2.0;
|
||||||
const HALF_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<u8> {
|
pub fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palette, map: &LookupTable2D, missing: Option<f64>, range_folded: Option<f64>, no_coverage: Option<f64>) -> Vec<u8> {
|
||||||
let mut image: Pixmap = Pixmap::new();
|
let mut image: Pixmap = Pixmap::new();
|
||||||
|
|
||||||
let denominator = 2.0_f64.powi(z) * tilesize as f64;
|
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 {
|
let color = match value {
|
||||||
-999.0 => Color { red: 0, green: 0, blue: 0, alpha: 30 },
|
c if Some(*c) == no_coverage => Color { red: 0, green: 0, blue: 0, alpha: 30 },
|
||||||
-99.0 => Color { red: 0, green: 0, blue: 0, alpha: 0 },
|
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 => {
|
value_at_pos => {
|
||||||
pal.colorize(*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_export]
|
||||||
macro_rules! grib2_handler {
|
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)]
|
#[::actix_web::get($path)]
|
||||||
async fn $f(path: ::actix_web::web::Path<(i32,u32,u32)>, data: ::actix_web::web::Data<crate::AppState>) -> ::actix_web::HttpResponse {
|
async fn $f(path: ::actix_web::web::Path<(i32,u32,u32)>, data: ::actix_web::web::Data<crate::AppState>) -> ::actix_web::HttpResponse {
|
||||||
crate::sources::grib2::reload_if_required(
|
crate::sources::grib2::reload_if_required(
|
||||||
|
@ -180,10 +187,17 @@ macro_rules! grib2_handler {
|
||||||
&data.lut_cache_timestamps,
|
&data.lut_cache_timestamps,
|
||||||
&data.lut_cache
|
&data.lut_cache
|
||||||
).await;
|
).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) {
|
if let Some(map) = data.lut_cache.read().await.get(&$lut_key) {
|
||||||
::actix_web::HttpResponse::Ok()
|
::actix_web::HttpResponse::Ok()
|
||||||
.insert_header(::actix_web::http::header::ContentType(::mime::IMAGE_PNG))
|
.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 {
|
} else {
|
||||||
::actix_web::HttpResponse::new(::actix_web::http::StatusCode::NOT_FOUND)
|
::actix_web::HttpResponse::new(::actix_web::http::StatusCode::NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use wxbox_pal::create_test_palette;
|
|
||||||
use crate::{grib2_handler, LutKey};
|
use crate::{grib2_handler, LutKey};
|
||||||
|
|
||||||
grib2_handler! {
|
grib2_handler! {
|
||||||
|
@ -8,5 +7,8 @@ grib2_handler! {
|
||||||
needs_gzip: true,
|
needs_gzip: true,
|
||||||
valid_for: 120,
|
valid_for: 120,
|
||||||
lut_with: LutKey::NoaaMrmsMergedCompositeReflectivityQc,
|
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)
|
||||||
}
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ActionReturn} from 'svelte/action';
|
import type {ActionReturn} from 'svelte/action';
|
||||||
import type { TileLayer, Map as LeafletMap } from 'leaflet';
|
|
||||||
import { tilerLayerAttribution, tilerLayerUrl } from '$lib/map/layer';
|
|
||||||
import type * as Leaflet from 'leaflet';
|
import type * as Leaflet from 'leaflet';
|
||||||
|
import {type Control, type Map as LeafletMap, type TileLayer} from 'leaflet';
|
||||||
|
import {tilerLayerAttribution, tilerLayerUrl} from '$lib/map/layer';
|
||||||
|
import {untrack} from "svelte";
|
||||||
|
import {SvelteDate} from "svelte/reactivity";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
map: LeafletMap | null;
|
map: LeafletMap | null;
|
||||||
|
@ -10,8 +12,9 @@
|
||||||
baseLayer: 'osm';
|
baseLayer: 'osm';
|
||||||
dataLayer: 'noaa_mrms_merged_composite_reflectivity_qc' | null;
|
dataLayer: 'noaa_mrms_merged_composite_reflectivity_qc' | null;
|
||||||
overlayLayers: string[];
|
overlayLayers: string[];
|
||||||
|
reload: boolean;
|
||||||
}
|
}
|
||||||
let { map = $bindable(null), selected, baseLayer, dataLayer }: Props = $props();
|
let { map = $bindable(null), selected, baseLayer, dataLayer, reload = $bindable(false) }: Props = $props();
|
||||||
|
|
||||||
let mapContainerElement: HTMLElement;
|
let mapContainerElement: HTMLElement;
|
||||||
// await import('leaflet') done at runtime
|
// await import('leaflet') done at runtime
|
||||||
|
@ -20,6 +23,35 @@
|
||||||
let layer0: TileLayer;
|
let layer0: TileLayer;
|
||||||
// Data layer - composite reflectivity, velocity (the actual data)
|
// Data layer - composite reflectivity, velocity (the actual data)
|
||||||
let layer1: TileLayer;
|
let layer1: TileLayer;
|
||||||
|
let attrControl: Control.Attribution | null = $state(null);
|
||||||
|
let dataValidity: SvelteDate | null = $state(null);
|
||||||
|
|
||||||
|
// redrawing
|
||||||
|
$effect(() => {
|
||||||
|
if (!L) return;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
if (reload == true) {
|
||||||
|
untrack(() => {reload = false});
|
||||||
|
if (!dataLayer) return;
|
||||||
|
|
||||||
|
if (layer1) {
|
||||||
|
layer1.setUrl(tilerLayerUrl(dataLayer) + "?r=" + Math.random())
|
||||||
|
layer1.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLayer) {
|
||||||
|
fetch(tilerLayerUrl(dataLayer).replace("{z}", "0").replace("{x}", "0").replace("{y}", "0")).then((r) => {
|
||||||
|
let headerval = r.headers.get("x-wxbox-tiler-data-valid-time");
|
||||||
|
if (!headerval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let valid_from = Number.parseInt(headerval);
|
||||||
|
dataValidity = new SvelteDate(valid_from * 1000);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Layer0 (base) updating
|
// Layer0 (base) updating
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
@ -58,9 +90,52 @@
|
||||||
attribution: tilerLayerAttribution(dataLayer)
|
attribution: tilerLayerAttribution(dataLayer)
|
||||||
});
|
});
|
||||||
layer1.addTo(map);
|
layer1.addTo(map);
|
||||||
|
// fetch the data validity
|
||||||
|
fetch(tilerLayerUrl(dataLayer).replace("{z}", "0").replace("{x}", "0").replace("{y}", "0")).then((r) => {
|
||||||
|
let headerval = r.headers.get("x-wxbox-tiler-data-valid-time");
|
||||||
|
if (!headerval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let valid_from = Number.parseInt(headerval);
|
||||||
|
dataValidity = new SvelteDate(valid_from * 1000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dataValidity = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ukrainianFlag = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8" class="leaflet-attribution-flag"><path fill="#4C7BE1" d="M0 0h12v4H0z"/><path fill="#FFD500" d="M0 4h12v3H0z"/><path fill="#E0BC00" d="M0 7h12v1H0z"/></svg>';
|
||||||
|
|
||||||
|
// Attribution control & data validity
|
||||||
|
$effect(() => {
|
||||||
|
if (!L) return;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
let basePrefix = `<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">${ukrainianFlag}Leaflet</a>`;
|
||||||
|
|
||||||
|
if (!attrControl) {
|
||||||
|
attrControl = L.control.attribution({
|
||||||
|
prefix: basePrefix
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!attrControl) return;
|
||||||
|
attrControl.addTo(map);
|
||||||
|
|
||||||
|
let prefix = basePrefix;
|
||||||
|
|
||||||
|
if (!dataValidity) {
|
||||||
|
prefix += " | Data validity unknown";
|
||||||
|
} else {
|
||||||
|
prefix += ` | Data valid from ${dataValidity.toLocaleTimeString('en-US')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attrControl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attrControl.setPrefix(prefix);
|
||||||
|
})
|
||||||
|
|
||||||
// ran when the div below (see use:mapAction) is created
|
// ran when the div below (see use:mapAction) is created
|
||||||
async function mapAction(): Promise<ActionReturn> {
|
async function mapAction(): Promise<ActionReturn> {
|
||||||
// dynamically imports leaflet, as it's a browser lib
|
// dynamically imports leaflet, as it's a browser lib
|
||||||
|
@ -72,7 +147,8 @@
|
||||||
map = L.map(mapContainerElement, {
|
map = L.map(mapContainerElement, {
|
||||||
// geo center of CONUS
|
// geo center of CONUS
|
||||||
center: [39.83, -98.583],
|
center: [39.83, -98.583],
|
||||||
zoom: 5
|
zoom: 5,
|
||||||
|
attributionControl: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!map) return {};
|
if (!map) return {};
|
||||||
|
@ -85,10 +161,10 @@
|
||||||
.map {
|
.map {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 2px solid transparent;
|
border: 3px solid #000;
|
||||||
}
|
}
|
||||||
.mapselected {
|
.mapselected {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 2px solid red;
|
border: 3px solid green;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -32,3 +32,19 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px inset darkgreen;
|
||||||
|
padding: var(--size-1) var(--size-3);
|
||||||
|
background-color: green;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button[disabled] {
|
||||||
|
background-color: darkgreen;
|
||||||
|
color: green;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import "$lib/global.css";
|
||||||
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import type { View } from '$lib/map';
|
import type { View } from '$lib/map';
|
||||||
import type { Map as LMap } from 'leaflet';
|
import type { Map as LMap } from 'leaflet';
|
||||||
import { syncMaps } from '$lib/map/sync.svelte.js';
|
import { syncMaps } from '$lib/map/sync.svelte.js';
|
||||||
|
import {strToColor} from "$lib/color";
|
||||||
|
|
||||||
let map1: LMap | null = $state(null);
|
let map1: LMap | null = $state(null);
|
||||||
let map2: LMap | null = $state(null);
|
let map2: LMap | null = $state(null);
|
||||||
|
@ -298,13 +299,37 @@
|
||||||
let status: string = $derived.by(() => {
|
let status: string = $derived.by(() => {
|
||||||
return mode + ' ' + pane;
|
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<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!reloadInterval) {
|
||||||
|
reloadInterval = setInterval(() => {
|
||||||
|
timeUntilReload--;
|
||||||
|
if (timeUntilReload === 0) {
|
||||||
|
timeUntilReload = 60;
|
||||||
|
reload1 = true;
|
||||||
|
reload2 = true;
|
||||||
|
reload3 = true;
|
||||||
|
reload4 = true;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="outercontainer">
|
<div class="outercontainer">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h1>wxbox</h1>
|
<h1>wxbox</h1>
|
||||||
<span>{status}</span>
|
<span class="status">{status}</span>
|
||||||
<ButtonBar menu={registry[mode]} />
|
<ButtonBar menu={registry[mode]} />
|
||||||
|
<span class="dataState">data reload in {timeUntilReload}s</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Map
|
<Map
|
||||||
|
@ -313,6 +338,7 @@
|
||||||
baseLayer={baseLayer.map1}
|
baseLayer={baseLayer.map1}
|
||||||
dataLayer={dataLayer.map1}
|
dataLayer={dataLayer.map1}
|
||||||
overlayLayers={overlayLayers.map1}
|
overlayLayers={overlayLayers.map1}
|
||||||
|
bind:reload={reload1}
|
||||||
/>
|
/>
|
||||||
{#if view === 'two' || view === 'four'}
|
{#if view === 'two' || view === 'four'}
|
||||||
<Map
|
<Map
|
||||||
|
@ -321,6 +347,7 @@
|
||||||
baseLayer={baseLayer.map2}
|
baseLayer={baseLayer.map2}
|
||||||
dataLayer={dataLayer.map2}
|
dataLayer={dataLayer.map2}
|
||||||
overlayLayers={overlayLayers.map2}
|
overlayLayers={overlayLayers.map2}
|
||||||
|
bind:reload={reload2}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -332,6 +359,7 @@
|
||||||
baseLayer={baseLayer.map3}
|
baseLayer={baseLayer.map3}
|
||||||
dataLayer={dataLayer.map3}
|
dataLayer={dataLayer.map3}
|
||||||
overlayLayers={overlayLayers.map3}
|
overlayLayers={overlayLayers.map3}
|
||||||
|
bind:reload={reload3}
|
||||||
/>
|
/>
|
||||||
<Map
|
<Map
|
||||||
selected={pane === 'map4'}
|
selected={pane === 'map4'}
|
||||||
|
@ -339,29 +367,30 @@
|
||||||
baseLayer={baseLayer.map4}
|
baseLayer={baseLayer.map4}
|
||||||
dataLayer={dataLayer.map4}
|
dataLayer={dataLayer.map4}
|
||||||
overlayLayers={overlayLayers.map4}
|
overlayLayers={overlayLayers.map4}
|
||||||
|
bind:reload={reload4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="footer">
|
<div class="footer text-sm">
|
||||||
<p>built with <3</p>
|
<p>built with <3</p>
|
||||||
<p>coredoes.dev :)</p>
|
<p>u8.lc coredoes.dev :)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toolbar {
|
.toolbar h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
}
|
||||||
|
.toolbar {
|
||||||
|
margin: var(--size-1) var(--size-2);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-2);
|
||||||
}
|
}
|
||||||
.toolbar h1 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
:global(html body) {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -373,9 +402,12 @@
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-left: var(--size-2);
|
||||||
|
margin-right: var(--size-2);
|
||||||
}
|
}
|
||||||
.footer p {
|
.footer p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
Loading…
Reference in New Issue