Anchor Faktiska symbols to temperature badges

This commit is contained in:
b0txec
2026-08-19 17:44:46 +03:00
parent 632f377c2d
commit 340c370c46
4 changed files with 65 additions and 5 deletions
+2
View File
@@ -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. 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. 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 ## Latvia overview
+2
View File
@@ -53,6 +53,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
getWindData(), getWindData(),
[getTitle(), getSource()], [getTitle(), getSource()],
props, props,
productionTemplate,
); );
}; };
img.src = props.map; img.src = props.map;
@@ -76,6 +77,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
getWindData(), getWindData(),
[getTitle(), getSource()], [getTitle(), getSource()],
props, props,
productionTemplate,
); );
}); });
+29 -5
View File
@@ -1,5 +1,6 @@
import { ResolutionPropsValue, WindProps, windProps } from "./mapConsts"; import { ResolutionPropsValue, WindProps, windProps } from "./mapConsts";
import { drawRotatedImage, getAngleFromString } from "./windAngles"; import { drawRotatedImage, getAngleFromString } from "./windAngles";
import { faktiskaMarkerLayout, FaktiskaStation } from "../../pages/map-graphics/faktiskaConfig";
// cityName, {x, y}, weatherValue, icon // cityName, {x, y}, weatherValue, icon
export type CityData = [cityName: string, {x: number, y: number }, weatherValue: number, icon: string | undefined] 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], windData: [string, string, string, boolean, WindProps],
overlayData: [string, string], overlayData: [string, string],
props: ResolutionPropsValue, props: ResolutionPropsValue,
productionTemplate = false,
): void { ): void {
const [windDirection, windSpeed, windGusts, roundValues, windProp] = windData; const [windDirection, windSpeed, windGusts, roundValues, windProp] = windData;
const [bgImg, arrowImg] = imgArr; const [bgImg, arrowImg] = imgArr;
@@ -27,13 +29,13 @@ export function drawOnMap(
const stringValue = numericValue.toString().replace(".", ","); const stringValue = numericValue.toString().replace(".", ",");
ctx.font = `bold ${props.fontSize}px Rubik`; 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.fillStyle = "#FFFFFF";
ctx.fillRect(localX - badgeWidth / 2, localY - props.boxSize / 2, badgeWidth, props.boxSize); ctx.fillRect(localX - badgeWidth / 2, localY - props.boxSize / 2, badgeWidth, props.boxSize);
}); });
// weather values // weather values
cityData.forEach(([, {x, y}, value, icon]) => { cityData.forEach(([city, {x, y}, value, icon]) => {
const localX = props.offsetX + x * props.scale; const localX = props.offsetX + x * props.scale;
const localY = props.offsetY + y * props.scale; const localY = props.offsetY + y * props.scale;
@@ -47,9 +49,24 @@ export function drawOnMap(
if (icon) { if (icon) {
ctx.fillStyle = "#FFFFFF"; ctx.fillStyle = "#FFFFFF";
ctx.font = `normal ${props.fontSize * 5}px Daira by LTV grafika`; ctx.font = `normal ${props.fontSize * (productionTemplate ? 4.2 : 5)}px Daira by LTV grafika`;
const iconSize = ctx.measureText(icon); const iconMetrics = ctx.measureText(icon);
drawOpticallyCenteredGlyph(ctx, icon, localX + props.boxSize/2 + 10 + iconSize.width/2, localY); 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); 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 { function drawOpticallyCenteredGlyph(ctx: CanvasRenderingContext2D, glyph: string, centerX: number, centerY: number): void {
const metrics = ctx.measureText(glyph); const metrics = ctx.measureText(glyph);
ctx.textAlign = "left"; ctx.textAlign = "left";
@@ -5,6 +5,38 @@ export const faktiskaStations = [
export type FaktiskaStation = typeof faktiskaStations[number]; 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<FaktiskaStation, FaktiskaMarkerLayout> = {
"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 = { export const faktiskaExport = {
width: 3840, width: 3840,
height: 1440, height: 1440,