@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.
- package/.eslintignore +2 -14
- package/.github/workflows/test.yaml +26 -0
- package/.nvmrc +1 -0
- package/AGENTS.md +26 -0
- package/index.html +1 -1
- package/package.json +80 -22
- package/src/AlertClientVue.vue +160 -0
- package/src/App.vue +154 -296
- package/src/assets/img/ui/arrow-down.svg +4 -11
- package/src/assets/img/ui/arrow-up.svg +4 -11
- package/src/assets/img/ui/clear.svg +7 -21
- package/src/assets/img/ui/close.svg +4 -15
- package/src/assets/img/ui/toggle-selected.svg +5 -6
- package/src/assets/img/ui/toggle-unselected.svg +5 -6
- package/src/assets/img/warning/cold-weather.svg +3 -6
- package/src/assets/img/warning/flood-level-3.svg +4 -7
- package/src/assets/img/warning/forest-fire-weather.svg +2 -6
- package/src/assets/img/warning/grass-fire-weather.svg +2 -6
- package/src/assets/img/warning/hot-weather.svg +3 -6
- package/src/assets/img/warning/pedestrian-safety.svg +3 -7
- package/src/assets/img/warning/rain.svg +2 -7
- package/src/assets/img/warning/sea-icing.svg +2 -6
- package/src/assets/img/warning/sea-thunder-storm.svg +2 -5
- package/src/assets/img/warning/sea-water-height-high-water.svg +3 -8
- package/src/assets/img/warning/sea-water-height-shallow-water.svg +3 -7
- package/src/assets/img/warning/sea-wave-height.svg +4 -7
- package/src/assets/img/warning/sea-wind-legend.svg +2 -5
- package/src/assets/img/warning/sea-wind.svg +2 -5
- package/src/assets/img/warning/several.svg +2 -5
- package/src/assets/img/warning/thunder-storm.svg +2 -5
- package/src/assets/img/warning/traffic-weather.svg +2 -6
- package/src/assets/img/warning/uv-note.svg +2 -6
- package/src/assets/img/warning/wind.svg +2 -5
- package/src/components/AlertClient.vue +330 -251
- package/src/components/CollapsiblePanel.vue +281 -0
- package/src/components/DayLarge.vue +146 -110
- package/src/components/DaySmall.vue +97 -81
- package/src/components/Days.vue +229 -159
- package/src/components/DescriptionWarning.vue +63 -38
- package/src/components/GrayScaleToggle.vue +58 -54
- package/src/components/Legend.vue +102 -325
- package/src/components/MapLarge.vue +574 -351
- package/src/components/MapSmall.vue +137 -122
- package/src/components/PopupRow.vue +24 -12
- package/src/components/Region.vue +168 -118
- package/src/components/RegionWarning.vue +40 -33
- package/src/components/Regions.vue +189 -105
- package/src/components/Warning.vue +70 -45
- package/src/components/Warnings.vue +136 -72
- package/src/composables/useAlertClient.ts +360 -0
- package/src/composables/useConfig.ts +573 -0
- package/src/composables/useFields.ts +66 -0
- package/src/composables/useI18n.ts +62 -0
- package/src/composables/useKeyCodes.ts +16 -0
- package/src/composables/useMapPaths.ts +477 -0
- package/src/composables/useUtils.ts +683 -0
- package/src/composables/useWarningsProcessor.ts +1007 -0
- package/src/data/geometries.json +993 -0
- package/src/{main.js → main.ts} +1 -0
- package/src/mixins/geojsonsvg.d.ts +57 -0
- package/src/mixins/geojsonsvg.js +5 -3
- package/src/plugins/index.ts +5 -0
- package/src/scss/_utilities.scss +193 -0
- package/src/scss/constants.scss +2 -1
- package/src/scss/warningImages.scss +8 -3
- package/src/types/index.ts +509 -0
- package/src/vite-env.d.ts +23 -0
- package/src/vue.ts +41 -0
- package/svgo.config.js +45 -0
- package/tests/README.md +430 -0
- package/tests/fixtures/mockWarningData.ts +152 -0
- package/tests/integration/warning-flow.spec.ts +445 -0
- package/tests/setup.ts +41 -0
- package/tests/unit/components/AlertClient.spec.ts +701 -0
- package/tests/unit/components/DayLarge.spec.ts +348 -0
- package/tests/unit/components/DaySmall.spec.ts +352 -0
- package/tests/unit/components/Days.spec.ts +548 -0
- package/tests/unit/components/DescriptionWarning.spec.ts +385 -0
- package/tests/unit/components/GrayScaleToggle.spec.ts +318 -0
- package/tests/unit/components/Legend.spec.ts +295 -0
- package/tests/unit/components/MapLarge.spec.ts +448 -0
- package/tests/unit/components/MapSmall.spec.ts +367 -0
- package/tests/unit/components/PopupRow.spec.ts +270 -0
- package/tests/unit/components/Region.spec.ts +373 -0
- package/tests/unit/components/RegionWarning.snapshot.spec.ts +361 -0
- package/tests/unit/components/RegionWarning.spec.ts +381 -0
- package/tests/unit/components/Regions.spec.ts +503 -0
- package/tests/unit/components/Warning.snapshot.spec.ts +483 -0
- package/tests/unit/components/Warning.spec.ts +489 -0
- package/tests/unit/components/Warnings.spec.ts +343 -0
- package/tests/unit/components/__snapshots__/RegionWarning.snapshot.spec.ts.snap +41 -0
- package/tests/unit/components/__snapshots__/Warning.snapshot.spec.ts.snap +433 -0
- package/tests/unit/composables/useConfig.spec.ts +279 -0
- package/tests/unit/composables/useI18n.spec.ts +116 -0
- package/tests/unit/composables/useKeyCodes.spec.ts +27 -0
- package/tests/unit/composables/useUtils.spec.ts +213 -0
- package/tsconfig.json +43 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.js +96 -26
- package/vitest.config.js +40 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.dark.html +0 -20
- package/dist/index.en.html +0 -15
- package/dist/index.fi.html +0 -15
- package/dist/index.html +0 -15
- package/dist/index.js +0 -281
- package/dist/index.mjs +0 -281
- package/dist/index.mjs.map +0 -1
- package/dist/index.relative.html +0 -19
- package/dist/index.start.html +0 -20
- package/dist/index.sv.html +0 -15
- package/playwright.config.ts +0 -18
- package/public/index.relative.html +0 -19
- package/public/index.start.html +0 -20
- package/src/mixins/config.js +0 -1378
- package/src/mixins/fields.js +0 -26
- package/src/mixins/i18n.js +0 -25
- package/src/mixins/keycodes.js +0 -10
- package/src/mixins/panzoom.js +0 -900
- package/src/mixins/utils.js +0 -900
- package/src/plugins/index.js +0 -3
- package/test/snapshot.test.ts +0 -126
- package/vitest.config.ts +0 -6
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
<div id="fmi-warnings-list">
|
|
19
19
|
<Warning
|
|
20
20
|
v-for="warning in warnings"
|
|
21
|
-
:key="warning.
|
|
21
|
+
:key="warning.type"
|
|
22
22
|
:input="warning"
|
|
23
23
|
:hideable="warnings.length > 1"
|
|
24
24
|
:theme="theme"
|
|
25
25
|
:language="language"
|
|
26
|
-
@
|
|
26
|
+
@warning-toggled="onWarningToggled" />
|
|
27
27
|
</div>
|
|
28
28
|
<div class="row symbol-list-main-row">
|
|
29
29
|
<hr
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
<div class="symbol-list-cell symbol-list-cell-image">
|
|
36
36
|
<div
|
|
37
37
|
class="gray several symbol-list-image-column symbol-list-image warning-image"
|
|
38
|
-
aria-labelledby="symbol-list-several-warnings-text">
|
|
39
|
-
</div>
|
|
38
|
+
aria-labelledby="symbol-list-several-warnings-text"></div>
|
|
40
39
|
</div>
|
|
41
40
|
<div class="symbol-list-cell symbol-list-cell-text">
|
|
42
41
|
<div
|
|
@@ -52,8 +51,7 @@
|
|
|
52
51
|
<div class="symbol-list-cell symbol-list-cell-image">
|
|
53
52
|
<div
|
|
54
53
|
class="level-1 symbol-list-image-column symbol-list-image warning-image"
|
|
55
|
-
aria-labelledby="symbol-list-warning-level-1-text">
|
|
56
|
-
</div>
|
|
54
|
+
aria-labelledby="symbol-list-warning-level-1-text"></div>
|
|
57
55
|
</div>
|
|
58
56
|
<div class="symbol-list-cell symbol-list-cell-text">
|
|
59
57
|
<div
|
|
@@ -69,8 +67,7 @@
|
|
|
69
67
|
<div class="symbol-list-cell symbol-list-cell-image">
|
|
70
68
|
<div
|
|
71
69
|
class="level-2 symbol-list-image-column symbol-list-image warning-image"
|
|
72
|
-
aria-labelledby="symbol-list-warning-level-2-text">
|
|
73
|
-
</div>
|
|
70
|
+
aria-labelledby="symbol-list-warning-level-2-text"></div>
|
|
74
71
|
</div>
|
|
75
72
|
<div class="symbol-list-cell symbol-list-cell-text">
|
|
76
73
|
<div
|
|
@@ -86,8 +83,7 @@
|
|
|
86
83
|
<div class="symbol-list-cell symbol-list-cell-image">
|
|
87
84
|
<div
|
|
88
85
|
class="level-3 symbol-list-image-column symbol-list-image warning-image"
|
|
89
|
-
aria-labelledby="symbol-list-warning-level-3-text">
|
|
90
|
-
</div>
|
|
86
|
+
aria-labelledby="symbol-list-warning-level-3-text"></div>
|
|
91
87
|
</div>
|
|
92
88
|
<div class="symbol-list-cell symbol-list-cell-text">
|
|
93
89
|
<div
|
|
@@ -103,8 +99,7 @@
|
|
|
103
99
|
<div class="symbol-list-cell symbol-list-cell-image">
|
|
104
100
|
<div
|
|
105
101
|
class="level-4 symbol-list-image-column symbol-list-image warning-image"
|
|
106
|
-
aria-labelledby="symbol-list-warning-level-4-text">
|
|
107
|
-
</div>
|
|
102
|
+
aria-labelledby="symbol-list-warning-level-4-text"></div>
|
|
108
103
|
</div>
|
|
109
104
|
<div class="symbol-list-cell symbol-list-cell-text">
|
|
110
105
|
<div
|
|
@@ -119,69 +114,138 @@
|
|
|
119
114
|
</div>
|
|
120
115
|
</template>
|
|
121
116
|
|
|
122
|
-
<script>
|
|
123
|
-
import
|
|
117
|
+
<script setup lang="ts">
|
|
118
|
+
import { computed, toRef } from 'vue'
|
|
119
|
+
import { useI18n } from '@/composables/useI18n'
|
|
124
120
|
import Warning from './Warning.vue'
|
|
121
|
+
import type { LegendItem, Theme, Language } from '@/types'
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Props
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
const props = withDefaults(
|
|
128
|
+
defineProps<{
|
|
129
|
+
input?: LegendItem[]
|
|
130
|
+
visibleWarnings?: string[]
|
|
131
|
+
language?: Language
|
|
132
|
+
theme?: Theme | string
|
|
133
|
+
}>(),
|
|
134
|
+
{
|
|
135
|
+
input: () => [],
|
|
136
|
+
visibleWarnings: () => [],
|
|
137
|
+
language: undefined,
|
|
138
|
+
theme: 'light-theme',
|
|
139
|
+
}
|
|
140
|
+
)
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Emits
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
const emit = defineEmits<{
|
|
147
|
+
warningsToggled: [visibleWarnings: string[]]
|
|
148
|
+
showAllWarnings: []
|
|
149
|
+
}>()
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Composables
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
const { t } = useI18n(toRef(() => props.language))
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Computed Properties
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
const warnings = computed<LegendItem[]>(() => {
|
|
162
|
+
return props.input
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const hiddenWarnings = computed<boolean>(() => {
|
|
166
|
+
return props.visibleWarnings.length !== props.input.length
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const noWarnings = computed<boolean>(() => {
|
|
170
|
+
return warnings.value.length === 0
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const warningSymbolsText = computed<string>(() => {
|
|
174
|
+
return noWarnings.value ? t('noWarnings') : t('warningSymbols')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const warningSymbolDaysText = computed<string>(() => {
|
|
178
|
+
return noWarnings.value ? '' : t('warningSymbolDays')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const showWarningsText = computed<string>(() => {
|
|
182
|
+
return t('showWarnings')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const severalWarningsText = computed<string>(() => {
|
|
186
|
+
return t('severalWarnings')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const warningLevel1Text = computed<string>(() => {
|
|
190
|
+
return t('warningLevel1')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const warningLevel2Text = computed<string>(() => {
|
|
194
|
+
return t('warningLevel2')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const warningLevel3Text = computed<string>(() => {
|
|
198
|
+
return t('warningLevel3')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const warningLevel4Text = computed<string>(() => {
|
|
202
|
+
return t('warningLevel4')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// Methods
|
|
207
|
+
// ============================================================================
|
|
208
|
+
|
|
209
|
+
interface WarningToggleEvent {
|
|
210
|
+
warning: string
|
|
211
|
+
visible: boolean
|
|
184
212
|
}
|
|
213
|
+
|
|
214
|
+
const onWarningToggled = ({ warning, visible }: WarningToggleEvent): void => {
|
|
215
|
+
let newVisibleWarnings = [...props.visibleWarnings]
|
|
216
|
+
if (visible && !props.visibleWarnings.includes(warning)) {
|
|
217
|
+
newVisibleWarnings.push(warning)
|
|
218
|
+
} else if (!visible) {
|
|
219
|
+
newVisibleWarnings = newVisibleWarnings.filter(
|
|
220
|
+
(visibleWarning) => visibleWarning !== warning
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
emit('warningsToggled', newVisibleWarnings)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const showAll = (): void => {
|
|
227
|
+
emit('showAllWarnings')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Expose for tests
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
defineExpose({
|
|
235
|
+
warnings,
|
|
236
|
+
hiddenWarnings,
|
|
237
|
+
noWarnings,
|
|
238
|
+
warningSymbolsText,
|
|
239
|
+
warningSymbolDaysText,
|
|
240
|
+
showWarningsText,
|
|
241
|
+
severalWarningsText,
|
|
242
|
+
warningLevel1Text,
|
|
243
|
+
warningLevel2Text,
|
|
244
|
+
warningLevel3Text,
|
|
245
|
+
warningLevel4Text,
|
|
246
|
+
onWarningToggled,
|
|
247
|
+
showAll,
|
|
248
|
+
})
|
|
185
249
|
</script>
|
|
186
250
|
|
|
187
251
|
<style scoped lang="scss">
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core composable for AlertClient wrapper components.
|
|
3
|
+
* Contains shared logic for both web component (App.vue) and Vue component (AlertClientVue.vue).
|
|
4
|
+
*
|
|
5
|
+
* This composable provides:
|
|
6
|
+
* - Reactive state management (loading, warningsData, themeClass, etc.)
|
|
7
|
+
* - Computed properties for API queries
|
|
8
|
+
* - Methods for fetching warnings and handling events
|
|
9
|
+
*
|
|
10
|
+
* Components using this composable must provide:
|
|
11
|
+
* - Props with appropriate types
|
|
12
|
+
*/
|
|
13
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
14
|
+
import crossFetch from 'cross-fetch'
|
|
15
|
+
import type { Language, WarningsData } from '@/types'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Helper Functions (exported for use in components)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize string|boolean to boolean
|
|
23
|
+
*/
|
|
24
|
+
export const toBool = (val: unknown, defaultVal = true): boolean => {
|
|
25
|
+
if (typeof val === 'boolean') return val
|
|
26
|
+
if (typeof val === 'string') return val.toLowerCase() !== 'false'
|
|
27
|
+
return defaultVal
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize string|number to number
|
|
32
|
+
*/
|
|
33
|
+
export const toNum = (val: unknown, defaultVal = 0): number => {
|
|
34
|
+
if (typeof val === 'number') return val
|
|
35
|
+
if (typeof val === 'string') return Number(val)
|
|
36
|
+
return defaultVal
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Types
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
export interface UseAlertClientOptions {
|
|
44
|
+
/** Base URL for API requests */
|
|
45
|
+
baseUrl: Ref<string> | ComputedRef<string>
|
|
46
|
+
/** Language code */
|
|
47
|
+
language: Ref<Language> | ComputedRef<Language>
|
|
48
|
+
/** Theme name */
|
|
49
|
+
theme: Ref<string> | ComputedRef<string>
|
|
50
|
+
/** Pre-loaded warnings data (optional) */
|
|
51
|
+
warnings?:
|
|
52
|
+
| Ref<WarningsData | string | null>
|
|
53
|
+
| ComputedRef<WarningsData | string | null>
|
|
54
|
+
/** Current date for time calculations (optional) */
|
|
55
|
+
currentDate?: Ref<Date | string | null> | ComputedRef<Date | string | null>
|
|
56
|
+
/** Font scale factor (optional) */
|
|
57
|
+
fontScale?: Ref<number | string> | ComputedRef<number | string>
|
|
58
|
+
/** Debug mode (optional) */
|
|
59
|
+
debugMode?: Ref<boolean> | ComputedRef<boolean>
|
|
60
|
+
/** Custom weather updated query (optional) */
|
|
61
|
+
weatherUpdated?: Ref<string> | ComputedRef<string>
|
|
62
|
+
/** Custom flood updated query (optional) */
|
|
63
|
+
floodUpdated?: Ref<string> | ComputedRef<string>
|
|
64
|
+
/** Custom weather warnings query (optional) */
|
|
65
|
+
weatherWarnings?: Ref<string> | ComputedRef<string>
|
|
66
|
+
/** Custom flood warnings query (optional) */
|
|
67
|
+
floodWarnings?: Ref<string> | ComputedRef<string>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface UseAlertClientReturn {
|
|
71
|
+
// State
|
|
72
|
+
loading: Ref<number>
|
|
73
|
+
updatedAt: Ref<number | null>
|
|
74
|
+
refreshedAt: Ref<number | null>
|
|
75
|
+
themeClass: Ref<string>
|
|
76
|
+
warningsData: Ref<WarningsData | null>
|
|
77
|
+
visible: Ref<boolean>
|
|
78
|
+
|
|
79
|
+
// Computed
|
|
80
|
+
currentTime: ComputedRef<number>
|
|
81
|
+
weatherUpdatedQuery: ComputedRef<string>
|
|
82
|
+
floodUpdatedQuery: ComputedRef<string>
|
|
83
|
+
weatherWarningsQuery: ComputedRef<string>
|
|
84
|
+
floodWarningsQuery: ComputedRef<string>
|
|
85
|
+
|
|
86
|
+
// Methods
|
|
87
|
+
onLoaded: (loaded: number) => void
|
|
88
|
+
onThemeChanged: (newTheme: string | null) => void
|
|
89
|
+
fetchWarnings: () => Promise<void> | undefined
|
|
90
|
+
show: () => void
|
|
91
|
+
hide: () => void
|
|
92
|
+
initializeWarnings: () => void
|
|
93
|
+
applyFontScale: () => void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Constants
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
const WEATHER_UPDATED_TYPE = 'weather_update_time'
|
|
101
|
+
const FLOOD_UPDATED_TYPE = 'flood_update_time'
|
|
102
|
+
const WEATHER_WARNINGS_TYPE = 'weather_finland_active_all'
|
|
103
|
+
const FLOOD_WARNINGS_TYPE = 'flood_finland_active_all'
|
|
104
|
+
const FLOOD_SUPPORTED_SEVERITIES = ['moderate', 'severe', 'extreme'] as const
|
|
105
|
+
|
|
106
|
+
const QUERY_PREFIX =
|
|
107
|
+
'?service=WFS&version=1.0.0&request=GetFeature&maxFeatures=1000&outputFormat=application%2Fjson&typeName='
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Composable
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
export function useAlertClient(
|
|
114
|
+
options: UseAlertClientOptions
|
|
115
|
+
): UseAlertClientReturn {
|
|
116
|
+
const {
|
|
117
|
+
baseUrl,
|
|
118
|
+
language,
|
|
119
|
+
theme,
|
|
120
|
+
warnings,
|
|
121
|
+
currentDate,
|
|
122
|
+
fontScale,
|
|
123
|
+
debugMode,
|
|
124
|
+
weatherUpdated,
|
|
125
|
+
floodUpdated,
|
|
126
|
+
weatherWarnings,
|
|
127
|
+
floodWarnings,
|
|
128
|
+
} = options
|
|
129
|
+
|
|
130
|
+
// -------------------------------------------------------------------------
|
|
131
|
+
// Reactive State
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
const loading = ref<number>(1)
|
|
135
|
+
const updatedAt = ref<number | null>(null)
|
|
136
|
+
const refreshedAt = ref<number | null>(null)
|
|
137
|
+
const themeClass = ref<string>(`${theme.value}-theme`)
|
|
138
|
+
const warningsData = ref<WarningsData | null>(null)
|
|
139
|
+
const visible = ref<boolean>(true)
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// Helper: CAP Language mapping
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
const capLanguageMap: Record<Language, string> = {
|
|
146
|
+
fi: 'fi-FI',
|
|
147
|
+
sv: 'sv-SV',
|
|
148
|
+
en: 'en-US',
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const getCapLanguage = (): string => capLanguageMap[language.value] || 'fi-FI'
|
|
152
|
+
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
// Computed: Flood filter
|
|
155
|
+
// -------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
const floodFilter = computed<string>(() => {
|
|
158
|
+
const severityFilter = FLOOD_SUPPORTED_SEVERITIES.reduce(
|
|
159
|
+
(filter, severity, index) =>
|
|
160
|
+
`${filter}${index === 0 ? '' : ','}%27${severity.toUpperCase()}%27`,
|
|
161
|
+
'&cql_filter=severity%20IN%20('
|
|
162
|
+
)
|
|
163
|
+
return `${severityFilter})%20AND%20language=%27${getCapLanguage()}%27`
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
167
|
+
// Computed: Query URLs
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const weatherUpdatedQuery = computed<string>(() => {
|
|
171
|
+
return weatherUpdated?.value || `${QUERY_PREFIX}${WEATHER_UPDATED_TYPE}`
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const floodUpdatedQuery = computed<string>(() => {
|
|
175
|
+
return floodUpdated?.value || `${QUERY_PREFIX}${FLOOD_UPDATED_TYPE}`
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const weatherWarningsQuery = computed<string>(() => {
|
|
179
|
+
return weatherWarnings?.value || `${QUERY_PREFIX}${WEATHER_WARNINGS_TYPE}`
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const floodWarningsQuery = computed<string>(() => {
|
|
183
|
+
return (
|
|
184
|
+
floodWarnings?.value ||
|
|
185
|
+
`${QUERY_PREFIX}${FLOOD_WARNINGS_TYPE}${floodFilter.value}`
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// -------------------------------------------------------------------------
|
|
190
|
+
// Computed: Current time
|
|
191
|
+
// -------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
const currentTime = computed<number>(() => {
|
|
194
|
+
if (refreshedAt.value) {
|
|
195
|
+
return refreshedAt.value
|
|
196
|
+
}
|
|
197
|
+
if (currentDate?.value) {
|
|
198
|
+
const date =
|
|
199
|
+
currentDate.value instanceof Date
|
|
200
|
+
? currentDate.value
|
|
201
|
+
: new Date(currentDate.value)
|
|
202
|
+
return date.getTime()
|
|
203
|
+
}
|
|
204
|
+
return Date.now()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
// Methods
|
|
209
|
+
// -------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Initialize warnings from pre-loaded data (call in created/setup)
|
|
213
|
+
*/
|
|
214
|
+
const initializeWarnings = (): void => {
|
|
215
|
+
if (warnings?.value) {
|
|
216
|
+
warningsData.value =
|
|
217
|
+
typeof warnings.value === 'string'
|
|
218
|
+
? JSON.parse(warnings.value)
|
|
219
|
+
: warnings.value
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Apply font scale to document (call in onMounted)
|
|
225
|
+
*/
|
|
226
|
+
const applyFontScale = (): void => {
|
|
227
|
+
const fontScaleNum = toNum(fontScale?.value, 1)
|
|
228
|
+
if (fontScaleNum !== 1) {
|
|
229
|
+
let originalFontSize: number | undefined
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
typeof window !== 'undefined' &&
|
|
233
|
+
typeof document !== 'undefined' &&
|
|
234
|
+
document.documentElement &&
|
|
235
|
+
window.getComputedStyle
|
|
236
|
+
) {
|
|
237
|
+
const htmlElement = document.documentElement
|
|
238
|
+
const computedStyle = window.getComputedStyle(htmlElement)
|
|
239
|
+
originalFontSize = parseFloat(computedStyle.fontSize)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (originalFontSize == null || Number.isNaN(originalFontSize)) {
|
|
243
|
+
originalFontSize = 16 // Fallback
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const scaledFontSize = fontScaleNum * originalFontSize
|
|
247
|
+
const newFontSize = Math.round(scaledFontSize * 100) / 100
|
|
248
|
+
document.documentElement.style.fontSize = `${newFontSize}px`
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handle loaded event from child component
|
|
254
|
+
*/
|
|
255
|
+
const onLoaded = (loaded: number): void => {
|
|
256
|
+
if (loaded !== 0) {
|
|
257
|
+
loading.value = loaded === -1 ? -1 : 0
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Handle theme change
|
|
263
|
+
*/
|
|
264
|
+
const onThemeChanged = (newTheme: string | null): void => {
|
|
265
|
+
themeClass.value = `${
|
|
266
|
+
newTheme != null && newTheme.length > 0 ? newTheme : theme.value
|
|
267
|
+
}-theme`
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Fetch warnings from API
|
|
272
|
+
*/
|
|
273
|
+
const fetchWarnings = (): Promise<void> | undefined => {
|
|
274
|
+
if (warnings?.value) {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
loading.value = 1
|
|
279
|
+
|
|
280
|
+
if (debugMode?.value) {
|
|
281
|
+
console.log(`Updating warnings at ${new Date()}`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const queries = new Map<string, string>([
|
|
285
|
+
[`${baseUrl.value}${weatherUpdatedQuery.value}`, WEATHER_UPDATED_TYPE],
|
|
286
|
+
[`${baseUrl.value}${floodUpdatedQuery.value}`, FLOOD_UPDATED_TYPE],
|
|
287
|
+
[`${baseUrl.value}${weatherWarningsQuery.value}`, WEATHER_WARNINGS_TYPE],
|
|
288
|
+
[`${baseUrl.value}${floodWarningsQuery.value}`, FLOOD_WARNINGS_TYPE],
|
|
289
|
+
])
|
|
290
|
+
|
|
291
|
+
const responseData: Record<string, unknown> = {}
|
|
292
|
+
|
|
293
|
+
return Promise.allSettled(
|
|
294
|
+
[...queries.keys()].map(async (queryUrl) =>
|
|
295
|
+
crossFetch(queryUrl).then((response) =>
|
|
296
|
+
response
|
|
297
|
+
.json()
|
|
298
|
+
.then((json: unknown) => {
|
|
299
|
+
const currentTimeMs = Date.now()
|
|
300
|
+
if (updatedAt.value != null) {
|
|
301
|
+
refreshedAt.value = currentTimeMs
|
|
302
|
+
}
|
|
303
|
+
updatedAt.value = currentTimeMs
|
|
304
|
+
responseData[queries.get(queryUrl)!] = json
|
|
305
|
+
})
|
|
306
|
+
.catch((error: Error) => {
|
|
307
|
+
loading.value = -1
|
|
308
|
+
console.log(error)
|
|
309
|
+
})
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
).then(() => {
|
|
313
|
+
warningsData.value = responseData as WarningsData
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Show the component
|
|
319
|
+
*/
|
|
320
|
+
const show = (): void => {
|
|
321
|
+
visible.value = true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Hide the component
|
|
326
|
+
*/
|
|
327
|
+
const hide = (): void => {
|
|
328
|
+
visible.value = false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// -------------------------------------------------------------------------
|
|
332
|
+
// Return
|
|
333
|
+
// -------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
// State
|
|
337
|
+
loading,
|
|
338
|
+
updatedAt,
|
|
339
|
+
refreshedAt,
|
|
340
|
+
themeClass,
|
|
341
|
+
warningsData,
|
|
342
|
+
visible,
|
|
343
|
+
|
|
344
|
+
// Computed
|
|
345
|
+
currentTime,
|
|
346
|
+
weatherUpdatedQuery,
|
|
347
|
+
floodUpdatedQuery,
|
|
348
|
+
weatherWarningsQuery,
|
|
349
|
+
floodWarningsQuery,
|
|
350
|
+
|
|
351
|
+
// Methods
|
|
352
|
+
onLoaded,
|
|
353
|
+
onThemeChanged,
|
|
354
|
+
fetchWarnings,
|
|
355
|
+
show,
|
|
356
|
+
hide,
|
|
357
|
+
initializeWarnings,
|
|
358
|
+
applyFontScale,
|
|
359
|
+
}
|
|
360
|
+
}
|