@@ -1,54 +1,93 @@
import moment from "moment ";
import { createResource , createSignal , Show } from "solid-js" ;
import { SelectCity } from "../../components/SelectCity" ;
import { SelectTimeRange } from "../../components/SelectTimeRange" ;
import { createEffect , createResource , createSignal , For , Show } from "solid-js ";
import { MapView } from "../../components/map/MapView" ;
import { MapResolution } from "../../components/map/mapCo nsts" ;
import { FETCH_DELAY_MS , apiHost , ResultKeyVal } from "../../consts" ;
import { isQueryResult , QueryResult } from "../cities/helpers" ;
import { apiHost , ResultKeyVal } from "../../consts" ;
import { LoadingSpinner } from "../../components/spinner/LoadingSpinner" ;
import { faktiskaExport , faktiskaStations , FaktiskaStation } from "./faktiskaConfig" ;
import "./mapGraphics.css" ;
export function Faktiska() {
const now = new Date ( new Date ( ) . setMinutes ( 30 ) ) ;
const [ cities , setCities ] = createSignal < Set < string > > ( new Set ( ) ) ;
const [ start , setStart ] = createSignal ( moment ( now ) . subtract ( 1 , "day" ) . toDate ( ) ) ;
const [ end , setEnd ] = createSignal ( now ) ;
const [ field , setField ] = createSignal ( "tempMax" ) ;
const [ key , setKey ] = createSignal ( "max" ) ;
const [ resolution , setResolution ] = createSignal < "1920x1080" | "3840x1440" > ( "1920x1080" ) ;
const [ wind , setWind ] = createSignal ( false ) ;
const [ trigger , setTrigger ] = createSignal ( 0 ) ;
type LatestTemperature = { city : FaktiskaStation ; observedAt : string ; value : number | null } ;
const fetchData = async ( ) = > {
if ( ! cities ( ) . size ) return undefined ;
await new Promise ( resolve = > setTimeout ( resolve , FETCH_DELAY_MS ) ) ;
const cityPath = [ . . . cities ( ) ] . join ( "," ) ;
const range = ` ${ moment ( start ( ) ) . format ( "YYYYMMDD_HHmm" ) } - ${ moment ( end ( ) ) . format ( "YYYYMMDD_HHmm" ) } ` ;
const response = await fetch ( ` ${ apiHost } /api/query/city/ ${ cityPath } / ${ range } /hour/ ${ field ( ) } / ${ key ( ) } ` ) ;
if ( ! response . ok ) throw new Error ( ` Query failed ( ${ response . status } ) ` ) ;
const emptyValues = ( ) : Record < FaktiskaStation , string > = >
Object . fromEntries ( faktiskaStations . map ( city = > [ city , "" ] ) ) as Record < FaktiskaStation , string > ;
export function Faktiska() {
const [ values , setValues ] = createSignal ( emptyValues ( ) ) ;
const [ overrides , setOverrides ] = createSignal < Set < FaktiskaStation > > ( new Set ( ) ) ;
const fetchLatest = async ( ) : Promise < LatestTemperature [ ] > = > {
const response = await fetch ( ` ${ apiHost } /api/query/latest-temperatures/ ${ faktiskaStations . join ( "," ) } ` ) ;
if ( ! response . ok ) throw new Error ( ` Latest observations failed ( ${ response . status } ) ` ) ;
return response . json ( ) ;
} ;
const [ result ] = createResource ( trigger , fetchData ) ;
const mapType = ( ) : MapResolution = > ` map_ ${ resolution ( ) } ${ wind ( ) ? "_wind" : "" } ` as MapResolution ;
const cityData = ( ) : ResultKeyVal [ ] = > Object . entries ( ( result ( ) as QueryResult ) . result ) . map ( ( [ city , value ] ) = > [ city , typeof value === "number" ? value : - 99 ] ) ;
const [ latest , { refetch } ] = createResource ( fetchLatest ) ;
return < div class = "mapGraphicsPage pageWorkspace" >
< div class = "pageHeading" > < div > < span class = "eyebrow" > Daily newsroom production < / span > < h1 > Faktiskā < / h1 > < p > Create the actual - weather map with temperatures and weather symbols . < / p > < / div > < div class = "selectionCount" > { cities ( ) . size } cities selected < / div > < / div >
< div class = "graphicsSetup" >
< aside class = "cityPanel workspaceRail" > < h2 > 1 . Choose cities < / h2 > < SelectCity getCities = { cities } setCities = { setCities } / > < / aside >
< section class = "graphicsMain" >
< div class = "productionToolbar" >
< div class = "productionOptions" > < h2 > 2 . Choose output < / h2 >
< div class = "choiceGroup" > < span > Output < / span > < div > < button classList = { { active : resolution ( ) === "1920x1080" } } onClick = { ( ) = > setResolution ( "1920x1080" ) } > 1920 × 1080 < / button > < button classList = { { active : resolution ( ) === "3840x1440" } } onClick = { ( ) = > setResolution ( "3840x1440" ) } > 3840 × 1440 < / button > < label class = "windChoice" > < input type = "checkbox" checked = { wind ( ) } onChange = { e = > setWind ( e . target . checked ) } / > Wind layout < / label > < / div > < / div >
< / div >
< div class = "dataOptions" > < h2 > 3 . Retrieve temperatures < / h2 > < div class = "dataOptionsGrid" > < SelectTimeRange getStart = { start } setStart = { setStart } getEnd = { end } setEnd = { setEnd } / > < label > < span > Temperature field < / span > < select value = { field ( ) } onChange = { e = > setField ( e . target . value ) } > < option value = "tempMax" > Maximum temperature < / option > < option value = "tempMin" > Minimum temperature < / option > < option value = "tempAvg" > Average temperature < / option > < / select > < / label > < label > < span > Calculation < / span > < select value = { key ( ) } onChange = { e = > setKey ( e . target . value ) } > < option value = "max" > Maximum < / option > < option value = "min" > Minimum < / option > < option value = "avg" > Average < / option > < / select > < / label > < button class = "primary loadMapButton" disabled = { ! cities ( ) . size } onClick = { ( ) = > setTrigger ( Date . now ( ) ) } > Load map data < / button > < / div > < / div >
< / div >
< Show when = { result . loading } > < LoadingSpinner text = "Loading map data" / > < / Show >
< Show when = { result . error } > < div class = "emptyState" > < strong > Could not load map data < / strong > < span > { result . error . message } < / span > < / div > < / Show >
< Show when = { result ( ) && isQueryResult ( result ( ) ) } > < section class = "mapProduction" > < h2 > 4 . Add conditions and export < / h2 > < MapView type = { mapType ( ) } data = { cityData } mode = "faktiska" / > < / section > < / Show >
< Show when = { ! result ( ) && ! result . loading && ! result . error } > < div class = "emptyState" > < strong > Choose cities and load data < / strong > < span > The production controls and map preview will appear here . < / span > < / div > < / Show >
< / section >
createEffect ( ( ) = > {
const observations = latest ( ) ;
if ( ! observations ) return ;
const next = emptyValues ( ) ;
observations . forEach ( observation = > {
next [ observation . city ] = observation . value == null ? "" : observation . value . toString ( ) ;
} ) ;
setValues ( next ) ;
setOverrides ( new Set ( ) ) ;
} ) ;
const observationsByCity = ( ) = > new Map ( ( latest ( ) ? ? [ ] ) . map ( item = > [ item . city , item ] ) ) ;
const newestTimestamp = ( ) = > {
const timestamps = ( latest ( ) ? ? [ ] ) . map ( item = > Date . parse ( item . observedAt ) ) . filter ( Number . isFinite ) ;
return timestamps . length ? new Date ( Math . max ( . . . timestamps ) ) : undefined ;
} ;
const isStale = ( city : FaktiskaStation ) = > {
const newest = newestTimestamp ( ) ;
const observed = observationsByCity ( ) . get ( city ) ? . observedAt ;
return Boolean ( newest && observed && newest . getTime ( ) - Date . parse ( observed ) > 90 * 60 * 1000 ) ;
} ;
const cityData = ( ) : ResultKeyVal [ ] = > faktiskaStations . flatMap ( city = > {
const value = Number ( values ( ) [ city ] . replace ( "," , "." ) ) ;
return values ( ) [ city ] . trim ( ) && Number . isFinite ( value ) ? [ [ city , value ] as ResultKeyVal ] : [ ] ;
} ) ;
const updateValue = ( city : FaktiskaStation , value : string ) = > {
setValues ( current = > ( { . . . current , [ city ] : value } ) ) ;
setOverrides ( current = > new Set ( current ) . add ( city ) ) ;
} ;
const resetValue = ( city : FaktiskaStation ) = > {
const observation = observationsByCity ( ) . get ( city ) ;
setValues ( current = > ( { . . . current , [ city ] : observation ? . value == null ? "" : observation . value . toString ( ) } ) ) ;
setOverrides ( current = > {
const next = new Set ( current ) ;
next . delete ( city ) ;
return next ;
} ) ;
} ;
return < div class = "mapGraphicsPage pageWorkspace faktiskaWorkspace" >
< div class = "pageHeading" >
< div > < span class = "eyebrow" > Daily newsroom production < / span > < h1 > Faktiskā < / h1 > < p > Current temperatures with manually prepared weather symbols and wind . < / p > < / div >
< div class = "productionSize" > { faktiskaExport . width } × { faktiskaExport . height } PNG < / div >
< / div >
< section class = "faktiskaDataBand" >
< div class = "faktiskaStatus" >
< div > < span class = "stepLabel" > 1 . Latest temperatures < / span > < strong > { newestTimestamp ( ) ? newestTimestamp ( ) ! . toLocaleString ( "lv-LV" ) : "Waiting for observations" } < / strong > < small > Newest available reading in the configured station set < / small > < / div >
< button class = "secondary" disabled = { latest . loading } onClick = { ( ) = > refetch ( ) } > Refresh data < / button >
< / div >
< Show when = { latest . loading } > < LoadingSpinner text = "Loading latest temperatures" / > < / Show >
< Show when = { latest . error } > < div class = "inlineNotice error" > < strong > Could not load observations . < / strong > Existing manual fields remain available . < / div > < / Show >
< div class = "temperatureEditor" >
< For each = { faktiskaStations } > { city = > {
const observation = ( ) = > observationsByCity ( ) . get ( city ) ;
return < label class = "temperatureField" classList = { { manual : overrides ( ) . has ( city ) , stale : isStale ( city ) , missing : observation ( ) ? . value == null } } >
< span class = "temperatureFieldHeading" > < strong > { city } < / strong > < small > { overrides ( ) . has ( city ) ? "Manual" : isStale ( city ) ? "Older reading" : observation ( ) ? . observedAt ? new Date ( observation ( ) ! . observedAt ) . toLocaleTimeString ( "lv-LV" , { hour : "2-digit" , minute : "2-digit" } ) : "No data" } < / small > < / span >
< span class = "temperatureInput" > < input type = "text" inputmode = "decimal" value = { values ( ) [ city ] } onInput = { event = > updateValue ( city , event . currentTarget . value ) } aria-label = { ` ${ city } temperature ` } / > < span > ° C < / span > < / span >
< button type = "button" class = "resetTemperature" disabled = { ! overrides ( ) . has ( city ) } onClick = { ( ) = > resetValue ( city ) } > Reset < / button >
< / label > ;
} } < / For >
< / div >
< / section >
< section class = "mapProduction faktiskaProduction" >
< h2 > 2 . Add weather symbols and wind , then export < / h2 >
< MapView type = { faktiskaExport . mapType } data = { cityData } mode = "faktiska" productionTemplate / >
< / section >
< / div > ;
}