@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,7 +1,9 @@
1
1
  <template>
2
2
  <div class="map-large focus-ring" :class="theme" tabindex="0">
3
3
  <div v-if="spinnerEnabled && loading" class="spinner-container text-center">
4
- <BSpinner />
4
+ <div class="spinner-border" role="status">
5
+ <span class="visually-hidden"></span>
6
+ </div>
5
7
  </div>
6
8
  <div ref="dayMapLarge" class="day-map-large">
7
9
  <svg
@@ -35,8 +37,8 @@
35
37
  <path
36
38
  v-for="path in seaBorders"
37
39
  :id="path.key"
38
- class="border-path"
39
40
  :key="path.key"
41
+ class="border-path"
40
42
  :stroke="strokeColor"
41
43
  :stroke-width="path.strokeWidth"
42
44
  :stroke-opacity="strokeOpacity"
@@ -134,17 +136,17 @@
134
136
  :d="path.d"
135
137
  fill-opacity="0"
136
138
  style="cursor: pointer; pointer-events: none" />
137
- <path
138
- v-for="path in landBorders"
139
- :id="path.key"
140
- class="border-path"
141
- :key="path.key"
142
- :stroke="strokeColor"
143
- :stroke-width="2*path.strokeWidth"
144
- :stroke-opacity="strokeOpacity"
145
- :d="path.d"
146
- fill-opacity="0"
147
- style="cursor: pointer; pointer-events: none" />
139
+ <path
140
+ v-for="path in landBorders"
141
+ :id="path.key"
142
+ :key="path.key"
143
+ class="border-path"
144
+ :stroke="strokeColor"
145
+ :stroke-width="2 * Number(path.strokeWidth)"
146
+ :stroke-opacity="strokeOpacity"
147
+ :d="path.d"
148
+ fill-opacity="0"
149
+ style="cursor: pointer; pointer-events: none" />
148
150
  </g>
149
151
  <g v-if="!loading">
150
152
  <path
@@ -255,56 +257,107 @@
255
257
  </div>
256
258
  </template>
257
259
 
258
- <script>
259
- import { onMounted, onUnmounted, ref } from 'vue'
260
+ <script lang="ts">
261
+ import {
262
+ defineComponent,
263
+ onMounted,
264
+ onUnmounted,
265
+ ref,
266
+ computed,
267
+ toRef,
268
+ type PropType,
269
+ } from 'vue'
270
+ import Panzoom from '@panzoom/panzoom'
271
+ import type { PanzoomObject } from '@panzoom/panzoom'
260
272
 
261
- import config from '../mixins/config'
262
- import i18n from '../mixins/i18n'
263
- import Panzoom from '../mixins/panzoom'
264
- import utils from '../mixins/utils'
265
273
  import PopupRow from './PopupRow.vue'
