From 5a924653af6f3baf8cc71a8c378d64264239c5e0 Mon Sep 17 00:00:00 2001 From: b0txec Date: Sun, 23 Aug 2026 11:27:26 +0300 Subject: [PATCH] =?UTF-8?q?Auto-populate=20=C5=AAdens=20temperatures=20fro?= =?UTF-8?q?m=20real=20LV=C4=A2MC=20data,=20with=20manual=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Faktiskā pattern: fetch on load, auto-fill min/max per zone, track manual overrides, per-zone reset, loading/error states. --- .../water-temperature/WaterTemperature.tsx | 81 ++++++++++++++++--- .../water-temperature/waterTemperature.css | 2 +- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/web/src/pages/water-temperature/WaterTemperature.tsx b/web/src/pages/water-temperature/WaterTemperature.tsx index 1e33ecb..203f1c6 100644 --- a/web/src/pages/water-temperature/WaterTemperature.tsx +++ b/web/src/pages/water-temperature/WaterTemperature.tsx @@ -1,14 +1,17 @@ -import { createEffect, createSignal, For, onMount } from 'solid-js'; +import { createEffect, createResource, createSignal, For, onMount, Show } from 'solid-js'; import map1920 from '../../assets/production/UDENS_KARTE_PAMATS_1920.png'; import map3840 from '../../assets/production/UDENS_KARTE_PAMATS_3840.png'; +import { apiHost } from '../../consts'; import { download } from '../../helpers/download'; +import { LoadingSpinner } from '../../components/spinner/LoadingSpinner'; import './waterTemperature.css'; type OutputSize = '1920x1080' | '3840x1440'; type WaterArea = 'Jūra' | 'Līcis' | 'Kurzeme' | 'Zemgale' | 'Vidzeme' | 'Latgale'; type RangeValue = { min: string; max: string }; type LabelBox = { x: number; y: number; width: number; height: number }; +type WaterReading = { area: WaterArea; observedAt: string; value: number | null }; const areaNames: WaterArea[] = ['Jūra', 'Līcis', 'Kurzeme', 'Zemgale', 'Vidzeme', 'Latgale']; const layouts: Record }> = { @@ -37,6 +40,7 @@ const emptyRanges = (): Record => Object.fromEntries( export function WaterTemperature() { const [output, setOutput] = createSignal('3840x1440'); const [ranges, setRanges] = createSignal(emptyRanges()); + const [overrides, setOverrides] = createSignal>(new Set()); const [background, setBackground] = createSignal(); let canvas!: HTMLCanvasElement; @@ -46,8 +50,47 @@ export function WaterTemperature() { const complete = () => areaNames.every(name => ranges()[name].min.trim() && ranges()[name].max.trim()); + const fetchLatest = async (): Promise => { + const response = await fetch(`${apiHost}/api/water-temperatures`); + if (!response.ok) throw new Error(`Water temperatures failed (${response.status})`); + const body: { source: string; readings: WaterReading[] } = await response.json(); + return body.readings; + }; + const [latest, { refetch }] = createResource(fetchLatest); + + createEffect(() => { + const readings = latest(); + if (!readings) return; + const next = emptyRanges(); + readings.forEach(reading => { + if (reading.value == null) return; + const rounded = Math.round(reading.value).toString(); + next[reading.area] = { min: rounded, max: rounded }; + }); + setRanges(next); + setOverrides(new Set()); + }); + + const observationsByArea = () => new Map((latest() ?? []).map(reading => [reading.area, reading])); + const newestTimestamp = () => { + const timestamps = (latest() ?? []).map(reading => Date.parse(reading.observedAt)).filter(Number.isFinite); + return timestamps.length ? new Date(Math.max(...timestamps)) : undefined; + }; + const updateRange = (name: WaterArea, key: keyof RangeValue, value: string) => { setRanges(current => ({ ...current, [name]: { ...current[name], [key]: value } })); + setOverrides(current => new Set(current).add(name)); + }; + + const resetRange = (name: WaterArea) => { + const reading = observationsByArea().get(name); + const rounded = reading?.value == null ? '' : Math.round(reading.value).toString(); + setRanges(current => ({ ...current, [name]: { min: rounded, max: rounded } })); + setOverrides(current => { + const next = new Set(current); + next.delete(name); + return next; + }); }; const loadBackground = () => { @@ -84,31 +127,47 @@ export function WaterTemperature() { return
-
ĒTERA GRAFIKA

Ūdens temperatūra

Manuāli ievadi temperatūras diapazonus un sagatavo karti eksportam.

+
ĒTERA GRAFIKA

Ūdens temperatūra

Automātiski ielādētas temperatūras, ar iespēju tās manuāli labot pirms eksporta.

{complete() ? '6 diapazoni aizpildīti' : `${areaNames.filter(name => ranges()[name].min && ranges()[name].max).length} no 6 aizpildīti`}
+
+
+
1. Jaunākie ūdens temperatūras dati{newestTimestamp() ? newestTimestamp()!.toLocaleString('lv-LV') : 'Gaida novērojumus'}LVĢMC hidrometeoroloģiskie novērojumi
+ +
+ +
Novērojumus neizdevās ielādēt. Manuālie lauki joprojām ir pieejami.
+
+
-

1. Izvēlies izmēru

+

2. Izvēlies izmēru

-

2. Ievadi temperatūras

-
{name =>
- {name} - - ... - -
}
+

3. Ievadi temperatūras

+
{name => { + const observation = () => observationsByArea().get(name); + return
+ {name} +
+ {overrides().has(name) ? 'Manuāli' : observation()?.observedAt ? new Date(observation()!.observedAt).toLocaleTimeString('lv-LV', { hour: '2-digit', minute: '2-digit' }) : 'Nav datu'} + +
+ + ... + +
; + }}
-
3. Kartes priekšskatījums{dimensions().width} × {dimensions().height} px eksports
+
4. Kartes priekšskatījums{dimensions().width} × {dimensions().height} px eksports
{!complete() &&

Aizpildi visus sešus diapazonus, lai lejupielādētu gatavo karti.

}
diff --git a/web/src/pages/water-temperature/waterTemperature.css b/web/src/pages/water-temperature/waterTemperature.css index c007e6d..7bf79a5 100644 --- a/web/src/pages/water-temperature/waterTemperature.css +++ b/web/src/pages/water-temperature/waterTemperature.css @@ -1 +1 @@ -.waterPage{max-width:1900px}.waterHeading{margin-bottom:18px}.waterSetup{display:grid;grid-template-columns:260px minmax(0,1fr);gap:28px;padding:18px 0 24px;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}.waterSetup h2{margin:0 0 14px;font-size:20px}.resolutionChoices{display:grid;gap:9px}.resolutionChoices button{text-align:left}.resolutionChoices button.active{border-color:var(--accent);background:var(--accent-pale);color:var(--accent-strong);box-shadow:inset 0 0 0 1px var(--accent)}.rangeGrid{display:grid;grid-template-columns:repeat(3,minmax(250px,1fr));gap:12px}.rangeField{display:grid;grid-template-columns:1fr auto 1fr;gap:8px;align-items:end;margin:0;padding:10px 12px 12px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.46)}.rangeField legend{padding:0 5px;font-weight:700}.rangeField label{display:grid;gap:4px}.rangeField label span{font-size:11px;color:var(--text-muted)}.rangeField input{width:100%;font-size:18px;font-weight:700}.rangeSeparator{padding-bottom:9px;font-weight:700}.waterPreview{padding-top:24px}.waterNotice{margin:0 0 10px;color:var(--text-muted);font-size:13px}.waterPreview canvas{display:block;width:100%;height:auto}@media(max-width:1050px){.waterSetup{grid-template-columns:1fr}.resolutionChoices{grid-template-columns:repeat(2,minmax(0,1fr))}.rangeGrid{grid-template-columns:repeat(2,minmax(220px,1fr))}}@media(max-width:650px){.rangeGrid,.resolutionChoices{grid-template-columns:1fr}} +.waterPage{max-width:1900px}.waterHeading{margin-bottom:18px}.waterSetup{display:grid;grid-template-columns:260px minmax(0,1fr);gap:28px;padding:18px 0 24px;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}.waterSetup h2{margin:0 0 14px;font-size:20px}.resolutionChoices{display:grid;gap:9px}.resolutionChoices button{text-align:left}.resolutionChoices button.active{border-color:var(--accent);background:var(--accent-pale);color:var(--accent-strong);box-shadow:inset 0 0 0 1px var(--accent)}.rangeGrid{display:grid;grid-template-columns:repeat(3,minmax(250px,1fr));gap:12px}.rangeField{display:grid;grid-template-columns:1fr auto 1fr;gap:8px;align-items:end;margin:0;padding:10px 12px 12px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.46);transition:border-color 180ms,background 180ms}.rangeField.manual{border-color:rgba(36,119,197,.58);background:rgba(229,241,255,.76)}.rangeField.missing{border-style:dashed}.rangeField legend{padding:0 5px;font-weight:700}.rangeFieldMeta{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;gap:8px;margin:-2px 0 2px}.rangeFieldMeta span{font-size:10px;color:var(--text-muted)}.rangeField label{display:grid;gap:4px}.rangeField label span{font-size:11px;color:var(--text-muted)}.rangeField input{width:100%;font-size:18px;font-weight:700}.rangeSeparator{padding-bottom:9px;font-weight:700}.waterDataBand{padding:2px 0 22px;border-bottom:1px solid rgba(90,126,166,.22)}.waterStatus{display:flex;align-items:center;justify-content:space-between;gap:24px}.waterStatus>div,.waterStatus strong,.waterStatus small{display:block}.waterStatus strong{margin-top:5px;font-size:18px}.waterStatus small{margin-top:3px;color:var(--text-muted)}.waterPreview{padding-top:24px}.waterNotice{margin:0 0 10px;color:var(--text-muted);font-size:13px}.waterPreview canvas{display:block;width:100%;height:auto}@media(max-width:1050px){.waterSetup{grid-template-columns:1fr}.resolutionChoices{grid-template-columns:repeat(2,minmax(0,1fr))}.rangeGrid{grid-template-columns:repeat(2,minmax(220px,1fr))}}@media(max-width:650px){.rangeGrid,.resolutionChoices{grid-template-columns:1fr}.waterStatus{align-items:flex-start;flex-direction:column}}