@fmidev/smartmet-alert-client 4.4.19 → 4.7.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.eslintignore +2 -14
  2. package/.github/workflows/test.yaml +26 -0
  3. package/.nvmrc +1 -0
  4. package/AGENTS.md +26 -0
  5. package/index.html +1 -1
  6. package/package.json +80 -22
  7. package/src/AlertClientVue.vue +160 -0
  8. package/src/App.vue +154 -296
  9. package/src/assets/img/ui/arrow-down.svg +4 -11
  10. package/src/assets/img/ui/arrow-up.svg +4 -11
  11. package/src/assets/img/ui/clear.svg +7 -21
  12. package/src/assets/img/ui/close.svg +4 -15
  13. package/src/assets/img/ui/toggle-selected.svg +5 -6
  14. package/src/assets/img/ui/toggle-unselected.svg +5 -6
  15. package/src/assets/img/warning/cold-weather.svg +3 -6
  16. package/src/assets/img/warning/flood-level-3.svg +4 -7
  17. package/src/assets/img/warning/forest-fire-weather.svg +2 -6
  18. package/src/assets/img/warning/grass-fire-weather.svg +2 -6
  19. package/src/assets/img/warning/hot-weather.svg +3 -6
  20. package/src/assets/img/warning/pedestrian-safety.svg +3 -7
  21. package/src/assets/img/warning/rain.svg +2 -7
  22. package/src/assets/img/warning/sea-icing.svg +2 -6
  23. package/src/assets/img/warning/sea-thunder-storm.svg +2 -5
  24. package/src/assets/img/warning/sea-water-height-high-water.svg +3 -8
  25. package/src/assets/img/warning/sea-water-height-shallow-water.svg +3 -7
  26. package/src/assets/img/warning/sea-wave-height.svg +4 -7
  27. package/src/assets/img/warning/sea-wind-legend.svg +2 -5
  28. package/src/assets/img/warning/sea-wind.svg +2 -5
  29. package/src/assets/img/warning/several.svg +2 -5
  30. package/src/assets/img/warning/thunder-storm.svg +2 -5
  31. package/src/assets/img/warning/traffic-weather.svg +2 -6
  32. package/src/assets/img/warning/uv-note.svg +2 -6
  33. package/src/assets/img/warning/wind.svg +2 -5
  34. package/src/components/AlertClient.vue +330 -251
  35. package/src/components/CollapsiblePanel.vue +281 -0
  36. package/src/components/DayLarge.vue +146 -110
  37. package/src/components/DaySmall.vue +97 -81
  38. package/src/components/Days.vue +229 -159
  39. package/src/components/DescriptionWarning.vue +63 -38
  40. package/src/components/GrayScaleToggle.vue +58 -54
  41. package/src/components/Legend.vue +102 -325
  42. package/src/components/MapLarge.vue +574 -351
  43. package/src/components/MapSmall.vue +137 -122
  44. package/src/components/PopupRow.vue +24 -12
  45. package/src/components/Region.vue +168 -118
  46. package/src/components/RegionWarning.vue +40 -33
  47. package/src/components/Regions.vue +189 -105
  48. package/src/components/Warning.vue +70 -45
  49. package/src/components/Warnings.vue +136 -72
  50. package/src/composables/useAlertClient.ts +360 -0
  51. package/src/composables/useConfig.ts +573 -0
  52. package/src/composables/useFields.ts +66 -0
  53. package/src/composables/useI18n.ts +62 -0
  54. package/src/composables/useKeyCodes.ts +16 -0
  55. package/src/composables/useMapPaths.ts +477 -0
  56. package/src/composables/useUtils.ts +683 -0
  57. package/src/composables/useWarningsProcessor.ts +1007 -0
  58. package/src/data/geometries.json +993 -0
  59. package/src/{main.js → main.ts} +1 -0
  60. package/src/mixins/geojsonsvg.d.ts +57 -0
  61. package/src/mixins/geojsonsvg.js +5 -3
  62. package/src/plugins/index.ts +5 -0
  63. package/src/scss/_utilities.scss +193 -0
  64. package/src/scss/constants.scss +2 -1
  65. package/src/scss/warningImages.scss +8 -3
  66. package/src/types/index.ts +509 -0
  67. package/src/vite-env.d.ts +23 -0
  68. package/src/vue.ts +41 -0
  69. package/svgo.config.js +45 -0
  70. package/tests/README.md +430 -0
  71. package/tests/fixtures/mockWarningData.ts +152 -0
  72. package/tests/integration/warning-flow.spec.ts +445 -0
  73. package/tests/setup.ts +41 -0
  74. package/tests/unit/components/AlertClient.spec.ts +701 -0
  75. package/tests/unit/components/DayLarge.spec.ts +348 -0
  76. package/tests/unit/components/DaySmall.spec.ts +352 -0
  77. package/tests/unit/components/Days.spec.ts +548 -0
  78. package/tests/unit/components/DescriptionWarning.spec.ts +385 -0
  79. package/tests/unit/components/GrayScaleToggle.spec.ts +318 -0
  80. package/tests/unit/components/Legend.spec.ts +295 -0
  81. package/tests/unit/components/MapLarge.spec.ts +448 -0
  82. package/tests/unit/components/MapSmall.spec.ts +367 -0
  83. package/tests/unit/components/PopupRow.spec.ts +270 -0
  84. package/tests/unit/components/Region.spec.ts +373 -0
  85. package/tests/unit/components/RegionWarning.snapshot.spec.ts +361 -0
  86. package/tests/unit/components/RegionWarning.spec.ts +381 -0
  87. package/tests/unit/components/Regions.spec.ts +503 -0
  88. package/tests/unit/components/Warning.snapshot.spec.ts +483 -0
  89. package/tests/unit/components/Warning.spec.ts +489 -0
  90. package/tests/unit/components/Warnings.spec.ts +343 -0
  91. package/tests/unit/components/__snapshots__/RegionWarning.snapshot.spec.ts.snap +41 -0
  92. package/tests/unit/components/__snapshots__/Warning.snapshot.spec.ts.snap +433 -0
  93. package/tests/unit/composables/useConfig.spec.ts +279 -0
  94. package/tests/unit/composables/useI18n.spec.ts +116 -0
  95. package/tests/unit/composables/useKeyCodes.spec.ts +27 -0
  96. package/tests/unit/composables/useUtils.spec.ts +213 -0
  97. package/tsconfig.json +43 -0
  98. package/tsconfig.node.json +11 -0
  99. package/vite.config.js +96 -26
  100. package/vitest.config.js +40 -0
  101. package/dist/favicon.ico +0 -0
  102. package/dist/index.dark.html +0 -20
  103. package/dist/index.en.html +0 -15
  104. package/dist/index.fi.html +0 -15
  105. package/dist/index.html +0 -15
  106. package/dist/index.js +0 -281
  107. package/dist/index.mjs +0 -281
  108. package/dist/index.mjs.map +0 -1
  109. package/dist/index.relative.html +0 -19
  110. package/dist/index.start.html +0 -20
  111. package/dist/index.sv.html +0 -15
  112. package/playwright.config.ts +0 -18
  113. package/public/index.relative.html +0 -19
  114. package/public/index.start.html +0 -20
  115. package/src/mixins/config.js +0 -1378
  116. package/src/mixins/fields.js +0 -26
  117. package/src/mixins/i18n.js +0 -25
  118. package/src/mixins/keycodes.js +0 -10
  119. package/src/mixins/panzoom.js +0 -900
  120. package/src/mixins/utils.js +0 -900
  121. package/src/plugins/index.js +0 -3
  122. package/test/snapshot.test.ts +0 -126
  123. package/vitest.config.ts +0 -6