266
-
267
- export default {
274
+ import { useMapPaths } from '@/composables/useMapPaths'
275
+ import { useI18n } from '@/composables/useI18n'
276
+ import { useConfig, MULTIPLE } from '@/composables/useConfig'
277
+ import { isClientSide } from '@/composables/useUtils'
278
+ import type {
279
+ DayRegions,
280
+ WarningsMap,
281
+ Theme,
282
+ RegionGeometry,
283
+ Language,
284
+ Severity,
285
+ } from '@/types'
286
+
287
+ interface IconData {
288
+ key: string
289
+ x: string
290
+ y: string
291
+ width: string | number
292
+ height: string | number
293
+ version: string
294
+ viewBox: string
295
+ geom: string
296
+ regionId?: string
297
+ }
298
+
299
+ interface PopupWarning {
300
+ id: string
301
+ type: string
302
+ severity: Severity
303
+ direction: number
304
+ text: string
305
+ interval: string
306
+ }
307
+
308
+ interface PanCoords {
309
+ x: number
310
+ y: number
311
+ }
312
+
313
+ export default defineComponent({
268
314
  name: 'MapLarge',
269
315
  components: { PopupRow },
270
- mixins: [config, i18n, Panzoom, utils],
271
316
  props: {
272
317
  index: {
273
- type: Number,
318
+ type: Number as PropType<number>,
319
+ default: 0,
274
320
  },
275
321
  input: {
276
- type: Object,
322
+ type: Object as PropType<DayRegions>,
323
+ default: () => ({}),
277
324
  },
278
325
  visibleWarnings: {
279
- type: Array,
326
+ type: Array as PropType<string[]>,
280
327
  default: () => [],
281
328
  },
282
329
  warnings: {
283
- type: Object,
330
+ type: Object as PropType<WarningsMap | null>,
284
331
  default: null,
285
332
  },
286
333
  geometryId: {
287
- type: Number,
334
+ type: Number as PropType<number>,
335
+ default: 2021,
288
336
  },
289
337
  loading: {
290
338
  type: Boolean,
291
339
  default: true,
292
340
  },
293
341
  theme: {
294
- type: String,
342
+ type: String as PropType<Theme | string>,
295
343
  default: 'light-theme',
296
344
  },
297
345
  language: {
298
- type: String,
346
+ type: String as PropType<Language | string>,
347
+ default: 'fi',
299
348
  },
300
349
  spinnerEnabled: {
301
350
  type: Boolean,
302
351
  default: true,
303
352
  },
304
353
  },
305
- setup() {
306
- const windowWidth = ref(window.innerWidth)
307
- const updateWidth = () => {
354
+ emits: ['loaded'],
355
+ setup(props) {
356
+ // Window width tracking
357
+ const windowWidth = ref<number>(
358
+ typeof window !== 'undefined' ? window.innerWidth : 0
359
+ )
360
+ const updateWidth = (): void => {
308
361
  windowWidth.value = window.innerWidth
309
362
  }
310
363
  onMounted(() => {
@@ -313,136 +366,261 @@ export default {
313
366
  onUnmounted(() => {
314
367
  window.removeEventListener('resize', updateWidth)
315
368
  })
316
- return { windowWidth }
369
+
370
+ // Get config
371
+ const config = useConfig()
372
+ const {
373
+ geometries,
374
+ colors,
375
+ regionIds,
376
+ warningIcon,
377
+ panLimits,
378
+ maxMergedWeight,
379
+ coverageCriterion,
380
+ } = config
381
+
382
+ // Setup i18n
383
+ const languageRef = toRef(props, 'language')
384
+ const { t } = useI18n(languageRef)
385
+
386
+ // Setup refs for useMapPaths
387
+ const size = computed<'Large' | 'Small'>(() => 'Large')
388
+ const scale = ref<number>(1)
389
+ const strokeWidthComputed = computed<number>(
390
+ () => 1 - (scale.value - 1) / scale.value
391
+ )
392
+
393
+ const indexRef = toRef(props, 'index')
394
+ const inputRef = toRef(props, 'input')
395
+ const warningsRef = toRef(props, 'warnings')
396
+ const visibleWarningsRef = toRef(props, 'visibleWarnings')
397
+ const geometryIdRef = toRef(props, 'geometryId')
398
+ const themeRef = toRef(props, 'theme')
399
+ const loadingRef = toRef(props, 'loading')
400
+
401
+ // Setup map paths composable
402
+ const {
403
+ strokeColor,
404
+ bluePaths,
405
+ greenPaths,
406
+ yellowPaths,
407
+ orangePaths,
408
+ redPaths,
409
+ overlayPaths,
410
+ landBorders,
411
+ seaBorders,
412
+ yellowCoverages,
413
+ orangeCoverages,
414
+ redCoverages,
415
+ overlayCoverages,
416
+ coverageRegions,
417
+ coverageWarnings,
418
+ regionData,
419
+ regionVisualization,
420
+ } = useMapPaths({
421
+ size,
422
+ index: indexRef,
423
+ input: inputRef,
424
+ warnings: warningsRef,
425
+ visibleWarnings: visibleWarningsRef,
426
+ geometryId: geometryIdRef,
427
+ theme: themeRef,
428
+ loading: loadingRef,
429
+ strokeWidth: strokeWidthComputed,
430
+ })
431
+
432
+ return {
433
+ windowWidth,
434
+ t,
435
+ config,
436
+ geometries,
437
+ colors,
438
+ regionIds,
439
+ warningIcon,
440
+ panLimits,
441
+ maxMergedWeight,
442
+ coverageCriterion,
443
+ size,
444
+ scale,
445
+ strokeColor,
446
+ bluePaths,
447
+ greenPaths,
448
+ yellowPaths,
449
+ orangePaths,
450
+ redPaths,
451
+ overlayPaths,
452
+ landBorders,
453
+ seaBorders,
454
+ yellowCoverages,
455
+ orangeCoverages,
456
+ redCoverages,
457
+ overlayCoverages,
458
+ coverageRegions,
459
+ coverageWarnings,
460
+ regionData,
461
+ regionVisualization,
462
+ }
317
463
  },
318
464
  data() {
319
465
  return {
320
- warningsDate: '',
321
- updated: '',
322
- updatedDate: '',
323
- atTime: '',
324
- updatedTime: '',
325
- dataProviderFirst: '',
326
- dataProviderSecond: '',
327
- mapText: '',
328
- actionStarted: false,
329
- dragging: false,
330
- showTooltip: false,
331
- tooltipX: 0,
332
- tooltipY: 0,
466
+ warningsDate: '' as string,
467
+ updated: '' as string,
468
+ updatedDate: '' as string,
469
+ atTime: '' as string,
470
+ updatedTime: '' as string,
471
+ dataProviderFirst: '' as string,
472
+ dataProviderSecond: '' as string,
473
+ mapText: '' as string,
474
+ actionStarted: false as boolean,
475
+ dragging: false as boolean,
476
+ showTooltip: false as boolean,
477
+ tooltipX: 0 as number,
478
+ tooltipY: 0 as number,
333
479
  pan: {
334
480
  x: 0,
335
481
  y: 0,
336
- },
337
- scale: 1,
338
- popupRegion: {},
339
- popupLevel: '',
340
- popupWarnings: [],
341
- coverageRegions: {},
342
- coverageWarnings: [],
343
- strokeOpacity: '0.5',
482
+ } as PanCoords,
483
+ popupRegion: {} as Partial<RegionGeometry>,
484
+ popupLevel: '' as string,
485
+ popupWarnings: [] as PopupWarning[],
486
+ strokeOpacity: '0.5' as string,
487
+ panzoom: null as PanzoomObject | null,
344
488
  }
345
489
  },
346
490
  computed: {
347
- moveStep() {
491
+ moveStep(): number {
348
492
  return 25
349
493
  },
350
- minIconDistSqr() {
494
+ minIconDistSqr(): number {
351
495
  return 500
352
496
  },
353
- iconDistStep() {
497
+ iconDistStep(): number {
354
498
  return 10
355
499
  },
356
- iconMaxIter() {
500
+ iconMaxIter(): number {
357
501
  return 40
358
502
  },
359
- zoomInText() {
503
+ zoomInText(): string {
360
504
  return this.t('zoomIn')
361
505
  },
362
- zoomOutText() {
506
+ zoomOutText(): string {
363
507
  return this.t('zoomOut')
364
508
  },
365
- moveText() {
509
+ moveText(): string {
366
510
  return this.t('moveMap')
367
511
  },
368
- tooltipStyle() {
512
+ tooltipStyle(): string {
369
513
  return `left: ${this.tooltipX}px; top: ${this.tooltipY}px`
370
514
  },
371
- size() {
372
- return 'Large'
373
- },
374
- strokeWidth() {
515
+ strokeWidth(): string {
375
516
  return String(1 - (this.scale - 1) / this.scale)
376
517
  },
377
- iconSize() {
518
+ iconSize(): number {
378
519
  return 28 - 4 * this.scale
379
520
  },
380
- maxWarningIcons() {
521
+ maxWarningIcons(): number {
381
522
  return this.scale + 1
382
523
  },
383
- icons() {
384
- const data = []
524
+ icons(): IconData[] {
525
+ const data: IconData[] = []
385
526
  const warnings = this.warnings
386
527
  const maxWarningIcons = this.maxWarningIcons
387
- this.regionIds.forEach((regionId) => {
528
+ const geometriesData = this.geometries as Record<
529
+ string,
530
+ Record<string, RegionGeometry>
531
+ >
532
+ const maxMergedWeightVal = this.maxMergedWeight as number
533
+
534
+ this.regionIds.forEach((regionId: string) => {
388
535
  const region = this.regionData(regionId)
389
- const geometry = this.geometries[this.geometryId][regionId]
536
+ const geometry = geometriesData?.[this.geometryId]?.[regionId]
390
537
  if (
538
+ geometry &&
391
539
  region != null &&
392
540
  geometry.children.length === 0 &&
393
541
  (!this.mergedRegions.has(regionId) ||
394
- (geometry.weight > this.maxMergedWeight && region?.warnings?.filter((warning) =>
395
- this.visibleWarnings.includes(warning.type)).length === 1) &&
396
- !(geometry?.parent?.length && this.regionData(geometry.parent)?.warnings?.some((warning) =>
397
- this.visibleWarnings.includes(warning.type))))
542
+ (geometry.weight > maxMergedWeightVal &&
543
+ region?.warnings?.filter((warning: { type: string }) =>
544
+ this.visibleWarnings.includes(warning.type)
545
+ ).length === 1 &&
546
+ !(
547
+ geometry?.parent?.length &&
548
+ this.regionData(geometry.parent)?.warnings?.some(
549
+ (warning: { type: string }) =>
550
+ this.visibleWarnings.includes(warning.type)
551
+ )
552
+ )))
398
553
  ) {
399
- const iconSizes = []
400
- const aspectRatios = []
401
- const keys = []
402
- const geoms = []
554
+ const iconSizes: [number, number][] = []
555
+ const aspectRatios: [number, number][] = []
556
+ const keys: string[] = []
557
+ const geoms: string[] = []
403
558
  region.warnings
404
- .filter((warning) => this.visibleWarnings.includes(warning.type) &&
405
- warning.coverage === 100 )
406
- .forEach((regionWarning, index, regionWarnings) => {
407
- const identifier = regionWarning.identifiers.find(
408
- (id) => warnings[id] && warnings[id].covRegions.size === 0
409
- )
410
- if (identifier && iconSizes.length < maxWarningIcons) {
411
- const icon =
412
- iconSizes.length === maxWarningIcons - 1 &&
413
- regionWarnings.length > maxWarningIcons
414
- ? this.warningIcon({ type: this.MULTIPLE })
415
- : this.warningIcon(warnings[identifier])
416
- const scale = icon.scale ? icon.scale : 1
417
- const width =
418
- (scale * icon.aspectRatio[0] * this.iconSize) /
419
- icon.aspectRatio[1]
420
- const height = scale * this.iconSize + 6
421
- iconSizes.push([width, height])
422
- aspectRatios.push(icon.aspectRatio)
423
- geoms.push(icon.geom)
424
- keys.push(`${regionId}-${identifier}`)
559
+ .filter(
560
+ (warning: { type: string; coverage: number }) =>
561
+ this.visibleWarnings.includes(warning.type) &&
562
+ warning.coverage === 100
563
+ )
564
+ .forEach(
565
+ (
566
+ regionWarning: { identifiers: string[] },
567
+ _index: number,
568
+ regionWarnings: { identifiers: string[] }[]
569
+ ) => {
570
+ const identifier = regionWarning.identifiers.find(
571
+ (id: string) =>
572
+ warnings?.[id] && warnings[id].covRegions.size === 0
573
+ )
574
+ if (identifier && iconSizes.length < maxWarningIcons) {
575
+ const warningData = warnings![identifier]
576
+ const icon =
577
+ iconSizes.length === maxWarningIcons - 1 &&
578
+ regionWarnings.length > maxWarningIcons
579
+ ? this.warningIcon({ type: MULTIPLE, severity: 0 })
580
+ : warningData
581
+ ? this.warningIcon(warningData)
582
+ : null
583
+ if (!icon) return
584
+ const iconScale = icon.scale ? icon.scale : 1
585
+ const width =
586
+ (iconScale * icon.aspectRatio[0] * this.iconSize) /
587
+ icon.aspectRatio[1]
588
+ const height = iconScale * this.iconSize + 6
589
+ iconSizes.push([width, height])
590
+ aspectRatios.push(icon.aspectRatio)
591
+ geoms.push(icon.geom || '')
592
+ keys.push(`${regionId}-${identifier}`)
593
+ }
425
594
  }
426
- })
595
+ )
596
+ const regionGeom = geometriesData[this.geometryId]?.[regionId] as
597
+ | RegionGeometry
598
+ | undefined
599
+ if (!regionGeom) return
600
+ const lastIconWidth = iconSizes[iconSizes.length - 1]?.[0] ?? 0
427
601
  let offsetX =
428
- iconSizes.length > 0 &&
429
- this.geometries[this.geometryId][regionId].align === 'right'
602
+ iconSizes.length > 0 && regionGeom.align === 'right'
430
603
  ? -iconSizes.reduce(
431
604
  (acc, iconSize) => acc + iconSize[0],
432
- -iconSizes[iconSizes.length - 1][0] / 2
605
+ -lastIconWidth / 2
433
606
  )
434
607
  : -iconSizes.reduce((acc, iconSize) => acc + iconSize[0], 0) / 2
435
- const coords = this.geometries[this.geometryId][regionId].center
436
- iconSizes.forEach((iconSize, index) => {
608
+ const coords = regionGeom.center
609
+ if (!coords) return
610
+ iconSizes.forEach((iconSize, idx) => {
611
+ const aspectRatio = aspectRatios[idx]
612
+ const geom = geoms[idx]
613
+ const key = keys[idx]
614
+ if (!aspectRatio || geom == null || !key) return
437
615
  data.push({
438
- key: keys[index],
616
+ key,
439
617
  x: `${coords[0] + offsetX}px`,
440
618
  y: `${coords[1] - iconSize[1] / 2}px`,
441
619
  width: `${iconSize[0]}px`,
442
620
  height: `${iconSize[1]}px`,
443
621
  version: '1.1',
444
- viewBox: `0 0 ${aspectRatios[index][0]} ${aspectRatios[index][1]}`,
445
- geom: geoms[index],
622
+ viewBox: `0 0 ${aspectRatio[0]} ${aspectRatio[1]}`,
623
+ geom,
446
624
  regionId,
447
625
  })
448
626
  offsetX += iconSize[0]
@@ -451,95 +629,129 @@ export default {
451
629
  })
452
630
  return data
453
631
  },
454
- coverageIcons() {
632
+ coverageIcons(): IconData[] {
455
633
  const warnings = this.warnings
456
- return this.coverageWarnings.reduce((iconData, warningId) => {
457
- const warning = warnings[warningId]
458
- if (
459
- this.visibleWarnings.includes(warning.type) &&
460
- warning.coveragesLarge.length > 0
461
- ) {
462
- let reference = warning.coveragesLarge[0].reference
463
- let iterIndex = 0
464
- let radius
465
- let angle
466
- // Prevent too close warning symbols
467
- while (
468
- !this.validIconLocation(reference, warningId) &&
469
- iterIndex < this.iconMaxIter
634
+
635
+ return this.coverageWarnings.reduce(
636
+ (iconData: IconData[], warningId: string) => {
637
+ const warning = warnings?.[warningId]
638
+ const coverageLarge = warning?.coveragesLarge[0]
639
+ const baseReference = coverageLarge?.reference
640
+ if (
641
+ warning &&
642
+ this.visibleWarnings.includes(warning.type) &&
643
+ warning.coveragesLarge.length > 0 &&
644
+ baseReference &&
645
+ baseReference.length === 2
470
646
  ) {
471
- angle = 0.25 * Math.PI * iterIndex
472
- iterIndex++
473
- radius = Math.ceil(iterIndex / 8) * this.iconDistStep
474
- reference = [
475
- warning.coveragesLarge[0].reference[0] + radius * Math.cos(angle),
476
- warning.coveragesLarge[0].reference[1] + radius * Math.sin(angle),
647
+ let reference: [number, number] = [
648
+ baseReference[0],
649
+ baseReference[1],
477
650
  ]
651
+ let iterIndex = 0
652
+ let radius: number
653
+ let angle: number
654
+ // Prevent too close warning symbols
655
+ while (
656
+ !this.validIconLocation(reference, warningId) &&
657
+ iterIndex < this.iconMaxIter
658
+ ) {
659
+ angle = 0.25 * Math.PI * iterIndex
660
+ iterIndex++
661
+ radius = Math.ceil(iterIndex / 8) * this.iconDistStep
662
+ reference = [
663
+ baseReference[0] + radius * Math.cos(angle),
664
+ baseReference[1] + radius * Math.sin(angle),
665
+ ]
666
+ }
667
+ if (iterIndex >= this.iconMaxIter) {
668
+ reference = [baseReference[0], baseReference[1]]
669
+ }
670
+ const icon = this.warningIcon(warning)
671
+ const iconScale = icon.scale ? icon.scale : 1
672
+ const width =
673
+ (iconScale * icon.aspectRatio[0] * this.iconSize) /
674
+ icon.aspectRatio[1]
675
+ const height = iconScale * this.iconSize
676
+ iconData.push({
677
+ key: warningId + Math.random(),
678
+ x: `${reference[0] - width / 2}px`,
679
+ y: `${reference[1] - height / 2}px`,
680
+ width,
681
+ height,
682
+ version: '1.1',
683
+ viewBox: `0 0 ${icon.aspectRatio[0]} ${icon.aspectRatio[1]}`,
684
+ geom: icon.geom || '',
685
+ })
478
686
  }
479
- if (iterIndex >= this.iconMaxIter) {
480
- reference = warning.coveragesLarge[0].reference
481
- }
482
- const icon = this.warningIcon(warning)
483
- const scale = icon.scale ? icon.scale : 1
484
- const width =
485
- (scale * icon.aspectRatio[0] * this.iconSize) / icon.aspectRatio[1]
486
- const height = scale * this.iconSize
487
- iconData.push({
488
- key: warningId + Math.random(),
489
- x: `${reference[0] - width / 2}px`,
490
- y: `${reference[1] - height / 2}px`,
491
- width,
492
- height,
493
- version: '1.1',
494
- viewBox: `0 0 ${icon.aspectRatio[0]} ${icon.aspectRatio[1]}`,
495
- geom: icon.geom,
496
- })
497
- }
498
- return iconData
499
- }, [])
687
+ return iconData
688
+ },
689
+ []
690
+ )
500
691
  },
501
- regionTitle() {
502
- return this.t(this.popupRegion.name)
692
+ regionTitle(): string {
693
+ return this.t(this.popupRegion.name || '')
503
694
  },
504
- regionSets() {
505
- const map = new Map()
695
+ regionSets(): Map<string, Set<string>> {
696
+ const map = new Map<string, Set<string>>()
506
697
  const warnings = this.warnings
698
+ const geometriesData = this.geometries as Record<
699
+ string,
700
+ Record<string, RegionGeometry>
701
+ >
702
+
703
+ if (!this.input?.land) return map
704
+
705
+ const geomYear = geometriesData[this.geometryId]
507
706
  this.input.land
508
707
  .filter(
509
- (regionItem) =>
510
- this.geometries[this.geometryId][regionItem.key].neighbours.length >
511
- 0
708
+ (regionItem: { key: string }) =>
709
+ (geomYear?.[regionItem.key]?.neighbours?.length ?? 0) > 0
512
710
  )
513
- .forEach((regionItem) => {
514
- const serialized = regionItem.warnings.reduce((reduced, warning) => {
515
- if (!this.visibleWarnings.includes(warning.type)) {
516
- return reduced
517
- }
518
- const warningIdentifier = warning.identifiers.find((identifier) => {
519
- const warningById = warnings[identifier]
520
- return (
521
- Object.keys(warningById.regions).length >
522
- warningById.covRegions.size
523
- )
524
- })
525
- if (warningIdentifier == null) {
526
- return reduced
711
+ .forEach(
712
+ (regionItem: {
713
+ key: string
714
+ warnings: Array<{ type: string; identifiers: string[] }>
715
+ }) => {
716
+ const serialized = regionItem.warnings.reduce(
717
+ (reduced: string, warning) => {
718
+ if (!this.visibleWarnings.includes(warning.type)) {
719
+ return reduced
720
+ }
721
+ const warningIdentifier = warning.identifiers.find(
722
+ (identifier) => {
723
+ const warningById = warnings?.[identifier]
724
+ return (
725
+ warningById &&
726
+ Object.keys(warningById.regions).length >
727
+ warningById.covRegions.size
728
+ )
729
+ }
730
+ )
731
+ if (warningIdentifier == null) {
732
+ return reduced
733
+ }
734
+ const w = warnings?.[warningIdentifier]
735
+ if (!w) return reduced
736
+ return `${reduced}:${w.type}:${w.severity}:${w.value}:${w.direction}`
737
+ },
738
+ ''
739
+ )
740
+ if (serialized) {
741
+ const set = map.has(serialized)
742
+ ? map.get(serialized)!
743
+ : new Set<string>()
744
+ set.add(regionItem.key)
745
+ map.set(serialized, set)
527
746
  }
528
- const w = warnings[warningIdentifier]
529
- return `${reduced}:${w.type}:${w.severity}:${w.value}:${w.direction}`
530
- }, '')
531
- if (serialized) {
532
- const set = map.has(serialized) ? map.get(serialized) : new Set()
533
- set.add(regionItem.key)
534
- map.set(serialized, set)
535
747
  }
536
- })
748
+ )
537
749
  return map
538
750
  },
539
- networks() {
540
- let allNetworks = []
751
+ networks(): string[][] {
752
+ let allNetworks: Set<string>[] = []
541
753
  this.regionSets.forEach((regionSet) => {
542
- const networks = []
754
+ const networks: Set<string>[] = []
543
755
  regionSet.forEach((region) => {
544
756
  networks.push(new Set([region]))
545
757
  })
@@ -547,7 +759,7 @@ export default {
547
759
  while (this.mergeNetworks(networks)) {}
548
760
  allNetworks = allNetworks.concat(networks)
549
761
  })
550
- const arrayNetworks = []
762
+ const arrayNetworks: string[][] = []
551
763
  allNetworks.forEach((network) => {
552
764
  if (network.size > 1) {
553
765
  arrayNetworks.push(Array.from(network.keys()))
@@ -555,52 +767,63 @@ export default {
555
767
  })
556
768
  return arrayNetworks
557
769
  },
558
- networkCenters() {
770
+ networkCenters(): [number, number][] {
771
+ const geometriesData = this.geometries as Record<
772
+ string,
773
+ Record<string, RegionGeometry>
774
+ >
775
+ const geomYear = geometriesData[this.geometryId]
776
+
559
777
  return this.networks.map((network) => {
560
778
  const arrayNetwork = Array.from(network)
561
- const weightSum = arrayNetwork.reduce(
562
- (sum, region) =>
563
- sum + this.geometries[this.geometryId][region].weight,
564
- 0
565
- )
779
+ const weightSum = arrayNetwork.reduce((sum, region) => {
780
+ const geom = geomYear?.[region]
781
+ return sum + (geom?.weight ?? 0)
782
+ }, 0)
566
783
  return arrayNetwork
567
784
  .reduce(
568
785
  (sum, region) => {
569
- const geom = this.geometries[this.geometryId][region]
570
- return sum.map(
571
- (sumByIndex, index) =>
572
- sumByIndex + geom.weight * geom.center[index]
573
- )
786
+ const geom = geomYear?.[region]
787
+ if (!geom?.center) return sum
788
+ return [
789
+ sum[0] + geom.weight * geom.center[0],
790
+ sum[1] + geom.weight * geom.center[1],
791
+ ] as [number, number]
574
792
  },
575
- [0, 0]
793
+ [0, 0] as [number, number]
576
794
  )
577
- .map((weightedSumByIndex) => weightedSumByIndex / weightSum)
795
+ .map((weightedSumByIndex) => weightedSumByIndex / weightSum) as [
796
+ number,
797
+ number,
798
+ ]
578
799
  })
579
800
  },
580
- networkReps() {
581
- return this.networks.map(
582
- (network, networkIndex) =>
583
- network[
584
- this.indexOfSmallest(
585
- network.map(
586
- (region) =>
587
- [0, 1].reduce(
588
- (sum, coordIndex) =>
589
- sum +
590
- (this.geometries[this.geometryId][region].center[
591
- coordIndex
592
- ] -
593
- this.networkCenters[networkIndex][coordIndex]) **
594
- 2,
595
- 0
596
- ) / this.geometries[this.geometryId][region].weight
597
- )
801
+ networkReps(): string[] {
802
+ const geometriesData = this.geometries as Record<
803
+ string,
804
+ Record<string, RegionGeometry>
805
+ >
806
+ const geomYear = geometriesData[this.geometryId]
807
+
808
+ return this.networks
809
+ .map((network, networkIndex) => {
810
+ const distances = network.map((region) => {
811
+ const geom = geomYear?.[region]
812
+ if (!geom?.center) return Infinity
813
+ const networkCenter = this.networkCenters[networkIndex]
814
+ if (!networkCenter) return Infinity
815
+ return (
816
+ ((geom.center[0] - networkCenter[0]) ** 2 +
817
+ (geom.center[1] - networkCenter[1]) ** 2) /
818
+ geom.weight
598
819
  )
599
- ]
600
- )
820
+ })
821
+ return network[this.indexOfSmallest(distances)]
822
+ })
823
+ .filter((rep): rep is string => rep !== undefined)
601
824
  },
602
- mergedRegions() {
603
- const merged = new Set()
825
+ mergedRegions(): Set<string> {
826
+ const merged = new Set<string>()
604
827
  this.networks.forEach((network, index) => {
605
828
  network.forEach((region) => {
606
829
  if (region !== this.networkReps[index]) {
@@ -612,7 +835,7 @@ export default {
612
835
  },
613
836
  },
614
837
  watch: {
615
- scale() {
838
+ scale(): void {
616
839
  if (this.panzoom != null) {
617
840
  if (this.scale === 1) {
618
841
  this.panzoom.setOptions({
@@ -628,27 +851,30 @@ export default {
628
851
  }
629
852
  }
630
853
  },
631
- input() {
854
+ input(): void {
632
855
  this.coverageRegions = {}
633
856
  this.coverageWarnings = []
634
857
  },
635
- warnings() {
858
+ warnings(): void {
636
859
  this.showTooltip = false
637
860
  },
638
- visibleWarnings() {
861
+ visibleWarnings(): void {
639
862
  this.showTooltip = false
640
863
  },
641
- windowWidth() {
864
+ windowWidth(): void {
642
865
  this.showTooltip = false
643
- if (this.$refs.zoomButton.clientHeight === 0 && this.scale > 1) {
866
+ const zoomButton = this.$refs.zoomButton as HTMLButtonElement | undefined
867
+ if (zoomButton?.clientHeight === 0 && this.scale > 1) {
644
868
  this.scale = 1
645
869
  }
646
870
  },
647
871
  },
648
872
  mounted() {
649
- if (this.isClientSide()) {
650
- const finlandLarge = this.$el.querySelector('svg#finland-large')
651
- if (this.isAttached(finlandLarge)) {
873
+ if (isClientSide()) {
874
+ const finlandLarge = this.$el.querySelector(
875
+ 'svg#finland-large'
876
+ ) as SVGSVGElement | null
877
+ if (finlandLarge && this.isAttached(finlandLarge)) {
652
878
  this.panzoom = Panzoom(finlandLarge, {
653
879
  disableZoom: true,
654
880
  panOnlyWhenZoomed: true,
@@ -659,7 +885,7 @@ export default {
659
885
  touchAction: '',
660
886
  })
661
887
  finlandLarge.addEventListener('panzoomzoom', () => {
662
- this.scale = this.panzoom.getScale()
888
+ this.scale = this.panzoom!.getScale()
663
889
  this.showTooltip = false
664
890
  })
665
891
  finlandLarge.addEventListener('panzoompan', (event) => {
@@ -667,12 +893,12 @@ export default {
667
893
  if (!this.actionStarted) {
668
894
  return
669
895
  }
670
- const eventDetail = event.detail
896
+ const eventDetail = (event as CustomEvent).detail as PanCoords | null
671
897
  if (eventDetail == null) {
672
898
  return
673
899
  }
674
900
  let panned = false
675
- ;['x', 'y'].forEach((axis) => {
901
+ ;(['x', 'y'] as const).forEach((axis) => {
676
902
  if (eventDetail[axis] !== this.pan[axis]) {
677
903
  this.pan[axis] = eventDetail[axis]
678
904
  panned = true
@@ -703,70 +929,53 @@ export default {
703
929
  }
704
930
  },
705
931
  methods: {
706
- paths(options) {
707
- return this.regionIds.reduce((regions, regionId) => {
708
- if (
709
- this.geometries[this.geometryId][regionId].pathLarge &&
710
- (this.geometries[this.geometryId][regionId].type === options.type) ===
711
- (this.geometries[this.geometryId][regionId].subType == null)
712
- ) {
713
- const visualization = this.regionVisualization(regionId)
714
- if (
715
- options.severity == null ||
716
- visualization.severity === options.severity
717
- ) {
718
- regions.push({
719
- key: `${regionId}${this.size}${this.index}Path`,
720
- fill:
721
- this.loading && this.isClientSide()
722
- ? this.colors[this.theme].missing
723
- : visualization.color,
724
- d: visualization.visible ? visualization.geom.pathLarge : '',
725
- opacity: '1',
726
- dataRegion: regionId,
727
- dataSeverity: visualization.severity,
728
- strokeWidth:
729
- this.geometries[this.geometryId][regionId].type === 'sea' &&
730
- this.geometries[this.geometryId][regionId].subType !== 'lake'
731
- ? this.strokeWidth
732
- : 0,
733
- })
734
- }
735
- }
736
- return regions
737
- }, [])
738
- },
739
- regionClicked(event) {
740
- const regionId = event.target.getAttribute('data-region')
741
- let severity = Number(event.target.getAttribute('data-severity'))
742
- this.popupRegion = this.geometries[this.geometryId][regionId]
743
- const region = this.input[this.popupRegion.type].find(
744
- (regionWarning) => regionWarning.key === regionId
932
+ regionClicked(event: MouseEvent): void {
933
+ const target = event.target as SVGPathElement
934
+ const regionId = target.getAttribute('data-region')
935
+ if (!regionId) return
936
+
937
+ let severity = Number(target.getAttribute('data-severity'))
938
+ const geometriesData = this.geometries as Record<
939
+ string,
940
+ Record<string, RegionGeometry>
941
+ >
942
+ const coverageCriterionVal = this.coverageCriterion as number
943
+
944
+ const regionGeom = geometriesData[this.geometryId]?.[regionId]
945
+ if (!regionGeom) return
946
+ this.popupRegion = regionGeom
947
+ const regionType = this.popupRegion.type as 'land' | 'sea'
948
+ const region = this.input?.[regionType]?.find(
949
+ (regionWarning: { key: string }) => regionWarning.key === regionId
745
950
  )
746
- let popupWarnings = []
951
+ let popupWarningsData: PopupWarning[] = []
747
952
  if (region != null) {
748
953
  region.warnings
749
954
  .filter(
750
- (warning) =>
955
+ (warning: { type: string; coverage: number }) =>
751
956
  this.visibleWarnings.includes(warning.type) &&
752
- warning.coverage >= this.coverageCriterion
957
+ warning.coverage >= coverageCriterionVal
753
958
  )
754
- .forEach((warningByType) => {
959
+ .forEach((warningByType: { type: string; identifiers: string[] }) => {
755
960
  warningByType.identifiers.forEach((identifier) => {
756
- const warning = this.warnings[identifier]
757
- popupWarnings.push({
758
- type: warningByType.type,
759
- severity: warning.severity,
760
- direction: warning.direction,
761
- text: warning.text != null ? warning.text : '',
762
- interval: warning.validInterval,
763
- })
961
+ const warning = this.warnings?.[identifier]
962
+ if (warning) {
963
+ popupWarningsData.push({
964
+ id: identifier,
965
+ type: warningByType.type,
966
+ severity: warning.severity,
967
+ direction: warning.direction,
968
+ text: warning.text != null ? warning.text : '',
969
+ interval: warning.validInterval,
970
+ })
971
+ }
764
972
  })
765
973
  })
766
974
  }
767
- if (popupWarnings.length === 0) {
768
- popupWarnings = [
975
+ if (popupWarningsData.length === 0) {
976
+ popupWarningsData = [
769
977
  {
978
+ id: 'no-warnings',
770
979
  type: '',
771
980
  severity: 0,
772
981
  direction: 0,
@@ -781,53 +990,66 @@ export default {
781
990
  severity = this.coverageRegions[regionId]
782
991
  }
783
992
  this.popupLevel = `level-${severity}`
784
- this.popupWarnings = popupWarnings
785
- const mapRect = this.$refs.dayMapLarge.getBoundingClientRect()
993
+ this.popupWarnings = popupWarningsData
994
+ const dayMapLarge = this.$refs.dayMapLarge as HTMLDivElement | undefined
995
+ const mapRect = dayMapLarge?.getBoundingClientRect()
786
996
  if (
787
- [
788
- mapRect,
789
- mapRect.x,
790
- mapRect.y,
791
- window,
792
- window.scrollX,
793
- window.scrollY,
794
- ].every((item) => item != null)
997
+ mapRect &&
998
+ [mapRect.x, mapRect.y, window.scrollX, window.scrollY].every(
999
+ (item) => item != null
1000
+ )
795
1001
  ) {
796
1002
  this.tooltipX = event.pageX - mapRect.x - window.scrollX
797
1003
  this.tooltipY = event.pageY - mapRect.y - window.scrollY
798
1004
  this.showTooltip = true
799
1005
  }
800
1006
  },
801
- validIconLocation(coord, warningId) {
1007
+ validIconLocation(coord: [number, number], warningId: string): boolean {
802
1008
  const warnings = this.warnings
803
- const warning = warnings[warningId]
804
- const activeIconRegions = {}
1009
+ const warning = warnings?.[warningId]
1010
+ if (!warning) return true
1011
+
1012
+ const geometriesData = this.geometries as Record<
1013
+ string,
1014
+ Record<string, RegionGeometry>
1015
+ >
1016
+
1017
+ const activeIconRegions: Record<string, boolean> = {}
805
1018
  this.icons.forEach((icon) => {
806
- activeIconRegions[icon.regionId] = true
1019
+ if (icon.regionId) {
1020
+ activeIconRegions[icon.regionId] = true
1021
+ }
807
1022
  })
1023
+ const geomYear = geometriesData[this.geometryId]
808
1024
  return ![...warning.covRegions.keys()].some((covRegion) => {
809
1025
  if (!activeIconRegions[covRegion]) {
810
1026
  return false
811
1027
  }
812
- const center = this.geometries[this.geometryId][covRegion].center
1028
+ const center = geomYear?.[covRegion]?.center
1029
+ if (!center) return false
813
1030
  return (
814
1031
  (center[0] - coord[0]) ** 2 + (center[1] - coord[1]) ** 2 <
815
1032
  this.minIconDistSqr
816
1033
  )
817
1034
  })
818
1035
  },
819
- mergeNetworks(networks) {
1036
+ mergeNetworks(networks: Set<string>[]): boolean {
1037
+ const geometriesData = this.geometries as Record<
1038
+ string,
1039
+ Record<string, RegionGeometry>
1040
+ >
1041
+ const geomYear = geometriesData[this.geometryId]
1042
+
820
1043
  return networks.some((network1, index1) => {
821
1044
  const neighbours = Array.from(network1.keys()).reduce(
822
- (reduced, region) => {
823
- this.geometries[this.geometryId][region].neighbours.forEach(
824
- (neighbour) => {
825
- reduced.add(neighbour)
826
- }
827
- )
1045
+ (reduced: Set<string>, region: string) => {
1046
+ const regionGeom = geomYear?.[region]
1047
+ regionGeom?.neighbours?.forEach((neighbour: string) => {
1048
+ reduced.add(neighbour)
1049
+ })
828
1050
  return reduced
829
1051
  },
830
- new Set()
1052
+ new Set<string>()
831
1053
  )
832
1054
  return networks.some((network2, index2) => {
833
1055
  if (index2 <= index1) {
@@ -837,7 +1059,10 @@ export default {
837
1059
  (neighbour) => network2.has(neighbour)
838
1060
  )
839
1061
  if (ngbrIndex >= 0) {
840
- network2.forEach(networks[index1].add, networks[index1])
1062
+ const targetNetwork = networks[index1]
1063
+ if (targetNetwork) {
1064
+ network2.forEach(targetNetwork.add, targetNetwork)
1065
+ }
841
1066
  networks.splice(index2, 1)
842
1067
  return true
843
1068
  }
@@ -845,68 +1070,79 @@ export default {
845
1070
  })
846
1071
  })
847
1072
  },
848
- indexOfSmallest(array) {
1073
+ indexOfSmallest(array: number[]): number {
849
1074
  let lowest = 0
850
1075
  for (let i = 1; i < array.length; i++) {
851
- if (array[i] < array[lowest]) lowest = i
1076
+ const current = array[i]
1077
+ const lowestVal = array[lowest]
1078
+ if (
1079
+ current !== undefined &&
1080
+ lowestVal !== undefined &&
1081
+ current < lowestVal
1082
+ ) {
1083
+ lowest = i
1084
+ }
852
1085
  }
853
1086
  return lowest
854
1087
  },
855
- zoomIn() {
1088
+ zoomIn(): void {
856
1089
  if (this.panzoom != null) {
857
1090
  this.panzoom.zoom(this.panzoom.getScale() + 1, {
858
1091
  force: true,
859
1092
  })
860
1093
  }
861
1094
  },
862
- zoomOut() {
1095
+ zoomOut(): void {
863
1096
  if (this.panzoom != null) {
864
1097
  this.panzoom.zoom(this.panzoom.getScale() - 1, {
865
1098
  force: true,
866
1099
  })
867
1100
  }
868
1101
  },
869
- closeTooltip(event) {
1102
+ closeTooltip(event: MouseEvent): void {
870
1103
  event.preventDefault()
871
1104
  this.showTooltip = false
872
1105
  },
873
- moveWest(event) {
1106
+ moveWest(event: KeyboardEvent): void {
874
1107
  event.preventDefault()
875
- this.panzoom.pan(this.moveStep, 0, {
1108
+ this.panzoom?.pan(this.moveStep, 0, {
876
1109
  relative: true,
877
1110
  })
878
1111
  this.limitPan()
879
1112
  },
880
- moveEast(event) {
1113
+ moveEast(event: KeyboardEvent): void {
881
1114
  event.preventDefault()
882
- this.panzoom.pan(-this.moveStep, 0, {
1115
+ this.panzoom?.pan(-this.moveStep, 0, {
883
1116
  relative: true,
884
1117
  })
885
1118
  this.limitPan()
886
1119
  },
887
- moveNorth(event) {
1120
+ moveNorth(event: KeyboardEvent): void {
888
1121
  event.preventDefault()
889
- this.panzoom.pan(0, this.moveStep, {
1122
+ this.panzoom?.pan(0, this.moveStep, {
890
1123
  relative: true,
891
1124
  })
892
1125
  this.limitPan()
893
1126
  },
894
- moveSouth(event) {
1127
+ moveSouth(event: KeyboardEvent): void {
895
1128
  event.preventDefault()
896
- this.panzoom.pan(0, -this.moveStep, {
1129
+ this.panzoom?.pan(0, -this.moveStep, {
897
1130
  relative: true,
898
1131
  })
899
1132
  this.limitPan()
900
1133
  },
901
- limitPan() {
1134
+ limitPan(): void {
1135
+ if (!this.panzoom) return
1136
+
902
1137
  const pan = this.panzoom.getPan()
1138
+ const panLimitsVal = this.panLimits as { x: number; y: number }
903
1139
  let panChanged = false
904
- ;['x', 'y'].forEach((coord) => {
905
- if (pan[coord] > this.panLimits[coord]) {
906
- pan[coord] = this.panLimits[coord]
1140
+ ;(['x', 'y'] as const).forEach((coord) => {
1141
+ if (pan[coord] > panLimitsVal[coord]) {
1142
+ pan[coord] = panLimitsVal[coord]
907
1143
  panChanged = true
908
- } else if (pan[coord] < -this.panLimits[coord]) {
909
- pan[coord] = -this.panLimits[coord]
1144
+ } else if (pan[coord] < -panLimitsVal[coord]) {
1145
+ pan[coord] = -panLimitsVal[coord]
910
1146
  panChanged = true
911
1147
  }
912
1148
  })
@@ -914,20 +1150,21 @@ export default {
914
1150
  this.panzoom.pan(pan.x, pan.y)
915
1151
  }
916
1152
  },
917
- isAttached(node) {
918
- let currentNode = node
1153
+ isAttached(node: Node | null): boolean {
1154
+ let currentNode: Node | null = node
919
1155
  while (currentNode != null && currentNode.parentNode != null) {
920
1156
  if (currentNode.parentNode === document) {
921
1157
  return true
922
1158
  }
923
- currentNode = currentNode.parentNode instanceof ShadowRoot
924
- ? currentNode.parentNode.host
925
- : currentNode.parentNode
1159
+ currentNode =
1160
+ currentNode.parentNode instanceof ShadowRoot
1161
+ ? currentNode.parentNode.host
1162
+ : currentNode.parentNode
926
1163
  }
927
1164
  return false
928
- }
1165
+ },
929
1166
  },
930
- }
1167
+ })
931
1168
  </script>
932
1169
 
933
1170
  <style lang="scss">
@@ -975,22 +1212,8 @@ button.fmi-warnings-map-tool {
975
1212
  background-repeat: no-repeat;
976
1213
  background-position: center;
977
1214
  cursor: pointer;
978
- }
979
-
980
- .light-theme button.fmi-warnings-map-tool {
981
- border-color: $light-button-border-color;
982
- }
983
-
984
- .dark-theme button.fmi-warnings-map-tool {
985
- border-color: $dark-button-border-color;
986
- }
987
-
988
- .light-gray-theme button.fmi-warnings-map-tool {
989
- border-color: $light-gray-button-border-color;
990
- }
991
-
992
- .dark-gray-theme button.fmi-warnings-map-tool {
993
- border-color: $dark-gray-button-border-color;
1215
+ border: none;
1216
+ padding: 0;
994
1217
  }
995
1218
 
996
1219
  div.map-large div.day-map-large button {