From 4c40c244dfc7d3a7e1ac11e22d8bb843ea1127e5 Mon Sep 17 00:00:00 2001 From: b0txec Date: Tue, 25 Aug 2026 11:21:11 +0300 Subject: [PATCH] =?UTF-8?q?Add=20click-to-edit=20temperature=20badges=20on?= =?UTF-8?q?=20the=20Faktisk=C4=81=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/src/components/map/MapView.tsx | 90 +++++++++++++++++++++- web/src/pages/map-graphics/MapGraphics.tsx | 2 +- web/src/pages/map-graphics/mapGraphics.css | 2 + 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/web/src/components/map/MapView.tsx b/web/src/components/map/MapView.tsx index 883475c..a74df1e 100644 --- a/web/src/components/map/MapView.tsx +++ b/web/src/components/map/MapView.tsx @@ -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(); 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(); + const [editingCity, setEditingCity] = createSignal(); + 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 (
-
{productionTemplate ? "Kartes priekšskatījums" : "Map preview"}{props.width} × {props.height} px {productionTemplate ? "eksports" : "export"}
+
{productionTemplate ? "Kartes priekšskatījums" : "Map preview"}{props.width} × {props.height} px {productionTemplate ? "eksports" : "export"}{clickToEditEnabled && Klikšķini uz temperatūras kartē, lai to mainītu}
{productionTemplate ? "Kartes noformējums" : "Broadcast overlay controls"}{!productionTemplate && Optional manual wind and weather-symbol overrides} {!productionTemplate &&

These settings do not change the queried city values. Use them only when preparing a finished broadcast graphic.

} @@ -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} /> + {city => { + const marker = () => faktiskaTemplates[type as FaktiskaTemplate].markers[city() as keyof typeof faktiskaTemplates[FaktiskaTemplate]["markers"]]; + const pos = () => screenPointFromCanvas(marker().x, marker().y); + return { 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`} + />; + }}
); diff --git a/web/src/pages/map-graphics/MapGraphics.tsx b/web/src/pages/map-graphics/MapGraphics.tsx index 16e350c..17b1435 100644 --- a/web/src/pages/map-graphics/MapGraphics.tsx +++ b/web/src/pages/map-graphics/MapGraphics.tsx @@ -127,7 +127,7 @@ export function Faktiska() { {currentTemplate => - + updateValue(city as FaktiskaStation, value)}/> } ; diff --git a/web/src/pages/map-graphics/mapGraphics.css b/web/src/pages/map-graphics/mapGraphics.css index 86941b0..cfba74b 100644 --- a/web/src/pages/map-graphics/mapGraphics.css +++ b/web/src/pages/map-graphics/mapGraphics.css @@ -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}