initial commit

This commit is contained in:
core 2024-10-18 21:34:08 -04:00
commit 3f9078a48b
Signed by: core
GPG Key ID: 9D0DAED5555DD0B4
33 changed files with 13267 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*/target
target
node_modules

2264
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

7
Cargo.toml Normal file
View File

@ -0,0 +1,7 @@
[workspace]
resolver = "2"
members = ["wxbox-tiler"]
[profile.release]
codegen-units = 1
lto = "fat"

Binary file not shown.

Binary file not shown.

BIN
heartbeat-50m.bin Normal file

Binary file not shown.

1018
log.txt Normal file

File diff suppressed because one or more lines are too long

7022
log2.txt Normal file

File diff suppressed because it is too large Load Diff

1
wxbox-tiler/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

16
wxbox-tiler/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "wxbox-tiler"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4"
thiserror = "1"
grib = { git = "https://github.com/noritada/grib-rs" }
ordered-float = "4"
tikv-jemallocator = "0.6"
reqwest = "0.12"
flate2 = "1"
tokio = "1"

213
wxbox-tiler/src/coords.rs Normal file
View File

@ -0,0 +1,213 @@
/// Constant representing the value of PI
const PI: f64 = std::f64::consts::PI;
/// Constant representing the conversion factor from radians to degrees
const R2D: f64 = 180.0f64 / PI;
/// Earth radius in meters
const RE: f64 = 6378137.0;
/// Circumference of the Earth
const CE: f64 = 2.0f64 * PI * RE;
/// Represents a tile on a map
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Tile {
pub x: i32,
pub y: i32,
pub z: i32,
}
impl Tile {
/// Creates a new Tile object with the specified x, y, and zoom level
pub fn new(x: i32, y: i32, z: i32) -> Self {
let (lo, hi) = minmax(z);
if !(lo <= x && x <= hi) || !(lo <= y && y <= hi) {
panic!("require tile x and y to be within the range (0, 2 ** zoom)");
}
Tile { x, y, z }
}
}
/// Returns the minimum and maximum values for a tile at the given zoom level
fn minmax(z: i32) -> (i32, i32) {
let max_value = 2_i32.pow(z as u32);
(0, max_value - 1)
}
/// Represents a geographical coordinate in longitude and latitude
#[derive(Debug, PartialEq)]
pub struct LngLat {
pub lng: f64,
pub lat: f64,
}
/// Represents a point in web mercator projected coordinates
#[derive(Debug, PartialEq)]
pub struct XY {
pub x: f64,
pub y: f64,
}
/// Represents a bounding box in geographical coordinates
#[derive(Debug, PartialEq)]
pub struct LngLatBbox {
/// Westernmost longitude of the bounding box
pub west: f64,
/// Southernmost latitude of the bounding box
pub south: f64,
/// Easternmost longitude of the bounding box
pub east: f64,
/// Northernmost latitude of the bounding box
pub north: f64,
}
/// Represents a bounding box in web mercator projected coordinates
#[derive(Debug, PartialEq)]
pub struct Bbox {
/// Left coordinate of the bounding box
pub left: f64,
/// Bottom coordinate of the bounding box
pub bottom: f64,
/// Right coordinate of the bounding box
pub right: f64,
/// Top coordinate of the bounding box
pub top: f64,
}
/// Calculates the upper-left geographical coordinates of a given tile
pub fn ul(tile: Tile) -> LngLat {
let z2 = 2.0_f64.powf(tile.z as f64);
let lon_deg = tile.x as f64 / z2 * 360.0 - 180.0;
let lat_rad = (PI * (1.0 - 2.0 * tile.y as f64 / z2)).sinh().atan();
let lat_deg = lat_rad.to_degrees();
LngLat { lng: lon_deg, lat: lat_deg }
}
/// Calculates the bounding box of a given tile in geographical coordinates
pub fn bounds(tile: Tile) -> LngLatBbox {
let z2 = 2.0_f64.powf(tile.z as f64);
let west = tile.x as f64 / z2 * 360.0 - 180.0;
let north_rad = (PI * (1.0 - 2.0 * tile.y as f64 / z2)).sinh().atan();
let north = north_rad.to_degrees();
let east = (tile.x + 1) as f64 / z2 * 360.0 - 180.0;
let south_rad = (PI * (1.0 - 2.0 * (tile.y + 1) as f64 / z2)).sinh().atan();
let south = south_rad.to_degrees();
LngLatBbox { west, south, east, north }
}
/// Calculates the bounding box of a given tile in web mercator projected coordinates
pub fn xy_bounds(tile: Tile) -> Bbox {
let tile_size = CE / 2.0_f64.powf(tile.z as f64);
let left = tile.x as f64 * tile_size - CE / 2.0;
let right = left + tile_size;
let top = CE / 2.0 - tile.y as f64 * tile_size;
let bottom = top - tile_size;
Bbox { left, bottom, right, top }
}
/// Converts geographical coordinates (LngLat) to web mercator projected coordinates (XY)
pub fn convert_xy(lng_lat: LngLat) -> XY {
let x = RE * lng_lat.lng.to_radians();
let y: f64;
if lng_lat.lat <= -90.0 {
y = f64::NEG_INFINITY;
} else if lng_lat.lat >= 90.0 {
y = f64::INFINITY;
} else {
y = RE * ((PI * 0.25) + (0.5 * lng_lat.lat.to_radians())).tan().ln();
}
XY { x, y }
}
/// Converts web mercator projected coordinates (XY) to geographical coordinates (LngLat)
pub fn convert_lng_lat(xy: XY) -> LngLat {
let lng = xy.x * R2D / RE;
let lat = ((PI * 0.5) - 2.0 * (-xy.y / RE).exp().atan()) * R2D;
LngLat { lng, lat }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ul() {
let tile: Tile = Tile::new(486, 332, 10);
let result = ul(tile);
let expected_result = LngLat { lng: -9.140625, lat: 53.33087298301705 };
assert_eq!(result, expected_result)
}
#[test]
fn test_bounds() {
let tile: Tile = Tile::new(486, 332, 10);
let result = bounds(tile);
let expected_result = LngLatBbox {
west: -9.140625,
south: 53.120405283106564,
east: -8.7890625,
north: 53.33087298301705,
};
assert_eq!(result, expected_result)
}
#[test]
fn test_xy_bounds() {
let tile: Tile = Tile::new(486, 332, 10);
let result = xy_bounds(tile);
let expected_result = Bbox {
left: -1017529.7205322646,
bottom: 7005300.768279833,
right: -978393.9620502543,
top: 7044436.526761843,
};
assert_eq!(result, expected_result)
}
#[test]
fn test_xy_positive() {
let lng_lat = LngLat { lng: -9.140625, lat: 53.33087298301705 };
let result = convert_xy(lng_lat);
let expected = XY { x: -1017529.7205322663, y: 7044436.526761846 };
assert_eq!(result, expected);
}
#[test]
fn test_xy_negative() {
let lng_lat = LngLat { lng: -122.4194, lat: -100.0 }; // Latitude less than -90
let result = convert_xy(lng_lat);
let expected = XY { x: -13627665.271218073, y: f64::NEG_INFINITY };
assert_eq!(result, expected);
}
#[test]
fn test_xy_positive_infinity() {
let lng_lat = LngLat { lng: -122.4194, lat: 100.0 }; // Latitude greater than 90
let result = convert_xy(lng_lat);
let expected = XY {
x: -13627665.271218073,
y: f64::INFINITY,
};
assert_eq!(result, expected);
}
#[test]
fn test_lng_lat() {
let xy = XY { x: -1017529.7205322663, y: 7044436.526761846 };
let result = convert_lng_lat(xy);
let expected = LngLat { lng: -9.140625000000002, lat: 53.33087298301706 };
assert_eq!(result, expected);
}
}

