Add click-to-edit temperature badges on the Faktiskā map
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:
@@ -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 { ResultKeyVal } from "../../consts";
|
||||||
import { WindInputs, WindSignals } from "./WindInputs";
|
import { WindInputs, WindSignals } from "./WindInputs";
|
||||||
@@ -10,8 +10,10 @@ import { CityData, drawOnMap } from "./canvasDraw";
|
|||||||
import { IconInputs } from "../weatherIcons/IconInputs";
|
import { IconInputs } from "../weatherIcons/IconInputs";
|
||||||
import { preloadWeatherIcons } from "../weatherIcons/weatherIconAssets";
|
import { preloadWeatherIcons } from "../weatherIcons/weatherIconAssets";
|
||||||
import { download } from '../../helpers/download'
|
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 [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||||
const [getImg, setImg] = createSignal(new Image());
|
const [getImg, setImg] = createSignal(new Image());
|
||||||
const [fontReady, setFontReady] = createSignal(false);
|
const [fontReady, setFontReady] = createSignal(false);
|
||||||
@@ -24,6 +26,64 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
|
|||||||
let lastCoords: CityData[] = [];
|
let lastCoords: CityData[] = [];
|
||||||
const props = resolutionProps[type];
|
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();
|
const arrowImg = new Image();
|
||||||
arrowImg.src = arrowUrl;
|
arrowImg.src = arrowUrl;
|
||||||
|
|
||||||
@@ -105,7 +165,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="mapWorkspace">
|
<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}>
|
<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>
|
<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>}
|
{!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}
|
ref={setCanvas}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function Faktiska() {
|
|||||||
<button classList={{ active: template() === "faktiska_3840x1440" }} onClick={() => setTemplate("faktiska_3840x1440")}>3840 × 1440</button>
|
<button classList={{ active: template() === "faktiska_3840x1440" }} onClick={() => setTemplate("faktiska_3840x1440")}>3840 × 1440</button>
|
||||||
</div>
|
</div>
|
||||||
<Show when={template()} keyed>{currentTemplate =>
|
<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>
|
}</Show>
|
||||||
</section>
|
</section>
|
||||||
</div>;
|
</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}
|
.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}}
|
.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}
|
.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}
|
.mapProduction>h2{font-size:22px}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user