@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
@@ -1,900 +0,0 @@
1
- import 'url-search-params-polyfill'
2
-
3
- import { DOMParser } from '@xmldom/xmldom'
4
- import he from 'he'
5
- import xpath from 'xpath'
6
-
7
- import config from './config'
8
- import geojsonsvg from './geojsonsvg'
9
-
10
- export default {
11
- mixins: [config, geojsonsvg],
12
- computed: {
13
- NUMBER_OF_DAYS: () => 5,
14
- REGION_LAND: () => 'land',
15
- REGION_SEA: () => 'sea',
16
- REGION_LAKE: () => 'lake',
17
- WEATHER_UPDATE_TIME: () => 'weather_update_time',
18
- FLOOD_UPDATE_TIME: () => 'flood_update_time',
19
- UPDATE_TIME: () => 'update_time',
20
- WEATHER_WARNINGS: () => 'weather_finland_active_all',
21
- FLOOD_WARNINGS: () => 'flood_finland_active_all',
22
- INFO_FI: () => 'info_fi',
23
- INFO_SV: () => 'info_sv',
24
- INFO_EN: () => 'info_en',
25
- PHYSICAL_DIRECTION: () => 'physical_direction',
26
- PHYSICAL_VALUE: () => 'physical_value',
27
- EFFECTIVE_FROM: () => 'effective_from',
28
- EFFECTIVE_UNTIL: () => 'effective_until',
29
- ONSET: () => 'onset',
30
- EXPIRES: () => 'expires',
31
- WARNING_CONTEXT: () => 'warning_context',
32
- SEVERITY: () => 'severity',
33
- CONTEXT_EXTENSION: () => 'context_extension',
34
- WIND: () => 'wind',
35
- SEA_WIND: () => 'sea-wind',
36
- FLOOD_LEVEL_TYPE: () => 'floodLevel',
37
- MULTIPLE: () => 'multiple',
38
- WARNING_LEVELS: () => ['level-1', 'level-2', 'level-3', 'level-4'],
39
- FLOOD_LEVELS: () => ({
40
- minor: 1,
41
- moderate: 2,
42
- severe: 3,
43
- extreme: 4,
44
- }),
45
- strokeColor() {
46
- return this.colors[this.theme].stroke
47
- },
48
- bluePaths() {
49
- return this.paths({
50
- type: this.REGION_SEA,
51
- })
52
- },
53
- greenPaths() {
54
- return this.paths({
55
- type: this.REGION_LAND,
56
- severity: 0,
57
- })
58
- },
59
- yellowPaths() {
60
- return this.paths({
61
- type: this.REGION_LAND,
62
- severity: 2,
63
- })
64
- },
65
- orangePaths() {
66
- return this.paths({
67
- type: this.REGION_LAND,
68
- severity: 3,
69
- })
70
- },
71
- redPaths() {
72
- return this.paths({
73
- type: this.REGION_LAND,
74
- severity: 4,
75
- })
76
- },
77
- overlayPaths() {
78
- return this.regionIds.reduce((regions, regionId) => {
79
- if (
80
- this.geometries[this.geometryId][regionId].pathLarge &&
81
- (this.geometries[this.geometryId][regionId].type === 'land' ||
82
- this.geometries[this.geometryId][regionId].subType === 'lake')
83
- ) {
84
- const visualization = this.regionVisualization(regionId)
85
- regions.push({
86
- key: `${regionId}${this.size}${this.index}Overlay`,
87
- d: visualization.visible
88
- ? visualization.geom[`path${this.size}`]
89
- : '',
90
- opacity: '1',
91
- strokeWidth: this.strokeWidth,
92
- })
93
- }
94
- return regions
95
- }, [])
96
- },
97
- landBorders() {
98
- return this.areaBorders('land')
99
- },
100
- seaBorders() {
101
- return this.areaBorders('sea')
102
- },
103
- yellowCoverages() {
104
- return this.coverageGeom(`coverages${this.size}`, 0, 1, 2)
105
- },
106
- orangeCoverages() {
107
- return this.coverageGeom(`coverages${this.size}`, 0, 1, 3)
108
- },
109
- redCoverages() {
110
- return this.coverageGeom(`coverages${this.size}`, 0, 1, 4)
111
- },
112
- overlayCoverages() {
113
- return this.coverageGeom(
114
- `coverages${this.size}`,
115
- 1.1 * this.strokeWidth,
116
- 0
117
- )
118
- },
119
- },
120
- methods: {
121
- uncapitalize(value) {
122
- if (!value) return ''
123
- const stringValue = value.toString()
124
- return stringValue.charAt(0).toLowerCase() + stringValue.slice(1)
125
- },
126
- warningType(properties) {
127
- return this.uncapitalize(
128
- (
129
- properties[this.WARNING_CONTEXT] +
130
- (properties[this.CONTEXT_EXTENSION]
131
- ? `-${properties[this.CONTEXT_EXTENSION]}`
132
- : '')
133
- )
134
- .split('-')
135
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
136
- .join('')
137
- )
138
- },
139
- areaBorders(area) {
140
- return [
141
- {
142
- key: `border.${area}`,
143
- d: this.geometries[this.geometryId]['borders'][area][`path${this.size}`],
144
- opacity: '1',
145
- strokeWidth: this.strokeWidth,
146
- },
147
- ]
148
- },
149
- relativeCoverageFromReference(reference) {
150
- if (reference == null) {
151
- return 0
152
- }
153
- let paramString = ''
154
- const urlSplit = reference.split('?')
155
- if (urlSplit.length <= 1) {
156
- return 0
157
- }
158
- paramString = urlSplit[1].split('#')[0]
159
- const searchParams = new URLSearchParams(paramString)
160
- const relativeCoverage = searchParams.get('c')
161
- if (relativeCoverage == null) {
162
- return 0
163
- }
164
- return Number(relativeCoverage)
165
- },
166
- regionFromReference(reference) {
167
- return reference
168
- .split(',')
169
- .map((url) => {
170
- let subUrl = url.substring(url.lastIndexOf('#') + 1)
171
- // Saimaa
172
- if (subUrl.indexOf('.') !== subUrl.lastIndexOf('.')) {
173
- subUrl = subUrl.replace('.', '_')
174
- }
175
- return subUrl
176
- })
177
- .reduce((regionId, rawId, index, array) => {
178
- const parts = rawId.split('.')
179
- if (index === 0) {
180
- // eslint-disable-next-line no-param-reassign
181
- regionId += parts[0]
182
- }
183
- return regionId + (index === array.length - 1 ? '.' : '_') + parts[1]
184
- }, '')
185
- },
186
- validInterval(start, end) {
187
- return [this.toTimeZone(start), this.toTimeZone(end)]
188
- .map(
189
- (moment) =>
190
- `${moment.day}.${moment.month}. ${this.twoDigits(
191
- moment.hour
192
- )}:${this.twoDigits(moment.minute)}`
193
- )
194
- .join(' – ')
195
- },
196
- msSinceStartOfDay(timestamp) {
197
- const moment = this.toTimeZone(timestamp)
198
- const ms = ((moment.hour * 60 + moment.minute) * 60 + moment.second) * 1000 + moment.millisecond
199
- // Daylight saving time
200
- const ref = this.toTimeZone(timestamp - ms)
201
- if (ref.day !== moment.day) {
202
- return ms - 60 * 60 * 1000
203
- }
204
- return ms + ref.hour * 60 * 60 * 1000
205
- },
206
- effectiveDays(start, end, dailyWarning) {
207
- const offset = this.timeOffset
208
- const referenceTime =
209
- this.startFrom === 'updated' ? this.updatedAt : this.currentTime
210
- const day = 1000 * 60 * 60 * 24
211
- return [...Array(this.NUMBER_OF_DAYS).keys()].map((index) => {
212
- const dayTime = referenceTime + index * day
213
- const dayStartOffset = this.msSinceStartOfDay(dayTime)
214
- let startOfDay = dayTime - dayStartOffset
215
-
216
- const nextDayTime = referenceTime + (index + 1) * day
217
- const nextDayStartOffset = this.msSinceStartOfDay(nextDayTime)
218
- let startOfNextDay = nextDayTime - nextDayStartOffset
219
-
220
- if (!dailyWarning) {
221
- startOfDay = startOfDay + offset
222
- startOfNextDay = startOfNextDay + offset
223
- }
224
- return (
225
- new Date(start).getTime() < startOfNextDay &&
226
- new Date(end).getTime() > startOfDay
227
- )
228
- })
229
- },
230
- text(properties) {
231
- return properties[this.WARNING_CONTEXT] === this.SEA_WIND
232
- ? properties[this.PHYSICAL_VALUE]
233
- : ''
234
- },
235
- createWeatherWarning(warning) {
236
- let direction = 0
237
- let severity = Number(warning.properties.severity.slice(-1))
238
- switch (warning.properties[this.WARNING_CONTEXT]) {
239
- case this.SEA_WIND:
240
- direction = warning.properties[this.PHYSICAL_DIRECTION] - 180
241
- if (warning.properties[this.SEVERITY] === this.WARNING_LEVELS[0]) {
242
- severity += 1
243
- }
244
- break
245
- case this.WIND:
246
- direction = warning.properties[this.PHYSICAL_DIRECTION] - 90
247
- break
248
- default:
249
- }
250
- const regionId = this.regionFromReference(warning.properties.reference)
251
- const type = this.warningType(warning.properties)
252
- return {
253
- type,
254
- id: warning.properties.identifier,
255
- regions: this.geometries[this.geometryId][regionId]
256
- ? {
257
- [this.regionFromReference(warning.properties.reference)]: true,
258
- }
259
- : {},
260
- covRegions: new Map(),
261
- coveragesLarge: [],
262
- coveragesSmall: [],
263
- effectiveFrom: warning.properties[this.EFFECTIVE_FROM],
264
- effectiveUntil: warning.properties[this.EFFECTIVE_UNTIL],
265
- effectiveDays: this.effectiveDays(
266
- warning.properties[this.EFFECTIVE_FROM],
267
- warning.properties[this.EFFECTIVE_UNTIL],
268
- this.dailyWarningTypes.includes(type)
269
- ),
270
- validInterval: this.validInterval(
271
- warning.properties[this.EFFECTIVE_FROM],
272
- warning.properties[this.EFFECTIVE_UNTIL]
273
- ),
274
- severity,
275
- direction,
276
- value: warning.properties[this.PHYSICAL_VALUE],
277
- text: this.text(warning.properties),
278
- info: {
279
- fi:
280
- warning.properties[this.INFO_FI] != null
281
- ? he.decode(warning.properties[this.INFO_FI])
282
- : '',
283
- sv:
284
- warning.properties[this.INFO_SV] != null
285
- ? he.decode(warning.properties[this.INFO_SV])
286
- : '',
287
- en:
288
- warning.properties[this.INFO_EN] != null
289
- ? he.decode(warning.properties[this.INFO_EN])
290
- : '',
291
- },
292
- link: '',
293
- linkText: '',
294
- }
295
- },
296
- createFloodWarning(warning) {
297
- let info = ''
298
- try {
299
- info = JSON.parse(
300
- decodeURIComponent(
301
- warning.properties.description != null
302
- ? warning.properties.description
303
- : '[%22%22]'
304
- ).replace(/[\n|\t]/g, ' ')
305
- )[0]
306
- } catch (e) {
307
- this.handleError(e.name)
308
- }
309
- return {
310
- type: this.FLOOD_LEVEL_TYPE,
311
- id: warning.properties.identifier,
312
- regions: {
313
- [this.regionFromReference(warning.properties.reference)]: true,
314
- },
315
- covRegions: new Map(),
316
- coveragesLarge: [],
317
- coveragesSmall: [],
318
- effectiveFrom: warning.properties[this.ONSET],
319
- effectiveUntil: warning.properties[this.EXPIRES],
320
- effectiveDays: this.effectiveDays(
321
- warning.properties[this.ONSET],
322
- warning.properties[this.EXPIRES],
323
- this.dailyWarningTypes.includes(this.FLOOD_LEVEL_TYPE)
324
- ),
325
- validInterval: this.validInterval(
326
- warning.properties[this.ONSET],
327
- warning.properties[this.EXPIRES]
328
- ),
329
- severity: this.FLOOD_LEVELS[warning.properties.severity.toLowerCase()],
330
- direction: 0,
331
- value: 0,
332
- text: '',
333
- info: {
334
- [warning.properties.language.substr(0, 2).toLowerCase()]: info,
335
- },
336
- link: this.t('floodLink'),
337
- linkText: this.t('floodLinkText'),
338
- }
339
- },
340
- createDays(warnings) {
341
- const updatedAtTz = this.toTimeZone(this.updatedAt)
342
- const updatedDate =
343
- this.updatedAt != null
344
- ? `${updatedAtTz.day}.${updatedAtTz.month}.${updatedAtTz.year}`
345
- : ''
346
- const updatedTime =
347
- this.updatedAt != null
348
- ? `${this.twoDigits(updatedAtTz.hour)}:${this.twoDigits(
349
- updatedAtTz.minute
350
- )}`
351
- : ''
352
- return [...Array(this.NUMBER_OF_DAYS).keys()].map((index) => {
353
- const referenceTime =
354
- this.startFrom === 'updated' ? this.updatedAt : this.currentTime
355
- const date = new Date(referenceTime)
356
- date.setDate(date.getDate() + index)
357
- const moment = this.toTimeZone(date)
358
- return {
359
- weekdayName: moment.weekday,
360
- day: moment.day,
361
- month: moment.month,
362
- year: moment.year,
363
- severity: Object.values(warnings).reduce(
364
- (maxSeverity, warning) =>
365
- warning.effectiveDays[index]
366
- ? Math.max(warning.severity, maxSeverity)
367
- : maxSeverity,
368
- 0
369
- ),
370
- updatedDate,
371
- updatedTime,
372
- }
373
- })
374
- },
375
- getMaxSeverities(warnings) {
376
- return Object.values(warnings).reduce((maxSeverities, warning) => {
377
- if (
378
- warning.effectiveDays.some((effectiveDay) => effectiveDay) &&
379
- (maxSeverities[warning.type] == null ||
380
- maxSeverities[warning.type] < warning.severity)
381
- ) {
382
- // eslint-disable-next-line no-param-reassign
383
- maxSeverities[warning.type] = warning.severity
384
- }
385
- return maxSeverities
386
- }, {})
387
- },
388
- createLegend(severities) {
389
- const warningKeys = Object.keys(severities)
390
- return [4, 3, 2].reduce((orderedSeverities, severity) => {
391
- const warningTypesBySeverity = warningKeys.filter(
392
- (key) => severities[key] === severity
393
- )
394
- this.warningTypes.forEach((regionType, warningType) => {
395
- if (warningTypesBySeverity.includes(warningType)) {
396
- orderedSeverities.push({
397
- type: warningType,
398
- severity: severities[warningType],
399
- visible: true,
400
- })
401
- }
402
- })
403
- return orderedSeverities
404
- }, [])
405
- },
406
- createRegions(warnings) {
407
- const warningKeys = Object.keys(warnings)
408
- return [4, 3, 2].reduce(
409
- (regionWarnings, severity) => {
410
- const warningsBySeverity = warningKeys.filter(
411
- (key) => warnings[key].severity === severity
412
- )
413
- ;[...Array(this.NUMBER_OF_DAYS).keys()].forEach((day) => {
414
- const warningsByDay = warningsBySeverity.filter(
415
- (key) => warnings[key].effectiveDays[day]
416
- )
417
- this.warningTypes.forEach((regionType, warningType) => {
418
- const warningsByType = warningsByDay.filter(
419
- (key) => warnings[key].type === warningType
420
- )
421
- warningsByType.sort((key1, key2) => {
422
- if (warnings[key1].severity !== warnings[key2].severity) {
423
- return warnings[key2].severity - warnings[key1].severity
424
- }
425
- if (warnings[key1].value !== warnings[key2].value) {
426
- return warnings[key2].value - warnings[key1].value
427
- }
428
- const effectiveFrom1 = new Date(
429
- warnings[key1].effectiveFrom
430
- ).getTime()
431
- const effectiveFrom2 = new Date(
432
- warnings[key2].effectiveFrom
433
- ).getTime()
434
- if (effectiveFrom1 !== effectiveFrom2) {
435
- return effectiveFrom1 - effectiveFrom2
436
- }
437
- const effectiveUntil1 = new Date(
438
- warnings[key1].effectiveUntil
439
- ).getTime()
440
- const effectiveUntil2 = new Date(
441
- warnings[key2].effectiveUntil
442
- ).getTime()
443
- return effectiveUntil1 - effectiveUntil2
444
- })
445
- warningsByType.forEach((key) => {
446
- this.regionIds.forEach((regionId, regionIndex) => {
447
- if (warnings[key].regions[regionId]) {
448
- const regionItems =
449
- regionWarnings[day][
450
- this.geometries[this.geometryId][regionId].type
451
- ]
452
- let regionItem = regionItems.find(
453
- (regionWarning) => regionWarning.key === regionId
454
- )
455
- if (regionItem == null) {
456
- regionItem = {
457
- key: regionId,
458
- regionIndex,
459
- name: this.geometries[this.geometryId][regionId].name,
460
- warnings: [],
461
- }
462
- regionItems.push(regionItem)
463
- }
464
- let warningItem = regionItem.warnings.find(
465
- (warning) => warning.type === warningType
466
- )
467
- if (warningItem == null) {
468
- warningItem = {
469
- type: warningType,
470
- identifiers: [],
471
- coverage: 0,
472
- }
473
- regionItem.warnings.push(warningItem)
474
- }
475
- if (!warningItem.identifiers.includes(key)) {
476
- warningItem.identifiers.push(key)
477
- }
478
- const covRegions = warnings[key].covRegions
479
- if (covRegions.has(regionId)) {
480
- warningItem.coverage += covRegions.get(regionId)
481
- } else {
482
- warningItem.coverage = 100
483
- }
484
- }
485
- })
486
- })
487
- })
488
- })
489
- return regionWarnings
490
- },
491
- [...Array(this.NUMBER_OF_DAYS).keys()].map(() => ({
492
- [this.REGION_LAND]: [],
493
- [this.REGION_SEA]: [],
494
- }))
495
- )
496
- },
497
-
498
- isValid(warning) {
499
- if (warning == null || warning.properties == null) {
500
- return false
501
- }
502
- const regionId = this.regionFromReference(warning.properties.reference)
503
- if (
504
- warning.geometry == null &&
505
- this.geometries[this.geometryId][regionId] == null
506
- ) {
507
- return false
508
- }
509
- const warningType =
510
- warning.properties.warning_context != null
511
- ? this.warningType(warning.properties)
512
- : 'floodLevel'
513
- if (
514
- this.geometries[this.geometryId][regionId] != null &&
515
- this.warningTypes.get(warningType) !==
516
- this.geometries[this.geometryId][regionId].type
517
- ) {
518
- return false
519
- }
520
- // Valid flood warning
521
- if (
522
- warning.properties.severity != null &&
523
- Object.keys(this.FLOOD_LEVELS).includes(
524
- warning.properties.severity.toLowerCase()
525
- )
526
- ) {
527
- return true
528
- }
529
- return (
530
- this.WARNING_LEVELS.slice(1).includes(warning.properties.severity) ||
531
- (warning.properties[this.WARNING_CONTEXT] === this.SEA_WIND &&
532
- this.WARNING_LEVELS.includes(warning.properties.severity))
533
- )
534
- },
535
-
536
- coverageGeom(coverageProperty, strokeWidth, fillOpacity, severity) {
537
- const coverageData = []
538
- const visibleWarnings = this.visibleWarnings
539
- Object.keys(this.warnings ?? {}).forEach((key) => {
540
- if (
541
- (severity == null || this.warnings[key].severity === severity) &&
542
- this.warnings[key].effectiveDays[this.index] &&
543
- visibleWarnings.includes(this.warnings[key].type) &&
544
- this.warnings[key].coveragesLarge.length > 0
545
- ) {
546
- if (!this.coverageWarnings.includes(key)) {
547
- ;[...this.warnings[key].covRegions.keys()].forEach((covRegion) => {
548
- if (
549
- (this.coverageRegions[covRegion] == null ||
550
- this.coverageRegions[covRegion] < this.warnings[key].severity) &&
551
- this.warnings[key].covRegions.get(covRegion) >= this.coverageCriterion
552
- ) {
553
- this.coverageRegions[covRegion] = this.warnings[key].severity
554
- }
555
- })
556
- this.coverageWarnings.push(key)
557
- }
558
- this.warnings[key][coverageProperty].forEach((coverage) => {
559
- coverageData.push({
560
- key: `${key}${this.size}${this.index}${fillOpacity}Coverage`,
561
- d: coverage.path,
562
- fillOpacity,
563
- strokeWidth,
564
- fill: this.colors[this.theme].levels[this.warnings[key].severity],
565
- })
566
- })
567
- }
568
- })
569
- return coverageData
570
- },
571
-
572
- createCoverage(coverage, width, height, reference) {
573
- const data = {
574
- type: 'FeatureCollection',
575
- features: [coverage, this.bbox],
576
- totalFeatures: 2,
577
- crs: {
578
- type: 'name',
579
- properties: {
580
- name: 'urn:ogc:def:crs:EPSG::3067',
581
- },
582
- },
583
- }
584
- if (reference != null) {
585
- data.features.push({
586
- type: 'Feature',
587
- id: 'reference',
588
- properties: {},
589
- geometry: {
590
- type: 'Point',
591
- coordinates: reference,
592
- },
593
- })
594
- data.totalFeatures++
595
- }
596
- return this.geoJSONToSVG(data, width, height)
597
- },
598
-
599
- coverageData(coverage) {
600
- const doc = new DOMParser().parseFromString(coverage)
601
- const paths = xpath.select(
602
- '//*[name()="svg"]//*[local-name()="path" and @id!="bbox"]',
603
- doc
604
- )
605
- const circle = xpath.select(
606
- '//*[name()="svg"]//*[local-name()="circle" and @id="reference"]',
607
- doc
608
- )
609
- return paths.map((path, index) => ({
610
- path: path.getAttribute('d'),
611
- reference:
612
- index === 0 && circle.length > 0
613
- ? [
614
- Number(circle[0].getAttribute('cx')),
615
- Number(circle[0].getAttribute('cy')),
616
- ]
617
- : [],
618
- }))
619
- },
620
-
621
- handleMapWarnings(data) {
622
- const warnings = {}
623
- const parents = {}
624
- this.errors = []
625
- const allUpdateTimes = [this.WEATHER_UPDATE_TIME, this.FLOOD_UPDATE_TIME]
626
- .filter((warningUpdateTime) => data[warningUpdateTime] != null)
627
- .reduce((updateTimes, warningUpdateTime) => {
628
- if (
629
- data[warningUpdateTime].features != null &&
630
- data[warningUpdateTime].features.length > 0 &&
631
- data[warningUpdateTime].features[0].properties != null
632
- ) {
633
- const updateTime = new Date(
634
- data[warningUpdateTime].features[0].properties[this.UPDATE_TIME]
635
- ).getTime()
636
- updateTimes.push(updateTime)
637
- if (
638
- this.currentTime - updateTime >
639
- this.maxUpdateDelay[warningUpdateTime]
640
- ) {
641
- this.handleError(`${warningUpdateTime}_outdated`)
642
- }
643
- } else {
644
- this.handleError(warningUpdateTime)
645
- }
646
- return updateTimes
647
- }, [])
648
- .sort()
649
- .reverse()
650
- this.updatedAt = allUpdateTimes.length > 0 ? allUpdateTimes[0] : null
651
- if (!this.staticDays) {
652
- const startTime =
653
- this.startFrom === 'updated' ? this.updatedAt : this.currentTime
654
- this.timeOffset = this.msSinceStartOfDay(startTime)
655
- }
656
- const createWarnings = {
657
- [this.WEATHER_WARNINGS]: this.createWeatherWarning,
658
- [this.FLOOD_WARNINGS]: this.createFloodWarning,
659
- }
660
- const warningTypes = Object.keys(createWarnings)
661
- for (const warningType of warningTypes) {
662
- let features = []
663
- if (data[warningType] == null) {
664
- this.handleError(`Missing data: ${warningType}`)
665
- this.onDataError()
666
- // eslint-disable-next-line no-continue
667
- continue
668
- }
669
- features = data[warningType].features
670
- for (const warning of features) {
671
- if (this.isValid(warning)) {
672
- let regionId
673
- const regionIds = []
674
- const warningId = warning.properties.identifier
675
- if (warnings[warningId] == null) {
676
- warnings[warningId] = createWarnings[warningType](warning)
677
- const warningRegions = Object.keys(warnings[warningId].regions)
678
- if (warningRegions.length > 0) {
679
- regionId = warningRegions[0]
680
- }
681
- if (this.dailyWarningTypes.includes(warnings[warningId].type)) {
682
- warnings[warningId].dailyWarning = true
683
- }
684
- } else {
685
- regionId = this.regionFromReference(warning.properties.reference)
686
- if (this.geometries[this.geometryId][regionId]) {
687
- warnings[warningId].regions[regionId] = true
688
- }
689
- }
690
- if (warning.properties.coverage_references != null) {
691
- // Space after comma is needed for merged areas
692
- warning.properties.coverage_references
693
- .split(', ')
694
- .filter((reference) => reference.length > 0)
695
- .forEach((reference) => {
696
- const refRegionId = this.regionFromReference(reference)
697
- const regionCoverage =
698
- this.relativeCoverageFromReference(reference) / 100
699
- if (this.geometries[this.geometryId][refRegionId]) {
700
- warnings[warningId].regions[refRegionId] = true
701
- warnings[warningId].covRegions.set(
702
- refRegionId,
703
- regionCoverage
704
- )
705
- regionIds.push(refRegionId)
706
- }
707
- })
708
- if (warning.geometry != null) {
709
- const coverage = this.createCoverage(warning, 440, 550, [
710
- warning.properties.representative_x,
711
- warning.properties.representative_y,
712
- ])
713
- const coverageSmall = this.createCoverage(warning, 75, 120)
714
- warnings[warningId].coveragesLarge = this.coverageData(coverage)
715
- warnings[warningId].coveragesSmall =
716
- this.coverageData(coverageSmall)
717
- }
718
- }
719
- if (
720
- regionId != null &&
721
- this.geometries[this.geometryId][regionId]
722
- ) {
723
- this.geometries[this.geometryId][regionId].children.forEach(
724
- (id) => {
725
- warnings[warningId].regions[id] = true
726
- }
727
- )
728
- if (regionIds.length === 0) {
729
- regionIds.push(regionId)
730
- }
731
- }
732
- regionIds.forEach((id) => {
733
- const parentId = this.geometries[this.geometryId][id].parent
734
- if (parentId) {
735
- if (parents[parentId] == null) {
736
- parents[parentId] = [false, false, false, false, false]
737
- }
738
- warnings[warningId].effectiveDays.forEach((override, index) => {
739
- if (override) {
740
- parents[parentId][index] = true
741
- }
742
- })
743
- }
744
- })
745
- }
746
- }
747
- }
748
- const days = this.createDays(warnings)
749
- const maxSeverities = this.getMaxSeverities(warnings)
750
- const legend = this.createLegend(maxSeverities)
751
- const regions = this.createRegions(warnings)
752
- this.optimizeCovRegions(warnings, regions)
753
- return {
754
- warnings,
755
- days,
756
- regions,
757
- parents,
758
- legend,
759
- }
760
- },
761
- isClientSide() {
762
- return typeof document !== 'undefined' && document
763
- },
764
- regionData(regionId) {
765
- const regionType = this.geometries[this.geometryId][regionId].type
766
- return this.input[regionType].find(
767
- (regionData) => regionData.key === regionId
768
- )
769
- },
770
- regionSeverity(regionId) {
771
- const region = this.regionData(regionId)
772
- let severity = 0
773
- if (region != null) {
774
- region.warnings.find((warning) => {
775
- if (this.visibleWarnings.includes(warning.type)) {
776
- const topIdentifier = warning.identifiers.find(
777
- (id) =>
778
- this.warnings[id] && this.warnings[id].covRegions.size === 0
779
- )
780
- if (topIdentifier != null) {
781
- severity = this.warnings[topIdentifier].severity
782
- return true
783
- }
784
- }
785
- return false
786
- })
787
- }
788
- const parentId = this.geometries[this.geometryId][regionId].parent
789
- if (parentId) {
790
- severity = Math.max(severity, this.regionSeverity(parentId))
791
- }
792
- return severity
793
- },
794
- regionVisualization(regionId) {
795
- const geom = this.geometries[this.geometryId][regionId]
796
- const severity = this.regionSeverity(regionId)
797
- const isLand =
798
- this.geometries[this.geometryId][regionId].type === this.REGION_LAND
799
- const color =
800
- severity || isLand
801
- ? this.colors[this.theme].levels[severity]
802
- : this.colors[this.theme].sea
803
- const visible = severity > 0 || geom.subType !== this.REGION_LAKE
804
- return {
805
- geom,
806
- severity,
807
- color,
808
- visible,
809
- }
810
- },
811
- // Include also lakes to prevent overlapping symbols in Saimaa
812
- optimizeCovRegions(warnings, regions) {
813
- Object.keys(this.geometries[this.geometryId]).filter((regionId) =>
814
- this.geometries[this.geometryId][regionId]?.type === 'sea' &&
815
- this.geometries[this.geometryId][regionId]?.subType === 'lake'
816
- ).filter((regionId) => regions.some((day) =>
817
- day['sea'].some((region) => region['key'] === regionId
818
- ))).forEach((regionId) =>
819
- Object.keys(warnings).filter((warningKey) =>
820
- warnings[warningKey].covRegions.size > 0
821
- ).forEach((warningKey) => {
822
- warnings[warningKey].covRegions.set(regionId, 0)
823
- }
824
- ))
825
- },
826
- regionsDefault() {
827
- return [
828
- {
829
- land: [],
830
- sea: [],
831
- },
832
- {
833
- land: [],
834
- sea: [],
835
- },
836
- {
837
- land: [],
838
- sea: [],
839
- },
840
- {
841
- land: [],
842
- sea: [],
843
- },
844
- {
845
- land: [],
846
- sea: [],
847
- },
848
- ]
849
- },
850
- twoDigits(value) {
851
- return `0${value}`.slice(-2)
852
- },
853
- toTimeZone(date) {
854
- date = new Date(date)
855
- const parts = new Intl.DateTimeFormat(this.dateTimeFormatLocale, {
856
- timeZoneName: 'short',
857
- timeZone: this.timeZone,
858
- year: 'numeric',
859
- month: 'numeric',
860
- day: 'numeric',
861
- weekday: 'short',
862
- hour12: false,
863
- hour: 'numeric',
864
- minute: 'numeric',
865
- second: 'numeric',
866
- fractionalSecondDigits: 3,
867
- }).formatToParts(date)
868
- const whole = this.partsToWhole(parts)
869
- whole.timeZone = this.timeZone
870
- return whole
871
- },
872
- partsToWhole(parts) {
873
- const whole = { millisecond: 0 }
874
- parts.forEach(function (part) {
875
- let val = part.value
876
- switch (part.type) {
877
- case 'literal':
878
- return
879
- case 'timeZoneName':
880
- break
881
- case 'month':
882
- val = parseInt(val, 10)
883
- break
884
- case 'weekday':
885
- break
886
- case 'hour':
887
- val = parseInt(val, 10) % 24
888
- break
889
- case 'fractionalSecond':
890
- whole.millisecond = parseInt(val, 10)
891
- return
892
- default:
893
- val = parseInt(val, 10)
894
- }
895
- whole[part.type] = val
896
- })
897
- return whole
898
- },
899
- },
900
- }