client work
This commit is contained in:
parent
d862865830
commit
dd2f25a5e8
8 changed files with 110 additions and 25 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -3663,6 +3663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148"
|
checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -6333,10 +6334,12 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
"chrono",
|
||||||
"flate2",
|
"flate2",
|
||||||
"geographiclib",
|
"geographiclib",
|
||||||
"image",
|
"image",
|
||||||
"moka",
|
"moka",
|
||||||
|
"quick-xml 0.37.3",
|
||||||
"rayon",
|
"rayon",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -38,18 +38,6 @@
|
||||||
}
|
}
|
||||||
load_image('radar-rect-green');
|
load_image('radar-rect-green');
|
||||||
load_image('radar-rect-red');
|
load_image('radar-rect-red');
|
||||||
map.addSource('kcys-bref-0.5', {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: [
|
|
||||||
'http://localhost:3000/nexrad/base_reflectivity_halfdegree/KCYS/{z}/{x}/{y}@2x.png'
|
|
||||||
],
|
|
||||||
'tileSize': 512
|
|
||||||
});
|
|
||||||
map.addLayer({
|
|
||||||
id: 'kcys',
|
|
||||||
type: 'raster',
|
|
||||||
source: 'kcys-bref-0.5'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let idToNameCache: Record<string, string> = $state({});
|
let idToNameCache: Record<string, string> = $state({});
|
||||||
|
let idToTileUrlCache: Record<string, string> = $state({});
|
||||||
|
|
||||||
let currentlySelectingFor: string | null = $state(null);
|
let currentlySelectingFor: string | null = $state(null);
|
||||||
let currentlySelectingCategory: PrimaryLayer[] | null = $state(null);
|
let currentlySelectingCategory: PrimaryLayer[] | null = $state(null);
|
||||||
|
@ -65,6 +66,25 @@
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
selectedPrimaryLayer = data.id;
|
selectedPrimaryLayer = data.id;
|
||||||
idToNameCache[data.id] = data.layer;
|
idToNameCache[data.id] = data.layer;
|
||||||
|
|
||||||
|
if (!map.getSource(`${selectedSite}-${data.id}`)) {
|
||||||
|
idToTileUrlCache[data.id] = data.tileUrl;
|
||||||
|
map.addSource(`${selectedSite}-${data.id}`, {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [
|
||||||
|
data.tileUrl.replace('{site}', selectedSite)
|
||||||
|
],
|
||||||
|
'tileSize': 512
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (map.getLayer("data")) {
|
||||||
|
map.removeLayer('data');
|
||||||
|
}
|
||||||
|
map.addLayer({
|
||||||
|
id: 'data',
|
||||||
|
type: 'raster',
|
||||||
|
source: `${selectedSite}-${data.id}`
|
||||||
|
});
|
||||||
}}>{data.layer}</DropdownMenu.Item
|
}}>{data.layer}</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -145,10 +165,29 @@
|
||||||
// did we already have a site set?
|
// did we already have a site set?
|
||||||
let alreadyHadSite = selectedSite != null;
|
let alreadyHadSite = selectedSite != null;
|
||||||
pickingSiteForCategory = false;
|
pickingSiteForCategory = false;
|
||||||
selectedSite = e.features[0].properties.id;
|
selectedSite = e.features[0].properties.icao;
|
||||||
if (!alreadyHadSite) {
|
if (!alreadyHadSite) {
|
||||||
selectedPrimaryLayer = null;
|
selectedPrimaryLayer = null;
|
||||||
open = true;
|
open = true;
|
||||||
|
} else {
|
||||||
|
if (!map.getSource(`${selectedSite}-${selectedPrimaryLayer}`)) {
|
||||||
|
|
||||||
|
map.addSource(`${selectedSite}-${selectedPrimaryLayer}`, {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [
|
||||||
|
idToTileUrlCache[selectedPrimaryLayer].replace('{site}', selectedSite)
|
||||||
|
],
|
||||||
|
'tileSize': 512
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (map.getLayer("data")) {
|
||||||
|
map.removeLayer('data');
|
||||||
|
}
|
||||||
|
map.addLayer({
|
||||||
|
id: 'data',
|
||||||
|
type: 'raster',
|
||||||
|
source: `${selectedSite}-${selectedPrimaryLayer}`
|
||||||
|
});
|
||||||
} // preserve the layer the user had if they click while already selected.
|
} // preserve the layer the user had if they click while already selected.
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,15 +37,8 @@ export const load: PageServerLoad = async () => {
|
||||||
layer: 'Super Resolution Base Reflectivity (Tilt 1)',
|
layer: 'Super Resolution Base Reflectivity (Tilt 1)',
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
tileUrl:
|
tileUrl:
|
||||||
'https://tiler.weather.ax/grib2/noaa_mrms_composite_reflectivity_qcd/{z}/{x}/{y}.png'
|
'http://localhost:3000/nexrad/base_reflectivity_halfdegree/{site}/{z}/{x}/{y}@2x.png'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: '01JV7SEN0GFB76NR843B1GFW5N',
|
|
||||||
layer: 'Super Resolution Base Velocity (Tilt 1)',
|
|
||||||
type: 'raster',
|
|
||||||
tileUrl:
|
|
||||||
'https://tiler.weather.ax/grib2/noaa_mrms_composite_reflectivity_qcd/{z}/{x}/{y}.png'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -14,7 +14,7 @@ moka = { version = "0.12", features = ["future"] }
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
# error handling
|
# error handling
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
@ -43,3 +43,5 @@ strum_macros = "0.27"
|
||||||
|
|
||||||
# meta
|
# meta
|
||||||
wxbox-common = { path = "../common" }
|
wxbox-common = { path = "../common" }
|
||||||
|
chrono = "0.4"
|
||||||
|
quick-xml = { version = "0.37", features = ["serialize"] }
|
|
@ -3,6 +3,7 @@ mod error;
|
||||||
mod grib2;
|
mod grib2;
|
||||||
mod nexrad;
|
mod nexrad;
|
||||||
mod tiles;
|
mod tiles;
|
||||||
|
mod nexrad_list_response;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::grib2::{Grib2DataCache, Grib2TileCache, grib2_handler, grib2_metadata};
|
use crate::grib2::{Grib2DataCache, Grib2TileCache, grib2_handler, grib2_metadata};
|
||||||
|
@ -16,6 +17,7 @@ use std::fmt::{Debug, Formatter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use axum::http::Method;
|
use axum::http::Method;
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use wxbox_grib2::GribMessage;
|
use wxbox_grib2::GribMessage;
|
||||||
|
@ -41,6 +43,7 @@ impl Debug for AppState {
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
tracing_subscriber::fmt::fmt()
|
tracing_subscriber::fmt::fmt()
|
||||||
.with_span_events(FmtSpan::CLOSE)
|
.with_span_events(FmtSpan::CLOSE)
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let config_file = args().nth(1).expect("usage: wxbox-tiler config.toml");
|
let config_file = args().nth(1).expect("usage: wxbox-tiler config.toml");
|
||||||
|
|
|
@ -20,6 +20,7 @@ use std::{fs, io};
|
||||||
use std::io::{Cursor, ErrorKind};
|
use std::io::{Cursor, ErrorKind};
|
||||||
use std::num::TryFromIntError;
|
use std::num::TryFromIntError;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use chrono::{Datelike, Utc};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{debug, info_span};
|
use tracing::{debug, info_span};
|
||||||
use wxbox_ar2::sites::wsr88d::{SITES, Wsr88dSite};
|
use wxbox_ar2::sites::wsr88d::{SITES, Wsr88dSite};
|
||||||
|
@ -27,6 +28,7 @@ use wxbox_ar2::{DATA_BYTES, MomentValue, Radial, Scan, Sweep, parse};
|
||||||
use wxbox_grib2::GribMessage;
|
use wxbox_grib2::GribMessage;
|
||||||
use wxbox_grib2::wgs84::LatLong;
|
use wxbox_grib2::wgs84::LatLong;
|
||||||
use wxbox_pal::{ColorPalette, Palette};
|
use wxbox_pal::{ColorPalette, Palette};
|
||||||
|
use crate::nexrad_list_response::ListBucketResult;
|
||||||
|
|
||||||
pub type NexradDataCache = Cache<DataId, Arc<wxbox_ar2::Scan>>;
|
pub type NexradDataCache = Cache<DataId, Arc<wxbox_ar2::Scan>>;
|
||||||
pub type NexradTileCache = Cache<TileId, Arc<Vec<u8>>>;
|
pub type NexradTileCache = Cache<TileId, Arc<Vec<u8>>>;
|
||||||
|
@ -49,6 +51,7 @@ pub async fn nexrad_handler(
|
||||||
Path((source, site, z, x, y)): Path<(String, String, usize, usize, String)>,
|
Path((source, site, z, x, y)): Path<(String, String, usize, usize, String)>,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
debug!("here");
|
||||||
let mut y = y
|
let mut y = y
|
||||||
.strip_suffix(".png")
|
.strip_suffix(".png")
|
||||||
.ok_or(io::Error::new(ErrorKind::InvalidInput, "invalid"))?;
|
.ok_or(io::Error::new(ErrorKind::InvalidInput, "invalid"))?;
|
||||||
|
@ -106,10 +109,48 @@ async fn load_nexrad_data(
|
||||||
) -> anyhow::Result<Arc<Scan>> {
|
) -> anyhow::Result<Arc<Scan>> {
|
||||||
let _load_span = info_span!("load_nexrad_data");
|
let _load_span = info_span!("load_nexrad_data");
|
||||||
|
|
||||||
// todo: find the correct
|
let mut searchdate = Utc::now();
|
||||||
let url = format!("https://noaa-nexrad-level2.s3.amazonaws.com/2025/05/15/KCYS/KCYS20250515_003010_V06");
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
let mut url;
|
||||||
|
let mut days_went_back = 0;
|
||||||
|
|
||||||
|
'outer: loop {
|
||||||
|
let year = searchdate.year();
|
||||||
|
let month = searchdate.month();
|
||||||
|
let day = searchdate.day();
|
||||||
|
let listing_url = format!("https://noaa-nexrad-level2.s3.amazonaws.com/?list-type=2&delimiter=/&prefix={year}/{month:02}/{day:02}/{site}/");
|
||||||
|
debug!("downloading listing from {}", listing_url);
|
||||||
|
let r = client.get(listing_url).send().await?;
|
||||||
|
if !r.status().is_success() {
|
||||||
|
bail!("nexrad data failed to load: {}", r.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let r: ListBucketResult = quick_xml::de::from_str(&r.text().await?)?;
|
||||||
|
|
||||||
|
if let Some(contents) = r.Contents {
|
||||||
|
for file in contents.iter().rev() {
|
||||||
|
if file.Key.ends_with("_MDM") {
|
||||||
|
// fake. continue
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
debug!("nexrad: found l2 file for {} date {}", site, file.LastModified);
|
||||||
|
url = format!("https://noaa-nexrad-level2.s3.amazonaws.com/{}", file.Key);
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchdate -= chrono::Duration::days(1);
|
||||||
|
days_went_back += 1;
|
||||||
|
if days_went_back > 7 {
|
||||||
|
bail!("no recent radar data");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
debug!("downloading l2 file {}", url);
|
||||||
let r = client.get(url).send().await?;
|
let r = client.get(url).send().await?;
|
||||||
|
|
||||||
if !r.status().is_success() {
|
if !r.status().is_success() {
|
||||||
|
|
16
crates/tiler/src/nexrad_list_response.rs
Normal file
16
crates/tiler/src/nexrad_list_response.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ListBucketResult {
|
||||||
|
pub Name: String,
|
||||||
|
pub Prefix: String,
|
||||||
|
pub KeyCount: usize,
|
||||||
|
pub MaxKeys: usize,
|
||||||
|
|
||||||
|
pub Contents: Option<Vec<ListBucketResultContents>>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ListBucketResultContents {
|
||||||
|
pub Key: String,
|
||||||
|
pub LastModified: String
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue