From 340c370c466466a43c6d68bbd56f6b80c3d356cf Mon Sep 17 00:00:00 2001 From: b0txec Date: Wed, 19 Aug 2026 17:44:46 +0300 Subject: [PATCH] Anchor Faktiska symbols to temperature badges --- docs/PRODUCT_WORKFLOWS.md | 2 ++ web/src/components/map/MapView.tsx | 2 ++ web/src/components/map/canvasDraw.ts | 34 +++++++++++++++++--- web/src/pages/map-graphics/faktiskaConfig.ts | 32 ++++++++++++++++++ 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/docs/PRODUCT_WORKFLOWS.md b/docs/PRODUCT_WORKFLOWS.md index 19ad119..57b8cdf 100644 --- a/docs/PRODUCT_WORKFLOWS.md +++ b/docs/PRODUCT_WORKFLOWS.md @@ -67,6 +67,8 @@ Fixed positions: The weather symbols are not automatically derived from the historical temperature query. They remain editorial/manual inputs until a reliable phenomena-data mapping is designed and validated. +Symbol selection is manual, but placement is automatic. Each Faktiskā position owns a fixed marker layout that attaches the selected font glyph to the actual rendered edge of its temperature badge. Operators do not position symbols for each export; per-station offsets can be refined centrally against the production master. + Valmiera is included in the development seed so all 13 fixed positions can be exercised. Monta is the production typeface; Rubik remains temporary until the authorized Monta font files are supplied. The export dimensions are locked, but final pixel-level placement must still be compared against the production masters after Monta is installed. ## Latvia overview diff --git a/web/src/components/map/MapView.tsx b/web/src/components/map/MapView.tsx index 1712184..c2f1611 100644 --- a/web/src/components/map/MapView.tsx +++ b/web/src/components/map/MapView.tsx @@ -53,6 +53,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[ getWindData(), [getTitle(), getSource()], props, + productionTemplate, ); }; img.src = props.map; @@ -76,6 +77,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[ getWindData(), [getTitle(), getSource()], props, + productionTemplate, ); }); diff --git a/web/src/components/map/canvasDraw.ts b/web/src/components/map/canvasDraw.ts index f2d16f0..299844b 100644 --- a/web/src/components/map/canvasDraw.ts +++ b/web/src/components/map/canvasDraw.ts @@ -1,5 +1,6 @@ import { ResolutionPropsValue, WindProps, windProps } from "./mapConsts"; import { drawRotatedImage, getAngleFromString } from "./windAngles"; +import { faktiskaMarkerLayout, FaktiskaStation } from "../../pages/map-graphics/faktiskaConfig"; // cityName, {x, y}, weatherValue, icon export type CityData = [cityName: string, {x: number, y: number }, weatherValue: number, icon: string | undefined] @@ -11,6 +12,7 @@ export function drawOnMap( 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; @@ -27,13 +29,13 @@ export function drawOnMap( const stringValue = numericValue.toString().replace(".", ","); ctx.font = `bold ${props.fontSize}px Rubik`; - const badgeWidth = Math.max(props.boxSize, Math.ceil(ctx.measureText(stringValue).width + props.fontSize * 0.8)); + 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(([, {x, y}, value, icon]) => { + cityData.forEach(([city, {x, y}, value, icon]) => { const localX = props.offsetX + x * props.scale; const localY = props.offsetY + y * props.scale; @@ -47,9 +49,24 @@ export function drawOnMap( if (icon) { ctx.fillStyle = "#FFFFFF"; - ctx.font = `normal ${props.fontSize * 5}px Daira by LTV grafika`; - const iconSize = ctx.measureText(icon); - drawOpticallyCenteredGlyph(ctx, icon, localX + props.boxSize/2 + 10 + iconSize.width/2, localY); + ctx.font = `normal ${props.fontSize * (productionTemplate ? 4.2 : 5)}px Daira by LTV grafika`; + const iconMetrics = ctx.measureText(icon); + const glyphWidth = iconMetrics.actualBoundingBoxLeft + iconMetrics.actualBoundingBoxRight; + const badgeWidth = getBadgeWidth(ctx, stringValue, props, true); + + if (productionTemplate) { + const layout = faktiskaMarkerLayout[city as FaktiskaStation]; + const side = layout?.iconSide ?? "right"; + const gap = layout?.iconGap ?? 22; + const offsetX = layout?.iconOffsetX ?? 0; + const offsetY = layout?.iconOffsetY ?? 0; + const iconCenterX = side === "right" + ? localX + badgeWidth / 2 + gap + glyphWidth / 2 + offsetX + : localX - badgeWidth / 2 - gap - glyphWidth / 2 + offsetX; + drawOpticallyCenteredGlyph(ctx, icon, iconCenterX, localY + offsetY); + } else { + drawOpticallyCenteredGlyph(ctx, icon, localX + props.boxSize / 2 + 10 + iconMetrics.width / 2, localY); + } } }); @@ -81,6 +98,13 @@ export function drawOnMap( ctx.fillText("M/S", windProp.offsetX + boxMiddleX + gustsWidth / 2 - 22, (windProp.offsetY + 805)*windProp.scaleY); } +function getBadgeWidth(ctx: CanvasRenderingContext2D, value: string, props: ResolutionPropsValue, restoreValueFont = false): number { + if (restoreValueFont) ctx.font = `bold ${props.fontSize}px Rubik`; + const width = Math.max(props.boxSize, Math.ceil(ctx.measureText(value).width + props.fontSize * 0.8)); + if (restoreValueFont) ctx.font = `normal ${props.fontSize * 4.2}px Daira by LTV grafika`; + return width; +} + function drawOpticallyCenteredGlyph(ctx: CanvasRenderingContext2D, glyph: string, centerX: number, centerY: number): void { const metrics = ctx.measureText(glyph); ctx.textAlign = "left"; diff --git a/web/src/pages/map-graphics/faktiskaConfig.ts b/web/src/pages/map-graphics/faktiskaConfig.ts index 6006316..283d522 100644 --- a/web/src/pages/map-graphics/faktiskaConfig.ts +++ b/web/src/pages/map-graphics/faktiskaConfig.ts @@ -5,6 +5,38 @@ export const faktiskaStations = [ export type FaktiskaStation = typeof faktiskaStations[number]; +export type FaktiskaMarkerLayout = { + iconSide: "left" | "right"; + iconGap: number; + iconOffsetX: number; + iconOffsetY: number; +}; + +const rightMarker = (): FaktiskaMarkerLayout => ({ + iconSide: "right", + iconGap: 22, + iconOffsetX: 0, + iconOffsetY: 0, +}); + +// These placements are part of the fixed newsroom template. Operators select +// the glyph; the renderer owns its position relative to the temperature badge. +export const faktiskaMarkerLayout: Record = { + "Liepāja": rightMarker(), + "Ventspils": rightMarker(), + "Stende": rightMarker(), + "Saldus": rightMarker(), + "Rīga": rightMarker(), + "Jelgava": rightMarker(), + "Ainaži": rightMarker(), + "Valmiera": rightMarker(), + "Madona": rightMarker(), + "Alūksne": rightMarker(), + "Zīlāni": rightMarker(), + "Daugavpils": rightMarker(), + "Rēzekne": rightMarker(), +}; + export const faktiskaExport = { width: 3840, height: 1440,