228
wxbox-tiler/src/grib2.rs Normal file
View File

@ -0,0 +1,228 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::{BufReader, Cursor, Read};
use flate2::read::GzDecoder;
use grib::codetables::{CodeTable4_2, CodeTable4_3, Lookup};
use grib::{GribError, SectionBody};
use ordered_float::OrderedFloat;
use reqwest::Response;
use thiserror::Error;
pub fn closest_key<V>(map: &BTreeMap<OrderedFloat<f64>, V>, val: f64) -> Option<OrderedFloat<f64>> {
let mut r1 = map.range(OrderedFloat(val)..);
let mut r2 = map.range(..OrderedFloat(val));
let o1 = r1.next();
let o2 = r2.next();
match (o1, o2) {
(None, None) => None,
(Some(i), None) => Some(*i.0),
(None, Some(i)) => Some(*i.0),
(Some(i1), Some(i2)) => {
// abs(f - i)
let i1_dist = (i1.0 - val).abs();
let i2_dist = (i2.0 - val).abs();
return Some(if i1_dist < i2_dist {
println!("closest_key() {} -> ({}, {}), ({}, {}) => {}", val, i1.0, i2.0, i1_dist, i2_dist, i1.0);
*i1.0
} else {
println!("closest_key() {} -> ({}, {}), ({}, {}) => {}", val, i1.0, i2.0, i1_dist, i2_dist, i2.0);
*i2.0
})
}
}
}
pub fn lookup<V>(map: &BTreeMap<OrderedFloat<f64>, V>, val: f64) -> Option<&V> {
let mut r1 = map.range(OrderedFloat(val)..);
let mut r2 = map.range(..OrderedFloat(val));
let o1 = r1.next();
let o2 = r2.next();
match (o1, o2) {
(None, None) => None,
(Some(i), None) => Some(i.1),
(None, Some(i)) => Some(i.1),
(Some(i1), Some(i2)) => {
// abs(f - i)
let i1_dist = (i1.0 - val).abs();
let i2_dist = (i2.0 - val).abs();
return Some(if i1_dist < i2_dist {
i1.1
} else {
i2.1
})
}
}
}
#[derive(Error, Debug)]
pub enum GribMapError {
#[error("grib2 parse error: {0}")]
ParseError(#[from] GribError),
#[error("reqwest error: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("empty data")]
EmptyData,
#[error("i/o error")]
IoError(#[from] std::io::Error)
}
pub type GriddedLookupTable = BTreeMap<OrderedFloat<f64>, BTreeMap<OrderedFloat<f64>, f64>>;
pub async fn map_grib2(i: Response) -> Result<GriddedLookupTable, GribMapError> {
let b = Cursor::new(i.bytes().await?.to_vec());
let mut decoder = GzDecoder::new(b);
let mut data = vec![];
decoder.read_to_end(&mut data)?;
let r = BufReader::new(Cursor::new(data));
let grib = grib::from_reader(r)?;
if grib.is_empty() {
return Err(GribMapError::EmptyData);
}
let mi = grib
.iter()
.filter(|((_, submessage_part), _)| *submessage_part == 0);
for (message_index, submessage) in mi {
if let (Some(SectionBody::Section0(sect0_body)), Some(SectionBody::Section1(sect1_body))) =
(&submessage.0.body.body, &submessage.1.body.body)
{
println!(
"{:?} D{} RT {} D/T {} C {} SC {} P/S {}",
message_index,
sect0_body.discipline,
sect1_body.ref_time()?,
sect1_body.data_type(),
sect1_body.centre_id(),
sect1_body.subcentre_id(),
sect1_body.prod_status(),
);
}
}
let mut map: BTreeMap<OrderedFloat<f64>, BTreeMap<OrderedFloat<f64>, f64>> = BTreeMap::new();
for (i, submessage) in grib.iter() {
let id = format!("{}.{}", i.0, i.1);
let prod_def = submessage.prod_def();
let category = prod_def
.parameter_category()
.zip(prod_def.parameter_number())
.map(|(c, n)| {
CodeTable4_2::new(submessage.indicator().discipline, c)
.lookup(usize::from(n))
.to_string()
})
.unwrap_or_default();
let generating_process = prod_def
.generating_process()
.map(|v| CodeTable4_3.lookup(usize::from(v)).to_string())
.unwrap_or_default();
let forecast_time = prod_def
.forecast_time()
.map(|ft| ft.to_string())
.unwrap_or_default();
let surfaces = prod_def
.fixed_surfaces()
.map(|(first, second)| (format_surface(&first), format_surface(&second)))
.unwrap_or((String::new(), String::new()));
let grid_def = submessage.grid_def();
let num_grid_points = grid_def.num_points();
let num_points_represented = submessage.repr_def().num_points();
let grid_type = grib::GridDefinitionTemplateValues::try_from(grid_def)
.map(|def| Cow::from(def.short_name()))
.unwrap_or_else(|_| {
Cow::from(format!("unknown (template {})", grid_def.grid_tmpl_num()))
});
println!(
"{} │ {}, {}, {}, {}, {}, │ {}/{} {}",
id,
category,
generating_process,
forecast_time,
surfaces.0,
surfaces.1,
num_grid_points - num_points_represented,
num_grid_points,
grid_type,
);
println!("ll");
let latlons = submessage.latlons();
println!("decoding");
let decoder = grib::Grib2SubmessageDecoder::from(submessage)?;
let values = decoder.dispatch()?;
println!("preparing");
let values = values.collect::<Vec<_>>().into_iter(); // workaround for mutability
let latlons = match latlons {
Ok(iter) => LatLonIteratorWrapper::LatLon(iter),
Err(GribError::NotSupported(_)) => {
let nan_iter = vec![(f32::NAN, f32::NAN); values.len()].into_iter();
LatLonIteratorWrapper::NaN(nan_iter)
}
Err(e) => panic!("{}", e),
};
let values = latlons.zip(values);
println!("mapping");
map = BTreeMap::new();
// prepare the map
for ((lat, long), value) in values {
let lat = OrderedFloat(lat as f64);
let long = OrderedFloat(long as f64);
let value = value as f64;
if !map.contains_key(&lat) {
map.insert(lat, BTreeMap::new());
}
map.get_mut(&lat).expect("unreachable").insert(long, value);
}
}
Ok(map)
}
fn format_surface(surface: &grib::FixedSurface) -> String {
let value = surface.value();
let unit = surface
.unit()
.map(|s| format!(" [{s}]"))
.unwrap_or_default();
format!("{value}{unit}")
}
#[derive(Clone)]
enum LatLonIteratorWrapper<L, N> {
LatLon(L),
NaN(N),
}
impl<L, N> Iterator for LatLonIteratorWrapper<L, N>
where
L: Iterator<Item = (f32, f32)>,
N: Iterator<Item = (f32, f32)>,
{
type Item = (f32, f32);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::LatLon(value) => value.next(),
Self::NaN(value) => value.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
Self::LatLon(value) => value.size_hint(),
Self::NaN(value) => value.size_hint(),
}
}
}

