@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
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warnings Processor Composable
|
|
3
|
+
*
|
|
4
|
+
* Handles processing of raw warning data from API into structured format
|
|
5
|
+
* for display in the AlertClient component.
|
|
6
|
+
*
|
|
7
|
+
* This composable provides the handleMapWarnings function and all its
|
|
8
|
+
* dependencies, migrated from the utils mixin.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { type Ref } from 'vue'
|
|
12
|
+
import type {
|
|
13
|
+
Warning,
|
|
14
|
+
WarningsMap,
|
|
15
|
+
Day,
|
|
16
|
+
LegendItem,
|
|
17
|
+
RegionsData,
|
|
18
|
+
DayRegions,
|
|
19
|
+
RegionType,
|
|
20
|
+
Severity,
|
|
21
|
+
WarningsDataResponse,
|
|
22
|
+
GeoJSONFeature,
|
|
23
|
+
GeoJSONFeatureCollection,
|
|
24
|
+
GeometryCollection,
|
|
25
|
+
RegionGeometry,
|
|
26
|
+
RegionListItem,
|
|
27
|
+
RegionWarningItem,
|
|
28
|
+
} from '@/types'
|
|
29
|
+
import {
|
|
30
|
+
NUMBER_OF_DAYS,
|
|
31
|
+
WEATHER_UPDATE_TIME,
|
|
32
|
+
FLOOD_UPDATE_TIME,
|
|
33
|
+
UPDATE_TIME,
|
|
34
|
+
WEATHER_WARNINGS,
|
|
35
|
+
FLOOD_WARNINGS,
|
|
36
|
+
WARNING_CONTEXT,
|
|
37
|
+
CONTEXT_EXTENSION,
|
|
38
|
+
SEVERITY,
|
|
39
|
+
EFFECTIVE_FROM,
|
|
40
|
+
EFFECTIVE_UNTIL,
|
|
41
|
+
ONSET,
|
|
42
|
+
EXPIRES,
|
|
43
|
+
PHYSICAL_DIRECTION,
|
|
44
|
+
PHYSICAL_VALUE,
|
|
45
|
+
INFO_FI,
|
|
46
|
+
INFO_SV,
|
|
47
|
+
INFO_EN,
|
|
48
|
+
SEA_WIND,
|
|
49
|
+
WIND,
|
|
50
|
+
FLOOD_LEVEL_TYPE,
|
|
51
|
+
WARNING_LEVELS,
|
|
52
|
+
FLOOD_LEVELS,
|
|
53
|
+
REGION_LAND,
|
|
54
|
+
REGION_SEA,
|
|
55
|
+
uncapitalize,
|
|
56
|
+
twoDigits,
|
|
57
|
+
toTimeZone,
|
|
58
|
+
validInterval,
|
|
59
|
+
msSinceStartOfDay,
|
|
60
|
+
coverageData,
|
|
61
|
+
} from './useUtils'
|
|
62
|
+
import he from 'he'
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Types
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
export interface ParentsMap {
|
|
69
|
+
[key: string]: boolean[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HandleMapWarningsResult {
|
|
73
|
+
warnings: WarningsMap
|
|
74
|
+
days: Day[]
|
|
75
|
+
regions: RegionsData
|
|
76
|
+
parents: ParentsMap
|
|
77
|
+
legend: LegendItem[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface WarningsProcessorContext {
|
|
81
|
+
geometryId: string
|
|
82
|
+
geometries: GeometryCollection
|
|
83
|
+
regionIds: string[]
|
|
84
|
+
warningTypes: Map<string, RegionType>
|
|
85
|
+
timeZone: string
|
|
86
|
+
locale: string
|
|
87
|
+
currentTime: number
|
|
88
|
+
startFrom: string
|
|
89
|
+
staticDays: boolean
|
|
90
|
+
dailyWarningTypes: string[]
|
|
91
|
+
maxUpdateDelay: { weather_update_time: number; flood_update_time: number }
|
|
92
|
+
bbox: GeoJSONFeature
|
|
93
|
+
geoJSONToSVG: (data: object, width: number, height: number) => string
|
|
94
|
+
t: (key: string) => string
|
|
95
|
+
handleError: (error: string) => void
|
|
96
|
+
onDataError: () => void
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface UseWarningsProcessorOptions {
|
|
100
|
+
geometryId: Ref<string>
|
|
101
|
+
geometries: Ref<GeometryCollection>
|
|
102
|
+
regionIds: Ref<string[]>
|
|
103
|
+
warningTypes: Ref<Map<string, RegionType>>
|
|
104
|
+
timeZone: Ref<string>
|
|
105
|
+
locale: Ref<string>
|
|
106
|
+
currentTime: Ref<number>
|
|
107
|
+
startFrom: Ref<string>
|
|
108
|
+
staticDays: Ref<boolean>
|
|
109
|
+
dailyWarningTypes: Ref<string[]>
|
|
110
|
+
maxUpdateDelay: Ref<{
|
|
111
|
+
weather_update_time: number
|
|
112
|
+
flood_update_time: number
|
|
113
|
+
}>
|
|
114
|
+
bbox: Ref<GeoJSONFeature>
|
|
115
|
+
geoJSONToSVG: (data: object, width: number, height: number) => string
|
|
116
|
+
t: (key: string) => string
|
|
117
|
+
handleError: (error: string) => void
|
|
118
|
+
onDataError: () => void
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Helper Functions (Pure)
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract warning type from properties
|
|
127
|
+
*/
|
|
128
|
+
function getWarningType(properties: Record<string, unknown>): string {
|
|
129
|
+
return uncapitalize(
|
|
130
|
+
(
|
|
131
|
+
(properties[WARNING_CONTEXT] as string) +
|
|
132
|
+
(properties[CONTEXT_EXTENSION] ? `-${properties[CONTEXT_EXTENSION]}` : '')
|
|
133
|
+
)
|
|
134
|
+
.split('-')
|
|
135
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
136
|
+
.join('')
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract region ID from reference URL
|
|
142
|
+
*/
|
|
143
|
+
function regionFromReference(reference: string): string {
|
|
144
|
+
return reference
|
|
145
|
+
.split(',')
|
|
146
|
+
.map((url) => {
|
|
147
|
+
let subUrl = url.substring(url.lastIndexOf('#') + 1)
|
|
148
|
+
// Saimaa special case
|
|
149
|
+
if (subUrl.indexOf('.') !== subUrl.lastIndexOf('.')) {
|
|
150
|
+
subUrl = subUrl.replace('.', '_')
|
|
151
|
+
}
|
|
152
|
+
return subUrl
|
|
153
|
+
})
|
|
154
|
+
.reduce((regionId, rawId, index, array) => {
|
|
155
|
+
const parts = rawId.split('.')
|
|
156
|
+
if (index === 0) {
|
|
157
|
+
regionId += parts[0] ?? ''
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
regionId + (index === array.length - 1 ? '.' : '_') + (parts[1] ?? '')
|
|
161
|
+
)
|
|
162
|
+
}, '')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract relative coverage from reference URL
|
|
167
|
+
*/
|
|
168
|
+
function relativeCoverageFromReference(
|
|
169
|
+
reference: string | null | undefined
|
|
170
|
+
): number {
|
|
171
|
+
if (reference == null) {
|
|
172
|
+
return 0
|
|
173
|
+
}
|
|
174
|
+
const urlSplit = reference.split('?')
|
|
175
|
+
if (urlSplit.length <= 1) {
|
|
176
|
+
return 0
|
|
177
|
+
}
|
|
178
|
+
const paramString = (urlSplit[1] ?? '').split('#')[0] ?? ''
|
|
179
|
+
const searchParams = new URLSearchParams(paramString)
|
|
180
|
+
const relativeCoverage = searchParams.get('c')
|
|
181
|
+
if (relativeCoverage == null) {
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
return Number(relativeCoverage)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get warning text based on properties
|
|
189
|
+
*/
|
|
190
|
+
function getWarningText(properties: Record<string, unknown>): string {
|
|
191
|
+
return properties[WARNING_CONTEXT] === SEA_WIND
|
|
192
|
+
? String(properties[PHYSICAL_VALUE] ?? '')
|
|
193
|
+
: ''
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Calculate effective days for a warning
|
|
198
|
+
*/
|
|
199
|
+
function calculateEffectiveDays(
|
|
200
|
+
start: string,
|
|
201
|
+
end: string,
|
|
202
|
+
dailyWarning: boolean,
|
|
203
|
+
updatedAt: number | null,
|
|
204
|
+
currentTime: number,
|
|
205
|
+
startFrom: string,
|
|
206
|
+
timeOffset: number,
|
|
207
|
+
timeZone: string,
|
|
208
|
+
locale: string
|
|
209
|
+
): boolean[] {
|
|
210
|
+
const referenceTime =
|
|
211
|
+
startFrom === 'updated' ? updatedAt ?? currentTime : currentTime
|
|
212
|
+
const day = 1000 * 60 * 60 * 24
|
|
213
|
+
|
|
214
|
+
return Array.from({ length: NUMBER_OF_DAYS }, (_, index) => {
|
|
215
|
+
const dayTime = referenceTime + index * day
|
|
216
|
+
const dayStartOffset = msSinceStartOfDay(dayTime, timeZone, locale)
|
|
217
|
+
let startOfDay = dayTime - dayStartOffset
|
|
218
|
+
|
|
219
|
+
const nextDayTime = referenceTime + (index + 1) * day
|
|
220
|
+
const nextDayStartOffset = msSinceStartOfDay(nextDayTime, timeZone, locale)
|
|
221
|
+
let startOfNextDay = nextDayTime - nextDayStartOffset
|
|
222
|
+
|
|
223
|
+
if (!dailyWarning) {
|
|
224
|
+
startOfDay = startOfDay + timeOffset
|
|
225
|
+
startOfNextDay = startOfNextDay + timeOffset
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
new Date(start).getTime() < startOfNextDay &&
|
|
230
|
+
new Date(end).getTime() > startOfDay
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if a warning is valid
|
|
237
|
+
*/
|
|
238
|
+
function isValidWarning(
|
|
239
|
+
warning: GeoJSONFeature | null,
|
|
240
|
+
geometryId: string,
|
|
241
|
+
geometries: GeometryCollection,
|
|
242
|
+
warningTypes: Map<string, RegionType>
|
|
243
|
+
): boolean {
|
|
244
|
+
if (warning == null || warning.properties == null) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const regionId = regionFromReference(warning.properties.reference as string)
|
|
249
|
+
const geometryData = geometries[geometryId]
|
|
250
|
+
|
|
251
|
+
if (warning.geometry == null && geometryData?.[regionId] == null) {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const warningType =
|
|
256
|
+
warning.properties.warning_context != null
|
|
257
|
+
? getWarningType(warning.properties)
|
|
258
|
+
: FLOOD_LEVEL_TYPE
|
|
259
|
+
|
|
260
|
+
const regionGeom = geometryData?.[regionId] as RegionGeometry | undefined
|
|
261
|
+
if (regionGeom != null && warningTypes.get(warningType) !== regionGeom.type) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Valid flood warning
|
|
266
|
+
if (
|
|
267
|
+
warning.properties.severity != null &&
|
|
268
|
+
Object.keys(FLOOD_LEVELS).includes(
|
|
269
|
+
(warning.properties.severity as string).toLowerCase()
|
|
270
|
+
)
|
|
271
|
+
) {
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
WARNING_LEVELS.slice(1).includes(warning.properties.severity as string) ||
|
|
277
|
+
(warning.properties[WARNING_CONTEXT] === SEA_WIND &&
|
|
278
|
+
WARNING_LEVELS.includes(warning.properties.severity as string))
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a weather warning from raw data
|
|
284
|
+
*/
|
|
285
|
+
function createWeatherWarning(
|
|
286
|
+
warning: GeoJSONFeature,
|
|
287
|
+
geometryId: string,
|
|
288
|
+
geometries: GeometryCollection,
|
|
289
|
+
dailyWarningTypes: string[],
|
|
290
|
+
updatedAt: number | null,
|
|
291
|
+
currentTime: number,
|
|
292
|
+
startFrom: string,
|
|
293
|
+
timeOffset: number,
|
|
294
|
+
timeZone: string,
|
|
295
|
+
locale: string
|
|
296
|
+
): Warning {
|
|
297
|
+
const properties = warning.properties
|
|
298
|
+
let direction = 0
|
|
299
|
+
let severity = Number(String(properties.severity ?? '').slice(-1)) as Severity
|
|
300
|
+
|
|
301
|
+
switch (properties[WARNING_CONTEXT]) {
|
|
302
|
+
case SEA_WIND:
|
|
303
|
+
direction = ((properties[PHYSICAL_DIRECTION] as number) ?? 0) - 180
|
|
304
|
+
if (properties[SEVERITY] === WARNING_LEVELS[0]) {
|
|
305
|
+
severity = (severity + 1) as Severity
|
|
306
|
+
}
|
|
307
|
+
break
|
|
308
|
+
case WIND:
|
|
309
|
+
direction = ((properties[PHYSICAL_DIRECTION] as number) ?? 0) - 90
|
|
310
|
+
break
|
|
311
|
+
default:
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const regionId = regionFromReference(properties.reference as string)
|
|
315
|
+
const type = getWarningType(properties)
|
|
316
|
+
const geometryData = geometries[geometryId]
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
type,
|
|
320
|
+
id: properties.identifier as string,
|
|
321
|
+
regions: geometryData?.[regionId] ? { [regionId]: true } : {},
|
|
322
|
+
covRegions: new Map(),
|
|
323
|
+
coveragesLarge: [],
|
|
324
|
+
coveragesSmall: [],
|
|
325
|
+
effectiveFrom: properties[EFFECTIVE_FROM] as string,
|
|
326
|
+
effectiveUntil: properties[EFFECTIVE_UNTIL] as string,
|
|
327
|
+
effectiveDays: calculateEffectiveDays(
|
|
328
|
+
properties[EFFECTIVE_FROM] as string,
|
|
329
|
+
properties[EFFECTIVE_UNTIL] as string,
|
|
330
|
+
dailyWarningTypes.includes(type),
|
|
331
|
+
updatedAt,
|
|
332
|
+
currentTime,
|
|
333
|
+
startFrom,
|
|
334
|
+
timeOffset,
|
|
335
|
+
timeZone,
|
|
336
|
+
locale
|
|
337
|
+
),
|
|
338
|
+
validInterval: validInterval(
|
|
339
|
+
properties[EFFECTIVE_FROM] as string,
|
|
340
|
+
properties[EFFECTIVE_UNTIL] as string,
|
|
341
|
+
timeZone,
|
|
342
|
+
locale
|
|
343
|
+
),
|
|
344
|
+
severity,
|
|
345
|
+
direction,
|
|
346
|
+
value: (properties[PHYSICAL_VALUE] as number) ?? 0,
|
|
347
|
+
text: getWarningText(properties),
|
|
348
|
+
info: {
|
|
349
|
+
fi: properties[INFO_FI] ? he.decode(properties[INFO_FI] as string) : '',
|
|
350
|
+
sv: properties[INFO_SV] ? he.decode(properties[INFO_SV] as string) : '',
|
|
351
|
+
en: properties[INFO_EN] ? he.decode(properties[INFO_EN] as string) : '',
|
|
352
|
+
},
|
|
353
|
+
link: '',
|
|
354
|
+
linkText: '',
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Create a flood warning from raw data
|
|
360
|
+
*/
|
|
361
|
+
function createFloodWarning(
|
|
362
|
+
warning: GeoJSONFeature,
|
|
363
|
+
dailyWarningTypes: string[],
|
|
364
|
+
updatedAt: number | null,
|
|
365
|
+
currentTime: number,
|
|
366
|
+
startFrom: string,
|
|
367
|
+
timeOffset: number,
|
|
368
|
+
timeZone: string,
|
|
369
|
+
locale: string,
|
|
370
|
+
t: (key: string) => string,
|
|
371
|
+
handleError: (error: string) => void
|
|
372
|
+
): Warning {
|
|
373
|
+
const properties = warning.properties
|
|
374
|
+
let info = ''
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
info = JSON.parse(
|
|
378
|
+
decodeURIComponent(
|
|
379
|
+
properties.description != null
|
|
380
|
+
? (properties.description as string)
|
|
381
|
+
: '[%22%22]'
|
|
382
|
+
).replace(/[\n|\t]/g, ' ')
|
|
383
|
+
)[0]
|
|
384
|
+
} catch (e) {
|
|
385
|
+
handleError((e as Error).name)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const regionId = regionFromReference(properties.reference as string)
|
|
389
|
+
const langKey = (properties.language as string)
|
|
390
|
+
?.substring(0, 2)
|
|
391
|
+
?.toLowerCase() as 'fi' | 'sv' | 'en'
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
type: FLOOD_LEVEL_TYPE,
|
|
395
|
+
id: properties.identifier as string,
|
|
396
|
+
regions: { [regionId]: true },
|
|
397
|
+
covRegions: new Map(),
|
|
398
|
+
coveragesLarge: [],
|
|
399
|
+
coveragesSmall: [],
|
|
400
|
+
effectiveFrom: properties[ONSET] as string,
|
|
401
|
+
effectiveUntil: properties[EXPIRES] as string,
|
|
402
|
+
effectiveDays: calculateEffectiveDays(
|
|
403
|
+
properties[ONSET] as string,
|
|
404
|
+
properties[EXPIRES] as string,
|
|
405
|
+
dailyWarningTypes.includes(FLOOD_LEVEL_TYPE),
|
|
406
|
+
updatedAt,
|
|
407
|
+
currentTime,
|
|
408
|
+
startFrom,
|
|
409
|
+
timeOffset,
|
|
410
|
+
timeZone,
|
|
411
|
+
locale
|
|
412
|
+
),
|
|
413
|
+
validInterval: validInterval(
|
|
414
|
+
properties[ONSET] as string,
|
|
415
|
+
properties[EXPIRES] as string,
|
|
416
|
+
timeZone,
|
|
417
|
+
locale
|
|
418
|
+
),
|
|
419
|
+
severity: (FLOOD_LEVELS[(properties.severity as string)?.toLowerCase()] ??
|
|
420
|
+
0) as Severity,
|
|
421
|
+
direction: 0,
|
|
422
|
+
value: 0,
|
|
423
|
+
text: '',
|
|
424
|
+
info: { [langKey]: info },
|
|
425
|
+
link: t('floodLink'),
|
|
426
|
+
linkText: t('floodLinkText'),
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Create days array from warnings
|
|
432
|
+
*/
|
|
433
|
+
function createDays(
|
|
434
|
+
warnings: WarningsMap,
|
|
435
|
+
updatedAt: number | null,
|
|
436
|
+
currentTime: number,
|
|
437
|
+
startFrom: string,
|
|
438
|
+
timeZone: string,
|
|
439
|
+
locale: string
|
|
440
|
+
): Day[] {
|
|
441
|
+
const updatedAtTz = updatedAt ? toTimeZone(updatedAt, timeZone, locale) : null
|
|
442
|
+
const updatedDate = updatedAtTz
|
|
443
|
+
? `${updatedAtTz.day}.${updatedAtTz.month}.${updatedAtTz.year}`
|
|
444
|
+
: ''
|
|
445
|
+
const updatedTime = updatedAtTz
|
|
446
|
+
? `${twoDigits(updatedAtTz.hour)}:${twoDigits(updatedAtTz.minute)}`
|
|
447
|
+
: ''
|
|
448
|
+
|
|
449
|
+
const referenceTime =
|
|
450
|
+
startFrom === 'updated' ? updatedAt ?? currentTime : currentTime
|
|
451
|
+
|
|
452
|
+
return Array.from({ length: NUMBER_OF_DAYS }, (_, index) => {
|
|
453
|
+
const date = new Date(referenceTime)
|
|
454
|
+
date.setDate(date.getDate() + index)
|
|
455
|
+
const moment = toTimeZone(date, timeZone, locale)
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
weekdayName: moment.weekday,
|
|
459
|
+
day: moment.day,
|
|
460
|
+
month: moment.month,
|
|
461
|
+
year: moment.year,
|
|
462
|
+
severity: Object.values(warnings).reduce(
|
|
463
|
+
(maxSeverity, warning) =>
|
|
464
|
+
warning.effectiveDays[index]
|
|
465
|
+
? (Math.max(warning.severity, maxSeverity) as Severity)
|
|
466
|
+
: maxSeverity,
|
|
467
|
+
0 as Severity
|
|
468
|
+
),
|
|
469
|
+
updatedDate,
|
|
470
|
+
updatedTime,
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get maximum severities by warning type
|
|
477
|
+
*/
|
|
478
|
+
function getMaxSeverities(warnings: WarningsMap): Record<string, Severity> {
|
|
479
|
+
return Object.values(warnings).reduce(
|
|
480
|
+
(maxSeverities, warning) => {
|
|
481
|
+
const currentMax = maxSeverities[warning.type]
|
|
482
|
+
if (
|
|
483
|
+
warning.effectiveDays.some((effectiveDay) => effectiveDay) &&
|
|
484
|
+
(currentMax == null || currentMax < warning.severity)
|
|
485
|
+
) {
|
|
486
|
+
maxSeverities[warning.type] = warning.severity
|
|
487
|
+
}
|
|
488
|
+
return maxSeverities
|
|
489
|
+
},
|
|
490
|
+
{} as Record<string, Severity>
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Create legend from severities
|
|
496
|
+
*/
|
|
497
|
+
function createLegend(
|
|
498
|
+
severities: Record<string, Severity>,
|
|
499
|
+
warningTypes: Map<string, RegionType>
|
|
500
|
+
): LegendItem[] {
|
|
501
|
+
const warningKeys = Object.keys(severities)
|
|
502
|
+
return [4, 3, 2].reduce<LegendItem[]>((orderedSeverities, severity) => {
|
|
503
|
+
const warningTypesBySeverity = warningKeys.filter(
|
|
504
|
+
(key) => severities[key] === severity
|
|
505
|
+
)
|
|
506
|
+
warningTypes.forEach((_, warnType) => {
|
|
507
|
+
if (warningTypesBySeverity.includes(warnType)) {
|
|
508
|
+
const warnSeverity = severities[warnType]
|
|
509
|
+
if (warnSeverity !== undefined) {
|
|
510
|
+
orderedSeverities.push({
|
|
511
|
+
type: warnType,
|
|
512
|
+
severity: warnSeverity,
|
|
513
|
+
visible: true,
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
return orderedSeverities
|
|
519
|
+
}, [])
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Create regions data structure from warnings
|
|
524
|
+
*/
|
|
525
|
+
function createRegions(
|
|
526
|
+
warnings: WarningsMap,
|
|
527
|
+
geometryId: string,
|
|
528
|
+
geometries: GeometryCollection,
|
|
529
|
+
regionIds: string[],
|
|
530
|
+
warningTypes: Map<string, RegionType>
|
|
531
|
+
): RegionsData {
|
|
532
|
+
const warningKeys = Object.keys(warnings)
|
|
533
|
+
const geometryData = geometries[geometryId]
|
|
534
|
+
|
|
535
|
+
return [4, 3, 2].reduce(
|
|
536
|
+
(regionWarnings, severity) => {
|
|
537
|
+
const warningsBySeverity = warningKeys.filter(
|
|
538
|
+
(key) => warnings[key]?.severity === severity
|
|
539
|
+
)
|
|
540
|
+
;[...Array(NUMBER_OF_DAYS).keys()].forEach((day) => {
|
|
541
|
+
const warningsByDay = warningsBySeverity.filter(
|
|
542
|
+
(key) => warnings[key]?.effectiveDays[day]
|
|
543
|
+
)
|
|
544
|
+
warningTypes.forEach((_regionType, warningType) => {
|
|
545
|
+
const warningsByType = warningsByDay.filter(
|
|
546
|
+
(key) => warnings[key]?.type === warningType
|
|
547
|
+
)
|
|
548
|
+
warningsByType.sort((key1, key2) => {
|
|
549
|
+
const w1 = warnings[key1]
|
|
550
|
+
const w2 = warnings[key2]
|
|
551
|
+
if (!w1 || !w2) return 0
|
|
552
|
+
if (w1.severity !== w2.severity) {
|
|
553
|
+
return w2.severity - w1.severity
|
|
554
|
+
}
|
|
555
|
+
if (w1.value !== w2.value) {
|
|
556
|
+
return w2.value - w1.value
|
|
557
|
+
}
|
|
558
|
+
const effectiveFrom1 = new Date(w1.effectiveFrom).getTime()
|
|
559
|
+
const effectiveFrom2 = new Date(w2.effectiveFrom).getTime()
|
|
560
|
+
if (effectiveFrom1 !== effectiveFrom2) {
|
|
561
|
+
return effectiveFrom1 - effectiveFrom2
|
|
562
|
+
}
|
|
563
|
+
const effectiveUntil1 = new Date(w1.effectiveUntil).getTime()
|
|
564
|
+
const effectiveUntil2 = new Date(w2.effectiveUntil).getTime()
|
|
565
|
+
return effectiveUntil1 - effectiveUntil2
|
|
566
|
+
})
|
|
567
|
+
warningsByType.forEach((key) => {
|
|
568
|
+
const warning = warnings[key]
|
|
569
|
+
if (!warning) return
|
|
570
|
+
regionIds.forEach((regionId, regionIndex) => {
|
|
571
|
+
if (warning.regions[regionId]) {
|
|
572
|
+
const regionGeom = geometryData?.[regionId] as
|
|
573
|
+
| RegionGeometry
|
|
574
|
+
| undefined
|
|
575
|
+
if (!regionGeom) return
|
|
576
|
+
|
|
577
|
+
const dayRegions = regionWarnings[day]
|
|
578
|
+
if (!dayRegions) return
|
|
579
|
+
const regionItems =
|
|
580
|
+
dayRegions[regionGeom.type as keyof DayRegions]
|
|
581
|
+
let regionItem = regionItems.find(
|
|
582
|
+
(regionWarning: RegionListItem) =>
|
|
583
|
+
regionWarning.key === regionId
|
|
584
|
+
)
|
|
585
|
+
if (regionItem == null) {
|
|
586
|
+
regionItem = {
|
|
587
|
+
key: regionId,
|
|
588
|
+
regionIndex,
|
|
589
|
+
name: regionGeom.name,
|
|
590
|
+
warnings: [],
|
|
591
|
+
}
|
|
592
|
+
regionItems.push(regionItem)
|
|
593
|
+
}
|
|
594
|
+
let warningItem = regionItem.warnings.find(
|
|
595
|
+
(w: RegionWarningItem) => w.type === warningType
|
|
596
|
+
)
|
|
597
|
+
if (warningItem == null) {
|
|
598
|
+
warningItem = {
|
|
599
|
+
type: warningType,
|
|
600
|
+
identifiers: [],
|
|
601
|
+
coverage: 0,
|
|
602
|
+
}
|
|
603
|
+
regionItem.warnings.push(warningItem)
|
|
604
|
+
}
|
|
605
|
+
if (!warningItem.identifiers.includes(key)) {
|
|
606
|
+
warningItem.identifiers.push(key)
|
|
607
|
+
}
|
|
608
|
+
const covRegions = warning.covRegions
|
|
609
|
+
if (covRegions.has(regionId)) {
|
|
610
|
+
warningItem.coverage += covRegions.get(regionId) ?? 0
|
|
611
|
+
} else {
|
|
612
|
+
warningItem.coverage = 100
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
return regionWarnings
|
|
620
|
+
},
|
|
621
|
+
[...Array(NUMBER_OF_DAYS).keys()].map(() => ({
|
|
622
|
+
[REGION_LAND]: [] as RegionListItem[],
|
|
623
|
+
[REGION_SEA]: [] as RegionListItem[],
|
|
624
|
+
})) as RegionsData
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Create coverage SVG from geometry
|
|
630
|
+
*/
|
|
631
|
+
function createCoverage(
|
|
632
|
+
coverage: GeoJSONFeature,
|
|
633
|
+
width: number,
|
|
634
|
+
height: number,
|
|
635
|
+
reference: [number, number] | null,
|
|
636
|
+
bbox: GeoJSONFeature,
|
|
637
|
+
geoJSONToSVG: (data: object, width: number, height: number) => string
|
|
638
|
+
): string {
|
|
639
|
+
const data = {
|
|
640
|
+
type: 'FeatureCollection',
|
|
641
|
+
features: [coverage, bbox],
|
|
642
|
+
totalFeatures: 2,
|
|
643
|
+
crs: {
|
|
644
|
+
type: 'name',
|
|
645
|
+
properties: {
|
|
646
|
+
name: 'urn:ogc:def:crs:EPSG::3067',
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
} as GeoJSONFeatureCollection & { totalFeatures: number }
|
|
650
|
+
|
|
651
|
+
if (reference != null) {
|
|
652
|
+
data.features.push({
|
|
653
|
+
type: 'Feature',
|
|
654
|
+
id: 'reference',
|
|
655
|
+
properties: {},
|
|
656
|
+
geometry: {
|
|
657
|
+
type: 'Point',
|
|
658
|
+
coordinates: reference,
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
data.totalFeatures++
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return geoJSONToSVG(data, width, height)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Optimize coverage regions to prevent overlapping symbols in Saimaa
|
|
669
|
+
*/
|
|
670
|
+
function optimizeCovRegions(
|
|
671
|
+
warnings: WarningsMap,
|
|
672
|
+
regions: RegionsData,
|
|
673
|
+
geometryId: string,
|
|
674
|
+
geometries: GeometryCollection
|
|
675
|
+
): void {
|
|
676
|
+
const geometryData = geometries[geometryId]
|
|
677
|
+
if (!geometryData) return
|
|
678
|
+
|
|
679
|
+
Object.keys(geometryData)
|
|
680
|
+
.filter((regionId) => {
|
|
681
|
+
const region = geometryData[regionId] as RegionGeometry | undefined
|
|
682
|
+
return region?.type === 'sea' && region?.subType === 'lake'
|
|
683
|
+
})
|
|
684
|
+
.filter((regionId) =>
|
|
685
|
+
regions.some((day) => day.sea.some((region) => region.key === regionId))
|
|
686
|
+
)
|
|
687
|
+
.forEach((regionId) =>
|
|
688
|
+
Object.keys(warnings)
|
|
689
|
+
.filter((warningKey) => {
|
|
690
|
+
const w = warnings[warningKey]
|
|
691
|
+
return w && w.covRegions.size > 0
|
|
692
|
+
})
|
|
693
|
+
.forEach((warningKey) => {
|
|
694
|
+
const w = warnings[warningKey]
|
|
695
|
+
if (w) w.covRegions.set(regionId, 0)
|
|
696
|
+
})
|
|
697
|
+
)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ============================================================================
|
|
701
|
+
// Main Processing Function
|
|
702
|
+
// ============================================================================
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Process raw warning data into structured format
|
|
706
|
+
*/
|
|
707
|
+
export function processWarnings(
|
|
708
|
+
data: WarningsDataResponse,
|
|
709
|
+
ctx: WarningsProcessorContext
|
|
710
|
+
): HandleMapWarningsResult & { updatedAt: number | null; timeOffset: number } {
|
|
711
|
+
const warnings: WarningsMap = {}
|
|
712
|
+
const parents: ParentsMap = {}
|
|
713
|
+
let updatedAt: number | null = null
|
|
714
|
+
let timeOffset = 0
|
|
715
|
+
|
|
716
|
+
// Process update times
|
|
717
|
+
const allUpdateTimes = [WEATHER_UPDATE_TIME, FLOOD_UPDATE_TIME]
|
|
718
|
+
.filter(
|
|
719
|
+
(warningUpdateTime) =>
|
|
720
|
+
data[warningUpdateTime as keyof WarningsDataResponse] != null
|
|
721
|
+
)
|
|
722
|
+
.reduce((updateTimes: number[], warningUpdateTime) => {
|
|
723
|
+
const updateData = data[
|
|
724
|
+
warningUpdateTime as keyof WarningsDataResponse
|
|
725
|
+
] as GeoJSONFeatureCollection | undefined
|
|
726
|
+
|
|
727
|
+
if (
|
|
728
|
+
updateData?.features != null &&
|
|
729
|
+
updateData.features.length > 0 &&
|
|
730
|
+
updateData.features[0]?.properties != null
|
|
731
|
+
) {
|
|
732
|
+
const updateTime = new Date(
|
|
733
|
+
updateData.features[0].properties[UPDATE_TIME] as string
|
|
734
|
+
).getTime()
|
|
735
|
+
updateTimes.push(updateTime)
|
|
736
|
+
|
|
737
|
+
const maxDelay =
|
|
738
|
+
ctx.maxUpdateDelay[
|
|
739
|
+
warningUpdateTime as keyof typeof ctx.maxUpdateDelay
|
|
740
|
+
]
|
|
741
|
+
if (ctx.currentTime - updateTime > maxDelay) {
|
|
742
|
+
ctx.handleError(`${warningUpdateTime}_outdated`)
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
ctx.handleError(warningUpdateTime)
|
|
746
|
+
}
|
|
747
|
+
return updateTimes
|
|
748
|
+
}, [])
|
|
749
|
+
.sort()
|
|
750
|
+
.reverse()
|
|
751
|
+
|
|
752
|
+
const firstUpdateTime = allUpdateTimes[0]
|
|
753
|
+
updatedAt =
|
|
754
|
+
allUpdateTimes.length > 0 && firstUpdateTime != null
|
|
755
|
+
? firstUpdateTime
|
|
756
|
+
: null
|
|
757
|
+
|
|
758
|
+
if (!ctx.staticDays) {
|
|
759
|
+
const startTime = ctx.startFrom === 'updated' ? updatedAt : ctx.currentTime
|
|
760
|
+
if (startTime != null) {
|
|
761
|
+
timeOffset = msSinceStartOfDay(startTime, ctx.timeZone, ctx.locale)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Create warning factories
|
|
766
|
+
const createWarnings: Record<string, (warning: GeoJSONFeature) => Warning> = {
|
|
767
|
+
[WEATHER_WARNINGS]: (warning: GeoJSONFeature) =>
|
|
768
|
+
createWeatherWarning(
|
|
769
|
+
warning,
|
|
770
|
+
ctx.geometryId,
|
|
771
|
+
ctx.geometries,
|
|
772
|
+
ctx.dailyWarningTypes,
|
|
773
|
+
updatedAt,
|
|
774
|
+
ctx.currentTime,
|
|
775
|
+
ctx.startFrom,
|
|
776
|
+
timeOffset,
|
|
777
|
+
ctx.timeZone,
|
|
778
|
+
ctx.locale
|
|
779
|
+
),
|
|
780
|
+
[FLOOD_WARNINGS]: (warning: GeoJSONFeature) =>
|
|
781
|
+
createFloodWarning(
|
|
782
|
+
warning,
|
|
783
|
+
ctx.dailyWarningTypes,
|
|
784
|
+
updatedAt,
|
|
785
|
+
ctx.currentTime,
|
|
786
|
+
ctx.startFrom,
|
|
787
|
+
timeOffset,
|
|
788
|
+
ctx.timeZone,
|
|
789
|
+
ctx.locale,
|
|
790
|
+
ctx.t,
|
|
791
|
+
ctx.handleError
|
|
792
|
+
),
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Process warnings
|
|
796
|
+
const warningTypes = Object.keys(createWarnings)
|
|
797
|
+
for (const warningType of warningTypes) {
|
|
798
|
+
const warningData = data[warningType as keyof WarningsDataResponse] as
|
|
799
|
+
| GeoJSONFeatureCollection
|
|
800
|
+
| undefined
|
|
801
|
+
|
|
802
|
+
if (warningData == null) {
|
|
803
|
+
ctx.handleError(`Missing data: ${warningType}`)
|
|
804
|
+
ctx.onDataError()
|
|
805
|
+
continue
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const features = warningData.features ?? []
|
|
809
|
+
|
|
810
|
+
for (const warning of features) {
|
|
811
|
+
if (
|
|
812
|
+
isValidWarning(
|
|
813
|
+
warning,
|
|
814
|
+
ctx.geometryId,
|
|
815
|
+
ctx.geometries,
|
|
816
|
+
ctx.warningTypes
|
|
817
|
+
)
|
|
818
|
+
) {
|
|
819
|
+
let regionId: string | undefined
|
|
820
|
+
const regionIds: string[] = []
|
|
821
|
+
const warningId = warning.properties.identifier as string
|
|
822
|
+
|
|
823
|
+
if (warnings[warningId] == null) {
|
|
824
|
+
const createFn = createWarnings[warningType]
|
|
825
|
+
if (!createFn) continue
|
|
826
|
+
warnings[warningId] = createFn(warning)
|
|
827
|
+
const createdWarning = warnings[warningId]
|
|
828
|
+
if (!createdWarning) continue
|
|
829
|
+
const warningRegions = Object.keys(createdWarning.regions)
|
|
830
|
+
if (warningRegions.length > 0) {
|
|
831
|
+
regionId = warningRegions[0]
|
|
832
|
+
}
|
|
833
|
+
if (ctx.dailyWarningTypes.includes(createdWarning.type)) {
|
|
834
|
+
createdWarning.dailyWarning = true
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
regionId = regionFromReference(warning.properties.reference as string)
|
|
838
|
+
const geometryData = ctx.geometries[ctx.geometryId]
|
|
839
|
+
const existingWarning = warnings[warningId]
|
|
840
|
+
if (geometryData?.[regionId] && existingWarning) {
|
|
841
|
+
existingWarning.regions[regionId] = true
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Get the current warning object
|
|
846
|
+
const currentWarning = warnings[warningId]
|
|
847
|
+
if (!currentWarning) continue
|
|
848
|
+
|
|
849
|
+
// Handle coverage references
|
|
850
|
+
if (warning.properties.coverage_references != null) {
|
|
851
|
+
// Space after comma is needed for merged areas
|
|
852
|
+
;(warning.properties.coverage_references as string)
|
|
853
|
+
.split(', ')
|
|
854
|
+
.filter((reference) => reference.length > 0)
|
|
855
|
+
.forEach((reference) => {
|
|
856
|
+
const refRegionId = regionFromReference(reference)
|
|
857
|
+
const regionCoverage =
|
|
858
|
+
relativeCoverageFromReference(reference) / 100
|
|
859
|
+
const geometryData = ctx.geometries[ctx.geometryId]
|
|
860
|
+
if (geometryData?.[refRegionId]) {
|
|
861
|
+
currentWarning.regions[refRegionId] = true
|
|
862
|
+
currentWarning.covRegions.set(refRegionId, regionCoverage)
|
|
863
|
+
regionIds.push(refRegionId)
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
if (warning.geometry != null) {
|
|
868
|
+
const coverageSvg = createCoverage(
|
|
869
|
+
warning,
|
|
870
|
+
440,
|
|
871
|
+
550,
|
|
872
|
+
[
|
|
873
|
+
warning.properties.representative_x as number,
|
|
874
|
+
warning.properties.representative_y as number,
|
|
875
|
+
],
|
|
876
|
+
ctx.bbox,
|
|
877
|
+
ctx.geoJSONToSVG
|
|
878
|
+
)
|
|
879
|
+
const coverageSmallSvg = createCoverage(
|
|
880
|
+
warning,
|
|
881
|
+
75,
|
|
882
|
+
120,
|
|
883
|
+
null,
|
|
884
|
+
ctx.bbox,
|
|
885
|
+
ctx.geoJSONToSVG
|
|
886
|
+
)
|
|
887
|
+
currentWarning.coveragesLarge = coverageData(coverageSvg)
|
|
888
|
+
currentWarning.coveragesSmall = coverageData(coverageSmallSvg)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Handle children and parents
|
|
893
|
+
const geometryData = ctx.geometries[ctx.geometryId]
|
|
894
|
+
if (regionId != null && geometryData?.[regionId]) {
|
|
895
|
+
const regionGeom = geometryData[regionId] as RegionGeometry
|
|
896
|
+
regionGeom.children?.forEach((id) => {
|
|
897
|
+
currentWarning.regions[id] = true
|
|
898
|
+
})
|
|
899
|
+
if (regionIds.length === 0) {
|
|
900
|
+
regionIds.push(regionId)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
regionIds.forEach((id) => {
|
|
905
|
+
const regionGeom = geometryData?.[id] as RegionGeometry | undefined
|
|
906
|
+
const parentId = regionGeom?.parent
|
|
907
|
+
if (parentId) {
|
|
908
|
+
if (parents[parentId] == null) {
|
|
909
|
+
parents[parentId] = [false, false, false, false, false]
|
|
910
|
+
}
|
|
911
|
+
const parentDays = parents[parentId]
|
|
912
|
+
if (parentDays) {
|
|
913
|
+
currentWarning.effectiveDays.forEach((override, index) => {
|
|
914
|
+
if (override) {
|
|
915
|
+
parentDays[index] = true
|
|
916
|
+
}
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
})
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Create derived data
|
|
926
|
+
const days = createDays(
|
|
927
|
+
warnings,
|
|
928
|
+
updatedAt,
|
|
929
|
+
ctx.currentTime,
|
|
930
|
+
ctx.startFrom,
|
|
931
|
+
ctx.timeZone,
|
|
932
|
+
ctx.locale
|
|
933
|
+
)
|
|
934
|
+
const maxSeverities = getMaxSeverities(warnings)
|
|
935
|
+
const legend = createLegend(maxSeverities, ctx.warningTypes)
|
|
936
|
+
const regions = createRegions(
|
|
937
|
+
warnings,
|
|
938
|
+
ctx.geometryId,
|
|
939
|
+
ctx.geometries,
|
|
940
|
+
ctx.regionIds,
|
|
941
|
+
ctx.warningTypes
|
|
942
|
+
)
|
|
943
|
+
optimizeCovRegions(warnings, regions, ctx.geometryId, ctx.geometries)
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
warnings,
|
|
947
|
+
days,
|
|
948
|
+
regions,
|
|
949
|
+
parents,
|
|
950
|
+
legend,
|
|
951
|
+
updatedAt,
|
|
952
|
+
timeOffset,
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ============================================================================
|
|
957
|
+
// Composable
|
|
958
|
+
// ============================================================================
|
|
959
|
+
|
|
960
|
+
export interface UseWarningsProcessorReturn {
|
|
961
|
+
handleMapWarnings: (data: WarningsDataResponse) => HandleMapWarningsResult & {
|
|
962
|
+
updatedAt: number | null
|
|
963
|
+
timeOffset: number
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Warnings processor composable
|
|
969
|
+
*
|
|
970
|
+
* Provides the handleMapWarnings function for processing raw API data
|
|
971
|
+
* into structured warning data for display.
|
|
972
|
+
*/
|
|
973
|
+
export function useWarningsProcessor(
|
|
974
|
+
options: UseWarningsProcessorOptions
|
|
975
|
+
): UseWarningsProcessorReturn {
|
|
976
|
+
const handleMapWarnings = (
|
|
977
|
+
data: WarningsDataResponse
|
|
978
|
+
): HandleMapWarningsResult & {
|
|
979
|
+
updatedAt: number | null
|
|
980
|
+
timeOffset: number
|
|
981
|
+
} => {
|
|
982
|
+
const ctx: WarningsProcessorContext = {
|
|
983
|
+
geometryId: options.geometryId.value,
|
|
984
|
+
geometries: options.geometries.value,
|
|
985
|
+
regionIds: options.regionIds.value,
|
|
986
|
+
warningTypes: options.warningTypes.value,
|
|
987
|
+
timeZone: options.timeZone.value,
|
|
988
|
+
locale: options.locale.value,
|
|
989
|
+
currentTime: options.currentTime.value,
|
|
990
|
+
startFrom: options.startFrom.value,
|
|
991
|
+
staticDays: options.staticDays.value,
|
|
992
|
+
dailyWarningTypes: options.dailyWarningTypes.value,
|
|
993
|
+
maxUpdateDelay: options.maxUpdateDelay.value,
|
|
994
|
+
bbox: options.bbox.value,
|
|
995
|
+
geoJSONToSVG: options.geoJSONToSVG,
|
|
996
|
+
t: options.t,
|
|
997
|
+
handleError: options.handleError,
|
|
998
|
+
onDataError: options.onDataError,
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return processWarnings(data, ctx)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
handleMapWarnings,
|
|
1006
|
+
}
|
|
1007
|
+
}
|