feat: client work
Some checks are pending
Verify Latest Dependencies / Verify Latest Dependencies (push) Waiting to run
build and test / wxbox - latest (push) Waiting to run

This commit is contained in:
core 2025-05-17 16:30:02 -04:00
parent e090501624
commit 4ee16ada3c
Signed by: core
GPG key ID: FDBF740DADDCEECF
6 changed files with 358 additions and 13 deletions

View file

@ -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;
}

View file

@ -0,0 +1,91 @@
<script lang="ts">
import {c, type RGBColor, typeLookupTable} from "$lib/alertLayer";
import {Separator} from "$lib/components/ui/separator";
import * as Tooltip from "$lib/components/ui/tooltip";
import BoxTooltip from "$lib/BoxTooltip.svelte";
interface Props {
event: string;
urgency: string;
severity: string;
certainty: string;
description: string;
showFull: boolean;
}
let { event, urgency, severity, certainty, showFull, description }: Props = $props();
let color: RGBColor = $state(c('#a21caf'));
let code: string = $state("UNK");
if (Object.keys(typeLookupTable).includes(event)) {
color = typeLookupTable[event][0];
code = typeLookupTable[event][1];
}
const colorString = $derived(`rgb(${color[1]},${color[2]},${color[3]})`);
function inverse() {
let lumi = 0.2126*color[1] + 0.7152*color[2] + 0.0722*color[3];
if (lumi < 140) {
return "text-white";
} else {
return "text-black";
}
}
</script>
<Tooltip.Provider>
<div class="flex flex-col gap-2">
<h1 class="text-sm font-bold leading-loose font-sans">
<span class="px-2 py-1 mr-2 {inverse()}" style="background-color: {colorString}">{code}</span>
{event}
</h1>
<div class="flex flex-row gap-2">
<h4 class="text-xs font-sans">
{#if urgency === "Immediate"}
<BoxTooltip colors="bg-red-600 text-red-950" text="Immediate" hover="This event is actively occurring or will happen in the immediate future." />
{:else if urgency === "Expected"}
<BoxTooltip colors="bg-slate-800" text="Expected" hover="This event is expected to happen in the near future." />
{:else if urgency === "Future"}
<BoxTooltip colors="bg-slate-800" text="Expected" hover="This event will happen in the future." />
{:else if urgency === "Past"}
<BoxTooltip colors="bg-slate-800" text="Past" hover="This event has happened." />
{:else}
<BoxTooltip colors="bg-slate-800" text={urgency} hover="This event's urgency is unknown." />
{/if}
</h4>
<h4 class="text-xs font-sans">
{#if severity === "Extreme"}
<BoxTooltip colors="bg-red-600 text-red-950" text="Extreme" hover="This event is historically extreme and is likely very dangerous to property and life." />
{:else if severity === "Severe"}
<BoxTooltip colors="bg-red-400 text-red-950" text="Severe" hover="This event is severe and may be dangerous to property and/or life." />
{:else if severity === "Moderate"}
<BoxTooltip colors="bg-amber-950 text-amber-600" text="Moderate" hover="This event may cause damage to property or cause injury." />
{:else if severity === "Minor"}
<BoxTooltip colors="bg-slate-800" text="Minor" hover="This is a minor event." />
{:else}
<BoxTooltip colors="bg-slate-800" text={severity} hover="This event's severity is unknown." />
{/if}
</h4>
<h4 class="text-xs font-sans">
{#if certainty === "Observed"}
<BoxTooltip colors="bg-sky-600 text-sky-50" text="Observed" hover="This event has been visually confirmed by spotters or law enforcement." />
{:else if certainty === "Likely"}
<BoxTooltip colors="bg-amber-500 text-amber-950" text="Likely" hover="This event is expected to occur." />
{:else if certainty === "Possible"}
<BoxTooltip colors="bg-amber-950 text-amber-600" text="Possible" hover="It is possible for this event to occur." />
{:else if certainty === "Unlikely"}
<BoxTooltip colors="bg-slate-800" text="Unlikely" hover="It is unlikely for this event to occur." />
{:else}
<BoxTooltip colors="bg-slate-800" text={certainty} hover="This event's certainty is unknown." />
{/if}
</h4>
</div>
{#if showFull}
<pre>{description}</pre>
{/if}
</div>
</Tooltip.Provider>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as Tooltip from "$lib/components/ui/tooltip";
interface Props {
text: string;
hover: string;
colors: string;
}
let { text, hover, colors }: Props = $props();
</script>
<Tooltip.Root>
<Tooltip.Trigger class="px-2 py-1 {colors} font-semibold">{text}</Tooltip.Trigger>
<Tooltip.Content>
<p>{hover}</p>
</Tooltip.Content>
</Tooltip.Root>

View file

@ -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(`<div id="${uniqid}"></div>`)
.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(`<div id="${uniqid}"></div>`).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 @@
</div>
<!-- center -->
<div class="flex flex-1 grow flex-row justify-center">
<ToolbarProductSelector
bind:pickingSiteForCategory
bind:selectedSite
bind:selectedPrimaryLayer
{categories}
{map}
{stations}
/>
<div class="flex flex-1 grow flex-row justify-center gap-2">
<!-- center-left -->
<div class="flex flex-1 grow flex-row justify-end">
<Toggle bind:pressed={showAlertLayer} onPressedChange={updateAlertLayer}>
<CloudAlertIcon class="size-4" />
</Toggle>
</div>
<!-- center-center -->
<div class="flex flex-1 grow flex-row justify-center">
<ToolbarProductSelector
bind:pickingSiteForCategory
bind:selectedSite
bind:selectedPrimaryLayer
{categories}
{map}
{stations}
/>
</div>
<!-- center-center -->
<div class="flex flex-1 grow flex-row justify-start">
</div>
</div>
<div class="flex flex-1 grow flex-row justify-end gap-1"></div>

View file

@ -83,7 +83,7 @@
id: 'data',
type: 'raster',
source: `${selectedSite}-${data.id}`
});
}, 'alerts');
}}>{data.layer}</DropdownMenu.Item
>
{/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}
<DropdownMenu.Item onclick={() => {selectedPrimaryLayer = null; if (map.getLayer('data')) {map.removeLayer('data')}}}>
None
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -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<string, [RGBColor, string]> = {
'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);