/** * Alert client configuration composable * * Provides configuration data including geometries, colors, and warning icons. * This is a large static configuration that includes region paths and warning icons. */ import svgpath from 'svgpath' import type { ThemeColorMap, WarningType, WarningIcon, WarningIconInput, RegionType, GeometryCollection, } from '@/types' // Import geometry data from JSON file import geometryData from '@/data/geometries.json' export interface BboxGeometry { type: string coordinates: number[][][] } export interface BboxData { type: string id: string geometry: { type: string geometries: BboxGeometry[] } geometry_name: string properties: Record } // Constants used across the application export const NUMBER_OF_DAYS = 5 export const REGION_LAND = 'land' export const REGION_SEA = 'sea' export const REGION_LAKE = 'lake' // API field names export const WEATHER_UPDATE_TIME = 'weather_update_time' export const FLOOD_UPDATE_TIME = 'flood_update_time' export const UPDATE_TIME = 'update_time' export const WEATHER_WARNINGS = 'weather_finland_active_all' export const FLOOD_WARNINGS = 'flood_finland_active_all' // Warning property keys export const INFO_FI = 'info_fi' export const INFO_SV = 'info_sv' export const INFO_EN = 'info_en' export const PHYSICAL_DIRECTION = 'physical_direction' export const PHYSICAL_VALUE = 'physical_value' export const EFFECTIVE_FROM = 'effective_from' export const EFFECTIVE_UNTIL = 'effective_until' export const ONSET = 'onset' export const EXPIRES = 'expires' export const WARNING_CONTEXT = 'warning_context' export const SEVERITY = 'severity' export const CONTEXT_EXTENSION = 'context_extension' // Warning types export const WIND = 'wind' export const SEA_WIND = 'sea-wind' export const FLOOD_LEVEL_TYPE = 'floodLevel' export const MULTIPLE = 'multiple' // Warning levels export const WARNING_LEVELS = [ 'level-1', 'level-2', 'level-3', 'level-4', ] as const export type WarningLevelType = (typeof WARNING_LEVELS)[number] // Flood severity mapping export const FLOOD_LEVELS: Record = { minor: 1, moderate: 2, severe: 3, extreme: 4, } export interface ConfigData { defaultGeometryId: string timeZone: string dateTimeFormatLocale: string panLimits: { x: number; y: number } coverageCriterion: number maxMergedWeight: number maxUpdateDelay: Record warningTypes: Map regionIds: string[] geometries: GeometryCollection colors: ThemeColorMap bbox: BboxData } export interface UseConfigReturn extends ConfigData { /** * Build the inline SVG fragment for a warning icon. * * The optional `instanceKey` is used to prefix the `id` attributes * that live inside icons which internally reference themselves (sea * water height high/low — masks and ). When a map contains * several of those icons each one must get a unique prefix, otherwise * the browser's SVG *image decoder* refuses to load the document via * a Blob URL because of duplicate `id` values. Inline rendering is * forgiving, image-element loading is not. */ warningIcon: ( warning: WarningIconInput, instanceKey?: string ) => WarningIcon } // Type assertion for imported JSON data const geometriesData = geometryData.geometries as unknown as GeometryCollection const bboxData = geometryData.bbox as BboxData const colorsData: ThemeColorMap = { 'light-theme': { sea: '#bee2f1', levels: ['#6af86a', '#6af86a', '#f8f800', '#f9bf2e', '#ff0909'], missing: '#f8f8f8', stroke: '#000000', }, 'dark-theme': { sea: '#bee2f1', levels: ['#6af86a', '#6af86a', '#f8f800', '#f9bf2e', '#ff0909'], missing: '#f8f8f8', stroke: '#282828', }, 'light-gray-theme': { sea: '#ffffff', levels: ['#ffffff', '#ffffff', '#aaaaaa', '#555555', '#000000'], missing: '#f8f8f8', stroke: '#666666', }, 'dark-gray-theme': { sea: '#ffffff', levels: ['#ffffff', '#ffffff', '#aaaaaa', '#555555', '#000000'], missing: '#f8f8f8', stroke: '#808080', }, } const warningTypesMap = new Map([ ['thunderStorm', 'land'], ['wind', 'land'], ['rain', 'land'], ['trafficWeather', 'land'], ['pedestrianSafety', 'land'], ['forestFireWeather', 'land'], ['grassFireWeather', 'land'], ['hotWeather', 'land'], ['coldWeather', 'land'], ['uvNote', 'land'], ['floodLevel', 'land'], ['seaWind', 'sea'], ['seaThunderStorm', 'sea'], ['seaWaterHeightHighWater', 'sea'], ['seaWaterHeightShallowWater', 'sea'], ['seaWaveHeight', 'sea'], ['seaIcing', 'sea'], ]) const regionIdsData = [ 'sea_region.B5E', 'sea_region.B5W', 'sea_region.B7E', 'sea_region.B7W', 'sea_region.B4W', 'sea_region.B4E', 'sea_region.B3S', 'sea_region.B3N', 'sea_region.B2', 'sea_region.B1S', 'sea_region.B1N', 'county.21', 'county.2', 'county.4', 'county.1', 'county.5', 'county.7', 'county.8', 'county.9', 'county.6', 'county.13', 'county.10', 'county.11', 'county.12', 'county.14', 'county.15', 'county.16', 'county.17', 'county.18', 'county.19', 'municipality_139_208_244_317_425_436_483_494_535_563_564_625_626_630_678_69_691_71_72_746_748_785_791_859_889_9.977', 'municipality.615', 'municipality.832', 'municipality.305', 'municipality.751', 'municipality.683', 'municipality.614', 'municipality.240', 'municipality.241', 'municipality.851', 'municipality.976', 'municipality.845', 'municipality.854', 'municipality.698', 'municipality.320', 'municipality.732', 'municipality.273', 'municipality.498', 'municipality.261', 'municipality.758', 'municipality.583', 'municipality.742', 'municipality.47', 'municipality.148', 'municipality.890', 'sea_region_south.FI-115978', 'sea_region_north.FI-115978', ] /** * Sanitise an arbitrary caller-provided key into a safe SVG `id` * fragment: only letters, digits, `_` and `-` survive. Needed for the * `mask`/``-bearing icons whose id attributes are referenced from * `url(#…)` and `href="#…"` and therefore must be unique across the * entire outer SVG document. */ function safeId(key: string): string { return key.replace(/[^a-zA-Z0-9_-]/g, '_') } /** * Generate SVG geometry for a warning icon based on type and severity. * * `instanceKey` is threaded through to the few icons that internally * use `` / `` — see {@link getHighSeaLevelIcon} and * {@link getLowSeaLevelIcon}. Icons that do not carry internal * references simply ignore the parameter. */ function warningIcon( warning: WarningIconInput, instanceKey: string = 'default' ): WarningIcon { const icon: WarningIcon = { aspectRatio: [1, 1], scale: 1, } switch (warning.type) { case 'thunderStorm': icon.aspectRatio = [14, 22] icon.geom = ` thunder-symbol ` break case 'wind': icon.aspectRatio = [16, 16] icon.scale = 0.9 icon.geom = ` wind-symbol }` break case 'rain': icon.aspectRatio = [20, 17] icon.scale = 0.8 icon.geom = ` rain-symbol ` break case 'trafficWeather': icon.aspectRatio = [23, 23] icon.geom = ` traffic-symbol ` break case 'pedestrianSafety': icon.aspectRatio = [21, 21] icon.geom = ` pedestrian-symbol ` break case 'forestFireWeather': case 'grassFireWeather': icon.aspectRatio = [19, 22] icon.geom = ` grassfire-symbol ` break case 'hotWeather': icon.aspectRatio = [16, 30] icon.scale = 1.2 icon.geom = ` heat-symbol ` break case 'coldWeather': icon.aspectRatio = [16, 30] icon.scale = 1.2 icon.geom = ` freeze-symbol ` break case 'uvNote': icon.aspectRatio = [23, 12] icon.scale = 0.55 icon.geom = ` ultraviolet-symbol ` break case 'floodLevel': icon.aspectRatio = warning.severity < 3 ? [24, 21] : [24, 20] if (warning.severity === 3) { icon.geom = getFloodLevel3Icon() } else if (warning.severity === 4) { icon.geom = getFloodLevel4Icon() } else { icon.geom = getFloodLevel2Icon() } break case 'seaWind': { const dx = -1.5 * Math.sin(((warning.direction ?? 0) * Math.PI) / 180) icon.aspectRatio = [22, 22] icon.scale = 1.5 icon.geom = ` seawind-symbol ${ warning.text ?? '' } ` break } case 'seaThunderStorm': icon.aspectRatio = [14, 22] icon.geom = ` seathunder-symbol ` break case 'seaWaterHeightHighWater': icon.aspectRatio = [25, 22] icon.geom = getHighSeaLevelIcon(instanceKey) break case 'seaWaterHeightShallowWater': icon.aspectRatio = [23, 22] icon.geom = getLowSeaLevelIcon(instanceKey) break case 'seaWaveHeight': icon.aspectRatio = [21, 16] icon.scale = 0.8 icon.geom = getWaveHeightIcon() break case 'seaIcing': icon.aspectRatio = [22, 17] icon.scale = 0.9 icon.geom = getIcingIcon() break case 'multiple': icon.aspectRatio = [15, 13] icon.scale = 0.65 icon.geom = getMultipleIcon() break default: break } return icon } // Helper functions for complex icons function getFloodLevel2Icon(): string { return ` flood-level-2 ` } function getFloodLevel3Icon(): string { return ` flood-level-3 ` } function getFloodLevel4Icon(): string { return ` flood-level-4 ` } /** * High sea level warning icon. Its three wavy paths are stencilled * through `` + `` references to `` shapes in * ``, so those id attributes must be unique across the full * outer SVG document. `instanceKey` becomes the shared prefix. */ function getHighSeaLevelIcon(instanceKey: string): string { const p = `hsl-${safeId(instanceKey)}` return ` highsealevel-symbol ` } /** * Low sea level warning icon. Same mask/use pattern as the high-sea * one above; `instanceKey` is used as the shared id prefix. */ function getLowSeaLevelIcon(instanceKey: string): string { const p = `lsl-${safeId(instanceKey)}` return ` lowsealevel-symbol ` } function getWaveHeightIcon(): string { return ` waveheight-symbol ` } function getIcingIcon(): string { return ` icing-symbol ` } function getMultipleIcon(): string { return ` multiple-symbol ` } /** * Get alert client configuration * * Note: This composable exports large static geometry data. * The geometry data is imported from geometries.json. */ export function useConfig(): UseConfigReturn { return { defaultGeometryId: '2021', timeZone: 'Europe/Helsinki', dateTimeFormatLocale: 'fi-FI', panLimits: { x: 175, y: 275 }, coverageCriterion: 0.2, maxMergedWeight: 7, maxUpdateDelay: { weather_update_time: 12 * 60 * 60 * 1000, flood_update_time: 12 * 60 * 60 * 1000, }, warningTypes: warningTypesMap, regionIds: regionIdsData, geometries: geometriesData, colors: colorsData, bbox: bboxData, warningIcon, } }