import { createEffect, createMemo, createResource, createSignal, For, onCleanup, Show } from 'solid-js'; import { Portal } from 'solid-js/web'; import { AlertTriangle, CloudFog, CloudLightning, CloudRain, CloudSnow, Flame, ThermometerSnowflake, Waves, Wind, X } from 'lucide-solid'; import { apiHost } from '../../consts'; import { download } from '../../helpers/download'; import { LoadingSpinner } from '../../components/spinner/LoadingSpinner'; import { drawWarningMap, type WeatherWarning, warningSeverity } from './warningDraw'; import { warningTemplates, type WarningOutput } from './warningTemplates'; import './warnings.css'; function phenomenonIcon(phenomenon: string) { const normalized = (phenomenon || '').toLocaleLowerCase('lv-LV'); if (normalized.includes('vēj')) return Wind; if (normalized.includes('pērkon') || normalized.includes('negais')) return CloudLightning; if (normalized.includes('lietus') || normalized.includes('lietav')) return CloudRain; if (normalized.includes('snieg')) return CloudSnow; if (normalized.includes('migl')) return CloudFog; if (normalized.includes('saln') || normalized.includes('sals')) return ThermometerSnowflake; if (normalized.includes('karstum')) return Flame; if (normalized.includes('plūd') || normalized.includes('ūdens')) return Waves; return AlertTriangle; } function PhenomenonIcon(props: { phenomenon: string; size?: number }) { const Icon = phenomenonIcon(props.phenomenon); return ; } type WarningResponse = { source: string; fetchedAt: string; warnings: WeatherWarning[] }; const fetchWarnings = async (): Promise => { const response = await fetch(`${apiHost}/api/warnings`); if (!response.ok) throw new Error(`Warnings failed (${response.status})`); return response.json(); }; export function Warnings() { const [output, setOutput] = createSignal('3840x1440'); const [title, setTitle] = createSignal(''); const [selectedDate, setSelectedDate] = createSignal('all'); const [phenomenon, setPhenomenon] = createSignal('all'); const [artwork, setArtwork] = createSignal<{ background: HTMLImageElement; border: HTMLImageElement }>(); const [data, { refetch }] = createResource(fetchWarnings); let canvas!: HTMLCanvasElement; const template = () => warningTemplates[output()]; const loadedWarnings = () => data.error ? [] : data()?.warnings ?? []; const availableDates = createMemo(() => warningDates(loadedWarnings())); const phenomena = createMemo(() => [...new Set(loadedWarnings().map(item => item.phenomenon).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'lv'))); const filteredWarnings = createMemo(() => loadedWarnings().filter(warning => { const dateMatches = selectedDate() === 'all' || activeOnDate(warning, selectedDate()); const phenomenonMatches = phenomenon() === 'all' || warning.phenomenon === phenomenon(); return dateMatches && phenomenonMatches; })); const [selectedId, setSelectedId] = createSignal(); const selectedWarning = createMemo(() => filteredWarnings().find(item => item.id === selectedId())); const [detailOpen, setDetailOpen] = createSignal(false); const openWarning = (id: string) => { setSelectedId(id); setDetailOpen(true); }; const closeDetail = () => setDetailOpen(false); createEffect(() => { if (!detailOpen()) return; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') closeDetail(); }; document.addEventListener('keydown', onKeyDown); onCleanup(() => document.removeEventListener('keydown', onKeyDown)); }); const loadArtwork = () => { const current = template(); setArtwork(undefined); const background = new Image(); const border = new Image(); let loaded = 0; const finish = () => { loaded += 1; if (loaded === 2 && template() === current) setArtwork({ background, border }); }; background.onload = finish; border.onload = finish; background.src = current.map; border.src = current.border; }; createEffect(() => { output(); loadArtwork(); }); createEffect(() => { const list = filteredWarnings(); if (!list.some(item => item.id === selectedId())) setSelectedId(list[0]?.id); }); createEffect(() => { const warning = selectedWarning(); setTitle(warning ? defaultTitle(warning.phenomenon) : ''); }); createEffect(() => { const images = artwork(); const current = template(); const warning = selectedWarning(); const currentTitle = title(); if (!canvas || !images) return; if (canvas.width !== current.width) canvas.width = current.width; if (canvas.height !== current.height) canvas.height = current.height; void document.fonts.load(`700 ${current.title.fontSize}px Monda`).then(() => drawWarningMap(canvas.getContext('2d')!, images.background, images.border, current, warning, currentTitle), ); }); const downloadPng = () => canvas.toBlob(blob => blob && download(blob, `bridinajumi-${output()}.png`), 'image/png'); return
ĒTERA GRAFIKA

Brīdinājumi

Izvēlies vienu LVĢMC brīdinājumu, kas jārāda kartē; virsraksts aizpildās automātiski.

{filteredWarnings().length} {filteredWarnings().length === 1 ? 'brīdinājums' : 'brīdinājumi'}

1. Izvēlies izmēru

2. Izvēlies brīdinājumu

Brīdinājumu datus neizdevās ielādēt.
Izvēlētajam periodam nav aktīvu brīdinājumu. Karti joprojām var priekšskatīt, bet tajā nebūs iekrāsotu teritoriju.
0}>
{warning => }
3. Kartes priekšskatījums{template().width} × {template().height} px eksports
{warning =>
event.stopPropagation()}>
{warning().phenomenon || 'Brīdinājums'}{warning().intensity} · {warning().regions}

{formatPeriod(warning().validFrom, warning().validUntil)}

{warning().description}

{warning().risks}

}
; } function defaultTitle(phenomenon: string) { return phenomenon ? `Brīdinājums: ${phenomenon}` : ''; } function datePart(value: string) { const match = value.trim().match(/^(\d{4}-\d{2}-\d{2})/); return match?.[1]; } function warningDates(warnings: WeatherWarning[]) { const dates = new Set(); warnings.forEach(warning => { const from = datePart(warning.validFrom); const until = datePart(warning.validUntil); if (!from || !until) return; const cursor = new Date(`${from}T12:00:00Z`); const last = new Date(`${until}T12:00:00Z`); while (cursor.getTime() <= last.getTime()) { dates.add(cursor.toISOString().slice(0, 10)); cursor.setUTCDate(cursor.getUTCDate() + 1); } }); return [...dates].sort(); } function activeOnDate(warning: WeatherWarning, date: string) { const from = datePart(warning.validFrom); const until = datePart(warning.validUntil); if (!from || !until) return false; return from <= date && until >= date; } function formatDate(value: string) { return new Intl.DateTimeFormat('lv-LV', { timeZone: 'Europe/Riga', weekday: 'short', day: '2-digit', month: '2-digit' }).format(new Date(`${value}T12:00:00Z`)); } function formatPeriod(fromValue: string, untilValue: string) { const compact = (value: string) => value.trim().replace(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}).*$/, '$3.$2. $4:$5'); return `${compact(fromValue)}–${compact(untilValue)}`; }