@@ -0,0 +1,683 @@
1
+ /**
2
+ * Utility functions and constants composable
3
+ *
4
+ * Provides helper functions for warning data processing, time handling,
5
+ * and region visualization.
6
+ */
7
+
8
+ import { computed, type Ref, type ComputedRef } from 'vue'
9
+ import { DOMParser } from '@xmldom/xmldom'
10
+ import he from 'he'
11
+ import xpath from 'xpath'
12
+ import type {
13
+ Warning,
14
+ WarningsMap,
15
+ Day,
16
+ LegendItem,
17
+ RegionsData,
18
+ RegionType,
19
+ Severity,
20
+ GeometryCollection,
21
+ ThemeColorMap,
22
+ LocalizedText,
23
+ } from '@/types'
24
+
25
+ // ============================================================================
26
+ // Constants
27
+ // ============================================================================
28
+
29
+ export const NUMBER_OF_DAYS = 5
30
+ export const REGION_LAND = 'land'
31
+ export const REGION_SEA = 'sea'
32
+ export const REGION_LAKE = 'lake'
33
+
34
+ // Property keys
35
+ export const WEATHER_UPDATE_TIME = 'weather_update_time'
36
+ export const FLOOD_UPDATE_TIME = 'flood_update_time'
37
+ export const UPDATE_TIME = 'update_time'
38
+ export const WEATHER_WARNINGS = 'weather_finland_active_all'
39
+ export const FLOOD_WARNINGS = 'flood_finland_active_all'
40
+ export const INFO_FI = 'info_fi'
41
+ export const INFO_SV = 'info_sv'
42
+ export const INFO_EN = 'info_en'
43
+ export const PHYSICAL_DIRECTION = 'physical_direction'
44
+ export const PHYSICAL_VALUE = 'physical_value'
45
+ export const EFFECTIVE_FROM = 'effective_from'
46
+ export const EFFECTIVE_UNTIL = 'effective_until'
47
+ export const ONSET = 'onset'
48
+ export const EXPIRES = 'expires'
49
+ export const WARNING_CONTEXT = 'warning_context'
50
+ export const SEVERITY = 'severity'
51
+ export const CONTEXT_EXTENSION = 'context_extension'
52
+ export const WIND = 'wind'
53
+ export const SEA_WIND = 'sea-wind'
54
+ export const FLOOD_LEVEL_TYPE = 'floodLevel'
55
+ export const MULTIPLE = 'multiple'
56
+
57
+ export const WARNING_LEVELS = ['level-1', 'level-2', 'level-3', 'level-4']
58
+ export const FLOOD_LEVELS: Record<string, number> = {
59
+ minor: 1,
60
+ moderate: 2,
61
+ severe: 3,
62
+ extreme: 4,
63
+ }
64
+
65
+ // ============================================================================
66
+ // Pure Utility Functions
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Uncapitalize first letter of a string
71
+ */
72
+ export function uncapitalize(value: string | null | undefined): string {
73
+ if (!value) return ''
74
+ const stringValue = value.toString()
75
+ return stringValue.charAt(0).toLowerCase() + stringValue.slice(1)
76
+ }
77
+
78
+ /**
79
+ * Format number with leading zero if needed
80
+ */
81
+ export function twoDigits(value: number): string {
82
+ return `0${value}`.slice(-2)
83
+ }
84
+
85
+ /**
86
+ * Check if running in browser environment
87
+ */
88
+ export function isClientSide(): boolean {
89
+ return typeof document !== 'undefined' && !!document
90
+ }
91
+
92
+ /**
93
+ * Extract warning type from properties
94
+ */
95
+ export function warningType(properties: Record<string, unknown>): string {
96
+ return uncapitalize(
97
+ (
98
+ (properties[WARNING_CONTEXT] as string) +
99
+ (properties[CONTEXT_EXTENSION] ? `-${properties[CONTEXT_EXTENSION]}` : '')
100
+ )
101
+ .split('-')
102
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
103
+ .join('')
104
+ )
105
+ }
106
+
107
+ /**
108
+ * Extract region ID from reference URL
109
+ */
110
+ export function regionFromReference(reference: string): string {
111
+ return reference
112
+ .split(',')
113
+ .map((url) => {
114
+ let subUrl = url.substring(url.lastIndexOf('#') + 1)
115
+ // Saimaa special case
116
+ if (subUrl.indexOf('.') !== subUrl.lastIndexOf('.')) {
117
+ subUrl = subUrl.replace('.', '_')
118
+ }
119
+ return subUrl
120
+ })
121
+ .reduce((regionId, rawId, index, array) => {
122
+ const parts = rawId.split('.')
123
+ if (index === 0) {
124
+ regionId += parts[0] ?? ''
125
+ }
126
+ return (
127
+ regionId + (index === array.length - 1 ? '.' : '_') + (parts[1] ?? '')
128
+ )
129
+ }, '')
130
+ }
131
+
132
+ /**
133
+ * Extract relative coverage from reference URL
134
+ */
135
+ export function relativeCoverageFromReference(
136
+ reference: string | null | undefined
137
+ ): number {
138
+ if (reference == null) {
139
+ return 0
140
+ }
141
+ const urlSplit = reference.split('?')
142
+ if (urlSplit.length <= 1) {
143
+ return 0
144
+ }
145
+ const paramString = (urlSplit[1] ?? '').split('#')[0] ?? ''
146
+ const searchParams = new URLSearchParams(paramString)
147
+ const relativeCoverage = searchParams.get('c')
148
+ if (relativeCoverage == null) {
149
+ return 0
150
+ }
151
+ return Number(relativeCoverage)
152
+ }
153
+
154
+ /**
155
+ * Get warning text based on properties
156
+ */
157
+ export function getWarningText(properties: Record<string, unknown>): string {
158
+ return properties[WARNING_CONTEXT] === SEA_WIND
159
+ ? String(properties[PHYSICAL_VALUE] ?? '')
160
+ : ''
161
+ }
162
+
163
+ /**
164
+ * Create default regions structure
165
+ */
166
+ export function regionsDefault(): RegionsData {
167
+ return Array.from({ length: NUMBER_OF_DAYS }, () => ({
168
+ land: [],
169
+ sea: [],
170
+ }))
171
+ }
172
+
173
+ // ============================================================================
174
+ // Date/Time Formatting Interface
175
+ // ============================================================================
176
+
177
+ export interface TimeZoneMoment {
178
+ year: number
179
+ month: number
180
+ day: number
181
+ weekday: string
182
+ hour: number
183
+ minute: number
184
+ second: number
185
+ millisecond: number
186
+ timeZoneName?: string
187
+ timeZone?: string
188
+ }
189
+
190
+ /**
191
+ * Convert DateTimeFormat parts to whole object
192
+ */
193
+ export function partsToWhole(parts: Intl.DateTimeFormatPart[]): TimeZoneMoment {
194
+ const whole: TimeZoneMoment = {
195
+ year: 0,
196
+ month: 0,
197
+ day: 0,
198
+ weekday: '',
199
+ hour: 0,
200
+ minute: 0,
201
+ second: 0,
202
+ millisecond: 0,
203
+ }
204
+
205
+ parts.forEach((part) => {
206
+ const val: string = part.value
207
+ const partType = part.type as string
208
+ switch (partType) {
209
+ case 'literal':
210
+ return
211
+ case 'timeZoneName':
212
+ whole.timeZoneName = val
213
+ break
214
+ case 'month':
215
+ whole.month = parseInt(val, 10)
216
+ break
217
+ case 'weekday':
218
+ whole.weekday = val
219
+ break
220
+ case 'hour':
221
+ whole.hour = parseInt(val, 10) % 24
222
+ break
223
+ case 'fractionalSecond':
224
+ whole.millisecond = parseInt(val, 10)
225
+ return
226
+ case 'year':
227
+ whole.year = parseInt(val, 10)
228
+ break
229
+ case 'day':
230
+ whole.day = parseInt(val, 10)
231
+ break
232
+ case 'second':
233
+ whole.second = parseInt(val, 10)
234
+ break
235
+ case 'minute':
236
+ whole.minute = parseInt(val, 10)
237
+ break
238
+ default:
239
+ break
240
+ }
241
+ })
242
+
243
+ return whole
244
+ }
245
+
246
+ /**
247
+ * Convert date to timezone-specific moment
248
+ */
249
+ export function toTimeZone(
250
+ date: Date | number | string,
251
+ timeZone: string,
252
+ locale: string
253
+ ): TimeZoneMoment {
254
+ const dateObj = new Date(date)
255
+ const parts = new Intl.DateTimeFormat(locale, {
256
+ timeZoneName: 'short',
257
+ timeZone,
258
+ year: 'numeric',
259
+ month: 'numeric',
260
+ day: 'numeric',
261
+ weekday: 'short',
262
+ hour12: false,
263
+ hour: 'numeric',
264
+ minute: 'numeric',
265
+ second: 'numeric',
266
+ fractionalSecondDigits: 3,
267
+ } as Intl.DateTimeFormatOptions).formatToParts(dateObj)
268
+
269
+ const whole = partsToWhole(parts)
270
+ whole.timeZone = timeZone
271
+ return whole
272
+ }
273
+
274
+ /**
275
+ * Format valid interval string
276
+ */
277
+ export function validInterval(
278
+ start: string,
279
+ end: string,
280
+ timeZone: string,
281
+ locale: string
282
+ ): string {
283
+ return [
284
+ toTimeZone(start, timeZone, locale),
285
+ toTimeZone(end, timeZone, locale),
286
+ ]
287
+ .map(
288
+ (moment) =>
289
+ `${moment.day}.${moment.month}. ${twoDigits(moment.hour)}:${twoDigits(
290
+ moment.minute
291
+ )}`
292
+ )
293
+ .join(' – ')
294
+ }
295
+
296
+ /**
297
+ * Calculate milliseconds since start of day
298
+ */
299
+ export function msSinceStartOfDay(
300
+ timestamp: number,
301
+ timeZone: string,
302
+ locale: string
303
+ ): number {
304
+ const moment = toTimeZone(timestamp, timeZone, locale)
305
+ const ms =
306
+ ((moment.hour * 60 + moment.minute) * 60 + moment.second) * 1000 +
307
+ moment.millisecond
308
+ // Daylight saving time adjustment
309
+ const ref = toTimeZone(timestamp - ms, timeZone, locale)
310
+ if (ref.day !== moment.day) {
311
+ return ms - 60 * 60 * 1000
312
+ }
313
+ return ms + ref.hour * 60 * 60 * 1000
314
+ }
315
+
316
+ // ============================================================================
317
+ // Coverage Data Parsing
318
+ // ============================================================================
319
+
320
+ export interface CoveragePathData {
321
+ path: string
322
+ reference: [number, number] | []
323
+ }
324
+
325
+ /**
326
+ * Parse coverage data from SVG string
327
+ */
328
+ export function coverageData(coverage: string): CoveragePathData[] {
329
+ const doc = new DOMParser().parseFromString(coverage, 'text/xml')
330
+ const paths = xpath.select(
331
+ '//*[name()="svg"]//*[local-name()="path" and @id!="bbox"]',
332
+ doc
333
+ ) as Element[]
334
+ const circle = xpath.select(
335
+ '//*[name()="svg"]//*[local-name()="circle" and @id="reference"]',
336
+ doc
337
+ ) as Element[]
338
+
339
+ return paths.map((path, index) => {
340
+ const firstCircle = circle[0]
341
+ return {
342
+ path: path.getAttribute('d') ?? '',
343
+ reference:
344
+ index === 0 && circle.length > 0 && firstCircle
345
+ ? ([
346
+ Number(firstCircle.getAttribute('cx')),
347
+ Number(firstCircle.getAttribute('cy')),
348
+ ] as [number, number])
349
+ : ([] as []),
350
+ }
351
+ })
352
+ }
353
+
354
+ // ============================================================================
355
+ // Warning Creation Helpers
356
+ // ============================================================================
357
+
358
+ export interface WarningCreationContext {
359
+ geometryId: string
360
+ geometries: GeometryCollection
361
+ timeZone: string
362
+ locale: string
363
+ currentTime: number
364
+ updatedAt: number | null
365
+ startFrom: string
366
+ staticDays: boolean
367
+ timeOffset: number
368
+ dailyWarningTypes: string[]
369
+ warningTypes: Map<string, RegionType>
370
+ t: (key: string) => string
371
+ handleError: (error: string) => void
372
+ }
373
+
374
+ /**
375
+ * Calculate effective days for a warning
376
+ */
377
+ export function effectiveDays(
378
+ start: string,
379
+ end: string,
380
+ dailyWarning: boolean,
381
+ context: WarningCreationContext
382
+ ): boolean[] {
383
+ const { timeOffset, startFrom, updatedAt, currentTime, timeZone, locale } =
384
+ context
385
+ const referenceTime =
386
+ startFrom === 'updated' ? updatedAt ?? currentTime : currentTime
387
+ const day = 1000 * 60 * 60 * 24
388
+
389
+ return Array.from({ length: NUMBER_OF_DAYS }, (_, index) => {
390
+ const dayTime = referenceTime + index * day
391
+ const dayStartOffset = msSinceStartOfDay(dayTime, timeZone, locale)
392
+ let startOfDay = dayTime - dayStartOffset
393
+
394
+ const nextDayTime = referenceTime + (index + 1) * day
395
+ const nextDayStartOffset = msSinceStartOfDay(nextDayTime, timeZone, locale)
396
+ let startOfNextDay = nextDayTime - nextDayStartOffset
397
+
398
+ if (!dailyWarning) {
399
+ startOfDay = startOfDay + timeOffset
400
+ startOfNextDay = startOfNextDay + timeOffset
401
+ }
402
+
403
+ return (
404
+ new Date(start).getTime() < startOfNextDay &&
405
+ new Date(end).getTime() > startOfDay
406
+ )
407
+ })
408
+ }
409
+
410
+ /**
411
+ * Create weather warning from raw data
412
+ */
413
+ export function createWeatherWarning(
414
+ warning: { properties: Record<string, unknown> },
415
+ context: WarningCreationContext
416
+ ): Warning {
417
+ const { geometryId, geometries, timeZone, locale, dailyWarningTypes } =
418
+ context
419
+ let direction = 0
420
+ let severity = Number(
421
+ String(warning.properties.severity ?? '').slice(-1)
422
+ ) as Severity
423
+
424
+ switch (warning.properties[WARNING_CONTEXT]) {
425
+ case SEA_WIND:
426
+ direction =
427
+ ((warning.properties[PHYSICAL_DIRECTION] as number) ?? 0) - 180
428
+ if (warning.properties[SEVERITY] === WARNING_LEVELS[0]) {
429
+ severity = (severity + 1) as Severity
430
+ }
431
+ break
432
+ case WIND:
433
+ direction = ((warning.properties[PHYSICAL_DIRECTION] as number) ?? 0) - 90
434
+ break
435
+ default:
436
+ }
437
+
438
+ const regionId = regionFromReference(warning.properties.reference as string)
439
+ const type = warningType(warning.properties)
440
+ const geometryData = geometries[geometryId]
441
+
442
+ return {
443
+ type,
444
+ id: warning.properties.identifier as string,
445
+ regions: geometryData?.[regionId] ? { [regionId]: true } : {},
446
+ covRegions: new Map(),
447
+ coveragesLarge: [],
448
+ coveragesSmall: [],
449
+ effectiveFrom: warning.properties[EFFECTIVE_FROM] as string,
450
+ effectiveUntil: warning.properties[EFFECTIVE_UNTIL] as string,
451
+ effectiveDays: effectiveDays(
452
+ warning.properties[EFFECTIVE_FROM] as string,
453
+ warning.properties[EFFECTIVE_UNTIL] as string,
454
+ dailyWarningTypes.includes(type),
455
+ context
456
+ ),
457
+ validInterval: validInterval(
458
+ warning.properties[EFFECTIVE_FROM] as string,
459
+ warning.properties[EFFECTIVE_UNTIL] as string,
460
+ timeZone,
461
+ locale
462
+ ),
463
+ severity,
464
+ direction,
465
+ value: (warning.properties[PHYSICAL_VALUE] as number) ?? 0,
466
+ text: getWarningText(warning.properties),
467
+ info: {
468
+ fi: warning.properties[INFO_FI]
469
+ ? he.decode(warning.properties[INFO_FI] as string)
470
+ : '',
471
+ sv: warning.properties[INFO_SV]
472
+ ? he.decode(warning.properties[INFO_SV] as string)
473
+ : '',
474
+ en: warning.properties[INFO_EN]
475
+ ? he.decode(warning.properties[INFO_EN] as string)
476
+ : '',
477
+ },
478
+ link: '',
479
+ linkText: '',
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Create flood warning from raw data
485
+ */
486
+ export function createFloodWarning(
487
+ warning: { properties: Record<string, unknown> },
488
+ context: WarningCreationContext
489
+ ): Warning {
490
+ const { timeZone, locale, dailyWarningTypes, t, handleError } = context
491
+
492
+ let info = ''
493
+ try {
494
+ info = JSON.parse(
495
+ decodeURIComponent(
496
+ warning.properties.description != null
497
+ ? (warning.properties.description as string)
498
+ : '[%22%22]'
499
+ ).replace(/[\n|\t]/g, ' ')
500
+ )[0]
501
+ } catch (e) {
502
+ handleError((e as Error).name)
503
+ }
504
+
505
+ const regionId = regionFromReference(warning.properties.reference as string)
506
+ const langKey = (warning.properties.language as string)
507
+ ?.substring(0, 2)
508
+ ?.toLowerCase() as keyof LocalizedText
509
+
510
+ return {
511
+ type: FLOOD_LEVEL_TYPE,
512
+ id: warning.properties.identifier as string,
513
+ regions: { [regionId]: true },
514
+ covRegions: new Map(),
515
+ coveragesLarge: [],
516
+ coveragesSmall: [],
517
+ effectiveFrom: warning.properties[ONSET] as string,
518
+ effectiveUntil: warning.properties[EXPIRES] as string,
519
+ effectiveDays: effectiveDays(
520
+ warning.properties[ONSET] as string,
521
+ warning.properties[EXPIRES] as string,
522
+ dailyWarningTypes.includes(FLOOD_LEVEL_TYPE),
523
+ context
524
+ ),
525
+ validInterval: validInterval(
526
+ warning.properties[ONSET] as string,
527
+ warning.properties[EXPIRES] as string,
528
+ timeZone,
529
+ locale
530
+ ),
531
+ severity: (FLOOD_LEVELS[
532
+ (warning.properties.severity as string)?.toLowerCase()
533
+ ] ?? 0) as Severity,
534
+ direction: 0,
535
+ value: 0,
536
+ text: '',
537
+ info: { [langKey]: info },
538
+ link: t('floodLink'),
539
+ linkText: t('floodLinkText'),
540
+ }
541
+ }
542
+
543
+ // ============================================================================
544
+ // Data Processing Functions
545
+ // ============================================================================
546
+
547
+ /**
548
+ * Create days array from warnings
549
+ */
550
+ export function createDays(
551
+ warnings: WarningsMap,
552
+ updatedAt: number | null,
553
+ currentTime: number,
554
+ startFrom: string,
555
+ timeZone: string,
556
+ locale: string
557
+ ): Day[] {
558
+ const updatedAtTz = updatedAt ? toTimeZone(updatedAt, timeZone, locale) : null
559
+ const updatedDate = updatedAtTz
560
+ ? `${updatedAtTz.day}.${updatedAtTz.month}.${updatedAtTz.year}`
561
+ : ''
562
+ const updatedTime = updatedAtTz
563
+ ? `${twoDigits(updatedAtTz.hour)}:${twoDigits(updatedAtTz.minute)}`
564
+ : ''
565
+
566
+ const referenceTime =
567
+ startFrom === 'updated' ? updatedAt ?? currentTime : currentTime
568
+
569
+ return Array.from({ length: NUMBER_OF_DAYS }, (_, index) => {
570
+ const date = new Date(referenceTime)
571
+ date.setDate(date.getDate() + index)
572
+ const moment = toTimeZone(date, timeZone, locale)
573
+
574
+ return {
575
+ weekdayName: moment.weekday,
576
+ day: moment.day,
577
+ month: moment.month,
578
+ year: moment.year,
579
+ severity: Object.values(warnings).reduce(
580
+ (maxSeverity, warning) =>
581
+ warning.effectiveDays[index]
582
+ ? (Math.max(warning.severity, maxSeverity) as Severity)
583
+ : maxSeverity,
584
+ 0 as Severity
585
+ ),
586
+ updatedDate,
587
+ updatedTime,
588
+ }
589
+ })
590
+ }
591
+
592
+ /**
593
+ * Get maximum severities by warning type
594
+ */
595
+ export function getMaxSeverities(
596
+ warnings: WarningsMap
597
+ ): Record<string, Severity> {
598
+ return Object.values(warnings).reduce(
599
+ (maxSeverities, warning) => {
600
+ const currentMax = maxSeverities[warning.type]
601
+ if (
602
+ warning.effectiveDays.some((effectiveDay) => effectiveDay) &&
603
+ (currentMax == null || currentMax < warning.severity)
604
+ ) {
605
+ maxSeverities[warning.type] = warning.severity
606
+ }
607
+ return maxSeverities
608
+ },
609
+ {} as Record<string, Severity>
610
+ )
611
+ }
612
+
613
+ /**
614
+ * Create legend from severities
615
+ */
616
+ export function createLegend(
617
+ severities: Record<string, Severity>,
618
+ warningTypes: Map<string, RegionType>
619
+ ): LegendItem[] {
620
+ const warningKeys = Object.keys(severities)
621
+ return [4, 3, 2].reduce<LegendItem[]>((orderedSeverities, severity) => {
622
+ const warningTypesBySeverity = warningKeys.filter(
623
+ (key) => severities[key] === severity
624
+ )
625
+ warningTypes.forEach((_, warnType) => {
626
+ if (warningTypesBySeverity.includes(warnType)) {
627
+ const warnSeverity = severities[warnType]
628
+ if (warnSeverity !== undefined) {
629
+ orderedSeverities.push({
630
+ type: warnType,
631
+ severity: warnSeverity,
632
+ visible: true,
633
+ })
634
+ }
635
+ }
636
+ })
637
+ return orderedSeverities
638
+ }, [])
639
+ }
640
+
641
+ // ============================================================================
642
+ // Composable
643
+ // ============================================================================
644
+
645
+ export interface UseUtilsOptions {
646
+ theme: Ref<string>
647
+ geometryId: Ref<string>
648
+ geometries: Ref<GeometryCollection>
649
+ colors: Ref<ThemeColorMap>
650
+ warnings: Ref<WarningsMap | null>
651
+ visibleWarnings: Ref<string[]>
652
+ index: Ref<number>
653
+ size: Ref<string>
654
+ strokeWidth: Ref<number>
655
+ regionIds: Ref<string[]>
656
+ warningTypes: Ref<Map<string, RegionType>>
657
+ coverageCriterion: Ref<number>
658
+ timeZone: Ref<string>
659
+ locale: Ref<string>
660
+ }
661
+
662
+ export interface UseUtilsReturn {
663
+ strokeColor: ComputedRef<string>
664
+ // Add more computed values as needed
665
+ }
666
+
667
+ /**
668
+ * Utils composable for reactive computed values
669
+ */
670
+ export function useUtils(options: UseUtilsOptions): UseUtilsReturn {
671
+ const { theme, colors } = options
672
+
673
+ const strokeColor = computed<string>(() => {
674
+ return (
675
+ colors.value?.[theme.value as keyof ThemeColorMap]?.stroke ??
676
+ 'DarkSlateGray'
677
+ )
678
+ })
679
+
680
+ return {
681
+ strokeColor,
682
+ }
683
+ }