2026-08-25 11:21:11 +03:00
import { Component , createEffect , createSignal , onMount , Show } from "solid-js" ;
2023-08-08 17:22:52 +03:00
2023-08-07 23:25:35 +03:00
import { ResultKeyVal } from "../../consts" ;
2023-08-08 17:22:52 +03:00
import { WindInputs , WindSignals } from "./WindInputs" ;
2023-08-08 19:00:22 +03:00
import arrowUrl from "../../assets/arrow.webp" ;
2025-03-14 11:31:47 +02:00
import { cityCoords } from "../cityCoords" ;
2024-02-11 21:44:29 +02:00
import { MapResolution , resolutionProps , windProps , WindProps } from "./mapConsts" ;
import { CityData , drawOnMap } from "./canvasDraw" ;
2025-03-14 11:31:47 +02:00
import { IconInputs } from "../weatherIcons/IconInputs" ;
2026-08-20 10:46:26 +03:00
import { preloadWeatherIcons } from "../weatherIcons/weatherIconAssets" ;
2026-08-18 21:44:09 +03:00
import { download } from '../../helpers/download'
2026-08-25 11:21:11 +03:00
import { faktiskaTemplates } from "../../pages/map-graphics/faktiskaTemplates" ;
import type { FaktiskaTemplate } from "../../pages/map-graphics/faktiskaTemplates" ;
2024-01-23 22:58:26 +02:00
2026-08-25 11:21:11 +03:00
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 }) => {
2023-08-07 23:25:35 +03:00
const [ getCanvas , setCanvas ] = createSignal < HTMLCanvasElement >();
const [ getImg , setImg ] = createSignal ( new Image ());
2026-08-19 22:07:16 +03:00
const [ fontReady , setFontReady ] = createSignal ( false );
2026-08-20 10:46:26 +03:00
const [ iconsReady , setIconsReady ] = createSignal ( mode !== "faktiska" );
2026-08-18 22:31:56 +03:00
const weatherIconsSignal = createSignal < {[ key : string ] : string ;} > ({});
const [ getWeatherIcons ] = weatherIconsSignal ;
2026-08-18 22:14:40 +03:00
const [ getTitle , setTitle ] = createSignal ( "" );
const [ getSource , setSource ] = createSignal ( "" );
2024-02-11 21:44:29 +02:00
2025-03-12 13:16:35 +02:00
let lastCoords : CityData [] = [];
2024-01-23 22:58:26 +02:00
const props = resolutionProps [ type ];
2023-08-07 23:25:35 +03:00
2026-08-25 11:21:11 +03:00
// 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 );
};
2023-08-08 19:00:22 +03:00
const arrowImg = new Image ();
arrowImg . src = arrowUrl ;
2023-08-08 17:22:52 +03:00
const windSignals : WindSignals = {
direction : createSignal ( "" ),
speed : createSignal ( "" ),
gusts : createSignal ( "" ),
2026-08-19 17:11:58 +03:00
roundValues : createSignal ( mode === "faktiska" ),
2023-08-08 17:22:52 +03:00
}
2024-02-08 15:25:51 +02:00
function getWindData () : [ string , string , string , boolean , WindProps ] {
2023-08-08 17:22:52 +03:00
return [
windSignals . direction [ 0 ](),
windSignals . speed [ 0 ](),
windSignals . gusts [ 0 ](),
2023-08-08 23:05:55 +03:00
windSignals . roundValues [ 0 ](),
2024-02-08 15:25:51 +02:00
windProps [ type ],
2023-08-08 17:22:52 +03:00
];
}
2023-08-07 23:25:35 +03:00
onMount (() => {
2026-08-19 22:07:16 +03:00
void document . fonts . load ( `700 ${ props . fontSize } px Monda` ). then (() => setFontReady ( true ));
2026-08-20 10:46:26 +03:00
if ( mode === "faktiska" ) void preloadWeatherIcons (). then (() => setIconsReady ( true ));
2023-08-07 23:25:35 +03:00
const ctx = getCanvas () ! . getContext ( "2d" ) ! ;
const img = new Image ();
img . onload = () => {
setImg ( img );
2023-08-08 23:05:55 +03:00
drawOnMap (
ctx ,
[ img , arrowImg ],
lastCoords ,
getWindData (),
2026-08-18 22:14:40 +03:00
[ getTitle (), getSource ()],
2024-01-23 22:58:26 +02:00
props ,
2026-08-19 17:44:46 +03:00
productionTemplate ,
2023-08-08 23:05:55 +03:00
);
2023-08-07 23:25:35 +03:00
};
2024-01-23 22:58:26 +02:00
img . src = props . map ;
2023-08-07 23:25:35 +03:00
});
createEffect (() => {
2026-08-19 22:07:16 +03:00
fontReady ();
2026-08-20 10:46:26 +03:00
iconsReady ();
2023-08-07 23:25:35 +03:00
const ctx = getCanvas () ! . getContext ( "2d" ) ! ;
2024-02-11 21:44:29 +02:00
const weatherIcons = getWeatherIcons ();
2025-03-12 13:16:35 +02:00
const coordsAndData : CityData [] = data ()
2023-08-07 23:25:35 +03:00
. map (([ city , value ]) => [
city ,
cityCoords [ city as keyof typeof cityCoords ],
typeof value === "number" ? value : - 99 ,
2024-02-11 21:44:29 +02:00
weatherIcons [ city ],
2023-08-07 23:25:35 +03:00
]);
lastCoords = coordsAndData ;
2023-08-08 23:05:55 +03:00
drawOnMap (
ctx ,
2024-02-11 21:44:29 +02:00
[ getImg (), arrowImg ],
2023-08-08 23:05:55 +03:00
coordsAndData ,
getWindData (),
2026-08-18 22:14:40 +03:00
[ getTitle (), getSource ()],
2024-01-23 22:58:26 +02:00
props ,
2026-08-19 17:44:46 +03:00
productionTemplate ,
2023-08-08 23:05:55 +03:00
);
2023-08-07 23:25:35 +03:00
});
2024-02-08 15:42:36 +02:00
const getStyleSize = () => {
2026-08-18 21:44:09 +03:00
return { "width" : "100%" , "height" : "auto" };
}
function downloadPng() {
2026-08-19 17:23:07 +03:00
const canvas = getCanvas () ! ;
2026-08-20 22:23:23 +03:00
if ( productionTemplate && ( canvas . width !== props . width || canvas . height !== props . height )) {
console . error ( `Faktiskā export blocked: expected ${ props . width } × ${ props . height } , received ${ canvas . width } × ${ canvas . height } ` );
2026-08-19 17:23:07 +03:00
return ;
}
2026-08-20 22:23:23 +03:00
const filename = productionTemplate ? `faktiska- ${ props . width } x ${ props . height } .png` : `weather- ${ type } .png` ;
2026-08-19 17:23:07 +03:00
canvas . toBlob ( blob => blob && download ( blob , filename ), 'image/png' )
2024-02-08 15:42:36 +02:00
}
2023-08-07 23:25:35 +03:00
return (
2026-08-18 21:44:09 +03:00
< div class = "mapWorkspace" >
2026-08-25 11:21:11 +03:00
< 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 >
2026-08-23 13:31:12 +03:00
< details class = "broadcastControls" open = { ! productionTemplate }>
2026-08-22 14:48:16 +03:00
< 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 >}
2026-08-19 17:11:58 +03:00
{ ! productionTemplate && < div class = "overlayFields" >
2026-08-18 22:14:40 +03:00
< label >< span > Title </ span >< input type = "text" placeholder = "19. APRĪĻA GAISA TEMPERATŪRAS REKORDI" value = { getTitle ()} onInput = { e => setTitle ( e . target . value )} /></ label >
< label >< span > Source </ span >< input type = "text" placeholder = "LVĢMC" value = { getSource ()} onInput = { e => setSource ( e . target . value )} /></ label >
2026-08-19 17:11:58 +03:00
</ div >}
2026-08-18 22:31:56 +03:00
< div class = "mapControlSections" >
< section class = "valueControls" >
2026-08-22 14:48:16 +03:00
< strong >{ productionTemplate ? "Temperatūra un vējš" : "Value and wind settings" }</ strong >
2026-08-19 17:34:04 +03:00
< div class = "windControlRow" >
< label class = "roundControl" >
2025-03-20 22:17:28 +02:00
< input
type = "checkbox"
checked = { windSignals . roundValues [ 0 ]()}
onChange = { e => windSignals . roundValues [ 1 ]( e . target . checked )}
/>
2026-08-22 14:48:16 +03:00
< span >{ productionTemplate ? "Noapaļot temperatūras" : "Round temperatures" }</ span >
2025-03-20 22:17:28 +02:00
</ label >
2026-08-23 13:41:02 +03:00
{ props . showWind && < WindInputs signals = { windSignals } productionTemplate = { productionTemplate } /> }
2025-03-20 22:17:28 +02:00
</ div >
2026-08-18 22:31:56 +03:00
</ section >
{ mode === "faktiska" && < IconInputs cities = { data (). map (([ city ]) => city )} weatherIconsSignal = { weatherIconsSignal } />}
2024-02-11 21:44:29 +02:00
</ div >
2026-08-18 21:44:09 +03:00
</ details >
< div class = "canvasFrame" >
2024-02-06 16:56:31 +02:00
< canvas
ref = { setCanvas }
2026-08-18 21:44:09 +03:00
width = { props . width }
height = { props . height }
2026-08-25 11:21:11 +03:00
style = {{ ... getStyleSize (), cursor : hoveredCity () ? "pointer" : "default" }}
onPointerMove = { onCanvasPointerMove }
onPointerLeave = {() => setHoveredCity ( undefined )}
onClick = { onCanvasClick }
2024-02-06 16:56:31 +02:00
/>
2026-08-25 11:21:11 +03:00
< 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 >
2026-08-18 21:44:09 +03:00
</ div >
</ div >
2023-08-07 23:25:35 +03:00
);
}