Compare commits
No commits in common. "core/v2" and "master" have entirely different histories.
48 changed files with 1206 additions and 4196 deletions
|
@ -1,3 +0,0 @@
|
||||||
[profile.dev]
|
|
||||||
opt-level = 1
|
|
||||||
incremental = true
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,3 @@
|
||||||
*/target
|
*/target
|
||||||
target
|
target
|
||||||
node_modules
|
node_modules
|
||||||
.cache
|
|
||||||
wxbox_client_wasm/dist
|
|
|
@ -6,12 +6,6 @@
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox-tiler/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/wxbox-tiler/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox-eccodes-sys/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/wxbox-eccodes-sys/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox-web/wxbox-eccodes-sys/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/wxbox-web/wxbox-eccodes-sys/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox-grib2/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox_client/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox_client_native/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox_client_wasm/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox_common/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/wxbox_web/src" isTestSource="false" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
|
3403
Cargo.lock
generated
3403
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [ "wxbox-grib2","wxbox-pal","wxbox-tiler", "wxbox_client", "wxbox_client_native", "wxbox_client_wasm", "wxbox_common"]
|
members = [ "wxbox-grib2","wxbox-pal","wxbox-tiler"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
Binary file not shown.
94
config.toml
94
config.toml
|
@ -1,72 +1,4 @@
|
||||||
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_CONUS]
|
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc]
|
||||||
from = "https://mrms.ncep.noaa.gov/data/2D/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
|
|
||||||
needs_gzip = true
|
|
||||||
valid_for = 120
|
|
||||||
palette = """
|
|
||||||
Color: 10 164 164 255 100 100 192
|
|
||||||
Color: 20 64 128 255 32 64 128
|
|
||||||
Color: 30 0 255 0 0 128 0
|
|
||||||
Color: 40 255 255 0 255 128 0
|
|
||||||
Color: 50 255 0 0 160 0 0
|
|
||||||
Color: 60 255 0 255 128 0 128
|
|
||||||
Color: 70 255 255 255 128 128 128
|
|
||||||
Color: 80 128 128 128
|
|
||||||
"""
|
|
||||||
missing = -99.0
|
|
||||||
no_coverage = -999.0
|
|
||||||
|
|
||||||
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_ALASKA]
|
|
||||||
from = "https://mrms.ncep.noaa.gov/data/2D/ALASKA/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
|
|
||||||
needs_gzip = true
|
|
||||||
valid_for = 120
|
|
||||||
palette = """
|
|
||||||
Color: 10 164 164 255 100 100 192
|
|
||||||
Color: 20 64 128 255 32 64 128
|
|
||||||
Color: 30 0 255 0 0 128 0
|
|
||||||
Color: 40 255 255 0 255 128 0
|
|
||||||
Color: 50 255 0 0 160 0 0
|
|
||||||
Color: 60 255 0 255 128 0 128
|
|
||||||
Color: 70 255 255 255 128 128 128
|
|
||||||
Color: 80 128 128 128
|
|
||||||
"""
|
|
||||||
missing = -99.0
|
|
||||||
no_coverage = -999.0
|
|
||||||
|
|
||||||
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_CARIB]
|
|
||||||
from = "https://mrms.ncep.noaa.gov/data/2D/CARIB/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
|
|
||||||
needs_gzip = true
|
|
||||||
valid_for = 120
|
|
||||||
palette = """
|
|
||||||
Color: 10 164 164 255 100 100 192
|
|
||||||
Color: 20 64 128 255 32 64 128
|
|
||||||
Color: 30 0 255 0 0 128 0
|
|
||||||
Color: 40 255 255 0 255 128 0
|
|
||||||
Color: 50 255 0 0 160 0 0
|
|
||||||
Color: 60 255 0 255 128 0 128
|
|
||||||
Color: 70 255 255 255 128 128 128
|
|
||||||
Color: 80 128 128 128
|
|
||||||
"""
|
|
||||||
missing = -99.0
|
|
||||||
no_coverage = -999.0
|
|
||||||
|
|
||||||
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_GUAM]
|
|
||||||
from = "https://mrms.ncep.noaa.gov/data/2D/GUAM/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
|
|
||||||
needs_gzip = true
|
|
||||||
valid_for = 120
|
|
||||||
palette = """
|
|
||||||
Color: 10 164 164 255 100 100 192
|
|
||||||
Color: 20 64 128 255 32 64 128
|
|
||||||
Color: 30 0 255 0 0 128 0
|
|
||||||
Color: 40 255 255 0 255 128 0
|
|
||||||
Color: 50 255 0 0 160 0 0
|
|
||||||
Color: 60 255 0 255 128 0 128
|
|
||||||
Color: 70 255 255 255 128 128 128
|
|
||||||
Color: 80 128 128 128
|
|
||||||
"""
|
|
||||||
missing = -99.0
|
|
||||||
no_coverage = -999.0
|
|
||||||
|
|
||||||
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc_HAWAII]
|
|
||||||
from = "https://mrms.ncep.noaa.gov/data/2D/HAWAII/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
|
from = "https://mrms.ncep.noaa.gov/data/2D/HAWAII/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
|
||||||
needs_gzip = true
|
needs_gzip = true
|
||||||
valid_for = 120
|
valid_for = 120
|
||||||
|
@ -82,27 +14,3 @@ Color: 80 128 128 128
|
||||||
"""
|
"""
|
||||||
missing = -99.0
|
missing = -99.0
|
||||||
no_coverage = -999.0
|
no_coverage = -999.0
|
||||||
|
|
||||||
[sources.grib2.noaa_mrms_merged_rhohv_3km_CONUS]
|
|
||||||
from = "https://mrms.ncep.noaa.gov/3DRhoHV/MergedRhoHV_03.00/MRMS_MergedRhoHV_03.00.latest.grib2.gz"
|
|
||||||
needs_gzip = true
|
|
||||||
valid_for = 300
|
|
||||||
palette = """
|
|
||||||
Color: 0.20 149 148 156
|
|
||||||
Color: 0.45 22 20 140
|
|
||||||
Color: 0.65 9 2 233
|
|
||||||
Color: 0.75 137 135 214
|
|
||||||
Color: 0.80 92 255 89
|
|
||||||
Color: 0.85 139 207 2
|
|
||||||
Color: 0.90 255 251 0
|
|
||||||
Color: 0.93 255 196 0
|
|
||||||
Color: 0.95 255 137 3
|
|
||||||
Color: 0.96 255 43 0
|
|
||||||
Color: 0.97 227 0 0
|
|
||||||
Color: 0.98 161 0 0
|
|
||||||
Color: 0.99 121 0 90
|
|
||||||
Color: 1.00 250 172 209
|
|
||||||
Color: 1.05 119 0 125
|
|
||||||
"""
|
|
||||||
missing = -99.0
|
|
||||||
no_coverage = -999.0
|
|
|
@ -6,7 +6,7 @@ use std::fmt::{Debug, Formatter};
|
||||||
use std::io::{Cursor, Read};
|
use std::io::{Cursor, Read};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use image::codecs::png::PngDecoder;
|
use image::codecs::png::PngDecoder;
|
||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, ImageDecoder, ImageFormat, ImageReader, Luma};
|
use image::{DynamicImage, ImageBuffer, ImageDecoder, ImageFormat, ImageReader, Luma};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use crate::error::GribError;
|
use crate::error::GribError;
|
||||||
use crate::LatLongVectorRelativity::{EasterlyAndNortherly, IncreasingXY};
|
use crate::LatLongVectorRelativity::{EasterlyAndNortherly, IncreasingXY};
|
||||||
|
@ -104,8 +104,6 @@ impl GribMessage {
|
||||||
let bitmap = bitmap.ok_or(GribError::MissingBitmap)?;
|
let bitmap = bitmap.ok_or(GribError::MissingBitmap)?;
|
||||||
let data = data.ok_or(GribError::MissingData)?;
|
let data = data.ok_or(GribError::MissingData)?;
|
||||||
|
|
||||||
debug!("{:?}", data_representation);
|
|
||||||
|
|
||||||
data_representation.load_data(data.data.clone())?;
|
data_representation.load_data(data.data.clone())?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -317,7 +315,7 @@ pub struct GridpointPNGDataRepresentation {
|
||||||
pub depth: u8,
|
pub depth: u8,
|
||||||
pub type_of_values: u8,
|
pub type_of_values: u8,
|
||||||
|
|
||||||
pub image: Option<DynamicImage>
|
pub image: Option<ImageBuffer<Luma<u16>, Vec<u16>>>
|
||||||
}
|
}
|
||||||
impl GridpointPNGDataRepresentation {
|
impl GridpointPNGDataRepresentation {
|
||||||
fn parse<R: Read>(_length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
|
fn parse<R: Read>(_length: u32, nommer: &mut NomReader<R>) -> Result<Self, GribError> {
|
||||||
|
@ -341,7 +339,7 @@ impl GridpointPNGDataRepresentation {
|
||||||
|
|
||||||
let image = image_reader.decode()?;
|
let image = image_reader.decode()?;
|
||||||
|
|
||||||
self.image = Some(image);
|
self.image = Some(image.to_luma16());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -349,50 +347,17 @@ impl GridpointPNGDataRepresentation {
|
||||||
fn get_image_coordinate(&self, x: u32, y: u32) -> Option<f32> {
|
fn get_image_coordinate(&self, x: u32, y: u32) -> Option<f32> {
|
||||||
match &self.image {
|
match &self.image {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
match self.depth {
|
|
||||||
1 | 2 | 4 | 8 | 16 => {
|
|
||||||
if x >= i.width() || y >= i.height() {
|
if x >= i.width() || y >= i.height() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let datapoint = i.as_luma16().unwrap().get_pixel(x, y).0[0] as f32;
|
let datapoint = i.get_pixel(x, y).0[0] as f32;
|
||||||
let diff = datapoint * 2.0_f32.powi(self.binary_scale_factor as i32);
|
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 dig_factor = 10_f32.powi(-(self.decimal_scale_factor as i32));
|
||||||
let value = (self.reference_value + diff) * dig_factor;
|
let value = (self.reference_value + diff) * dig_factor;
|
||||||
Some(value)
|
Some(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
24 => {
|
None => None,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,6 @@ tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
|
||||||
wxbox_common = { path = "../wxbox_common" }
|
|
||||||
image = "0.25"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
approx = "0.5"
|
approx = "0.5"
|
||||||
|
|
|
@ -36,7 +36,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
});
|
});
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.service(sources::grib2::source)
|
.service(sources::grib2::grib2_source)
|
||||||
.app_data(data.clone())
|
.app_data(data.clone())
|
||||||
})
|
})
|
||||||
.bind(("::", 8080))?
|
.bind(("::", 8080))?
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
use std::io::{BufWriter, Cursor, Read};
|
use std::io::{BufWriter, Cursor, Read};
|
||||||
use std::ops::{Add, Div, Mul, Sub};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use actix_web::error::UrlencodedError::ContentType;
|
use actix_web::error::UrlencodedError::ContentType;
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use actix_web::web::{Data, Query};
|
use actix_web::web::Data;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use png::{BitDepth, ColorType, Encoder};
|
use png::{BitDepth, ColorType, Encoder};
|
||||||
use serde::Deserialize;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use wxbox_common::TileRequestOptions;
|
|
||||||
use image::{ImageFormat, ImageReader};
|
|
||||||
use reqwest::ClientBuilder;
|
|
||||||
use wxbox_grib2::GribMessage;
|
use wxbox_grib2::GribMessage;
|
||||||
use wxbox_grib2::wgs84::LatLong;
|
use wxbox_grib2::wgs84::LatLong;
|
||||||
use wxbox_pal::{Color, ColorPalette, Palette};
|
use wxbox_pal::{Color, ColorPalette, Palette};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use tracing::{debug, info};
|
|
||||||
use crate::config::Grib2Source;
|
use crate::config::Grib2Source;
|
||||||
use crate::pixmap::Pixmap;
|
use crate::pixmap::Pixmap;
|
||||||
|
|
||||||
|
@ -75,7 +69,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 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>) -> 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;
|
||||||
|
@ -100,22 +94,9 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett
|
||||||
let color = match nearest {
|
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) == 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) == missing => Color { red: 0, green: 0, blue: 0, alpha: 0 },
|
||||||
Some(c) if Some(c) == range_folded => {
|
Some(c) if Some(c) == range_folded => Color { red: 141, green: 0, blue: 160, alpha: 0 },
|
||||||
if options.show_range_folded {
|
|
||||||
let color_raw = options.range_folded_color.to_be_bytes();
|
|
||||||
Color { red: color_raw[0], green: color_raw[1], blue: color_raw[2], alpha: color_raw[3] }
|
|
||||||
} else {
|
|
||||||
Color { red: 0, green: 0, blue: 0, alpha: 0 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(value_at_pos) => {
|
Some(value_at_pos) => {
|
||||||
let mut c = pal.colorize(value_at_pos);
|
pal.colorize(value_at_pos)
|
||||||
if c.red == 0 && c.green == 0 && c.blue == 0 {
|
|
||||||
c.alpha = 0;
|
|
||||||
} else {
|
|
||||||
c.alpha = coloru8(options.data_transparency);
|
|
||||||
}
|
|
||||||
c
|
|
||||||
},
|
},
|
||||||
None => Color { red: 0, green: 0, blue: 0, alpha: 30 }
|
None => Color { red: 0, green: 0, blue: 0, alpha: 30 }
|
||||||
};
|
};
|
||||||
|
@ -125,231 +106,12 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
image
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct QueryReq {
|
|
||||||
settings: String
|
|
||||||
}
|
|
||||||
|
|
||||||
fn colorf64(i: u8) -> f64 {
|
|
||||||
i as f64 / u8::MAX as f64
|
|
||||||
}
|
|
||||||
fn coloru8(i: f64) -> u8 {
|
|
||||||
(i * u8::MAX as f64).floor() as u8
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct ColorF64 {
|
|
||||||
pub red: f64,
|
|
||||||
pub green: f64,
|
|
||||||
pub blue: f64,
|
|
||||||
pub alpha: f64
|
|
||||||
}
|
|
||||||
impl From<Color> for ColorF64 {
|
|
||||||
fn from(value: Color) -> Self {
|
|
||||||
Self {
|
|
||||||
red: colorf64(value.red),
|
|
||||||
green: colorf64(value.green),
|
|
||||||
blue: colorf64(value.blue),
|
|
||||||
alpha: colorf64(value.alpha)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<ColorF64> for Color {
|
|
||||||
fn from(value: ColorF64) -> Self {
|
|
||||||
Self {
|
|
||||||
red: coloru8(value.red),
|
|
||||||
green: coloru8(value.green),
|
|
||||||
blue: coloru8(value.blue),
|
|
||||||
alpha: coloru8(value.alpha)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Add for ColorF64 {
|
|
||||||
type Output = ColorF64;
|
|
||||||
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Self {
|
|
||||||
red: self.red + rhs.red,
|
|
||||||
green: self.green + rhs.green,
|
|
||||||
blue: self.blue + rhs.blue,
|
|
||||||
alpha: self.alpha + rhs.alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Sub for ColorF64 {
|
|
||||||
type Output = ColorF64;
|
|
||||||
|
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
|
||||||
Self {
|
|
||||||
red: self.red - rhs.red,
|
|
||||||
green: self.green - rhs.green,
|
|
||||||
blue: self.blue - rhs.blue,
|
|
||||||
alpha: self.alpha - rhs.alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Sub<ColorF64> for f64 {
|
|
||||||
type Output = ColorF64;
|
|
||||||
|
|
||||||
fn sub(self, rhs: ColorF64) -> Self::Output {
|
|
||||||
ColorF64 {
|
|
||||||
red: rhs.red - self,
|
|
||||||
green: rhs.green - self,
|
|
||||||
blue: rhs.blue - self,
|
|
||||||
alpha: rhs.alpha - self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Mul<f64> for ColorF64 {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn mul(self, rhs: f64) -> Self::Output {
|
|
||||||
Self {
|
|
||||||
red: self.red * rhs,
|
|
||||||
blue: self.blue * rhs,
|
|
||||||
green: self.green * rhs,
|
|
||||||
alpha: self.alpha * rhs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Mul for ColorF64 {
|
|
||||||
type Output = ColorF64;
|
|
||||||
|
|
||||||
fn mul(self, rhs: Self) -> Self::Output {
|
|
||||||
Self {
|
|
||||||
red: self.red * rhs.red,
|
|
||||||
green: self.green * rhs.green,
|
|
||||||
blue: self.blue * rhs.blue,
|
|
||||||
alpha: self.alpha * rhs.alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Div for ColorF64 {
|
|
||||||
type Output = ColorF64;
|
|
||||||
|
|
||||||
fn div(self, rhs: Self) -> Self::Output {
|
|
||||||
Self {
|
|
||||||
red: self.red / rhs.red,
|
|
||||||
green: self.green / rhs.green,
|
|
||||||
blue: self.blue / rhs.blue,
|
|
||||||
alpha: self.alpha / rhs.alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Div<f64> for ColorF64 {
|
|
||||||
type Output = ColorF64;
|
|
||||||
|
|
||||||
fn div(self, rhs: f64) -> Self::Output {
|
|
||||||
Self {
|
|
||||||
red: self.red / rhs,
|
|
||||||
green: self.green / rhs,
|
|
||||||
blue: self.blue / rhs,
|
|
||||||
alpha: self.alpha / rhs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn merge(base: Pixmap, data: Pixmap) -> Pixmap {
|
|
||||||
let mut new = Pixmap::new();
|
|
||||||
|
|
||||||
for x in 0..256 {
|
|
||||||
for y in 0..256 {
|
|
||||||
let mut c_b: ColorF64 = base.get(x, y).into();
|
|
||||||
let mut c_s: ColorF64 = data.get(x, y).into();
|
|
||||||
|
|
||||||
if c_s.red == 0.0 && c_s.green == 0.0 && c_s.blue == 0.0 && c_s.alpha == 0.0 {
|
|
||||||
new.set(x, y, c_b.into());
|
|
||||||
} else {
|
|
||||||
let mut co = (c_s * c_s.alpha + c_b * c_b.alpha * (1.0 - c_s.alpha)) / (c_s.alpha + c_b.alpha * (1.0 - c_s.alpha));
|
|
||||||
co.alpha = 1.0;
|
|
||||||
|
|
||||||
new.set(x, y, co.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 {
|
|
||||||
let settings: TileRequestOptions = serde_json::from_str(&req.settings).unwrap();
|
|
||||||
|
|
||||||
// todo: load the base layer from external source
|
|
||||||
let base_layer = Pixmap::new();
|
|
||||||
|
|
||||||
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))
|
|
||||||
.send()
|
|
||||||
.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 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
|
|
||||||
} else {
|
|
||||||
debug!("not found baselayer");
|
|
||||||
return HttpResponse::new(StatusCode::NOT_FOUND)
|
|
||||||
};
|
|
||||||
|
|
||||||
// data layer
|
|
||||||
// valid types:
|
|
||||||
// - 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()) {
|
|
||||||
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;
|
|
||||||
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
|
|
||||||
} else {
|
|
||||||
debug!("not found grib2 after reload in base cache");
|
|
||||||
return HttpResponse::new(StatusCode::NOT_FOUND)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!("not found grib2 in configuration");
|
|
||||||
return HttpResponse::new(StatusCode::NOT_FOUND)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug!("not found datalayer registry");
|
|
||||||
return HttpResponse::new(StatusCode::NOT_FOUND)
|
|
||||||
};
|
|
||||||
|
|
||||||
let image = merge(base_layer, data_layer);
|
|
||||||
|
|
||||||
let mut buf: Vec<u8> = vec![];
|
let mut buf: Vec<u8> = vec![];
|
||||||
// borrow checker insanity
|
// borrow checker insanity
|
||||||
{
|
{
|
||||||
let mut cur: Cursor<_> = Cursor::new(&mut buf);
|
let mut cur: Cursor<_> = Cursor::new(&mut buf);
|
||||||
let w = &mut BufWriter::new(&mut cur);
|
let w = &mut BufWriter::new(&mut cur);
|
||||||
let mut encoder = Encoder::new(w, 256, 256);
|
let mut encoder = Encoder::new(w, tilesize as u32, tilesize as u32);
|
||||||
encoder.set_color(ColorType::Rgba);
|
encoder.set_color(ColorType::Rgba);
|
||||||
encoder.set_depth(BitDepth::Eight);
|
encoder.set_depth(BitDepth::Eight);
|
||||||
encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455));
|
encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455));
|
||||||
|
@ -366,14 +128,36 @@ pub async fn source(path: actix_web::web::Path<(i32, u32, u32)>, req: Query<Quer
|
||||||
writer.finish().unwrap();
|
writer.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::get("/grib2/{id}/{z}/{x}/{y}.png")]
|
||||||
|
pub async fn grib2_source(path: actix_web::web::Path<(String, i32, u32, u32)>, data: Data<AppState>) -> HttpResponse {
|
||||||
|
if let Some(known_source) = data.config.sources.grib2.get(&path.0) {
|
||||||
|
reload_if_required(
|
||||||
|
&known_source.from,
|
||||||
|
known_source.needs_gzip,
|
||||||
|
known_source.valid_for.into(),
|
||||||
|
&path.0,
|
||||||
|
&data.grib2_cache_timestamps,
|
||||||
|
&data.grib2_cache
|
||||||
|
).await;
|
||||||
|
let lct_reader = data.grib2_cache_timestamps.read().await;
|
||||||
|
if let Some(grib2) = data.grib2_cache.read().await.get(&path.0) {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.insert_header(actix_web::http::header::ContentType(mime::IMAGE_PNG))
|
.insert_header(actix_web::http::header::ContentType(mime::IMAGE_PNG))
|
||||||
// TODO: use the timestamp in the grib2 ID section
|
// TODO: use the timestamp in the grib2 ID section
|
||||||
//.insert_header(("x-wxbox-tiler-data-valid-time", lct_reader.get(&settings.data).expect("impossible").duration_since(::std::time::UNIX_EPOCH).expect("time went backwards").as_secs().to_string()))
|
.insert_header(("x-wxbox-tiler-data-valid-time", lct_reader.get(&path.0).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-Allow-Origin", "*"))
|
||||||
.insert_header(("Access-Control-Expose-Headers", "*"))
|
.insert_header(("Access-Control-Expose-Headers", "*"))
|
||||||
.insert_header(("Access-Control-Allow-Headers", "*"))
|
.insert_header(("Access-Control-Allow-Headers", "*"))
|
||||||
.body(buf)
|
.body(crate::sources::grib2::render(path.2 as f64, path.3 as f64, path.1, 256, wxbox_pal::parser::parse(&known_source.palette).unwrap(), grib2, known_source.missing, known_source.range_folded, known_source.no_coverage).await)
|
||||||
|
} else {
|
||||||
|
HttpResponse::new(StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HttpResponse::new(StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
|
16
wxbox-web/.dockerignore
Normal file
16
wxbox-web/.dockerignore
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
node_modules
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
.vscode
|
||||||
|
Makefile
|
||||||
|
helm-charts
|
||||||
|
.env
|
||||||
|
.editorconfig
|
||||||
|
.idea
|
||||||
|
cogerage*
|
||||||
|
target*
|
2
wxbox-web/.env.example
Normal file
2
wxbox-web/.env.example
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Base url of your wxbox-tiler instance
|
||||||
|
PUBLIC_TILER_URL_BASE=""
|
21
wxbox-web/.gitignore
vendored
Normal file
21
wxbox-web/.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
1
wxbox-web/.npmrc
Normal file
1
wxbox-web/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
4
wxbox-web/.prettierignore
Normal file
4
wxbox-web/.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
15
wxbox-web/.prettierrc
Normal file
15
wxbox-web/.prettierrc
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
23
wxbox-web/Dockerfile
Normal file
23
wxbox-web/Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
FROM oven/bun:1 AS base
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /tmp/dev
|
||||||
|
COPY package.json bun.lockb /tmp/dev
|
||||||
|
RUN cd /tmp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
RUN mkdir -p /tmp/prod
|
||||||
|
COPY package.json bun.lockb /tmp/prod
|
||||||
|
RUN cd /tmp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=install /tmp/dev/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN bun test
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
USER bun
|
||||||
|
EXPOSE 3000/tcp
|
||||||
|
ENTRYPOINT [ "bun", "--bun", "run", "./build" ]
|
38
wxbox-web/README.md
Normal file
38
wxbox-web/README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
BIN
wxbox-web/bun.lockb
Executable file
BIN
wxbox-web/bun.lockb
Executable file
Binary file not shown.
33
wxbox-web/eslint.config.js
Normal file
33
wxbox-web/eslint.config.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte'],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||||
|
}
|
||||||
|
);
|
37
wxbox-web/package.json
Normal file
37
wxbox-web/package.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "wxbox-web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "eslint . && prettier --check .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
|
"@sveltejs/adapter-node": "^5.2.11",
|
||||||
|
"@sveltejs/kit": "^2.7.3",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"@types/eslint": "^9.6.1",
|
||||||
|
"@types/leaflet": "^1.9.14",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.46.0",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
|
"svelte": "^5.1.3",
|
||||||
|
"svelte-check": "^4.0.5",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "^8.11.0",
|
||||||
|
"vite": "^5.4.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet.sync": "^0.2.4"
|
||||||
|
}
|
||||||
|
}
|
13
wxbox-web/src/app.d.ts
vendored
Normal file
13
wxbox-web/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
12
wxbox-web/src/app.html
Normal file
12
wxbox-web/src/app.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
wxbox-web/src/lib/color.ts
Normal file
12
wxbox-web/src/lib/color.ts
Normal file
|
@ -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
|
||||||
|
}
|
55
wxbox-web/src/lib/global.css
Normal file
55
wxbox-web/src/lib/global.css
Normal file
|
@ -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;
|
||||||
|
}
|
170
wxbox-web/src/lib/map/Map.svelte
Normal file
170
wxbox-web/src/lib/map/Map.svelte
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {ActionReturn} from 'svelte/action';
|
||||||
|
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 {
|
||||||
|
map: LeafletMap | null;
|
||||||
|
selected: boolean;
|
||||||
|
baseLayer: 'osm';
|
||||||
|
dataLayer: 'grib2/noaa_mrms_merged_composite_reflectivity_qc' | null;
|
||||||
|
overlayLayers: string[];
|
||||||
|
reload: boolean;
|
||||||
|
}
|
||||||
|
let { map = $bindable(null), selected, baseLayer, dataLayer, reload = $bindable(false) }: Props = $props();
|
||||||
|
|
||||||
|
let mapContainerElement: HTMLElement;
|
||||||
|
// await import('leaflet') done at runtime
|
||||||
|
let L: Leaflet | null = $state(null);
|
||||||
|
// Base layer - openstreetmap, carto, etc
|
||||||
|
let layer0: TileLayer;
|
||||||
|
// Data layer - composite reflectivity, velocity (the actual data)
|
||||||
|
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
|
||||||
|
$effect(() => {
|
||||||
|
// if leaflet hasn't been imported yet, skip
|
||||||
|
// this also sets a dependency, so we'll be re-ran once it has
|
||||||
|
if (!L) return;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// if there is already a layer0 and a map, remove the old one
|
||||||
|
if (layer0 && map) {
|
||||||
|
layer0.removeFrom(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenStreetMap
|
||||||
|
if (baseLayer === 'osm') {
|
||||||
|
layer0 = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution:
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
});
|
||||||
|
layer0.addTo(map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer1 (data) updating
|
||||||
|
$effect(() => {
|
||||||
|
if (!L) return;
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
// remove existing layer1, if there is one
|
||||||
|
if (layer1 && map) {
|
||||||
|
layer1.removeFrom(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLayer) {
|
||||||
|
layer1 = L.tileLayer(tilerLayerUrl(dataLayer), {
|
||||||
|
attribution: tilerLayerAttribution(dataLayer)
|
||||||
|
});
|
||||||
|
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
|
||||||
|
async function mapAction(): Promise<ActionReturn> {
|
||||||
|
// dynamically imports leaflet, as it's a browser lib
|
||||||
|
L = await import('leaflet');
|
||||||
|
// imports leaflet.sync, for syncing
|
||||||
|
await import('leaflet.sync');
|
||||||
|
|
||||||
|
// create the map
|
||||||
|
map = L.map(mapContainerElement, {
|
||||||
|
// geo center of CONUS
|
||||||
|
center: [39.83, -98.583],
|
||||||
|
zoom: 5,
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!map) return {};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="map" class:mapselected={selected} bind:this={mapContainerElement} use:mapAction></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.map {
|
||||||
|
flex: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 3px solid #000;
|
||||||
|
}
|
||||||
|
.mapselected {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 3px solid green;
|
||||||
|
}
|
||||||
|
</style>
|
2
wxbox-web/src/lib/map/index.ts
Normal file
2
wxbox-web/src/lib/map/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// There is no good-looking three-pane view
|
||||||
|
export type View = 'one' | 'two' | 'four';
|
32
wxbox-web/src/lib/map/layer.ts
Normal file
32
wxbox-web/src/lib/map/layer.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
|
export interface MutexLayerSet<ValidOpts> {
|
||||||
|
map1: ValidOpts;
|
||||||
|
map2: ValidOpts;
|
||||||
|
map3: ValidOpts;
|
||||||
|
map4: ValidOpts;
|
||||||
|
}
|
||||||
|
export interface OverlayLayerSet<ValidOpts> {
|
||||||
|
map1: ValidOpts[];
|
||||||
|
map2: ValidOpts[];
|
||||||
|
map3: ValidOpts[];
|
||||||
|
map4: ValidOpts[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseLayers = 'osm';
|
||||||
|
export type DataLayers = 'grib2/noaa_mrms_merged_composite_reflectivity_qc';
|
||||||
|
|
||||||
|
export function tilerLayerUrl(id: DataLayers): string {
|
||||||
|
if (!env.PUBLIC_TILER_URL_BASE) {
|
||||||
|
throw new Error('PUBLIC_TILER_URL_BASE env var not set!');
|
||||||
|
}
|
||||||
|
const base = new URL(env.PUBLIC_TILER_URL_BASE);
|
||||||
|
return (base + `${id}/{z}/{x}/{y}.png`).toString();
|
||||||
|
}
|
||||||
|
export function tilerLayerAttribution(id: DataLayers): string {
|
||||||
|
let base;
|
||||||
|
if (id === 'grib2/noaa_mrms_merged_composite_reflectivity_qc') {
|
||||||
|
base = '© NOAA';
|
||||||
|
}
|
||||||
|
return base + ', © wxbox';
|
||||||
|
}
|
60
wxbox-web/src/lib/map/sync.svelte.ts
Normal file
60
wxbox-web/src/lib/map/sync.svelte.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import type { Map as LMap } from 'leaflet';
|
||||||
|
import type { View } from '$lib/map';
|
||||||
|
|
||||||
|
export function syncMaps(
|
||||||
|
view: View,
|
||||||
|
map1: LMap | null,
|
||||||
|
map2: LMap | null,
|
||||||
|
map3: LMap | null,
|
||||||
|
map4: LMap | null
|
||||||
|
) {
|
||||||
|
// resize the shown maps
|
||||||
|
if (view === 'one' && map1) {
|
||||||
|
map1.invalidateSize();
|
||||||
|
} else if (view === 'two') {
|
||||||
|
if (map1) map1.invalidateSize();
|
||||||
|
if (map2) map2.invalidateSize();
|
||||||
|
} else if (view === 'four') {
|
||||||
|
if (map1) map1.invalidateSize();
|
||||||
|
if (map2) map2.invalidateSize();
|
||||||
|
if (map3) map3.invalidateSize();
|
||||||
|
if (map4) map4.invalidateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map1 && map2) {
|
||||||
|
map2.setView(map1.getCenter(), map1.getZoom());
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
map1.sync(map2);
|
||||||
|
}
|
||||||
|
if (map1 && map3) {
|
||||||
|
map3.setView(map1.getCenter(), map1.getZoom());
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
map1.sync(map3);
|
||||||
|
}
|
||||||
|
if (map1 && map4) {
|
||||||
|
map4.setView(map1.getCenter(), map1.getZoom());
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
map1.sync(map4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map2 && map1) map2.sync(map1);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map2 && map3) map2.sync(map3);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map2 && map4) map2.sync(map4);
|
||||||
|
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map3 && map1) map3.sync(map1);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map3 && map2) map3.sync(map2);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map3 && map4) map3.sync(map4);
|
||||||
|
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map4 && map1) map4.sync(map1);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map4 && map2) map4.sync(map2);
|
||||||
|
// @ts-expect-error leaflet.sync does not provide typedefs
|
||||||
|
if (map4 && map3) map4.sync(map3);
|
||||||
|
}
|
50
wxbox-web/src/lib/menubar/ButtonBar.svelte
Normal file
50
wxbox-web/src/lib/menubar/ButtonBar.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { MenuItem } from '$lib/menubar/buttonlib';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menu: MenuItem[];
|
||||||
|
}
|
||||||
|
let { menu }: Props = $props();
|
||||||
|
|
||||||
|
function key(e: KeyboardEvent) {
|
||||||
|
let k = e.key;
|
||||||
|
for (let menuItem of menu) {
|
||||||
|
if (k === menuItem.keyboard) {
|
||||||
|
menuItem.action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={key} />
|
||||||
|
|
||||||
|
{#each menu as menuItem}
|
||||||
|
{#if menuItem.visible}
|
||||||
|
{@const index = menuItem.display.indexOf(menuItem.keyboard)}
|
||||||
|
<button disabled={menuItem.disabled} onclick={menuItem.action}>
|
||||||
|
{#if index !== -1}
|
||||||
|
{menuItem.display.substring(0, index)}<u>{menuItem.display.charAt(index)}</u
|
||||||
|
>{menuItem.display.substring(index + 1)}
|
||||||
|
{:else}
|
||||||
|
{menuItem.display} (<u>{menuItem.keyboard}</u>)
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/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>
|
22
wxbox-web/src/lib/menubar/buttonlib.ts
Normal file
22
wxbox-web/src/lib/menubar/buttonlib.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export type Mode =
|
||||||
|
| 'global'
|
||||||
|
| 'view'
|
||||||
|
| 'paneSelect'
|
||||||
|
| 'pane'
|
||||||
|
| 'baseLayerSelect'
|
||||||
|
| 'dataLayerSelect'
|
||||||
|
| 'overlayLayerSelect'
|
||||||
|
| 'dataNOAA'
|
||||||
|
| 'dataNOAAMRMS';
|
||||||
|
|
||||||
|
export interface MenuItem {
|
||||||
|
display: string;
|
||||||
|
keyboard: string;
|
||||||
|
disabled: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
action: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuRegistry = {
|
||||||
|
[id in Mode]: MenuItem[];
|
||||||
|
};
|
13
wxbox-web/src/routes/+layout.svelte
Normal file
13
wxbox-web/src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
import "$lib/global.css";
|
||||||
|
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
let { children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
415
wxbox-web/src/routes/+page.svelte
Normal file
415
wxbox-web/src/routes/+page.svelte
Normal file
|
@ -0,0 +1,415 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Map from '$lib/map/Map.svelte';
|
||||||
|
import type { MenuRegistry, Mode } from '$lib/menubar/buttonlib';
|
||||||
|
import ButtonBar from '$lib/menubar/ButtonBar.svelte';
|
||||||
|
import type { BaseLayers, DataLayers, MutexLayerSet, OverlayLayerSet } from '$lib/map/layer';
|
||||||
|
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);
|
||||||
|
let map3: LMap | null = $state(null);
|
||||||
|
let map4: LMap | null = $state(null);
|
||||||
|
|
||||||
|
let view: View = $state('one');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
syncMaps(view, map1, map2, map3, map4);
|
||||||
|
});
|
||||||
|
|
||||||
|
let pane: 'map1' | 'map2' | 'map3' | 'map4' = $state('map1');
|
||||||
|
|
||||||
|
let baseLayer: MutexLayerSet<BaseLayers> = $state({
|
||||||
|
map1: 'osm',
|
||||||
|
map2: 'osm',
|
||||||
|
map3: 'osm',
|
||||||
|
map4: 'osm'
|
||||||
|
});
|
||||||
|
let dataLayer: MutexLayerSet<DataLayers | null> = $state({
|
||||||
|
map1: null,
|
||||||
|
map2: null,
|
||||||
|
map3: null,
|
||||||
|
map4: null
|
||||||
|
});
|
||||||
|
let overlayLayers: OverlayLayerSet<string> = $state({
|
||||||
|
map1: [],
|
||||||
|
map2: [],
|
||||||
|
map3: [],
|
||||||
|
map4: []
|
||||||
|
});
|
||||||
|
|
||||||
|
let mode: Mode = $state('global');
|
||||||
|
let registry: MenuRegistry = $derived({
|
||||||
|
global: [
|
||||||
|
{
|
||||||
|
display: 'view',
|
||||||
|
keyboard: 'v',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'view';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'pane',
|
||||||
|
keyboard: 'p',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'paneSelect';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
view: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'global';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'view: 1',
|
||||||
|
keyboard: '1',
|
||||||
|
disabled: view === 'one',
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
view = 'one';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'view: 2',
|
||||||
|
keyboard: '2',
|
||||||
|
disabled: view === 'two',
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
view = 'two';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'view: 4',
|
||||||
|
keyboard: '4',
|
||||||
|
disabled: view === 'four',
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
view = 'four';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
paneSelect: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'global';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'pane: 1',
|
||||||
|
keyboard: '1',
|
||||||
|
disabled: pane === 'map1',
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
pane = 'map1';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'pane: 2',
|
||||||
|
keyboard: '2',
|
||||||
|
disabled: pane === 'map2',
|
||||||
|
visible: view === 'two' || view === 'four',
|
||||||
|
action: () => {
|
||||||
|
pane = 'map2';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'pane: 3',
|
||||||
|
keyboard: '3',
|
||||||
|
disabled: pane === 'map3',
|
||||||
|
visible: view === 'four',
|
||||||
|
action: () => {
|
||||||
|
pane = 'map3';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'pane: 4',
|
||||||
|
keyboard: '4',
|
||||||
|
disabled: pane === 'map4',
|
||||||
|
visible: view === 'four',
|
||||||
|
action: () => {
|
||||||
|
pane = 'map4';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'select',
|
||||||
|
keyboard: 'Enter',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'pane';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pane: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'paneSelect';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'base layer',
|
||||||
|
keyboard: 'b',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'baseLayerSelect';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'data layer',
|
||||||
|
keyboard: 'd',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'dataLayerSelect';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'overlays',
|
||||||
|
keyboard: 'o',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'overlayLayerSelect';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
baseLayerSelect: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'pane';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'OpenStreetMap',
|
||||||
|
keyboard: 'o',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
baseLayer[pane] = 'osm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataLayerSelect: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'pane';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'none',
|
||||||
|
keyboard: '0',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
dataLayer[pane] = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'noaa',
|
||||||
|
keyboard: 'n',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'dataNOAA';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataNOAA: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'dataLayerSelect';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'multi-radar multi-sensor',
|
||||||
|
keyboard: 'm',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'dataNOAAMRMS';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataNOAAMRMS: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'dataNOAA';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'composite reflectivity - merged qc',
|
||||||
|
keyboard: 'r',
|
||||||
|
disabled: dataLayer[pane] === 'grib2/noaa_mrms_merged_composite_reflectivity_qc',
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
dataLayer[pane] = 'grib2/noaa_mrms_merged_composite_reflectivity_qc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
overlayLayerSelect: [
|
||||||
|
{
|
||||||
|
display: 'back',
|
||||||
|
keyboard: 'Escape',
|
||||||
|
disabled: false,
|
||||||
|
visible: true,
|
||||||
|
action: () => {
|
||||||
|
mode = 'pane';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
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<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>
|
||||||
|
|
||||||
|
<div class="outercontainer">
|
||||||
|
<div class="toolbar">
|
||||||
|
<h1>wxbox</h1>
|
||||||
|
<span class="status">{status}</span>
|
||||||
|
<ButtonBar menu={registry[mode]} />
|
||||||
|
<span class="dataState">data reload in {timeUntilReload}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<Map
|
||||||
|
selected={pane === 'map1'}
|
||||||
|
bind:map={map1}
|
||||||
|
baseLayer={baseLayer.map1}
|
||||||
|
dataLayer={dataLayer.map1}
|
||||||
|
overlayLayers={overlayLayers.map1}
|
||||||
|
bind:reload={reload1}
|
||||||
|
/>
|
||||||
|
{#if view === 'two' || view === 'four'}
|
||||||
|
<Map
|
||||||
|
selected={pane === 'map2'}
|
||||||
|
bind:map={map2}
|
||||||
|
baseLayer={baseLayer.map2}
|
||||||
|
dataLayer={dataLayer.map2}
|
||||||
|
overlayLayers={overlayLayers.map2}
|
||||||
|
bind:reload={reload2}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if view === 'four'}
|
||||||
|
<div class="container">
|
||||||
|
<Map
|
||||||
|
selected={pane === 'map3'}
|
||||||
|
bind:map={map3}
|
||||||
|
baseLayer={baseLayer.map3}
|
||||||
|
dataLayer={dataLayer.map3}
|
||||||
|
overlayLayers={overlayLayers.map3}
|
||||||
|
bind:reload={reload3}
|
||||||
|
/>
|
||||||
|
<Map
|
||||||
|
selected={pane === 'map4'}
|
||||||
|
bind:map={map4}
|
||||||
|
baseLayer={baseLayer.map4}
|
||||||
|
dataLayer={dataLayer.map4}
|
||||||
|
overlayLayers={overlayLayers.map4}
|
||||||
|
bind:reload={reload4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="footer text-sm">
|
||||||
|
<p>built with <3</p>
|
||||||
|
<p>u8.lc & coredoes.dev :)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toolbar h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
margin: var(--size-1) var(--size-2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.outercontainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-left: var(--size-2);
|
||||||
|
margin-right: var(--size-2);
|
||||||
|
}
|
||||||
|
.footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
BIN
wxbox-web/static/favicon.png
Normal file
BIN
wxbox-web/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
18
wxbox-web/svelte.config.js
Normal file
18
wxbox-web/svelte.config.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import adapter from '@sveltejs/adapter-node';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||||
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
19
wxbox-web/tsconfig.json
Normal file
19
wxbox-web/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
6
wxbox-web/vite.config.ts
Normal file
6
wxbox-web/vite.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
|
@ -1,12 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "wxbox_client"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
walkers = "0.32"
|
|
||||||
eframe = "0.30"
|
|
||||||
egui = "0.30"
|
|
||||||
egui_extras = "0.30"
|
|
||||||
wxbox_common = { path = "../wxbox_common" }
|
|
||||||
serde_json = "1"
|
|
|
@ -1,212 +0,0 @@
|
||||||
mod toggle_switch;
|
|
||||||
|
|
||||||
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 wxbox_common::TileRequestOptions;
|
|
||||||
use crate::toggle_switch::toggle;
|
|
||||||
|
|
||||||
pub struct WxboxApp {
|
|
||||||
provider: HttpTiles,
|
|
||||||
map_memory: MapMemory,
|
|
||||||
tile_request_options: TileRequestOptions,
|
|
||||||
|
|
||||||
rf_color: Color32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DynamicUrlSource {
|
|
||||||
pub url_query: String
|
|
||||||
}
|
|
||||||
impl DynamicUrlSource {
|
|
||||||
pub fn new_from(options: &TileRequestOptions) -> Self {
|
|
||||||
Self {
|
|
||||||
url_query: serde_json::to_string(options).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl TileSource for DynamicUrlSource {
|
|
||||||
fn tile_url(&self, tile_id: TileId) -> String {
|
|
||||||
format!(
|
|
||||||
"{}/{}/{}/{}.png?settings={}",
|
|
||||||
env!("TILER_BASE_URL"),
|
|
||||||
tile_id.zoom, tile_id.x, tile_id.y,
|
|
||||||
self.url_query
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn attribution(&self) -> Attribution {
|
|
||||||
Attribution {
|
|
||||||
text: "OpenStreetMap contributors, NOAA, wxbox",
|
|
||||||
url: "https://copyright.wxbox.e3t.cc",
|
|
||||||
logo_light: None,
|
|
||||||
logo_dark: None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl WxboxApp {
|
|
||||||
pub fn new(ctx: Context) -> Self {
|
|
||||||
egui_extras::install_image_loaders(&ctx);
|
|
||||||
|
|
||||||
let req_options = TileRequestOptions {
|
|
||||||
baselayer: "osm".to_string(),
|
|
||||||
|
|
||||||
data: "grib2/noaa_mrms_merged_composite_reflectivity_qc_CONUS".to_string(),
|
|
||||||
data_transparency: 0.9,
|
|
||||||
show_range_folded: false,
|
|
||||||
range_folded_color: 0x8d00a0ff,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
provider: HttpTiles::with_options(
|
|
||||||
DynamicUrlSource::new_from(&req_options),
|
|
||||||
HttpOptions {
|
|
||||||
cache: if cfg!(target_arch = "wasm32") {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(".cache".into())
|
|
||||||
},
|
|
||||||
user_agent: None,
|
|
||||||
},
|
|
||||||
ctx.clone()
|
|
||||||
),
|
|
||||||
map_memory: MapMemory::default(),
|
|
||||||
tile_request_options: req_options,
|
|
||||||
rf_color: Color32::from_hex("#8d00a0ff").unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for WxboxApp {
|
|
||||||
fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) {
|
|
||||||
let rimless = Frame {
|
|
||||||
fill: ctx.style().visuals.panel_fill,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
egui::CentralPanel::default()
|
|
||||||
.frame(rimless)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
let Self {
|
|
||||||
provider,
|
|
||||||
map_memory,
|
|
||||||
tile_request_options,
|
|
||||||
rf_color
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let position = Position::from_lat_lon(44.967243, -103.771556);
|
|
||||||
|
|
||||||
let tiles = provider;
|
|
||||||
let attribution = tiles.attribution();
|
|
||||||
|
|
||||||
let map = walkers::Map::new(Some(tiles), map_memory, position);
|
|
||||||
|
|
||||||
ui.add(map);
|
|
||||||
|
|
||||||
Window::new("Attribution")
|
|
||||||
.collapsible(false)
|
|
||||||
.resizable(false)
|
|
||||||
.title_bar(false)
|
|
||||||
.anchor(Align2::RIGHT_BOTTOM, [-10.0, -10.0])
|
|
||||||
.show(ui.ctx(), |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.hyperlink_to(attribution.text, attribution.url);
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("wxbox is made with <3 by ");
|
|
||||||
ui.hyperlink_to("core", "https://coredoes.dev");
|
|
||||||
ui.label(" and ");
|
|
||||||
ui.hyperlink_to("tm85", "https://u8.lc");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Window::new("wxbox")
|
|
||||||
.resizable(false)
|
|
||||||
.show(ui.ctx(), |ui| {
|
|
||||||
ui.heading("Welcome to wxbox!");
|
|
||||||
ui.label("We're glad you're here; Open up the Datasource panel to get started.");
|
|
||||||
ui.label("If you're using a mouse, you can click and drag to move around and Ctrl+Scroll to zoom.");
|
|
||||||
ui.label("On a touch-based device like a trackpad or touchscreen, you can tap/click and drag to move around and two-finger pinch to zoom.");
|
|
||||||
ui.label("You can close this window by clicking the arrow to the left of it's title.");
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut need_to_reset_for_next_frame = false;
|
|
||||||
|
|
||||||
Window::new("Datasource")
|
|
||||||
.resizable(false)
|
|
||||||
.default_open(false)
|
|
||||||
.show(ui.ctx(), |ui| {
|
|
||||||
ui.collapsing("NOAA", |ui| {
|
|
||||||
ui.collapsing("Multi-Radar Multi-Sensor", |ui| {
|
|
||||||
for location in ["CONUS", "ALASKA", "CARIB", "GUAM", "HAWAII"] {
|
|
||||||
ui.collapsing(location, |ui| {
|
|
||||||
ui.collapsing("Composite Reflectivity", |ui| {
|
|
||||||
if ui.radio_value(&mut tile_request_options.data, format!("grib2/noaa_mrms_merged_composite_reflectivity_qc_{}", location), "Composite Reflectivity (QCd)").changed() {
|
|
||||||
need_to_reset_for_next_frame = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if location == "CONUS" {
|
|
||||||
ui.collapsing("RhoHV (Correlation Coefficient)", |ui| {
|
|
||||||
if ui.radio_value(&mut tile_request_options.data, format!("grib2/noaa_mrms_merged_rhohv_3km_{}", location), "RhoHV @ 3.00 KM").changed() {
|
|
||||||
need_to_reset_for_next_frame = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
egui::Grid::new("dsconfig_grid")
|
|
||||||
.num_columns(2)
|
|
||||||
.spacing([40.0, 4.0])
|
|
||||||
.striped(true)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("Data opacity");
|
|
||||||
if ui.add(egui::Slider::new(&mut tile_request_options.data_transparency, 0.0..=1.0).suffix("%").custom_formatter(
|
|
||||||
|u, v| {
|
|
||||||
egui::emath::format_with_decimals_in_range(u * 100.0, v)
|
|
||||||
}
|
|
||||||
)).changed() {
|
|
||||||
need_to_reset_for_next_frame = true;
|
|
||||||
}
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("Show range folded areas");
|
|
||||||
if ui.add(toggle(&mut tile_request_options.show_range_folded)).changed() {
|
|
||||||
need_to_reset_for_next_frame = true;
|
|
||||||
}
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("Range folded color");
|
|
||||||
if ui.color_edit_button_srgba(rf_color).changed() {
|
|
||||||
let color = rf_color.to_array();
|
|
||||||
let single_number = u32::from_be_bytes(color);
|
|
||||||
tile_request_options.range_folded_color = single_number;
|
|
||||||
need_to_reset_for_next_frame = true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if need_to_reset_for_next_frame {
|
|
||||||
*tiles = HttpTiles::with_options(
|
|
||||||
DynamicUrlSource::new_from(tile_request_options),
|
|
||||||
HttpOptions {
|
|
||||||
cache: if cfg!(target_arch = "wasm32") {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(".cache".into())
|
|
||||||
},
|
|
||||||
user_agent: None,
|
|
||||||
},
|
|
||||||
ctx.clone()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
//! Source code example of how to create your own widget.
|
|
||||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
|
||||||
|
|
||||||
/// iOS-style toggle switch:
|
|
||||||
///
|
|
||||||
/// ``` text
|
|
||||||
/// _____________
|
|
||||||
/// / /.....\
|
|
||||||
/// | |.......|
|
|
||||||
/// \_______\_____/
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
/// ``` ignore
|
|
||||||
/// toggle_ui(ui, &mut my_bool);
|
|
||||||
/// ```
|
|
||||||
pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
|
||||||
// Widget code can be broken up in four steps:
|
|
||||||
// 1. Decide a size for the widget
|
|
||||||
// 2. Allocate space for it
|
|
||||||
// 3. Handle interactions with the widget (if any)
|
|
||||||
// 4. Paint the widget
|
|
||||||
|
|
||||||
// 1. Deciding widget size:
|
|
||||||
// You can query the `ui` how much space is available,
|
|
||||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
|
||||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
|
||||||
|
|
||||||
// 2. Allocating space:
|
|
||||||
// This is where we get a region of the screen assigned.
|
|
||||||
// We also tell the Ui to sense clicks in the allocated region.
|
|
||||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
|
||||||
|
|
||||||
// 3. Interact: Time to check for clicks!
|
|
||||||
if response.clicked() {
|
|
||||||
*on = !*on;
|
|
||||||
response.mark_changed(); // report back that the value changed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach some meta-data to the response which can be used by screen readers:
|
|
||||||
response.widget_info(|| {
|
|
||||||
egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "")
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Paint!
|
|
||||||
// Make sure we need to paint:
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
// Let's ask for a simple animation from egui.
|
|
||||||
// egui keeps track of changes in the boolean associated with the id and
|
|
||||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
|
||||||
let how_on = ui.ctx().animate_bool_responsive(response.id, *on);
|
|
||||||
// We will follow the current style by asking
|
|
||||||
// "how should something that is being interacted with be painted?".
|
|
||||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
|
||||||
let visuals = ui.style().interact_selectable(&response, *on);
|
|
||||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
|
||||||
let rect = rect.expand(visuals.expansion);
|
|
||||||
let radius = 0.5 * rect.height();
|
|
||||||
ui.painter()
|
|
||||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
|
||||||
// Paint the circle, animating it from left to right with `how_on`:
|
|
||||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
|
||||||
let center = egui::pos2(circle_x, rect.center().y);
|
|
||||||
ui.painter()
|
|
||||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All done! Return the interaction response so the user can check what happened
|
|
||||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Here is the same code again, but a bit more compact:
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
|
||||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
|
||||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
|
||||||
if response.clicked() {
|
|
||||||
*on = !*on;
|
|
||||||
response.mark_changed();
|
|
||||||
}
|
|
||||||
response.widget_info(|| {
|
|
||||||
egui::WidgetInfo::selected(egui::WidgetType::Checkbox, ui.is_enabled(), *on, "")
|
|
||||||
});
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
let how_on = ui.ctx().animate_bool_responsive(response.id, *on);
|
|
||||||
let visuals = ui.style().interact_selectable(&response, *on);
|
|
||||||
let rect = rect.expand(visuals.expansion);
|
|
||||||
let radius = 0.5 * rect.height();
|
|
||||||
ui.painter()
|
|
||||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
|
||||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
|
||||||
let center = egui::pos2(circle_x, rect.center().y);
|
|
||||||
ui.painter()
|
|
||||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))`
|
|
||||||
/// iOS-style toggle switch.
|
|
||||||
///
|
|
||||||
/// ## Example:
|
|
||||||
/// ``` ignore
|
|
||||||
/// ui.add(toggle(&mut my_bool));
|
|
||||||
/// ```
|
|
||||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
|
||||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "wxbox_client_native"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
wxbox_client = { path = "../wxbox_client" }
|
|
||||||
eframe = "0.30"
|
|
||||||
egui = "0.30"
|
|
||||||
tracing-subscriber = "0.3"
|
|
|
@ -1,10 +0,0 @@
|
||||||
use wxbox_client::WxboxApp;
|
|
||||||
|
|
||||||
fn main() -> Result<(), eframe::Error> {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
eframe::run_native(
|
|
||||||
"wxbox",
|
|
||||||
Default::default(),
|
|
||||||
Box::new(|cc| Ok(Box::new(WxboxApp::new(cc.egui_ctx.clone()))))
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "wxbox_client_wasm"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
console_error_panic_hook = "0.1"
|
|
||||||
wasm-tracing = "1"
|
|
||||||
eframe = "0.30"
|
|
||||||
wxbox_client = { path = "../wxbox_client" }
|
|
||||||
wasm-bindgen-futures = "0.4"
|
|
||||||
web-sys = "0.3"
|
|
|
@ -1,33 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>wxbox</title>
|
|
||||||
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<canvas id="target"></canvas>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,30 +0,0 @@
|
||||||
use wxbox_client::WxboxApp;
|
|
||||||
use web_sys::wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
wasm_tracing::set_as_global_default();
|
|
||||||
|
|
||||||
let web_options = eframe::WebOptions::default();
|
|
||||||
wasm_bindgen_futures::spawn_local(async {
|
|
||||||
let document = web_sys::window()
|
|
||||||
.expect("no window?")
|
|
||||||
.document()
|
|
||||||
.expect("no document?");
|
|
||||||
|
|
||||||
let canvas = document
|
|
||||||
.get_element_by_id("target")
|
|
||||||
.expect("failed to find target canvas for rendering")
|
|
||||||
.dyn_into::<web_sys::HtmlCanvasElement>()
|
|
||||||
.expect("#target is not a canvas");
|
|
||||||
|
|
||||||
eframe::WebRunner::new()
|
|
||||||
.start(
|
|
||||||
canvas,
|
|
||||||
web_options,
|
|
||||||
Box::new(|cc| Ok(Box::new(WxboxApp::new(cc.egui_ctx.clone()))))
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("failed to start eframe")
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "wxbox_common"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
|
@ -1,12 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct TileRequestOptions {
|
|
||||||
pub baselayer: String,
|
|
||||||
|
|
||||||
pub data: String,
|
|
||||||
pub data_transparency: f64,
|
|
||||||
|
|
||||||
pub show_range_folded: bool,
|
|
||||||
pub range_folded_color: u32
|
|
||||||
}
|
|
Loading…
Reference in a new issue