49
wxbox-tiler/src/main.rs Normal file
View File

@ -0,0 +1,49 @@
pub(crate) mod sources;
pub(crate) mod coords;
mod grib2;
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::env::args;
use std::fs::File;
use std::io::BufReader;
use tokio::sync::RwLock;
use std::time::SystemTime;
use actix_web::{App, get, HttpServer};
use actix_web::web::Data;
use grib::codetables::{CodeTable1_4, CodeTable4_2, CodeTable4_3, Lookup};
use grib::{GribError, SectionBody};
use ordered_float::OrderedFloat;
use crate::grib2::GriddedLookupTable;
use crate::sources::noaa::mrms_cref;
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
#[repr(usize)]
#[derive(Ord, PartialOrd, Eq, PartialEq, Debug, Clone)]
pub enum LutKey {
NoaaMrmsCref = 1
}
pub struct AppState {
lut_cache: RwLock<BTreeMap<LutKey, GriddedLookupTable>>,
lut_cache_timestamps: RwLock<BTreeMap<LutKey, SystemTime>>
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let data = Data::new(AppState {
lut_cache: RwLock::new(BTreeMap::new()),
lut_cache_timestamps: RwLock::new(BTreeMap::new())
});
HttpServer::new(move || {
App::new()
.service(mrms_cref)
.app_data(data.clone())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View File

@ -0,0 +1 @@
pub mod noaa;

View File

@ -0,0 +1,78 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::{BufReader, Cursor, Read};
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{get, HttpResponse, web};
use actix_web::http::StatusCode;
use actix_web::web::Data;
use flate2::read::GzDecoder;
use grib::{GribError, SectionBody};
use grib::codetables::{CodeTable4_2, CodeTable4_3, Lookup};
use ordered_float::OrderedFloat;
use crate::{AppState, LutKey};
use crate::coords::bounds;
use crate::grib2::{closest_key, lookup, map_grib2};
#[get("/mrms_cref/{z}/{x}/{y}.png")]
async fn mrms_cref(path: web::Path<(i32, i32, i32)>, data: Data<AppState>) -> HttpResponse {
let z = path.0;
let x = path.1;
let y = path.2;
let tile = crate::coords::Tile::new(x, y, z);
let bbox = bounds(tile);
println!("{:?}", bbox);
let mut needs_reload = false;
println!("{:?}", data.lut_cache_timestamps);
let lct_reader = data.lut_cache_timestamps.read().await;
if let Some(t) = lct_reader.get(&LutKey::NoaaMrmsCref) {
let dur = SystemTime::now().duration_since(*t).expect("time went backwards").as_secs();
if dur > 120 {
println!("cache busted, dur = {}", dur);
needs_reload = true;
}
} else {
println!("nothing in timestamp cache, redownloading");
needs_reload = true;
}
std::mem::drop(lct_reader);
if needs_reload {
let mut lct_writer = data.lut_cache_timestamps.write().await;
let mut lc_writer = data.lut_cache.write().await;
let f = reqwest::get("https://mrms.ncep.noaa.gov/data/2D/ReflectivityAtLowestAltitude/MRMS_ReflectivityAtLowestAltitude.latest.grib2.gz").await.unwrap();
let map = map_grib2(f).await.unwrap();
lc_writer.insert(LutKey::NoaaMrmsCref, map);
lct_writer.insert(LutKey::NoaaMrmsCref, SystemTime::now());
println!("{:?}", data.lut_cache_timestamps);
}
if let Some(map) = data.lut_cache.read().await.get(&LutKey::NoaaMrmsCref) {
let rows = map.range(closest_key(map, bbox.south).unwrap()..=closest_key(map, bbox.north).unwrap());
println!("{}", map.len());
println!("{:#?}", map.keys());
println!("N {}/{:?} S {}/{:?}", bbox.south, closest_key(map, bbox.south), bbox.north, closest_key(map, bbox.north));
let mut rowcount = 0;
let mut colcount = 0;
for row in rows {
rowcount += 1;
colcount = 0;
let cols = row.1.range(closest_key(row.1, bbox.west).unwrap()..=closest_key(row.1, bbox.east).unwrap());
for col in cols {
colcount += 1;
}
}
println!("{}x{}", rowcount, colcount);
return HttpResponse::new(StatusCode::NOT_FOUND);
} else {
println!("gridded LUT not available for LutKey::NoaaMrmsCref while handling /mrms_cref/{z}/{x}/{y} tile request");
return HttpResponse::new(StatusCode::NOT_FOUND);
}
}

21
wxbox-web/.gitignore vendored Normal file
View 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
View File

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

View File

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

8
wxbox-web/.prettierrc Normal file
View File

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

38
wxbox-web/README.md Normal file
View 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
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest 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://kit.svelte.dev/docs/adapters) for your target environment.

View File

@ -0,0 +1,32 @@
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
);

34
wxbox-web/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "wxbox-web",
"version": "0.0.1",
"private": true,
"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": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^9.6.0",
"@types/leaflet": "^1.9.12",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"leaflet": "^1.9.4",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3"
},
"type": "module"
}

