Add click-to-edit temperature badges on the Faktiskā map
CI / backend (push) Successful in 1m7s
CI / frontend (push) Successful in 41s

The "Rādīt stacijas" panel is the only way to fix a value today, and it
opens all 13 fields at once even when only one or two need a tweak. Since
most values are already correct, that's the wrong default interaction for
the common case.

Click a temperature badge directly on the map to edit it inline instead.
Scoped strictly to mode==="faktiska" (Kartes/Ūdens/Brīdinājumi share the
same MapView component and are unaffected). Mirrors Brīdinājumi's existing
canvas-coordinate pattern exactly (getBoundingClientRect scaling via
canvasPoint) rather than inventing a new one, hit-testing against
Faktiskā's already-fixed per-resolution marker positions (faktiskaTemplates)
-- no dragging or calibration needed, just a box test against 13 known
points. The floating input is a plain DOM element positioned over the
canvas, not drawn onto it, so it's preview-only like Brīdinājumi's drag
handles and never reaches the exported PNG.

Reuses the existing updateValue function via a new onEditValue callback
prop, so overrides/reset/the "Manuāli" badge in the stations panel all stay
in sync automatically -- no parallel state to maintain. Added a hover
cursor and a one-line hint ("Klikšķini uz temperatūras kartē, lai to
mainītu") for discoverability, since the whole point was fixing an
undiscoverable interaction, not trading it for another one.

Verified end-to-end: hover shows a pointer cursor over a badge; clicking
shows a correctly positioned input prefilled with the real current value;
Enter commits and the canvas redraws immediately; the change is reflected
in the "Rādīt stacijas" panel as a manual override; Escape cancels without
committing; works correctly at both 1920x1080 and 3840x1440 (different
badge size and marker positions per template); no console errors; Kartes
(a different MapView mode) is unaffected.
This commit is contained in:
b0txec
2026-08-25 11:21:11 +03:00
parent 07b16accb2
commit 4c40c244df
3 changed files with 89 additions and 5 deletions
+86 -4
View File
@@ -1,4 +1,4 @@
import { Component, createEffect, createSignal, onMount } from "solid-js";
import { Component, createEffect, createSignal, onMount, Show } from "solid-js";
import { ResultKeyVal } from "../../consts";
import { WindInputs, WindSignals } from "./WindInputs";
@@ -10,8 +10,10 @@ import { CityData, drawOnMap } from "./canvasDraw";
import { IconInputs } from "../weatherIcons/IconInputs";
import { preloadWeatherIcons } from "../weatherIcons/weatherIconAssets";
import { download } from '../../helpers/download'
import { faktiskaTemplates } from "../../pages/map-graphics/faktiskaTemplates";
import type { FaktiskaTemplate } from "../../pages/map-graphics/faktiskaTemplates";
export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[], mode?: "temperature" | "faktiska", productionTemplate?: boolean }> = ({ type, data, mode = "temperature", productionTemplate = false }) => {
export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[], mode?: "temperature" | "faktiska", productionTemplate?: boolean, onEditValue?: (city: string, value: string) => void }> = ({ type, data, mode = "temperature", productionTemplate = false, onEditValue }) => {
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
const [getImg, setImg] = createSignal(new Image());
const [fontReady, setFontReady] = createSignal(false);
@@ -24,6 +26,64 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
let lastCoords: CityData[] = [];
const props = resolutionProps[type];
// Faktiskā only: click a temperature badge on the map to edit it inline,
// instead of needing the "Rādīt stacijas" panel for a one-off tweak.
// Same canvas-coordinate conversion as Brīdinājumi's drag/resize handles
// (getBoundingClientRect scaling) -- preview-only, this input is a plain
// DOM element positioned over the canvas and never reaches the export.
const clickToEditEnabled = mode === "faktiska" && productionTemplate;
const [hoveredCity, setHoveredCity] = createSignal<string>();
const [editingCity, setEditingCity] = createSignal<string>();
const [editValue, setEditValue] = createSignal("");
const canvasPoint = (event: { clientX: number, clientY: number }) => {
const canvas = getCanvas()!;
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (canvas.width / rect.width),
y: (event.clientY - rect.top) * (canvas.height / rect.height),
};
};
const badgeHitTest = (point: { x: number, y: number }): string | undefined => {
if (!clickToEditEnabled) return undefined;
const faktiskaTemplate = faktiskaTemplates[type as FaktiskaTemplate];
const half = faktiskaTemplate.badgeSize / 2;
for (const [city, marker] of Object.entries(faktiskaTemplate.markers)) {
if (Math.abs(point.x - marker.x) <= half && Math.abs(point.y - marker.y) <= half) return city;
}
return undefined;
};
const screenPointFromCanvas = (x: number, y: number) => {
const canvas = getCanvas()!;
const rect = canvas.getBoundingClientRect();
return {
left: rect.left + x * (rect.width / canvas.width),
top: rect.top + y * (rect.height / canvas.height),
};
};
const onCanvasPointerMove = (event: PointerEvent) => {
if (!clickToEditEnabled) return;
setHoveredCity(badgeHitTest(canvasPoint(event)));
};
const onCanvasClick = (event: MouseEvent) => {
if (!clickToEditEnabled) return;
const city = badgeHitTest(canvasPoint(event));
if (!city) return;
const current = data().find(([c]) => c === city)?.[1];
setEditValue(typeof current === "number" ? current.toString().replace(".", ",") : "");
setEditingCity(city);
};
const commitEdit = () => {
const city = editingCity();
if (city) onEditValue?.(city, editValue());
setEditingCity(undefined);
};
const arrowImg = new Image();
arrowImg.src = arrowUrl;
@@ -105,7 +165,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
return (
<div class="mapWorkspace">
<div class="mapToolbar"><div><strong>{productionTemplate ? "Kartes priekšskatījums" : "Map preview"}</strong><span>{props.width} × {props.height} px {productionTemplate ? "eksports" : "export"}</span></div><button class="primary" onClick={downloadPng}>{productionTemplate ? "Lejupielādēt PNG" : "Download PNG"}</button></div>
<div class="mapToolbar"><div><strong>{productionTemplate ? "Kartes priekšskatījums" : "Map preview"}</strong><span>{props.width} × {props.height} px {productionTemplate ? "eksports" : "export"}</span>{clickToEditEnabled && <span class="mapEditHint">Klikšķini uz temperatūras kartē, lai to mainītu</span>}</div><button class="primary" onClick={downloadPng}>{productionTemplate ? "Lejupielādēt PNG" : "Download PNG"}</button></div>
<details class="broadcastControls" open={!productionTemplate}>
<summary><span><strong>{productionTemplate ? "Kartes noformējums" : "Broadcast overlay controls"}</strong>{!productionTemplate && <small>Optional manual wind and weather-symbol overrides</small>}</span></summary>
{!productionTemplate && <p class="controlHelp">These settings do not change the queried city values. Use them only when preparing a finished broadcast graphic.</p>}
@@ -136,8 +196,30 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
ref={setCanvas}
width={props.width}
height={props.height}
style={getStyleSize()}
style={{ ...getStyleSize(), cursor: hoveredCity() ? "pointer" : "default" }}
onPointerMove={onCanvasPointerMove}
onPointerLeave={() => setHoveredCity(undefined)}
onClick={onCanvasClick}
/>
<Show when={editingCity()}>{city => {
const marker = () => faktiskaTemplates[type as FaktiskaTemplate].markers[city() as keyof typeof faktiskaTemplates[FaktiskaTemplate]["markers"]];
const pos = () => screenPointFromCanvas(marker().x, marker().y);
return <input
class="mapValueEditor"
ref={el => { setTimeout(() => { el.focus(); el.select(); }, 0); }}
style={{ position: "fixed", left: `${pos().left}px`, top: `${pos().top}px`, transform: "translate(-50%, -50%)" }}
type="text"
inputmode="decimal"
value={editValue()}
onInput={e => setEditValue(e.currentTarget.value)}
onKeyDown={e => {
if (e.key === "Enter") commitEdit();
if (e.key === "Escape") setEditingCity(undefined);
}}
onBlur={commitEdit}
aria-label={`${city()} temperatūra`}
/>;
}}</Show>
</div>
</div>
);
+1 -1
View File
@@ -127,7 +127,7 @@ export function Faktiska() {
<button classList={{ active: template() === "faktiska_3840x1440" }} onClick={() => setTemplate("faktiska_3840x1440")}>3840 × 1440</button>
</div>
<Show when={template()} keyed>{currentTemplate =>
<MapView type={currentTemplate} data={cityData} mode="faktiska" productionTemplate/>
<MapView type={currentTemplate} data={cityData} mode="faktiska" productionTemplate onEditValue={(city, value) => updateValue(city as FaktiskaStation, value)}/>
}</Show>
</section>
</div>;
@@ -1,6 +1,8 @@
.mapProduction>h2{margin:0 0 10px}.symbolEditor{min-width:0}.symbolEditorHeading{display:flex;align-items:start;justify-content:space-between;gap:12px}.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}.clearSymbol{padding:5px;font-size:11px}
.symbolEditorHeading{gap:16px}.bulkSymbol{display:flex;align-items:center;gap:8px}.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}.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}}
.symbolEditor{display:grid;gap:16px}.symbolCard{padding:16px;border:1px solid var(--border);border-radius:14px;background:var(--surface)}.showAllCitiesToggle{margin-top:4px}
.mapEditHint{font-style:italic}
.mapValueEditor{z-index:20;width:74px;padding:6px 8px;border:2px solid var(--accent);border-radius:8px;background:#fff;font-size:18px;font-weight:700;text-align:center;box-shadow:0 6px 16px rgba(16,24,40,.22)}
.mapProduction>h2{font-size:22px}