Separate Faktiska workflow and refine symbol picker

This commit is contained in:
b0txec
2026-08-18 22:45:14 +03:00
parent ffb89024f1
commit 82199b8a45
7 changed files with 29 additions and 24 deletions
+3 -3
View File
@@ -11,7 +11,7 @@ import { WeatherIconStub } from './components/weatherIcons/WeatherIconStub';
import { Harmonie } from './pages/harmonie/Harmonie'
import { LvgmcForecast } from './pages/lvgmc-forecast/LvgmcForecast'
import { Home } from './pages/home/Home'
import { MapGraphics } from './pages/map-graphics/MapGraphics'
import { Faktiska } from './pages/map-graphics/MapGraphics'
console.log("env:", import.meta.env.MODE)
console.log("api host:", apiHost)
@@ -24,7 +24,7 @@ const App: Component = () => {
<Route path="/lvgmc-forecast" component={LvgmcForecast} />
<Route path="/station" component={Station} />
<Route path="/cities" component={Cities} />
<Route path="/map-graphics" component={MapGraphics} />
<Route path="/faktiska" component={Faktiska} />
<Route path="/latvia" component={Country} />
<Route path="/database" component={Database} />
</Router>
@@ -33,7 +33,7 @@ const App: Component = () => {
const AppLayout: ParentComponent = (props) => {
const pages = [
['/station', 'Stations'], ['/cities', 'City analysis'], ['/map-graphics', 'Map graphics'], ['/latvia', 'Latvia overview'],
['/station', 'Stations'], ['/cities', 'City analysis'], ['/faktiska', 'Faktiskā'], ['/latvia', 'Latvia overview'],
['/database', 'Data archive'], ['/harmonie', 'HARMONIE'], ['/lvgmc-forecast', 'LVĢMC forecast'],
]
return (
@@ -10,8 +10,9 @@ export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal
const applyAll = () => setIcons(Object.fromEntries(props.cities.map(city => [city, selected()])));
return <section class="symbolEditor">
<div class="symbolEditorHeading"><div><strong>Weather symbols</strong><p class="fieldHint">Select a font symbol, apply it to all cities, then change the exceptions.</p></div><button type="button" onClick={applyAll}>Apply selected to all</button></div>
<div class="symbolPalette" role="listbox" aria-label="Weather symbols"><For each={weatherIcons}>{icon => <button type="button" class="weatherIconButton" classList={{selected: selected() === icon}} onClick={() => setSelected(icon)}>{icon}</button>}</For></div>
<div class="citySymbols"><For each={props.cities}>{city => <div class="citySymbolRow"><span>{city}</span><button type="button" class="assignedSymbol" onClick={() => assign(city)} title="Assign selected symbol"><Show when={icons()[city]} fallback="Assign">{icons()[city]}</Show></button><button type="button" class="clearSymbol" disabled={!icons()[city]} onClick={() => clear(city)}>Clear</button></div>}</For></div>
<div class="symbolEditorHeading"><div><strong>Weather symbols</strong><p class="fieldHint">Choose one symbol, apply it broadly, then update the exceptions below.</p></div><div class="bulkSymbol"><span class="selectedPreview">{selected()}</span><button type="button" onClick={applyAll}>Apply to all cities</button></div></div>
<div class="symbolPalette" role="radiogroup" aria-label="Choose weather symbol"><For each={weatherIcons}>{icon => <button type="button" role="radio" aria-checked={selected() === icon} aria-label={`Weather symbol ${icon}`} class="weatherIconButton" classList={{selected: selected() === icon}} onClick={() => setSelected(icon)}>{icon}</button>}</For></div>
<div class="assignmentHeading"><strong>City assignments</strong><span>Use the selected symbol for each exception.</span></div>
<div class="citySymbols"><For each={props.cities}>{city => <div class="citySymbolRow"><span class="cityName">{city}</span><span class="currentSymbol" classList={{empty: !icons()[city]}}><Show when={icons()[city]} fallback="—">{icons()[city]}</Show></span><button type="button" class="assignAction" onClick={() => assign(city)}>Use selected</button><button type="button" class="clearSymbol" disabled={!icons()[city]} onClick={() => clear(city)} aria-label={`Clear ${city} symbol`}>×</button></div>}</For></div>
</section>;
};
+7 -7
View File
@@ -1,16 +1,15 @@
.weatherIconButton {
font-family: 'Daira by LTV grafika';
font-size: 30px;
display: inline-block;
background-color: #ccc;
padding: 3px 4px;
margin: 0 3px;
display:inline-flex;align-items:center;justify-content:center;
width:46px;height:46px;min-height:46px;
background:#f4f6f8;border:1px solid #d9e0e8;border-radius:10px;
padding:0;margin:0;
cursor: pointer;
}
.weatherIconButton:hover {
background-color: #333;
color: white;
background:#e9eef4;border-color:#aebccc;color:#17212d;
}
.weatherIconInput {
@@ -23,4 +22,5 @@
font-size: 20px;
color: white;
}
.weatherIconButton.selected{background:var(--accent);color:#fff;box-shadow:0 0 0 2px var(--accent-pale)}
.weatherIconButton.selected{background:#17212d;border-color:#17212d;color:#fff;box-shadow:0 0 0 3px var(--accent-pale)}
.weatherIconButton:focus-visible{outline:3px solid var(--accent-soft);outline-offset:2px}
+8 -3
View File
@@ -5,10 +5,11 @@ import { ResultKeyVal, resultOrder, ResultOrderKeys } from "../../../consts";
import { SelectOrder } from "../SelectOrder";
import { QueryResult } from "../helpers";
import { GridResult } from "./GridResult";
import { MapView } from "../../../components/map/MapView";
type ResultView = "grid" | "list";
const resultViews: Array<ResultView> = ["grid", "list"];
const viewLabels: Record<ResultView,string> = {grid:'grid',list:'list'}
type ResultView = "grid" | "list" | "map_1920x1080" | "map_1920x1080_wind" | "map_3840x1440" | "map_3840x1440_wind";
const resultViews: Array<ResultView> = ["grid", "list", "map_1920x1080", "map_1920x1080_wind", "map_3840x1440", "map_3840x1440_wind"];
const viewLabels: Record<ResultView,string> = {grid:'grid',list:'list',map_1920x1080:'map_1920x1080',map_1920x1080_wind:'map_1920x1080_wind',map_3840x1440:'map_3840x1440',map_3840x1440_wind:'map_3840x1440_wind'}
export const ResultView: Component<{
result: Resource<QueryResult>
@@ -72,6 +73,10 @@ export const ResultView: Component<{
/>)}
</div>
</Show>
<Show when={getResultView() === "map_1920x1080"}><MapView type="map_1920x1080" data={cityData}/></Show>
<Show when={getResultView() === "map_1920x1080_wind"}><MapView type="map_1920x1080_wind" data={cityData}/></Show>
<Show when={getResultView() === "map_3840x1440"}><MapView type="map_3840x1440" data={cityData}/></Show>
<Show when={getResultView() === "map_3840x1440_wind"}><MapView type="map_3840x1440_wind" data={cityData}/></Show>
</div>
</div>
);
+2 -2
View File
@@ -3,8 +3,8 @@ import './home.css'
const tools = [
['/station','Monitor','Stations','Inspect recent measurements on a map and open detailed station charts.','◉'],
['/cities','Analyse & export','City analysis','Compare cities, aggregate values, and prepare map graphics.','▦'],
['/map-graphics','Produce','Map graphics','Build temperature and Faktiskā newsroom graphics with export-ready overlays.','▣'],
['/cities','Analyse & export','City analysis','Compare cities, aggregate values, and prepare temperature maps.','▦'],
['/faktiska','Daily production','Faktiskā','Build the actual-weather map with temperatures, weather symbols, title, and source.','▣'],
['/latvia','Summarise','Latvia overview','Review country-wide minimum, maximum, average, and total values.','⌁'],
['/database','Browse','Data archive','Find available dates and inspect individual hourly records.','▤'],
['/harmonie','Forecast model','HARMONIE','Render forecast fields from locally stored HARMONIE GRIB files.','◫'],
+4 -6
View File
@@ -9,14 +9,13 @@ import { isQueryResult, QueryResult } from "../cities/helpers";
import { LoadingSpinner } from "../../components/spinner/LoadingSpinner";
import "./mapGraphics.css";
export function MapGraphics() {
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 [mode, setMode] = createSignal<"temperature" | "faktiska">("faktiska");
const [resolution, setResolution] = createSignal<"1920x1080" | "3840x1440">("1920x1080");
const [wind, setWind] = createSignal(false);
const [trigger, setTrigger] = createSignal(0);
@@ -35,18 +34,17 @@ export function MapGraphics() {
const cityData = (): ResultKeyVal[] => Object.entries((result() as QueryResult).result).map(([city, value]) => [city, typeof value === "number" ? value : -99]);
return <div class="mapGraphicsPage pageWorkspace">
<div class="pageHeading"><div><span class="eyebrow">Newsroom production</span><h1>Map graphics</h1><p>Create a temperature map or Faktiskā weather map and export the finished PNG.</p></div><div class="selectionCount">{cities().size} cities selected</div></div>
<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="panel cityPanel"><h2>1. Choose cities</h2><SelectCity getCities={cities} setCities={setCities}/></aside>
<section class="graphicsMain">
<div class="panel productionOptions"><h2>2. Choose graphic</h2>
<div class="choiceGroup"><span>Graphic type</span><div><button classList={{active: mode() === "temperature"}} onClick={() => setMode("temperature")}>Temperature map<small>Temperatures only</small></button><button classList={{active: mode() === "faktiska"}} onClick={() => setMode("faktiska")}>Faktiskā<small>Temperatures + weather symbols</small></button></div></div>
<div class="panel 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="panel 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></div><button class="primary" disabled={!cities().size} onClick={() => setTrigger(Date.now())}>Load map data</button></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. Finish and export</h2><MapView type={mapType()} data={cityData} mode={mode()}/></section></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>
</div>
@@ -1 +1,2 @@
.graphicsSetup{display:grid;grid-template-columns:240px minmax(0,1fr);gap:18px;align-items:start}.graphicsMain{display:grid;gap:18px}.productionOptions,.dataOptions{padding:20px}.choiceGroup{display:grid;grid-template-columns:130px 1fr;gap:16px;align-items:start;margin-top:16px}.choiceGroup>span,.dataOptions label>span{display:block;font-size:12px;font-weight:700}.choiceGroup>div{display:flex;flex-wrap:wrap;gap:9px}.choiceGroup button{min-width:150px;text-align:left}.choiceGroup button small{display:block;margin-top:3px;color:var(--text-muted);font-family:Inter,sans-serif}.choiceGroup button.active{border-color:var(--accent);background:var(--accent-pale);color:var(--accent-strong)}.windChoice{display:flex;align-items:center;gap:7px;padding:9px 12px}.dataOptionsGrid{display:grid;grid-template-columns:minmax(250px,1.4fr) 1fr 1fr;gap:18px;align-items:end;margin-bottom:16px}.dataOptions select{width:100%;margin-top:6px}.mapProduction>h2{margin:0 0 10px}.symbolEditor{min-width:0}.symbolEditorHeading{display:flex;align-items:start;justify-content:space-between;gap:12px}.symbolPalette{display:flex;flex-wrap:wrap;gap:5px;margin:10px 0 16px}.citySymbols{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.citySymbolRow{display:grid;grid-template-columns:1fr 52px 54px;gap:6px;align-items:center;padding:7px;border:1px solid var(--border);border-radius:9px}.assignedSymbol{font-family:'Daira by LTV grafika';font-size:28px;padding:2px}.clearSymbol{padding:5px;font-size:11px}@media(max-width:900px){.graphicsSetup{grid-template-columns:1fr}.dataOptionsGrid{grid-template-columns:1fr}.choiceGroup{grid-template-columns:1fr}}
.symbolEditorHeading{gap:16px}.bulkSymbol{display:flex;align-items:center;gap:8px}.selectedPreview,.currentSymbol{display:flex;align-items:center;justify-content:center;font-family:'Daira by LTV grafika';font-size:30px}.selectedPreview{width:42px;height:42px;border-radius:9px;background:#17212d;color:#fff}.symbolPalette{display:grid;grid-template-columns:repeat(auto-fill,46px);gap:8px;margin:14px 0 22px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--surface-soft)}.assignmentHeading{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}.assignmentHeading span{color:var(--text-muted);font-size:12px}.citySymbols{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}.citySymbolRow{grid-template-columns:minmax(90px,1fr) 48px 100px 36px;gap:8px;min-height:62px;padding:8px 10px;border-radius:12px;background:#fff}.cityName{overflow:hidden;text-overflow:ellipsis;font-weight:700}.currentSymbol{width:44px;height:44px;border:1px solid #d7dfe8;border-radius:9px;background:#f6f8fa}.currentSymbol.empty{font-family:Inter,sans-serif;font-size:18px;color:#98a2b3}.assignAction{padding:7px 9px;font-size:12px}.clearSymbol{width:36px;padding:0;font-size:20px;color:var(--text-muted)}@media(max-width:900px){.symbolEditorHeading{flex-direction:column}.citySymbols{grid-template-columns:1fr}}