2026-08-22 20:31:04 +03:00
import { createEffect , createMemo , createResource , createSignal , For , Show } from 'solid-js' ;
2026-08-22 20:11:20 +03:00
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' ;
type WarningResponse = { source : string ; fetchedAt : string ; warnings : WeatherWarning [] };
const fetchWarnings = async () : Promise < WarningResponse > => {
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 < WarningOutput >( '3840x1440' );
const [ title , setTitle ] = createSignal ( '' );
const [ selectedDate , setSelectedDate ] = createSignal ( 'all' );
const [ phenomenon , setPhenomenon ] = createSignal ( 'all' );
2026-08-22 20:31:04 +03:00
const [ artwork , setArtwork ] = createSignal < { background : HTMLImageElement ; border : HTMLImageElement } > ();
2026-08-22 20:11:20 +03:00
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 ;
}));
2026-08-22 20:31:04 +03:00
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 ;
2026-08-22 20:11:20 +03:00
};
2026-08-22 20:31:04 +03:00
createEffect (() => { output (); loadArtwork (); });
2026-08-22 20:11:20 +03:00
createEffect (() => {
2026-08-22 20:31:04 +03:00
const images = artwork ();
2026-08-22 20:11:20 +03:00
const current = template ();
const warnings = filteredWarnings ();
const currentTitle = title ();
2026-08-22 20:31:04 +03:00
if ( ! canvas || ! images ) return ;
2026-08-22 20:11:20 +03:00
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 (() =>
2026-08-22 20:31:04 +03:00
drawWarningMap ( canvas . getContext ( '2d' ) ! , images . background , images . border , current , warnings , currentTitle ),
2026-08-22 20:11:20 +03:00
);
});
const downloadPng = () => canvas . toBlob ( blob => blob && download ( blob , `bridinajumi- ${ output () } .png` ), 'image/png' );
return < section class = "pageWorkspace warningsPage" >
< div class = "pageHeading warningsHeading" >
< div >< span class = "eyebrow" > Ē TERA GRAFIKA </ span >< h1 > Brīdinājumi </ h1 >< p > Aktuālie LVĢMC brīdinājumu poligoni un manuāli ievadāms virsraksts .</ p ></ div >
< span class = "selectionCount" >{ filteredWarnings (). length } { filteredWarnings (). length === 1 ? 'brīdinājums' : 'brīdinājumi' }</ span >
</ div >
< section class = "warningsSetup" aria-labelledby = "warnings-settings" >
< div >< h2 id = "warnings-settings" > 1 . Izvēlies izmēru </ h2 >< div class = "resolutionChoices" >
< button classList = {{ active : output () === '1920x1080' }} onClick = {() => setOutput ( '1920x1080' )}> 1920 × 1080 </ button >
< button classList = {{ active : output () === '3840x1440' }} onClick = {() => setOutput ( '3840x1440' )}> 3840 × 1440 </ button >
</ div ></ div >
< div class = "warningFilters" >< h2 > 2 . Izvēlies brīdinājumu </ h2 >< div class = "warningFilterGrid" >
< label >< span > Diena </ span >< select value = { selectedDate ()} onChange = { event => setSelectedDate ( event . currentTarget . value )}>< option value = "all" > Viss periods </ option >< For each = { availableDates ()}>{ date => < option value = { date }>{ formatDate ( date )}</ option >}</ For ></ select ></ label >
< label >< span > Parādība </ span >< select value = { phenomenon ()} onChange = { event => setPhenomenon ( event . currentTarget . value )}>< option value = "all" > Visas parādības </ option >< For each = { phenomena ()}>{ item => < option value = { item }>{ item }</ option >}</ For ></ select ></ label >
< label class = "warningTitle" >< span > Virsraksts </ span >< input type = "text" value = { title ()} placeholder = "BRĪDINĀJUMS PAR STIPRU VĒJU" onInput = { event => setTitle ( event . currentTarget . value )} /></ label >
</ div ></ div >
</ section >
< Show when = { data . loading }>< LoadingSpinner text = "Ielādē LVĢMC brīdinājumus" /></ Show >
< Show when = { data . error }>< div class = "inlineNotice error" >< strong > Brīdinājumu datus neizdevās ielādēt .</ strong >< button class = "secondary" onClick = {() => refetch ()}> Mēģināt vēlreiz </ button ></ div ></ Show >
< Show when = { ! data . loading && ! data . error && filteredWarnings (). length === 0 }>< div class = "inlineNotice" > Izvēlētajam periodam nav aktīvu brīdinājumu . Karti joprojām var priekšskatīt , bet tajā nebūs iekrāsotu teritoriju .</ div ></ Show >
< Show when = { filteredWarnings (). length > 0 }>< div class = "warningSummary" >< For each = { filteredWarnings ()}>{ warning => < article class = { `warningCard ${ warningSeverity ( warning . intensity ) } ` }>< strong >{ warning . phenomenon || 'Brīdinājums' }</ strong >< span >{ warning . intensity } · { warning . regions }</ span >< small >{ formatPeriod ( warning . validFrom , warning . validUntil )}</ small ></ article >}</ For ></ div ></ Show >
< section class = "warningsPreview" >< div class = "mapToolbar" >< div >< strong > 3 . Kartes priekšskatījums </ strong >< span >{ template (). width } × { template (). height } px eksports </ span ></ div >< button class = "primary" disabled = { data . loading || Boolean ( data . error )} onClick = { downloadPng }> Lejupielādēt PNG </ button ></ div >< div class = "canvasFrame" >< canvas ref = { canvas } width = { template (). width } height = { template (). height }/></ div ></ section >
</ section >;
}
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 < string >();
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 ) } ` ;
}