2098
wxbox-web/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

13
wxbox-web/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// 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
View 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>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import './global.css';
import L from 'leaflet';
import type { Map as LeafletMap } from 'leaflet';
let map: LeafletMap | null = null;
function createMap(container: string | HTMLElement): LeafletMap {
let m = L.map(container, { preferCanvas: true }).setView([39.8923, -98.5795], 5);
L.tileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
maxZoom: 19
}
).addTo(m);
L.tileLayer(
'http://localhost:8080/mrms_cref/{z}/{x}/{y}.png',
{
attribution: '&copy; NOAA',
maxZoom: 19
}
).addTo(m);
return m;
}
function resize() {
if (map) {
map.invalidateSize();
}
}
function mapAction(container: HTMLElement) {
map = createMap(container);
return {
destroy: () => {
if (map) map.remove();
map = null;
}
}
}
</script>
<svelte:window on:resize={resize} />
<svelte:head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<title>wxbox</title>
</svelte:head>
<div class="map" use:mapAction />
<style lang="css">
.map {
width: 100vw;
height: 100vh;
}
</style>

View File

@ -0,0 +1 @@
export const ssr = false;

View File

@ -0,0 +1,4 @@
body {
margin: 0;
padding: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

19
wxbox-web/tsconfig.json Normal file
View 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://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/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
View File

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