import { ResolutionPropsValue, WindProps, windProps } from "./mapConsts"; import { drawRotatedImage, getAngleFromString } from "./windAngles"; import { faktiskaMarkerLayout, FaktiskaStation } from "../../pages/map-graphics/faktiskaConfig"; import { faktiskaTemplates, FaktiskaTemplate } from "../../pages/map-graphics/faktiskaTemplates"; import { getLoadedWeatherIcon } from "../weatherIcons/weatherIconAssets"; // cityName, {x, y}, weatherValue, icon export type CityData = [cityName: string, {x: number, y: number }, weatherValue: number, icon: string | undefined] export function drawOnMap( ctx: CanvasRenderingContext2D, imgArr: [HTMLImageElement, HTMLImageElement], cityData: CityData[], windData: [string, string, string, boolean, WindProps], overlayData: [string, string], props: ResolutionPropsValue, productionTemplate = false, ): void { const [windDirection, windSpeed, windGusts, roundValues, windProp] = windData; const [bgImg, arrowImg] = imgArr; const [title, source] = overlayData; const template = productionTemplate ? faktiskaTemplates[(props.width === 1920 ? "faktiska_1920x1080" : "faktiska_3840x1440") as FaktiskaTemplate] : undefined; // bg ctx.drawImage(bgImg, 0, 0); // City value badges use a consistent height and expand horizontally to fit the value. if (!template) cityData.forEach(([, {x, y}, value]) => { const localX = props.offsetX + x * props.scale; const localY = props.offsetY + y * props.scale; const numericValue = roundValues ? Math.round(value) : value.toFixed(1); const stringValue = numericValue.toString().replace(".", ","); ctx.font = `bold ${props.fontSize}px Monda`; const badgeWidth = getBadgeWidth(ctx, stringValue, props); ctx.fillStyle = "#FFFFFF"; ctx.fillRect(localX - badgeWidth / 2, localY - props.boxSize / 2, badgeWidth, props.boxSize); }); // weather values cityData.forEach(([city, {x, y}, value, icon]) => { const fixedMarker = template?.markers[city as FaktiskaStation]; const localX = fixedMarker?.x ?? props.offsetX + x * props.scale; const localY = fixedMarker?.y ?? props.offsetY + y * props.scale; ctx.font = `bold ${props.fontSize}px Monda`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = "#000000"; const numericValue = roundValues ? Math.round(value) : value.toFixed(1); const stringValue = numericValue.toString().replace(".", ","); ctx.fillText(stringValue, localX, localY); if (icon) { const iconImage = getLoadedWeatherIcon(icon); if (!iconImage) return; const iconSize = template?.iconSize ?? props.boxSize * 1.35; const badgeWidth = template?.badgeSize ?? getBadgeWidth(ctx, stringValue, props); if (productionTemplate) { const layout = faktiskaMarkerLayout[city as FaktiskaStation]; const side = layout?.iconSide ?? "right"; const gap = layout?.iconGap ?? template!.iconGap; const offsetX = layout?.iconOffsetX ?? 0; const offsetY = layout?.iconOffsetY ?? 0; const iconCenterX = side === "right" ? localX + badgeWidth / 2 + gap + iconSize / 2 + offsetX : localX - badgeWidth / 2 - gap - iconSize / 2 + offsetX; drawWeatherIcon(ctx, iconImage, iconCenterX, localY + offsetY, iconSize); } else { drawWeatherIcon(ctx, iconImage, localX + badgeWidth / 2 + 10 + iconSize / 2, localY, iconSize); } } }); if (!template) drawTitleOverlay(ctx, title, source, props.width); if (!props.showWind) return; if (template) { drawFaktiskaWind(ctx, arrowImg, windDirection, windSpeed, windGusts, template); return; } // generic wind values ctx.font = `bold ${props.windFontSize}px Monda`; ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#FFFFFF"; const boxMiddleX = 1675; const directionWidth = ctx.measureText(windDirection).width; ctx.fillText(windDirection, windProp.offsetX + boxMiddleX - directionWidth / 2 + 30, (windProp.offsetY + 370)*windProp.scaleY); const angleInDegrees = getAngleFromString(windDirection); drawRotatedImage(ctx, arrowImg, windProp.offsetX + boxMiddleX - directionWidth / 2 - 30, (windProp.offsetY + 370)*windProp.scaleY, angleInDegrees); const speedWidth = ctx.measureText(windSpeed).width; ctx.fillText(windSpeed, windProp.offsetX + boxMiddleX - speedWidth / 2 - 30, (windProp.offsetY + 575)*windProp.scaleY); ctx.font = "bold 25px Monda"; ctx.fillText("M/S", windProp.offsetX + boxMiddleX + speedWidth / 2 - 22, (windProp.offsetY + 590)*windProp.scaleY); ctx.font = `bold ${props.windFontSize}px Monda`; const gustsWidth = ctx.measureText(windGusts).width; ctx.fillText(windGusts, windProp.offsetX + boxMiddleX - gustsWidth / 2 - 30, (windProp.offsetY + 790)*windProp.scaleY); ctx.font = "bold 25px Monda"; ctx.fillText("M/S", windProp.offsetX + boxMiddleX + gustsWidth / 2 - 22, (windProp.offsetY + 805)*windProp.scaleY); } function drawFaktiskaWind( ctx: CanvasRenderingContext2D, arrowImg: HTMLImageElement, direction: string, speed: string, gusts: string, template: (typeof faktiskaTemplates)[FaktiskaTemplate], ) { const { wind } = template; ctx.fillStyle = "#FFFFFF"; ctx.textBaseline = "middle"; if (direction.trim()) { ctx.font = `bold ${wind.fontSize}px Monda`; const label = direction.trim().toUpperCase(); const labelWidth = ctx.measureText(label).width; const gap = wind.arrowSize * 0.28; const groupWidth = wind.arrowSize + gap + labelWidth; const arrowX = wind.x - groupWidth / 2 + wind.arrowSize / 2; const labelX = wind.x + groupWidth / 2 - labelWidth / 2; drawScaledRotatedImage(ctx, arrowImg, arrowX, wind.directionY, wind.arrowSize, getAngleFromString(label)); ctx.textAlign = "center"; ctx.fillText(label, labelX, wind.directionY); } drawWindMeasurement(ctx, speed, wind.x, wind.speedY, wind.fontSize, wind.unitFontSize); drawWindMeasurement(ctx, gusts, wind.x, wind.gustsY, wind.fontSize, wind.unitFontSize); } function drawScaledRotatedImage(ctx: CanvasRenderingContext2D, image: HTMLImageElement, centerX: number, centerY: number, size: number, angleInDegrees: number) { ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(angleInDegrees * Math.PI / 180); ctx.drawImage(image, -size / 2, -size / 2, size, size); ctx.restore(); } function drawWindMeasurement(ctx: CanvasRenderingContext2D, value: string, x: number, y: number, fontSize: number, unitFontSize: number) { if (!value.trim()) return; const label = value.trim(); ctx.font = `bold ${fontSize}px Monda`; const valueWidth = ctx.measureText(label).width; ctx.font = `bold ${unitFontSize}px Monda`; const unit = "M/S"; const unitWidth = ctx.measureText(unit).width; const gap = unitFontSize * 0.45; const totalWidth = valueWidth + gap + unitWidth; const startX = x - totalWidth / 2; ctx.textAlign = "left"; ctx.font = `bold ${fontSize}px Monda`; ctx.fillText(label, startX, y); ctx.font = `bold ${unitFontSize}px Monda`; ctx.fillText(unit, startX + valueWidth + gap, y + fontSize * 0.12); } function getBadgeWidth(ctx: CanvasRenderingContext2D, value: string, props: ResolutionPropsValue): number { ctx.font = `bold ${props.fontSize}px Monda`; return Math.max(props.boxSize, Math.ceil(ctx.measureText(value).width + props.fontSize * 0.8)); } export function drawWeatherIcon(ctx: CanvasRenderingContext2D, image: HTMLImageElement, centerX: number, centerY: number, size: number): void { ctx.drawImage(image, centerX - size / 2, centerY - size / 2, size, size); } function drawTitleOverlay(ctx: CanvasRenderingContext2D, title: string, source: string, canvasWidth: number): void { if (!title.trim() && !source.trim()) return; const scale = canvasWidth / 1920; // The two broadcast formats use different composition-safe areas. These // insets come from the approved 1920 and 3840 production references. const right = canvasWidth >= 3000 ? 376 : 88; const top = 38 * scale; const horizontalPadding = 24 * scale; const titleHeight = 62 * scale; const sourceHeight = 38 * scale; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.font = `bold ${30 * scale}px Monda`; const titleText = title.trim().toUpperCase(); const titleWidth = ctx.measureText(titleText).width + horizontalPadding * 2; const titleX = canvasWidth - right - titleWidth; ctx.fillStyle = "#9b0008"; ctx.fillRect(titleX, top, titleWidth, titleHeight); ctx.fillStyle = "#FFFFFF"; ctx.fillText(titleText, titleX + horizontalPadding, top + titleHeight / 2); if (source.trim()) { ctx.font = `bold ${19 * scale}px Monda`; const sourceText = source.trim().toUpperCase(); const sourceWidth = ctx.measureText(sourceText).width + horizontalPadding * 2; const sourceX = canvasWidth - right - sourceWidth; ctx.fillStyle = "#FFFFFF"; ctx.fillRect(sourceX, top + titleHeight, sourceWidth, sourceHeight); ctx.fillStyle = "#000000"; ctx.fillText(sourceText, sourceX + horizontalPadding, top + titleHeight + sourceHeight / 2); } }