break everything

This commit is contained in:
core 2025-01-03 22:23:08 -05:00
parent 01d7181aaf
commit 181db4e881
Signed by: core
GPG key ID: FDBF740DADDCEECF
43 changed files with 3620 additions and 1193 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
*/target
target
node_modules
node_modules
.cache

View file

@ -6,6 +6,12 @@
<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-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$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />

3376
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = [ "wxbox-grib2","wxbox-pal","wxbox-tiler"]
members = [ "wxbox-grib2","wxbox-pal","wxbox-tiler", "wxbox_client", "wxbox_client_native", "wxbox_client_wasm", "wxbox_common"]
[profile.release]
codegen-units = 1
@ -9,4 +9,4 @@ lto = "fat"
[profile.dev.package.image]
opt-level = 3
[profile.dev.package.png]
opt-level = 3
opt-level = 3

View file

@ -1,5 +1,5 @@
[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc]
from = "https://mrms.ncep.noaa.gov/data/2D/HAWAII/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
from = "https://mrms.ncep.noaa.gov/data/2D/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
needs_gzip = true
valid_for = 120
palette = """

View file

@ -21,6 +21,9 @@ tracing = "0.1"
tracing-subscriber = "0.3"
toml = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
wxbox_common = { path = "../wxbox_common" }
image = "0.25"
[dev-dependencies]
approx = "0.5"

View file

@ -36,7 +36,7 @@ async fn main() -> std::io::Result<()> {
});
HttpServer::new(move || {
App::new()
.service(sources::grib2::grib2_source)
.service(sources::grib2::source)
.app_data(data.clone())
})
.bind(("::", 8080))?

View file

@ -6,14 +6,19 @@ use std::time::SystemTime;
use actix_web::error::UrlencodedError::ContentType;
use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use actix_web::web::Data;
use actix_web::web::{Data, Query};
use flate2::read::GzDecoder;
use png::{BitDepth, ColorType, Encoder};
use serde::Deserialize;
use tokio::sync::RwLock;
use wxbox_common::TileRequestOptions;
use image::{ImageFormat, ImageReader};
use reqwest::ClientBuilder;
use wxbox_grib2::GribMessage;
use wxbox_grib2::wgs84::LatLong;
use wxbox_pal::{Color, ColorPalette, Palette};
use crate::AppState;
use tracing::debug;
use crate::config::Grib2Source;
use crate::pixmap::Pixmap;
@ -69,7 +74,7 @@ pub async fn reload_if_required(from: &str, needs_gzip: bool, valid_for: u64, lu
const TWO_PI: f64 = PI * 2.0;
const HALF_PI: f64 = PI / 2.0;
pub 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> {
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>) -> Pixmap {
let mut image: Pixmap = Pixmap::new();
let denominator = 2.0_f64.powi(z) * tilesize as f64;
@ -106,12 +111,127 @@ 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
}
pub fn merge(base: Pixmap, data: Pixmap, settings: &TileRequestOptions) -> Pixmap {
let mut new = Pixmap::new();
let a = settings.data_transparency;
for x in 0..256 {
for y in 0..256 {
let lower = base.get(x, y);
let upper = data.get(x, y);
let lr = colorf64(lower.red);
let lg = colorf64(lower.green);
let lb = colorf64(lower.blue);
let ur = colorf64(upper.red);
let ug = colorf64(upper.green);
let ub = colorf64(lower.blue);
new.set(x, y, Color {
red: coloru8(lr + (lr - ur * a).abs()),
green: coloru8(lg + (lg - ug * a).abs()),
blue: coloru8(lb + (lb - ub * a).abs()),
alpha: 255
});
}
}
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(255 - x as u32, y 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).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, &settings);
let image = base_layer;
let mut buf: Vec<u8> = vec![];
// borrow checker insanity
{
let mut cur: Cursor<_> = Cursor::new(&mut buf);
let w = &mut BufWriter::new(&mut cur);
let mut encoder = Encoder::new(w, tilesize as u32, tilesize as u32);
let mut encoder = Encoder::new(w, 256, 256);
encoder.set_color(ColorType::Rgba);
encoder.set_depth(BitDepth::Eight);
encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455));
@ -128,36 +248,14 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett
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()
.insert_header(actix_web::http::header::ContentType(mime::IMAGE_PNG))
// TODO: use the timestamp in the grib2 ID section
.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-Expose-Headers", "*"))
.insert_header(("Access-Control-Allow-Headers", "*"))
.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)
}
HttpResponse::Ok()
.insert_header(actix_web::http::header::ContentType(mime::IMAGE_PNG))
// 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(("Access-Control-Allow-Origin", "*"))
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header(("Access-Control-Allow-Headers", "*"))
.body(buf)
}
/*
#[macro_export]

View file

@ -1,16 +0,0 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
cogerage*
target*

View file

@ -1,2 +0,0 @@
# Base url of your wxbox-tiler instance
PUBLIC_TILER_URL_BASE=""

21
wxbox-web/.gitignore vendored
View file

@ -1,21 +0,0 @@
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-*

View file

@ -1 +0,0 @@
engine-strict=true

View file

@ -1,4 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View file

@ -1,15 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View file

@ -1,23 +0,0 @@
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" ]

View file

@ -1,38 +0,0 @@
# 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.

Binary file not shown.

View file

@ -1,33 +0,0 @@
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/']
}
);

View file

@ -1,37 +0,0 @@
{
"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"
}
}

View file

@ -1,13 +0,0 @@
// 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 {};

View file

@ -1,12 +0,0 @@
<!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>

View file

@ -1,12 +0,0 @@
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
}

View file

@ -1,55 +0,0 @@
: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;
}

View file

@ -1,170 +0,0 @@
<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:
'&copy; <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>

View file

@ -1,2 +0,0 @@
// There is no good-looking three-pane view
export type View = 'one' | 'two' | 'four';

View file

@ -1,32 +0,0 @@
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 = '&copy; NOAA';
}
return base + ', &copy; wxbox';
}

View file

@ -1,60 +0,0 @@
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);
}

View file

@ -1,50 +0,0 @@
<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>

View file

@ -1,22 +0,0 @@
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[];
};

View file

@ -1,13 +0,0 @@
<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()}

View file

@ -1,415 +0,0 @@
<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 &lt;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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,18 +0,0 @@
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;

View file

@ -1,19 +0,0 @@
{
"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
}

View file

@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});

12
wxbox_client/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[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"

96
wxbox_client/src/lib.rs Normal file
View file

@ -0,0 +1,96 @@
use std::collections::HashMap;
use std::env::var;
use egui::{Align2, Frame, Window};
use egui::Context;
use walkers::{HttpOptions, HttpTiles, MapMemory, Position, TileId, Tiles};
use walkers::sources::{Attribution, TileSource};
use wxbox_common::TileRequestOptions;
pub struct WxboxApp {
provider: HttpTiles,
map_memory: MapMemory
}
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={}",
var("TILER_BASE_URL").unwrap(),
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);
Self {
provider: HttpTiles::with_options(
DynamicUrlSource::new_from(&TileRequestOptions {
baselayer: "osm".to_string(),
data: "grib2/noaa_mrms_merged_composite_reflectivity_qc".to_string(),
data_transparency: 0.5,
}),
Default::default(),
ctx.clone()
),
map_memory: MapMemory::default()
}
}
}
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 position = Position::from_lat_lon(44.967243, -103.771556);
let tiles = &mut self.provider;
let attribution = tiles.attribution();
let map = walkers::Map::new(Some(tiles), &mut self.map_memory, position);
ui.add(map);
Window::new("Attribution")
.collapsible(false)
.resizable(false)
.title_bar(false)
.anchor(Align2::LEFT_TOP, [10.0, 10.0])
.show(ui.ctx(), |ui| {
ui.horizontal(|ui| {
ui.hyperlink_to(attribution.text, attribution.url);
})
})
});
}
}

View file

@ -0,0 +1,10 @@
[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"

View file

@ -0,0 +1,10 @@
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()))))
)
}

View file

@ -0,0 +1,6 @@
[package]
name = "wxbox_client_wasm"
version = "0.1.0"
edition = "2021"
[dependencies]

View file

@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

7
wxbox_common/Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "wxbox_common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1"

9
wxbox_common/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct TileRequestOptions {
pub baselayer: String,
pub data: String,
pub data_transparency: f64
}