diff --git a/client/src/app.css b/client/src/app.css
index d8b13b5..7f8ac39 100644
--- a/client/src/app.css
+++ b/client/src/app.css
@@ -72,4 +72,19 @@
body {
@apply bg-background text-foreground;
}
+}
+
+.maplibregl-popup-content {
+ background-color: hsl(var(--popover));
+ color: hsl(var(--popover-foreground))
+}
+.maplibregl-popup-tip {
+ border-bottom-color: hsl(var(--popover)) !important;
+ border-top-color: hsl(var(--popover)) !important;
+ color: hsl(var(--popover));
+}
+.maplibregl-popup-anchor-bottom {
+}
+.maplibregl-popup {
+ max-width: unset !important;
}
\ No newline at end of file
diff --git a/client/src/lib/AlertPopup.svelte b/client/src/lib/AlertPopup.svelte
new file mode 100644
index 0000000..4d4e316
--- /dev/null
+++ b/client/src/lib/AlertPopup.svelte
@@ -0,0 +1,91 @@
+
+
+
+
+
+ {code}
+ {event}
+
+
+
+
+ {#if urgency === "Immediate"}
+
+ {:else if urgency === "Expected"}
+
+ {:else if urgency === "Future"}
+
+ {:else if urgency === "Past"}
+
+ {:else}
+
+ {/if}
+
+
+ {#if severity === "Extreme"}
+
+ {:else if severity === "Severe"}
+
+ {:else if severity === "Moderate"}
+
+ {:else if severity === "Minor"}
+
+ {:else}
+
+ {/if}
+
+
+ {#if certainty === "Observed"}
+
+ {:else if certainty === "Likely"}
+
+
+ {:else if certainty === "Possible"}
+
+ {:else if certainty === "Unlikely"}
+
+ {:else}
+
+ {/if}
+
+
+
+ {#if showFull}
+
{description}
+ {/if}
+
+
\ No newline at end of file
diff --git a/client/src/lib/BoxTooltip.svelte b/client/src/lib/BoxTooltip.svelte
new file mode 100644
index 0000000..53c6ada
--- /dev/null
+++ b/client/src/lib/BoxTooltip.svelte
@@ -0,0 +1,17 @@
+
+
+
+ {text}
+
+ {hover}
+
+
\ No newline at end of file
diff --git a/client/src/lib/Map.svelte b/client/src/lib/Map.svelte
index edd2364..b510ba7 100644
--- a/client/src/lib/Map.svelte
+++ b/client/src/lib/Map.svelte
@@ -2,10 +2,14 @@
import maplibregl from 'maplibre-gl';
import ToolbarProductSelector from '$lib/ToolbarProductSelector.svelte';
import { Button } from '$lib/components/ui/button';
- import { onMount } from 'svelte';
+ import {mount, onMount} from 'svelte';
import type { LayerList } from './layerList';
import type { StationStatus } from './stationData';
import {toast} from "svelte-sonner";
+ import {Toggle} from "$lib/components/ui/toggle";
+ import CloudAlertIcon from "@lucide/svelte/icons/cloud-alert";
+ import {borderLUT, fillLUT} from "$lib/alertLayer";
+ import AlertPopup from "$lib/AlertPopup.svelte";
interface Props {
categories: LayerList;
@@ -20,6 +24,17 @@
let pickingSiteForCategory: boolean = $state(false);
let selectedSite: string | null = $state(null);
+ let showAlertLayer: boolean = $state(true);
+ function updateAlertLayer() {
+ if (!map) {
+ showAlertLayer = !showAlertLayer;
+ return;
+ }
+
+ map.setLayoutProperty('alerts', 'visibility', showAlertLayer ? "visible" : "none");
+ map.setLayoutProperty('alerts-outline', 'visibility', showAlertLayer ? "visible" : "none");
+ }
+
onMount(() => {
map = new maplibregl.Map({
container: id,
@@ -39,6 +54,82 @@
}
load_image('radar-rect-green');
load_image('radar-rect-red');
+
+ map.addSource('alerts', {
+ type: 'geojson',
+ data: 'https://api.weather.gov/alerts/active?status=actual'
+ });
+ map.addLayer({
+ id: 'alerts',
+ type: 'fill',
+ source: 'alerts',
+ paint: {
+ // @ts-expect-error this type is too complicated
+ 'fill-color': fillLUT,
+ }
+ });
+ map.addLayer({
+ id: 'alerts-outline',
+ type: 'line',
+ source: 'alerts',
+ layout: {
+ 'line-join': 'round'
+ },
+ paint: {
+ // @ts-expect-error this type is too complicated
+ 'line-color': borderLUT,
+ 'line-width': 3
+ }
+ });
+ let createPopup = (e, full: boolean) => {
+ const randLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26));
+ const uniqid = randLetter + Date.now();
+
+ new maplibregl.Popup({ className: 'popup' })
+ .setLngLat(e.lngLat)
+ .setHTML(`
`)
+
+ .addTo(map);
+ mount(AlertPopup, {
+ target: document.getElementById(uniqid)!,
+ props: {
+ showFull: full,
+ ...e.features[0].properties
+ }
+ });
+ };
+ map.on('click', 'alerts', (e) => {createPopup(e,true)});
+ let currentFeatureCoordinates = undefined;
+ const popup = new maplibregl.Popup({
+ closeButton: false,
+ closeOnClick: false
+ });
+ map.on('mousemove', 'alerts', (e) => {
+ // Change the cursor style as a UI indicator.
+ map.getCanvas().style.cursor = 'pointer';
+
+ // Populate the popup and set its coordinates
+ // based on the feature found.
+ const randLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26));
+ const uniqid = randLetter + Date.now();
+
+ popup.setLngLat(e.lngLat).setHTML(``).addTo(map);
+ mount(AlertPopup, {
+ target: document.getElementById(uniqid)!,
+ props: {
+ showFull: false,
+ ...e.features[0].properties
+ }
+ });
+
+ });
+ map.on('mouseenter', 'alerts', () => {
+ map.getCanvas().style.cursor = 'pointer';
+ });
+ map.on('mouseleave', 'alerts', () => {
+ map.getCanvas().style.cursor = '';
+ popup.remove();
+ });
});
map.on('error', (e) => {
console.error(e);
@@ -56,15 +147,31 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/lib/ToolbarProductSelector.svelte b/client/src/lib/ToolbarProductSelector.svelte
index 0584c45..53aad2b 100644
--- a/client/src/lib/ToolbarProductSelector.svelte
+++ b/client/src/lib/ToolbarProductSelector.svelte
@@ -83,7 +83,7 @@
id: 'data',
type: 'raster',
source: `${selectedSite}-${data.id}`
- });
+ }, 'alerts');
}}>{data.layer}
{/each}
@@ -129,7 +129,7 @@
id: 'data',
type: 'raster',
source: `${layer.id}`
- });
+ }, 'alerts');
}
}}
@@ -204,7 +204,7 @@
id: 'data',
type: 'raster',
source: `${selectedSite}-${selectedPrimaryLayer}`
- });
+ }, 'alerts');
} // preserve the layer the user had if they click while already selected.
}
});
@@ -219,6 +219,9 @@
{/if}
{/if}
{/each}
+
{selectedPrimaryLayer = null; if (map.getLayer('data')) {map.removeLayer('data')}}}>
+ None
+
diff --git a/client/src/lib/alertLayer.ts b/client/src/lib/alertLayer.ts
new file mode 100644
index 0000000..edaf026
--- /dev/null
+++ b/client/src/lib/alertLayer.ts
@@ -0,0 +1,112 @@
+export type RGBColor = ['rgb', number, number, number];
+export type RGBAColor = ['rgba', number, number, number, number];
+
+export function c(input: string): RGBColor {
+ // @ts-expect-error aaa
+ const m = input.match(/^#([0-9a-f]{6})$/i)[1];
+ if( m) {
+ return [
+ 'rgb',
+ parseInt(m.substring(0,2),16),
+ parseInt(m.substring(2,4),16),
+ parseInt(m.substring(4,6),16)
+ ];
+ }
+}
+
+export const typeLookupTable: Record
= {
+ 'Hazardous Weather Outlook': [c('#eee8aa'),'HWO'],
+
+ // Winter Weather/Cold Weather
+ 'Winter Storm Watch': [c('#4682b4'),'WSA'],
+ 'Blizzard Warning': [c('#ff4500'),'BZW'],
+ 'Winter Storm Warning': [c('#ff69b4'),'WSW'],
+ 'Ice Storm Warning': [c('#8b008b'),'WSW'],
+ 'Winter Weather Advisory': [c('#7b68ee'),'WSW'],
+ 'Freeze Watch': [c('#00ffff'),'NPW'],
+ 'Freeze Warning': [c('#483d8b'),'FZW'],
+ 'Frost Advisory': [c('#6495ed'),'NPW'],
+ 'Cold Weather Advisory': [c('#afeeee'),'NPW'],
+ 'Extreme Cold Warning': [c('#0000ff'),'NPW'],
+
+ // Fire Weather
+ 'Fire Weather Watch': [c('#ffdead'),'FWA'],
+ 'Red Flag Warning': [c('#ff1493'),'FWA'],
+
+ // Fog / Wind / Severe Weather
+ 'Dense Fog Advisory': [c('#708090'),'NPW'],
+ 'High Wind Watch': [c('#b8860b'),'HWA'],
+ 'High Wind Warning': [c('#daa520'),'HWW'],
+ 'Wind Advisory': [c('#d2b48c'),'NPW'],
+ 'Severe Thunderstorm Watch': [c('#db7093'),'SVA'],
+ 'Severe Thunderstorm Warning': [c('#ffa500'),'SVR'],
+ 'Tornado Watch': [c('#ffff00'),'TOA'],
+ 'Tornado Warning': [c('#ff0000'),'TOR'],
+ 'Extreme Wind Warning': [c('#ff8c00'),'EWW'],
+
+ // Marine
+ 'Small Craft Advisory': [c('#d8bfd8'),'SCA'],
+ 'Gale Warning': [c('#dda0dd'),'MWS'],
+ 'Storm Warning': [c('#9400d3'),'MWS'],
+ 'Hurricane Force Wind Warning': [c('#cd5c5c'),'NPW'],
+ 'Special Marine Warning': [c('#ffa500'),'SMW'],
+
+ // Flooding
+ 'Coastal Flood Watch': [c('#66cdaa'),'CFA'],
+ 'Coastal Flood Warning': [c('#228b22'),'CFW'],
+ 'Coastal Flood Advisory': [c('#7cfc00'),'CFW'],
+ 'Flood Advisory': [c('#2e8b57'),'FFA'],
+ 'Flash Flood Warning': [c('#8b0000'),'FFW'],
+ 'Flood Warning': [c('#8b0000'),'FFW'],
+ 'River Flood Watch': [c('#00ff7f'),'FLA'],
+ 'River Flood Warning': [c('#00ff00'),'FLW'],
+
+ // Excessive Heat
+ 'Excessive Heat Watch': [c('#800000'),'NPW'],
+ 'Excessive Heat Warning': [c('#c71585'),'NPW'],
+ 'Heat Advisory': [c('#ff7f50'),'NPW'],
+
+ // Tropical
+ 'Tropical Storm Watch': [c('#f08080'),'TRA'],
+ 'Tropical Storm Warning': [c('#b22222'),'TRW'],
+ 'Hurricane Watch': [c('#ff00ff'),'HUA'],
+ 'Hurricane Warning': [c('#dc143c'),'HUW'],
+ 'Storm Surge Warning': [c('#b524f7'),'SSW'],
+ 'Storm Surge Watch': [c('#db7ff7'),'SSA'],
+ 'Hurricane Local Statement': [c('#ffe4b5'),'HLS'],
+
+ 'Special Weather Statement': [c('#ffe4b5'), 'SPS']
+
+};
+const fallback: RGBColor = c('#a21caf');
+
+const areaOpacity = 0.1;
+const borderOpacity = 1.0;
+
+function generateLUT(opacity: number): any[] {
+ const arr: any[] = [
+ 'match',
+ ['get', 'event'],
+
+ ];
+
+ for (const [matcher, [value]] of Object.entries(typeLookupTable)) {
+ arr.push(matcher);
+
+ const rgba_value = structuredClone(value as unknown as RGBAColor);
+ rgba_value.push(opacity);
+ rgba_value[0] = 'rgba';
+
+ arr.push(rgba_value);
+ }
+
+ const rgba_value = structuredClone(fallback as unknown as RGBAColor);
+ rgba_value.push(opacity);
+ rgba_value[0] = 'rgba';
+
+ arr.push(rgba_value);
+
+ return arr;
+}
+export const borderLUT = generateLUT(borderOpacity);
+export const fillLUT = generateLUT(areaOpacity);