@opengeoweb/soundings 12.2.0 → 12.3.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 (30) hide show
  1. package/index.esm.js +1159 -345
  2. package/package.json +8 -8
  3. package/src/index.d.ts +4 -0
  4. package/src/lib/api/edr-api/api.d.ts +2 -2
  5. package/src/lib/api/edr-api/hooks.d.ts +3 -2
  6. package/src/lib/components/DrawUtils.d.ts +1 -2
  7. package/src/lib/components/MenuBar/LocationsMenu.d.ts +13 -0
  8. package/src/lib/components/{SoundingsMenuBar → MenuBar}/MenuBar.d.ts +2 -1
  9. package/src/lib/components/MenuBar/MenuBar.stories.d.ts +20 -0
  10. package/src/lib/components/MenuBar/SoundingsSourceMenu.d.ts +12 -0
  11. package/src/lib/components/Providers/Providers.d.ts +12 -0
  12. package/src/lib/components/SoundingsConnect.d.ts +1 -0
  13. package/src/lib/components/StaticLines/drawIsobars.d.ts +1 -0
  14. package/src/lib/components/WindBarbs/utils.d.ts +25 -0
  15. package/src/lib/components/WindBarbs/windsymbols.d.ts +5 -0
  16. package/src/lib/components/index.d.ts +2 -2
  17. package/src/lib/i18n.d.ts +1 -0
  18. package/src/lib/store/reducer.d.ts +7 -0
  19. package/src/lib/store/selectors.d.ts +105 -0
  20. package/src/lib/store/selectors.spec.d.ts +1 -0
  21. package/src/lib/store/store.d.ts +9 -0
  22. package/src/lib/store/types.d.ts +25 -0
  23. package/src/lib/components/SoundingsMenuBar/LocationsMenu.d.ts +0 -11
  24. package/src/lib/components/SoundingsMenuBar/MenuBar.stories.d.ts +0 -26
  25. package/src/lib/components/SoundingsSourceMenu.d.ts +0 -9
  26. /package/src/lib/{utils/testHandlers.d.ts → api/edr-api/fakeApi.d.ts} +0 -0
  27. /package/src/lib/components/{SoundingsMenuBar → MenuBar}/LocationsMenuConnect.d.ts +0 -0
  28. /package/src/lib/components/{SoundingsMenuBar → MenuBar}/MenuBarConnect.d.ts +0 -0
  29. /package/src/lib/components/{SoundingsMenuBar → MenuBar}/MenuBarConnect.spec.d.ts +0 -0
  30. /package/src/lib/components/{SoundingsSourceDialogConnect.d.ts → MenuBar/SoundingsSourceDialogConnect.d.ts} +0 -0
package/index.esm.js CHANGED
@@ -1,15 +1,95 @@
1
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import React, { useRef, useEffect, useState, Fragment as Fragment$1, useMemo } from 'react';
1
+ import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
+ import React, { useRef, useEffect, useState, Fragment, useMemo } from 'react';
3
3
  import * as d3 from 'd3';
4
- import { Card, List, ListItem, ListItemIcon, Checkbox, MenuItem, Box, ListItemText, ListSubheader, ListItemButton, Collapse, Button } from '@mui/material';
5
- import { useSetupDialog, uiTypes, uiSelectors, uiActions } from '@opengeoweb/store';
6
- import { ToolContainerDraggable, StyledSelect, dateUtils } from '@opengeoweb/shared';
7
- import 'i18next';
8
- import { useTranslation } from 'react-i18next';
4
+ import { Card, List, ListItem, ListItemIcon, Checkbox, MenuItem, Box, ListItemText, Typography, ListSubheader, ListItemButton, Collapse, Stack, LinearProgress } from '@mui/material';
5
+ import { selectorMemoizationOptions, useSetupDialog, uiTypes, uiSelectors, uiActions, uiReducer, mapReducer } from '@opengeoweb/store';
6
+ import { useDispatch, useSelector } from 'react-redux';
7
+ import { ToolContainerDraggable, StyledSelect, dateUtils, CustomToggleButton, AlertBanner } from '@opengeoweb/shared';
8
+ import i18n from 'i18next';
9
+ export { default as i18n } from 'i18next';
10
+ import { useTranslation, initReactI18next } from 'react-i18next';
9
11
  import { getHeaders, handleResponse } from '@opengeoweb/api';
10
- import { useQueries } from '@tanstack/react-query';
12
+ import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
13
+ import { useQueries, useQuery } from '@tanstack/react-query';
11
14
  import { ChevronDown, Sources } from '@opengeoweb/theme';
12
- import { useDispatch, useSelector } from 'react-redux';
15
+
16
+ var en = {
17
+ sources: "Sources",
18
+ latest: "Latest",
19
+ noInstancesAvailable: "No instances found",
20
+ noServicesFound: "No source available",
21
+ locations: "Locations",
22
+ "locations-menu-warning": "You have to choose your sources in the sources menu"
23
+ };
24
+ var no = {
25
+ sources: "Kilder",
26
+ latest: "Siste",
27
+ noInstancesAvailable: "Ingen instanser funnet",
28
+ noServicesFound: "Ingen tjenester funnet",
29
+ locations: "Lokasjoner",
30
+ "locations-menu-warning": "Du må velge kilder i kilde-menyen"
31
+ };
32
+ var nl = {
33
+ sources: "Bronnen",
34
+ latest: "Nieuwste",
35
+ noInstancesAvailable: "Geen exemplaren gevonden",
36
+ noServicesFound: "Geen bron beschikbaar",
37
+ locations: "Locaties",
38
+ "locations-menu-warning": "Selecteer eerst een bron in het bron menu"
39
+ };
40
+ var fi = {
41
+ sources: "Lähteet",
42
+ latest: "Viimeisin",
43
+ noInstancesAvailable: "Tapauksia ei löytynyt",
44
+ noServicesFound: "Lähdettä ei ole saatavilla",
45
+ locations: "Locations",
46
+ "locations-menu-warning": "Valitse ensin lähde lähdevalikosta"
47
+ };
48
+ var soundingsTranslations = {
49
+ en: en,
50
+ no: no,
51
+ nl: nl,
52
+ fi: fi
53
+ };
54
+
55
+ /* *
56
+ * Licensed under the Apache License, Version 2.0 (the "License");
57
+ * you may not use this file except in compliance with the License.
58
+ * You may obtain a copy of the License at
59
+ *
60
+ * http://www.apache.org/licenses/LICENSE-2.0
61
+ *
62
+ * Unless required by applicable law or agreed to in writing, software
63
+ * distributed under the License is distributed on an "AS IS" BASIS,
64
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
65
+ * See the License for the specific language governing permissions and
66
+ * limitations under the License.
67
+ *
68
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
69
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
70
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
71
+ * */
72
+ const isobarValues = [200, 300, 400, 500, 700, 850, 925, 1000];
73
+ const drawIsobars = ({
74
+ minTemperature,
75
+ maxTemperature,
76
+ xScale,
77
+ yScale,
78
+ svg
79
+ }) => {
80
+ // Calculate X and Y coordinates for each pressure level
81
+ for (const pressureVal of isobarValues) {
82
+ const Px = [];
83
+ const tempBounds = [minTemperature, maxTemperature];
84
+ for (const bound of tempBounds) {
85
+ Px.push([xScale(bound), yScale(pressureVal)]);
86
+ }
87
+ const lineGenerator = d3.line();
88
+ const isoPathString = lineGenerator(Px);
89
+ const strID = `${pressureVal}textId`;
90
+ svg.append('path').attr('id', strID).attr('type', 'isobar').attr('d', isoPathString).style('fill', 'none').style('stroke-width', 1.5).style('stroke', '#B5B51F').style('stroke-opacity', 0.5);
91
+ }
92
+ };
13
93
 
14
94
  /* *
15
95
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -38,20 +118,33 @@ const initializeSvg = ({
38
118
  }) => {
39
119
  const svg = d3.select(svgRef.current).attr('width', width + (margin.left + margin.right)).attr('height', height + (margin.top + margin.bottom)).attr('id', 'skewT').attr('data-testid', 'skewT').append('g').attr('transform', `translate(${margin.left + margin.right},${margin.top})`);
40
120
  svg.append('clipPath').attr('id', 'clip-skewTd3').append('rect').attr('width', width).attr('height', height).attr('x', 0).attr('y', 0);
41
- svg.append('rect').attr('height', height).attr('width', width).attr('fill', 'white').attr('opacity', 1).attr('id', 'skewTd3').attr('clip-path', 'url(#clip-skewTd3)');
121
+ svg.append('rect').attr('height', height).attr('width', width).attr('fill', 'none').attr('opacity', 1).attr('id', 'skewTd3').attr('clip-path', 'url(#clip-skewTd3)');
42
122
  return svg;
43
123
  };
44
124
  const drawAxes = ({
45
125
  svg,
46
126
  xScale,
47
127
  yScale,
48
- height,
49
- width
128
+ height
50
129
  }) => {
51
130
  // Create x-axis with intervals
52
- svg.append('g').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).ticks(10)).append('text').attr('fill', '#000').attr('x', `${width - 8}px`).attr('y', `${12}px`).attr('dy', '1em').attr('font-size', '12px').attr('text-anchor', 'middle').text('°C');
131
+ const xTicks = xScale.ticks(10);
132
+ const xMiddleValue = xTicks[Math.floor(xTicks.length / 2)];
133
+ svg.append('g').attr('type', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickValues(xTicks).tickSizeInner(0).tickSizeOuter(0));
134
+ // Reformat in the case of the middle tick label
135
+ // to add the unit part
136
+ svg.selectAll('g[type="x-axis"] .tick').filter(function () {
137
+ return parseInt(d3.select(this).select('text').text(), 10) === xMiddleValue;
138
+ }).each(function () {
139
+ const unitPart = '°C';
140
+ d3.select(this).select('text')
141
+ // tspan necessary to use a different font-size
142
+ .append('tspan').text(unitPart).attr('font-size', '11px').attr('dy', '0.04em');
143
+ });
53
144
  // Create y-axis with intervals
54
- svg.append('g').call(d3.axisLeft(yScale).ticks(0, '')).append('text').attr('fill', '#000').attr('transform', 'translate(0)').attr('y', `${0}px`).attr('x', `${ -16}px`).attr('font-size', '12px').attr('dy', '1em').attr('text-anchor', 'middle').text('hPa');
145
+ // Make sure all zeroes are printed (1000 vs 1k)
146
+ const formatTicks = d3.format('.0f');
147
+ svg.append('g').call(d3.axisLeft(yScale).tickValues(isobarValues).tickFormat(formatTicks).tickSizeInner(0).tickSizeOuter(0)).call(g => g.select('.domain').remove()).append('text').attr('fill', 'currentColor').attr('x', '0').attr('y', yScale(400)).attr('font-size', '11px').attr('dy', '-0.9em').attr('text-anchor', 'left').text('hPa');
55
148
  };
56
149
  const addLegends = ({
57
150
  svg,
@@ -81,6 +174,493 @@ const plotSounding = ({
81
174
  svg.append('path').attr('d', pathString).attr('x', 0).attr('y', 0).style('fill', 'none').style('stroke-width', 2).style('stroke', color).style('stroke-opacity', 1).style('stroke-dasharray', lineStyle).attr('clip-path', 'url(#clip-skewTd3)');
82
175
  };
83
176
 
177
+ /* *
178
+ * Licensed under the Apache License, Version 2.0 (the "License");
179
+ * you may not use this file except in compliance with the License.
180
+ * You may obtain a copy of the License at
181
+ *
182
+ * http://www.apache.org/licenses/LICENSE-2.0
183
+ *
184
+ * Unless required by applicable law or agreed to in writing, software
185
+ * distributed under the License is distributed on an "AS IS" BASIS,
186
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
187
+ * See the License for the specific language governing permissions and
188
+ * limitations under the License.
189
+ *
190
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
191
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
192
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
193
+ * */
194
+ const windSymbolDefinitions = {
195
+ calm: `<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
196
+ <path d="M25 45C36.0456 45 45 36.0456 45 25C45 13.9543 36.0456 5 25 5C13.9543 5 5 13.9543 5 25C5 36.0456 13.9543 45 25 45Z" stroke="black" stroke-width="1.8"/>
197
+ </svg>
198
+ `,
199
+ lightAir: `<svg width="14" height="48" viewBox="0 0 14 48" fill="none" xmlns="http://www.w3.org/2000/svg">
200
+ <path d="M7.22656 4.89307V48.0001M7.22656 43.0262V44.6842" stroke="black" stroke-width="1.8"/>
201
+ </svg>
202
+ `,
203
+ fiveKnots: `<svg width="15" height="49" viewBox="0 0 15 49" fill="none" xmlns="http://www.w3.org/2000/svg">
204
+ <path d="M8.26953 5.57324V48.608M8.26953 40.5757L0.269531 42.2309" stroke="black" stroke-width="1.8"/>
205
+ </svg>`,
206
+ tenKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
207
+ <path d="M17.0781 4.64502V44.9031L1.07812 47.9999" stroke="black" stroke-width="1.8"/>
208
+ </svg>`,
209
+ fifteenKnots: `<svg width="23" height="50" viewBox="0 0 23 50" fill="none" xmlns="http://www.w3.org/2000/svg">
210
+ <path d="M16.4727 5.55469V45.8128L0.472656 48.9095M16.4727 38.0708L9.61551 39.6192" stroke="black" stroke-width="1.8"/>
211
+ </svg>
212
+ `,
213
+ twentyKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
214
+ <path d="M16.8555 4.71924V44.9773L0.855469 48.0741M16.8555 38.4093L0.855469 41.5061" stroke="black" stroke-width="1.8"/>
215
+ </svg>`,
216
+ twentyFiveKnots: `<svg width="23" height="51" viewBox="0 0 23 51" fill="none" xmlns="http://www.w3.org/2000/svg">
217
+ <path d="M16.2383 4.94434V46.1541L0.238281 49.3241M16.2383 40.3787L0.238281 43.5486M16.2383 33.9801L9.38114 35.5651" stroke="black" stroke-width="1.8"/>
218
+ </svg>`,
219
+ thirtyKnots: `<svg width="23" height="50" viewBox="0 0 23 50" fill="none" xmlns="http://www.w3.org/2000/svg">
220
+ <path d="M16.1914 5.64502V45.9031L0.191406 48.9999M16.1914 40.3459L0.191406 43.4427M16.1914 34.9407L0.191406 38.0375" stroke="black" stroke-width="1.8"/>
221
+ </svg>`,
222
+ thirtyFiveKnots: `<svg width="19" height="50" viewBox="0 0 19 50" fill="none" xmlns="http://www.w3.org/2000/svg">
223
+ <path d="M12.0028 4.96729V45.2253L1.16406 48.3221M12.0028 39.6443L1.16406 42.7411M12.0028 34.4793L1.16406 37.576M12.0028 29.2735L7.35761 30.8219" stroke="black" stroke-width="1.8"/>
224
+ </svg>`,
225
+ fortyKnots: `<svg width="24" height="50" viewBox="0 0 24 50" fill="none" xmlns="http://www.w3.org/2000/svg">
226
+ <path d="M16.5391 5.40311V45.6612L0.539062 48.7579M16.5391 40.4525L0.539062 43.5493M16.5391 35.0449L0.539062 38.1417M16.5391 30.2009L0.539062 33.2976" stroke="black" stroke-width="1.8"/>
227
+ </svg>`,
228
+ fortyFiveKnots: `<svg width="23" height="50" viewBox="0 0 23 50" fill="none" xmlns="http://www.w3.org/2000/svg">
229
+ <path d="M16.4492 5.4043V45.6624L0.449219 48.7591M16.4492 40.1513L0.449219 43.2481M16.4492 34.7124L0.449219 37.8091M16.4492 29.2013L0.449219 32.2981M16.4492 24.3066L9.59208 25.855" stroke="black" stroke-width="1.8"/>
230
+ </svg>`,
231
+ fiftyKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
232
+ <path d="M16.9062 5.27002V45.5281L0.90625 48.6249L16.9062 37.7861" fill="black"/>
233
+ <path d="M16.9053 5.27002V45.5281L4.35132 47.2337L16.9053 37.7861" stroke="black" stroke-width="1.8"/>
234
+ </svg>
235
+ `,
236
+ fiftyFiveKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
237
+ <path d="M16.728 5.55371V45.8118L0.894531 48.9085L16.728 38.0698" fill="black"/>
238
+ <path d="M16.73 5.55371V45.8118L4.33984 47.5245L16.73 38.0698" stroke="black" stroke-width="1.8"/>
239
+ <path d="M16.7292 32.7358C13.6376 33.3406 8.8125 34.2842 8.8125 34.2842" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
240
+ </svg>`,
241
+ sixtyKnots: `<svg width="23" height="49" viewBox="0 0 23 49" fill="none" xmlns="http://www.w3.org/2000/svg">
242
+ <path d="M16.2891 5.27051V45.5286L0.289062 48.6253L16.2891 37.7866" fill="black"/>
243
+ <path d="M16.2897 5.27051V45.5286L3.7422 47.2478L16.2897 37.7866" stroke="black" stroke-width="1.8"/>
244
+ <path d="M16.2891 34.6899L0.289062 37.7867" stroke="black" stroke-width="1.8"/>
245
+ </svg>`,
246
+ sixtyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
247
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
248
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
249
+ <path d="M17 32.0788L1 35.1756M17 26.8862L9.5 28.5" stroke="black" stroke-width="1.8"/>
250
+ </svg>`,
251
+ seventyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
252
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
253
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
254
+ <path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
255
+ </svg>`,
256
+ seventyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
257
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
258
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
259
+ <path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
260
+ <path d="M16.9167 22C13.8251 22.6048 9 23.5484 9 23.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
261
+ </svg>`,
262
+ eightyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
263
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
264
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
265
+ <path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
266
+ <path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
267
+ </svg>`,
268
+ eightyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
269
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
270
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
271
+ <path d="M16.9167 17C13.8251 17.6048 9 18.5484 9 18.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
272
+ <path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
273
+ <path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
274
+ </svg>
275
+ `,
276
+ ninetyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
277
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
278
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
279
+ <path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
280
+ <path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
281
+ <path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
282
+ </svg>
283
+ `,
284
+ ninetyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
285
+ <path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
286
+ <path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
287
+ <path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
288
+ <path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
289
+ <path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
290
+ <path d="M16.9167 12C13.8251 12.6048 9 13.5484 9 13.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
291
+ </svg>`,
292
+ hundredKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
293
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
294
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
295
+ </svg>
296
+ `,
297
+ hundredFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
298
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
299
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
300
+ <path d="M16.9167 25C13.8251 25.6048 9 26.5484 9 26.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
301
+ </svg>
302
+ `,
303
+ hundredTenKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
304
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
305
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
306
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
307
+ </svg>`,
308
+ hundredFifteenKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
309
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
310
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
311
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
312
+ <path d="M16.9167 21C13.8251 21.6048 9 22.5484 9 22.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
313
+ </svg>`,
314
+ hundredTwentyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
315
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
316
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
317
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
318
+ <path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
319
+ </svg>`,
320
+ hundredTwentyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
321
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
322
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
323
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
324
+ <path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
325
+ <path d="M16.9167 17C13.8251 17.6048 9 18.5484 9 18.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
326
+ </svg>
327
+ `,
328
+ hundredThirtyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
329
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
330
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
331
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
332
+ <path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
333
+ <path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
334
+ </svg>`,
335
+ hundredThirtyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
336
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
337
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
338
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
339
+ <path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
340
+ <path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
341
+ <path d="M16.9167 13C13.8251 13.6048 9 14.5484 9 14.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
342
+ </svg>`,
343
+ hundredFortyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
344
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
345
+ <path d="M16.9984 3.26807V28M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34" stroke="black" stroke-width="1.8"/>
346
+ <path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
347
+ <path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
348
+ <path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
349
+ <path d="M17 13C10.7518 14.1718 1 16 1 16" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
350
+ </svg>`,
351
+ hundredFortyFiveKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
352
+ <path d="M17 5.26807V25.3971V45.5261L1 48.6229L17 37.7842" fill="black"/>
353
+ <path d="M16.9984 5.26807V30M16.9984 37.7842L4.35938 47.2985L16.9984 45.5261V37.5M16.9984 30C16.9984 30 5.67406 37.7842 4 39M16.9984 30V31.5M4 39C9.07621 38.4142 16.9984 37.5 16.9984 37.5M4 39C9.07621 36.0711 16.9984 31.5 16.9984 31.5M4 39C9.07682 36.6569 17 33 17 33M4 39C9.07621 37.2426 16.9984 34.5 16.9984 34.5M4 39C9.07621 37.8284 16.9984 36 16.9984 36M16.9984 37.5V36M16.9984 31.5V34.5M16.9984 34.5V36" stroke="black" stroke-width="1.8"/>
354
+ <path d="M16.9167 12C13.8251 12.6048 9 13.5484 9 13.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
355
+ <path d="M17 27C10.7518 28.1718 1 30 1 30" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
356
+ <path d="M17 23C10.7518 24.1718 1 26 1 26" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
357
+ <path d="M17 19C10.7518 20.1718 1 22 1 22" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
358
+ <path d="M17 15C10.7518 16.1718 1 18 1 18" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
359
+ </svg>`,
360
+ hundredFiftyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
361
+ <path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
362
+ <path d="M16.9984 3.26807V19M16.9984 35.7842L4.35938 45.2985L16.9984 43.5261V35.5M16.9984 28C16.9984 28 5.67406 35.7842 4 37M16.9984 28V29.5M16.9984 28V26.5M4 37C9.07621 36.4142 16.9984 35.5 16.9984 35.5M4 37C9.07621 34.0711 16.9984 29.5 16.9984 29.5M4 37C9.07682 34.6569 17 31 17 31M4 37C9.07621 35.2426 16.9984 32.5 16.9984 32.5M4 37C9.07621 35.8284 16.9984 34 16.9984 34M16.9984 35.5V34M16.9984 29.5V32.5M16.9984 32.5V34M16.9984 19C16.9984 19 8 25.5 4 29M16.9984 19V20.5M4 29C9.07621 28.0237 16.9984 26.5 16.9984 26.5M4 29C9.07621 25.6805 16.9984 20.5 16.9984 20.5M4 29C9.07621 26.4616 16.9984 22.5 16.9984 22.5M4 29C9.07621 27.0474 16.9984 24 16.9984 24M4 29C9.07621 27.5355 16.9984 25.25 16.9984 25.25M16.9984 26.5V25.25M16.9984 20.5V22.5M16.9984 22.5V24M16.9984 24V25.25" stroke="black" stroke-width="1.8"/>
363
+ </svg>`
364
+ };
365
+ const windSpeedRanges = [{
366
+ min: 0,
367
+ max: 1,
368
+ symbol: windSymbolDefinitions.calm
369
+ }, {
370
+ min: 1,
371
+ max: 4.5,
372
+ symbol: windSymbolDefinitions.lightAir
373
+ }, {
374
+ min: 4.5,
375
+ max: 7.5,
376
+ symbol: windSymbolDefinitions.fiveKnots
377
+ }, {
378
+ min: 7.5,
379
+ max: 12.5,
380
+ symbol: windSymbolDefinitions.tenKnots
381
+ }, {
382
+ min: 12.5,
383
+ max: 17.5,
384
+ symbol: windSymbolDefinitions.fifteenKnots
385
+ }, {
386
+ min: 17.5,
387
+ max: 22.5,
388
+ symbol: windSymbolDefinitions.twentyKnots
389
+ }, {
390
+ min: 22.5,
391
+ max: 27.5,
392
+ symbol: windSymbolDefinitions.twentyFiveKnots
393
+ }, {
394
+ min: 27.5,
395
+ max: 32.5,
396
+ symbol: windSymbolDefinitions.thirtyKnots
397
+ }, {
398
+ min: 32.5,
399
+ max: 37.5,
400
+ symbol: windSymbolDefinitions.thirtyFiveKnots
401
+ }, {
402
+ min: 37.5,
403
+ max: 42.5,
404
+ symbol: windSymbolDefinitions.fortyKnots
405
+ }, {
406
+ min: 42.5,
407
+ max: 47.5,
408
+ symbol: windSymbolDefinitions.fortyFiveKnots
409
+ }, {
410
+ min: 47.5,
411
+ max: 52.5,
412
+ symbol: windSymbolDefinitions.fiftyKnots
413
+ }, {
414
+ min: 52.5,
415
+ max: 57.5,
416
+ symbol: windSymbolDefinitions.fiftyFiveKnots
417
+ }, {
418
+ min: 57.5,
419
+ max: 62.5,
420
+ symbol: windSymbolDefinitions.sixtyKnots
421
+ }, {
422
+ min: 62.5,
423
+ max: 67.5,
424
+ symbol: windSymbolDefinitions.sixtyFiveKnots
425
+ }, {
426
+ min: 67.5,
427
+ max: 72.5,
428
+ symbol: windSymbolDefinitions.seventyKnots
429
+ }, {
430
+ min: 72.5,
431
+ max: 77.5,
432
+ symbol: windSymbolDefinitions.seventyFiveKnots
433
+ }, {
434
+ min: 77.5,
435
+ max: 82.5,
436
+ symbol: windSymbolDefinitions.eightyKnots
437
+ }, {
438
+ min: 82.5,
439
+ max: 87.5,
440
+ symbol: windSymbolDefinitions.eightyFiveKnots
441
+ }, {
442
+ min: 87.5,
443
+ max: 92.5,
444
+ symbol: windSymbolDefinitions.ninetyKnots
445
+ }, {
446
+ min: 92.5,
447
+ max: 97.5,
448
+ symbol: windSymbolDefinitions.ninetyFiveKnots
449
+ }, {
450
+ min: 97.5,
451
+ max: 102.5,
452
+ symbol: windSymbolDefinitions.hundredKnots
453
+ }, {
454
+ min: 102.5,
455
+ max: 107.5,
456
+ symbol: windSymbolDefinitions.hundredFiveKnots
457
+ }, {
458
+ min: 107.5,
459
+ max: 112.5,
460
+ symbol: windSymbolDefinitions.hundredTenKnots
461
+ }, {
462
+ min: 112.5,
463
+ max: 117.5,
464
+ symbol: windSymbolDefinitions.hundredFifteenKnots
465
+ }, {
466
+ min: 117.5,
467
+ max: 122.5,
468
+ symbol: windSymbolDefinitions.hundredTwentyKnots
469
+ }, {
470
+ min: 122.5,
471
+ max: 127.5,
472
+ symbol: windSymbolDefinitions.hundredTwentyFiveKnots
473
+ }, {
474
+ min: 127.5,
475
+ max: 132.5,
476
+ symbol: windSymbolDefinitions.hundredThirtyKnots
477
+ }, {
478
+ min: 132.5,
479
+ max: 137.5,
480
+ symbol: windSymbolDefinitions.hundredThirtyFiveKnots
481
+ }, {
482
+ min: 137.5,
483
+ max: 142.5,
484
+ symbol: windSymbolDefinitions.hundredFortyKnots
485
+ }, {
486
+ min: 142.5,
487
+ max: 147.5,
488
+ symbol: windSymbolDefinitions.hundredFortyFiveKnots
489
+ }, {
490
+ min: 147.5,
491
+ max: 152,
492
+ symbol: windSymbolDefinitions.hundredFiftyKnots
493
+ }];
494
+
495
+ /* *
496
+ * Licensed under the Apache License, Version 2.0 (the "License");
497
+ * you may not use this file except in compliance with the License.
498
+ * You may obtain a copy of the License at
499
+ *
500
+ * http://www.apache.org/licenses/LICENSE-2.0
501
+ *
502
+ * Unless required by applicable law or agreed to in writing, software
503
+ * distributed under the License is distributed on an "AS IS" BASIS,
504
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
505
+ * See the License for the specific language governing permissions and
506
+ * limitations under the License.
507
+ *
508
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
509
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
510
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
511
+ * */
512
+ const metersPerSecondToKnots = metersPerSecond => metersPerSecond * 1.94384;
513
+ const extendIsoBar = (path, horizontalBarbMargin) => {
514
+ const match = path.match(/^M([\d.-]+),([\d.-]+)\s*L([\d.-]+),([\d.-]+)/);
515
+ if (match) {
516
+ const startX = parseFloat(match[1]);
517
+ const startY = parseFloat(match[2]);
518
+ const endX = parseFloat(match[3]);
519
+ const endY = parseFloat(match[4]);
520
+ return `M${startX},${startY}L${endX + horizontalBarbMargin * 3},${endY}`;
521
+ }
522
+ return path;
523
+ };
524
+ const extendXAxis = (path, horizontalBarbMargin) => {
525
+ const match = path.match(/^M([\d.-]+),([\d.-]+)H([\d.-]+)/);
526
+ if (match) {
527
+ const endX = parseFloat(match[3]);
528
+ return path.replace(/H[\d.-]+/, `H${endX + horizontalBarbMargin * 3}`);
529
+ }
530
+ return path;
531
+ };
532
+ const drawWindBarbs = ({
533
+ svgRef,
534
+ data,
535
+ width,
536
+ height,
537
+ margin,
538
+ minPressure,
539
+ maxPressure,
540
+ barbColor = 'black'
541
+ }) => {
542
+ const barbsSize = 25;
543
+ const scalingFactor = 0.5;
544
+ const horizontalBarbMargin = barbsSize * scalingFactor;
545
+ const visibleItems = removeItemsByGrowthType(data.reverse(), GrowthType.EXPONENTIAL, 1.15, 1).filter(item => item.pressure >= 150);
546
+ // Measure width and height and make extra room for one column
547
+ // of windbarbs
548
+ const svg = d3.select(svgRef.current);
549
+ const currentWidth = svg.node()?.getBoundingClientRect().width;
550
+ if (!currentWidth) {
551
+ return;
552
+ }
553
+ // Add extra width to all isobars needed for the windbarbs
554
+ d3.selectAll('path[type="isobar"]').each(function () {
555
+ const path = d3.select(this).attr('d');
556
+ const newD = extendIsoBar(path, horizontalBarbMargin);
557
+ d3.select(this).attr('d', newD).attr('clip-path', null);
558
+ });
559
+ // Add extra width to x-axis needed for the windbarbs
560
+ d3.select('g[type="x-axis"] path.domain').each(function () {
561
+ const path = d3.select(this).attr('d');
562
+ const newD = extendXAxis(path, horizontalBarbMargin);
563
+ d3.select(this).attr('d', newD);
564
+ });
565
+ svg.attr('width', currentWidth + margin.right + margin.left + barbsSize);
566
+ const pressureScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]).clamp(true);
567
+ const barbGroup = svg.append('g').attr('class', 'wind-group').attr('transform',
568
+ // This translation should be revisited when implementing
569
+ // the plotting of several datasets at once
570
+ `translate(${(currentWidth ?? width) + horizontalBarbMargin}, ${margin.top})`);
571
+ barbGroup.selectAll('g').data(visibleItems).enter().append('g').attr('id', 'wind-barb').each(function handleBarb(data) {
572
+ const group = d3.select(this);
573
+ const windSymbol = getWindSymbol(data.windSpeed);
574
+ if (windSymbol) {
575
+ // Inner group will have rotation and scaling applied to it
576
+ const innerGroup = group.append('g');
577
+ innerGroup.node()?.appendChild(windSymbol);
578
+ // Update stroke color
579
+ innerGroup.selectAll('*').attr('stroke', barbColor);
580
+ // Only update the fill for the paths that already used a fill
581
+ // in their original svg form.
582
+ innerGroup.selectAll('*').each(function () {
583
+ const el = d3.select(this);
584
+ const currentFill = el.attr('fill');
585
+ if (currentFill && currentFill !== 'none') {
586
+ el.attr('fill', barbColor);
587
+ }
588
+ });
589
+ const bbox = windSymbol.getBBox();
590
+ // Central point
591
+ const cx = bbox.x + bbox.width / 2;
592
+ const cy = bbox.y + bbox.height / 2;
593
+ const rotation = 180 + data.windDirection;
594
+ const y = pressureScale(data.pressure);
595
+ // Place items in the correct vertical position
596
+ // Correcting for barbSize
597
+ group.attr('transform', `translate(0, ${y - barbsSize * scalingFactor / 8})`);
598
+ // Rotate accordingly, and scale
599
+ innerGroup.attr('transform', `rotate(${rotation}) scale(${scalingFactor}) translate(${-cx}, ${-cy})`);
600
+ }
601
+ });
602
+ };
603
+ const GrowthType = {
604
+ LINEAR: 'LINEAR',
605
+ EXPONENTIAL: 'EXPONENTIAL',
606
+ CUBIC: 'CUBIC'
607
+ };
608
+ const removeItemsByGrowthType = (array, growthType = 'LINEAR', base = 2, initial = 1) => {
609
+ if (!Object.values(GrowthType).includes(growthType)) {
610
+ throw new Error('Unsupported growth type');
611
+ }
612
+ if (!Array.isArray(array) || array.length === 0) {
613
+ return [];
614
+ }
615
+ if (growthType === GrowthType.LINEAR && base === 0) {
616
+ throw new Error('Base must be greater than 0');
617
+ }
618
+ if (growthType === GrowthType.EXPONENTIAL && base <= 1) {
619
+ throw new Error('Base must be greater than 1');
620
+ }
621
+ if (growthType === GrowthType.CUBIC && base <= 0) {
622
+ throw new Error('Base must be greater than 0');
623
+ }
624
+ const indicesToKeep = [];
625
+ let i = 0;
626
+ let nextIndex = 0;
627
+ while (nextIndex < array.length) {
628
+ switch (growthType) {
629
+ case GrowthType.LINEAR:
630
+ nextIndex = initial + i * base;
631
+ break;
632
+ case GrowthType.EXPONENTIAL:
633
+ nextIndex = Math.floor(initial * base ** i);
634
+ break;
635
+ case GrowthType.CUBIC:
636
+ nextIndex = initial + Math.floor(i ** 3 * base);
637
+ break;
638
+ }
639
+ if (nextIndex < array.length) {
640
+ indicesToKeep.push(nextIndex);
641
+ }
642
+ i += 1;
643
+ }
644
+ return array.filter((_, index) => indicesToKeep.includes(index));
645
+ };
646
+ const getWindSymbol = value => {
647
+ const valueKts = metersPerSecondToKnots(value);
648
+ const matchedRange = windSpeedRanges.find(({
649
+ min,
650
+ max
651
+ }) => valueKts >= min && valueKts < max);
652
+ // Out of range
653
+ if (!matchedRange) {
654
+ return null;
655
+ }
656
+ // SVG string to actual SVG element
657
+ const parser = new DOMParser();
658
+ const svgDoc = parser.parseFromString(matchedRange.symbol, 'image/svg+xml');
659
+ const svgNode = svgDoc.documentElement;
660
+ // Return the parsed SVG node
661
+ return svgNode;
662
+ };
663
+
84
664
  /* *
85
665
  * Licensed under the Apache License, Version 2.0 (the "License");
86
666
  * you may not use this file except in compliance with the License.
@@ -133,48 +713,6 @@ const drawDryAdiabats = ({
133
713
  }
134
714
  };
135
715
 
136
- /* *
137
- * Licensed under the Apache License, Version 2.0 (the "License");
138
- * you may not use this file except in compliance with the License.
139
- * You may obtain a copy of the License at
140
- *
141
- * http://www.apache.org/licenses/LICENSE-2.0
142
- *
143
- * Unless required by applicable law or agreed to in writing, software
144
- * distributed under the License is distributed on an "AS IS" BASIS,
145
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
146
- * See the License for the specific language governing permissions and
147
- * limitations under the License.
148
- *
149
- * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
150
- * Copyright 2025 - Finnish Meteorological Institute (FMI)
151
- * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
152
- * */
153
- const isobarValues = [200, 300, 400, 500, 700, 850, 925, 1000];
154
- const drawIsobars = ({
155
- minTemperature,
156
- maxTemperature,
157
- xScale,
158
- yScale,
159
- svg
160
- }) => {
161
- // Calculate X and Y coordinates for each pressure level
162
- for (const pressureVal of isobarValues) {
163
- const Px = [];
164
- const tempBounds = [minTemperature, maxTemperature];
165
- const Ppx = yScale(pressureVal);
166
- for (const bound of tempBounds) {
167
- Px.push([xScale(bound), yScale(pressureVal)]);
168
- }
169
- const lineGenerator = d3.line();
170
- const isoPathString = lineGenerator(Px);
171
- const strID = `${pressureVal}textId`;
172
- svg.append('path').attr('id', strID).attr('d', isoPathString).style('fill', 'none').style('stroke-width', 1.5).style('stroke', '#B5B51F').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
173
- svg.append('text').text(pressureVal).attr('text-anchor', 'middle').attr('x', `${ -24}px`).attr('y', Ppx + 5) // Y-coordinate of the label
174
- .attr('fill', '#000').attr('font-size', '12px');
175
- }
176
- };
177
-
178
716
  /* *
179
717
  * Licensed under the Apache License, Version 2.0 (the "License");
180
718
  * you may not use this file except in compliance with the License.
@@ -474,7 +1012,7 @@ const Soundings = ({
474
1012
  const staticPressureArray = [maxPressure];
475
1013
  const dp = -5;
476
1014
  const minTemperature = -40;
477
- const maxTemperature = 40;
1015
+ const maxTemperature = 40.6;
478
1016
  let pressure = maxPressure;
479
1017
  while (pressure > minPressure) {
480
1018
  pressure += dp;
@@ -488,8 +1026,7 @@ const Soundings = ({
488
1026
  svg,
489
1027
  xScale,
490
1028
  yScale,
491
- height,
492
- width
1029
+ height
493
1030
  });
494
1031
  // Draw static lines
495
1032
  drawStaticLines({
@@ -535,13 +1072,30 @@ const Soundings = ({
535
1072
  height,
536
1073
  svg
537
1074
  });
1075
+ const windData = features.map(feature => ({
1076
+ windDirection: feature.properties.windDirection,
1077
+ windSpeed: feature.properties.windSpeed,
1078
+ pressure: feature.properties.pressure
1079
+ }));
1080
+ drawWindBarbs({
1081
+ svgRef,
1082
+ data: windData,
1083
+ width,
1084
+ height,
1085
+ margin,
1086
+ minPressure,
1087
+ maxPressure,
1088
+ barbColor: SOUNDING_COLORS[0]
1089
+ });
1090
+ const svgRefCur = svgRef.current;
538
1091
  // eslint-disable-next-line consistent-return
539
1092
  return () => {
1093
+ d3.select(svgRefCur).selectAll('.wind-group').remove();
540
1094
  svg.selectAll('*').remove();
541
1095
  // svg.on('mousemove', null)
542
1096
  };
543
1097
  // eslint-disable-next-line react-hooks/exhaustive-deps
544
- }, []);
1098
+ }, [features]);
545
1099
  return jsx(Card, {
546
1100
  children: jsx("svg", {
547
1101
  ref: svgRef
@@ -567,80 +1121,55 @@ const Soundings = ({
567
1121
  * Copyright 2024 - The Norwegian Meteorological Institute (MET Norway)
568
1122
  * */
569
1123
  const SOUNDINGS_NAMESPACE = 'soundings';
1124
+ const defaultLanguage = 'en';
1125
+ const initSoundingsI18n = () => {
1126
+ void i18n.use(initReactI18next).init({
1127
+ lng: defaultLanguage,
1128
+ ns: SOUNDINGS_NAMESPACE,
1129
+ interpolation: {
1130
+ escapeValue: false
1131
+ },
1132
+ resources: {
1133
+ en: {
1134
+ [SOUNDINGS_NAMESPACE]: soundingsTranslations.en
1135
+ },
1136
+ nl: {
1137
+ [SOUNDINGS_NAMESPACE]: soundingsTranslations.nl
1138
+ },
1139
+ fi: {
1140
+ [SOUNDINGS_NAMESPACE]: soundingsTranslations.fi
1141
+ },
1142
+ no: {
1143
+ [SOUNDINGS_NAMESPACE]: soundingsTranslations.no
1144
+ }
1145
+ }
1146
+ });
1147
+ };
1148
+ const translateKeyOutsideComponents = (key, params = undefined) => i18n.t(key, {
1149
+ ns: [SOUNDINGS_NAMESPACE],
1150
+ ...params
1151
+ });
570
1152
  const useSoundingsTranslation = () => useTranslation(SOUNDINGS_NAMESPACE);
571
1153
 
572
1154
  const SoundingsSourceMenu = ({
573
1155
  soundingsCollections,
574
1156
  isOpen = false,
575
- handleClose
1157
+ handleClose,
1158
+ updateCollection,
1159
+ sourceSelections
576
1160
  }) => {
577
1161
  const {
578
1162
  t
579
1163
  } = useSoundingsTranslation();
580
- const [currentSelection, setCurrentSelection] = React.useState({});
581
- React.useEffect(() => {
582
- const initialSelection = {};
583
- soundingsCollections?.forEach(collection => {
584
- const initialInstanceId = collection.instances.length > 0 ? collection.instances[0].id : undefined;
585
- initialSelection[collection.id] = {
586
- isChecked: false,
587
- selectedInstanceId: initialInstanceId
588
- };
589
- });
590
- setCurrentSelection(initialSelection);
591
- }, [soundingsCollections]);
592
- // TODO move to redux
593
1164
  const handleSelectionChange = (collectionId, update) => {
594
- setCurrentSelection(prev => {
595
- const currentCollectionState = prev[collectionId] || {};
596
- return {
597
- ...prev,
598
- [collectionId]: {
599
- ...currentCollectionState,
600
- ...update
601
- }
602
- };
603
- });
1165
+ updateCollection(collectionId, update);
604
1166
  };
605
- const getDisplayValue = (collection, instanceId) => {
1167
+ const getDisplayValue = instanceId => {
606
1168
  if (!instanceId) {
607
- return jsx(Fragment, {
608
- children: t('noInstancesAvailable')
609
- });
610
- }
611
- const instanceIndex = collection.instances.findIndex(i => i.id === instanceId);
612
- if (instanceIndex === 0) {
613
- return jsxs(Box, {
614
- display: "flex",
615
- children: [jsx(Box, {
616
- sx: {
617
- minWidth: 200
618
- },
619
- children: collection.name
620
- }), jsx(Box, {
621
- sx: {
622
- textOverflow: 'ellipsis',
623
- overflow: 'hidden'
624
- },
625
- children: t('latest')
626
- })]
627
- });
1169
+ return t('noInstancesAvailable');
628
1170
  }
629
- return jsxs(Box, {
630
- display: "flex",
631
- children: [jsx(Box, {
632
- sx: {
633
- minWidth: 200
634
- },
635
- children: collection.name
636
- }), jsx(Box, {
637
- sx: {
638
- textOverflow: 'ellipsis',
639
- overflow: 'hidden'
640
- },
641
- children: dateUtils.dateToIsoString(dateUtils.isoStringToDate(instanceId))
642
- })]
643
- });
1171
+ const instanceAsDate = dateUtils.isoStringToDate(instanceId);
1172
+ return instanceAsDate ? dateUtils.dateToIsoString(instanceAsDate) : instanceId;
644
1173
  };
645
1174
  return jsx(ToolContainerDraggable, {
646
1175
  startSize: {
@@ -656,10 +1185,12 @@ const SoundingsSourceMenu = ({
656
1185
  pr: 1
657
1186
  },
658
1187
  children: soundingsCollections && soundingsCollections.length > 0 ? soundingsCollections.map(collection => {
659
- const currentCollectionState = currentSelection[collection.id];
660
- const isChecked = currentCollectionState?.isChecked ?? false;
661
- const selectedInstanceId = currentCollectionState?.selectedInstanceId ?? ''; // Use empty string for Select value if undefined
1188
+ const {
1189
+ isChecked,
1190
+ instanceId
1191
+ } = sourceSelections[collection.id] || {};
662
1192
  const hasInstances = collection.instances.length > 0;
1193
+ const selectedInstanceId = instanceId || collection.instances[0].id;
663
1194
  return jsxs(ListItem, {
664
1195
  disablePadding: true,
665
1196
  children: [jsx(ListItemIcon, {
@@ -667,25 +1198,43 @@ const SoundingsSourceMenu = ({
667
1198
  minWidth: 42
668
1199
  },
669
1200
  children: jsx(Checkbox, {
670
- checked: isChecked,
1201
+ checked: !!isChecked,
671
1202
  onChange: event => handleSelectionChange(collection.id, {
672
- isChecked: event.target.checked
1203
+ isChecked: event.target.checked,
1204
+ instanceId: selectedInstanceId
673
1205
  }),
674
1206
  disableRipple: true
675
1207
  })
676
1208
  }), jsxs(StyledSelect, {
677
1209
  isEnabled: true,
1210
+ additionalStyling: {
1211
+ width: 'calc(100% - 24px)'
1212
+ },
678
1213
  skipLocalStyling: false,
679
1214
  value: selectedInstanceId,
680
1215
  onChange: event => handleSelectionChange(collection.id, {
681
- selectedInstanceId: event.target.value
1216
+ instanceId: event.target.value
682
1217
  }),
683
1218
  renderValue: value => jsx(Box, {
684
1219
  sx: {
685
1220
  textOverflow: 'ellipsis',
686
1221
  overflow: 'hidden'
687
1222
  },
688
- children: getDisplayValue(collection, value)
1223
+ children: jsxs(Box, {
1224
+ display: "flex",
1225
+ children: [jsx(Box, {
1226
+ sx: {
1227
+ minWidth: 200
1228
+ },
1229
+ children: collection.name
1230
+ }), jsx(Box, {
1231
+ sx: {
1232
+ textOverflow: 'ellipsis',
1233
+ overflow: 'hidden'
1234
+ },
1235
+ children: getDisplayValue(value)
1236
+ })]
1237
+ })
689
1238
  }),
690
1239
  children: [!hasInstances && jsx(MenuItem, {
691
1240
  value: "",
@@ -694,9 +1243,9 @@ const SoundingsSourceMenu = ({
694
1243
  fontSize: 12
695
1244
  },
696
1245
  children: t('noInstancesAvailable')
697
- }), collection.instances.map((instance, index) => jsx(MenuItem, {
1246
+ }), collection.instances.map(instance => jsx(MenuItem, {
698
1247
  value: instance.id,
699
- children: index === 0 ? t('latest') : dateUtils.dateToIsoString(dateUtils.isoStringToDate(instance.id)) || instance.id
1248
+ children: getDisplayValue(instance.id)
700
1249
  }, instance.id))]
701
1250
  })]
702
1251
  }, collection.id);
@@ -736,15 +1285,131 @@ const getCollectionLocations = collectionId => fetch(`${BASE_URL}/collections/${
736
1285
  const getCollectionInstance = (baseURL, collectionId) => fetch(`${baseURL}/collections/${collectionId}/instances`, {
737
1286
  headers: getHeaders()
738
1287
  }).then(handleResponse);
1288
+ const getEdrCollectionForLocation = (collectionId, instanceId, locationId, parameters, datetime) => {
1289
+ const url = new URL(`${BASE_URL}/collections/${collectionId}${instanceId ? `/instances/${instanceId}` : ''}/locations/${locationId}`);
1290
+ url.searchParams.append('parameter-name', parameters.join(','));
1291
+ url.searchParams.append('datetime', datetime);
1292
+ url.searchParams.append('f', 'CoverageJSON');
1293
+ return fetch(url.toString(), {
1294
+ headers: getHeaders()
1295
+ }).then(handleResponse);
1296
+ };
1297
+ const getEdrCollectionForPosition = (collectionId, instanceId, coordinates, parameters, datetime) => {
1298
+ const url = new URL(`${BASE_URL}/collections/${collectionId}${instanceId ? `/instances/${instanceId}` : ''}/position`);
1299
+ url.searchParams.append('coords', `POINT(${coordinates[0]} ${coordinates[1]})`);
1300
+ url.searchParams.append('parameter-name', parameters.join(','));
1301
+ url.searchParams.append('datetime', datetime);
1302
+ url.searchParams.append('f', 'CoverageJSON');
1303
+ return fetch(url.toString(), {
1304
+ headers: getHeaders()
1305
+ }).then(handleResponse);
1306
+ };
1307
+
1308
+ /* *
1309
+ * Licensed under the Apache License, Version 2.0 (the "License");
1310
+ * you may not use this file except in compliance with the License.
1311
+ * You may obtain a copy of the License at
1312
+ *
1313
+ * http://www.apache.org/licenses/LICENSE-2.0
1314
+ *
1315
+ * Unless required by applicable law or agreed to in writing, software
1316
+ * distributed under the License is distributed on an "AS IS" BASIS,
1317
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1318
+ * See the License for the specific language governing permissions and
1319
+ * limitations under the License.
1320
+ *
1321
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1322
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1323
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1324
+ * */
1325
+ const initialSoundingsState = {
1326
+ selectedSources: {},
1327
+ selectedLocations: {}
1328
+ };
1329
+ const slice = createSlice({
1330
+ initialState: initialSoundingsState,
1331
+ name: 'soundings',
1332
+ reducers: {
1333
+ updateSelectedSource: (draft, payloadAction) => {
1334
+ const {
1335
+ sourceId,
1336
+ value
1337
+ } = payloadAction.payload;
1338
+ draft.selectedSources[sourceId] = {
1339
+ ...draft.selectedSources[sourceId],
1340
+ ...value
1341
+ };
1342
+ },
1343
+ updateSelectedLocation: (draft, payloadAction) => {
1344
+ const {
1345
+ sourceId,
1346
+ locationId
1347
+ } = payloadAction.payload;
1348
+ draft.selectedLocations[sourceId] = locationId;
1349
+ }
1350
+ }
1351
+ });
1352
+ const {
1353
+ reducer: soundingsReducer,
1354
+ actions: soundingsActions
1355
+ } = slice;
1356
+
1357
+ /* *
1358
+ * Licensed under the Apache License, Version 2.0 (the "License");
1359
+ * you may not use this file except in compliance with the License.
1360
+ * You may obtain a copy of the License at
1361
+ *
1362
+ * http://www.apache.org/licenses/LICENSE-2.0
1363
+ *
1364
+ * Unless required by applicable law or agreed to in writing, software
1365
+ * distributed under the License is distributed on an "AS IS" BASIS,
1366
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1367
+ * See the License for the specific language governing permissions and
1368
+ * limitations under the License.
1369
+ *
1370
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1371
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1372
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1373
+ * */
1374
+ const getSoundingsStore = store => store?.soundings || initialSoundingsState;
1375
+ const getSourceSelections = createSelector(getSoundingsStore, store => {
1376
+ return store.selectedSources;
1377
+ }, selectorMemoizationOptions);
1378
+ const getLocationSelections = createSelector(getSoundingsStore, store => store.selectedLocations, selectorMemoizationOptions);
1379
+ const getSelectedSources = createSelector(getSoundingsStore, store => {
1380
+ const sources = store.selectedSources;
1381
+ const activeSources = Object.keys(sources).reduce((list, key) => {
1382
+ const value = sources[key];
1383
+ if (value.isChecked === true) {
1384
+ return list.concat({
1385
+ collectionId: key,
1386
+ instanceId: value.instanceId
1387
+ });
1388
+ }
1389
+ return list;
1390
+ }, []);
1391
+ return activeSources;
1392
+ }, selectorMemoizationOptions);
1393
+ const getSelectedSourcesAmount = createSelector(getSelectedSources, activeSources => {
1394
+ return activeSources.length;
1395
+ }, selectorMemoizationOptions);
739
1396
 
740
1397
  const SoundingsSourceDialogConnect = ({
741
1398
  soundingsCollections
742
1399
  }) => {
743
1400
  const [soundingsCollectionsWithInstances, setSoundingsCollectionsWithInstances] = React.useState();
1401
+ const dispatch = useDispatch();
744
1402
  const {
745
1403
  isDialogOpen,
746
1404
  onCloseDialog
747
1405
  } = useSetupDialog(uiTypes.DialogTypes.SoundingsSources);
1406
+ const updateCollection = React.useCallback((sourceId, value) => {
1407
+ dispatch(soundingsActions.updateSelectedSource({
1408
+ sourceId,
1409
+ value
1410
+ }));
1411
+ }, [dispatch]);
1412
+ const sourceSelections = useSelector(getSourceSelections);
748
1413
  React.useEffect(() => {
749
1414
  if (soundingsCollections && isDialogOpen) {
750
1415
  try {
@@ -763,36 +1428,45 @@ const SoundingsSourceDialogConnect = ({
763
1428
  return jsx(SoundingsSourceMenu, {
764
1429
  soundingsCollections: soundingsCollectionsWithInstances,
765
1430
  isOpen: isDialogOpen,
1431
+ updateCollection: (sourceId, value) => updateCollection(sourceId, value),
1432
+ sourceSelections: sourceSelections,
766
1433
  handleClose: () => onCloseDialog()
767
1434
  });
768
1435
  };
769
1436
 
770
1437
  const LocationsMenu = ({
771
1438
  services,
1439
+ selectedLocations,
772
1440
  isOpen,
773
- onClose
1441
+ onClose,
1442
+ updateSelectedLocation
774
1443
  }) => {
775
1444
  const [collapseMap, setCollapseMap] = useState({});
776
1445
  const {
777
1446
  t
778
1447
  } = useSoundingsTranslation();
1448
+ const isChecked = (sourceId, locationId) => selectedLocations[sourceId] === locationId;
779
1449
  return jsx(ToolContainerDraggable, {
780
1450
  startSize: {
781
1451
  width: 320,
782
1452
  height: 500
783
1453
  },
784
1454
  title: t('locations'),
785
- headerSize: "medium",
786
1455
  isOpen: isOpen,
787
1456
  onClose: () => onClose(),
788
1457
  bounds: "parent",
789
- children: jsx(List, {
1458
+ children: jsxs(List, {
790
1459
  disablePadding: true,
791
1460
  sx: {
792
1461
  maxHeight: '100%',
793
1462
  overflowY: 'auto'
794
1463
  },
795
- children: services.map(service => jsxs(Fragment$1, {
1464
+ children: [services.length === 0 && jsx(Typography, {
1465
+ sx: {
1466
+ marginLeft: 1
1467
+ },
1468
+ children: t('locations-menu-warning')
1469
+ }), services.map(service => jsxs(Fragment, {
796
1470
  children: [jsx(ListSubheader, {
797
1471
  disableGutters: true,
798
1472
  sx: {
@@ -819,14 +1493,32 @@ const LocationsMenu = ({
819
1493
  overflow: 'hidden'
820
1494
  },
821
1495
  children: service.features?.map(location => jsx(ListItem, {
822
- sx: {
823
- pl: 4
824
- },
825
- children: location.properties.name
1496
+ disablePadding: true,
1497
+ disableGutters: true,
1498
+ children: jsxs(ListItemButton, {
1499
+ onClick: () => {
1500
+ updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
1501
+ },
1502
+ dense: true,
1503
+ children: [jsx(ListItemIcon, {
1504
+ children: jsx(Checkbox, {
1505
+ edge: "start",
1506
+ checked: isChecked(service.id, location.id),
1507
+ tabIndex: -1,
1508
+ disableRipple: true,
1509
+ inputProps: {
1510
+ 'aria-labelledby': location.id
1511
+ }
1512
+ })
1513
+ }), jsx(ListItemText, {
1514
+ id: location.id,
1515
+ primary: location.properties.name
1516
+ })]
1517
+ })
826
1518
  }, `${service.id}-${location.id}`))
827
1519
  })
828
1520
  })]
829
- }, service.id))
1521
+ }, service.id))]
830
1522
  })
831
1523
  });
832
1524
  };
@@ -834,10 +1526,14 @@ const LocationsMenu = ({
834
1526
  const LocationsMenuConnect = ({
835
1527
  soundingsCollections
836
1528
  }) => {
1529
+ const dispatch = useDispatch();
837
1530
  const {
838
1531
  isDialogOpen,
839
1532
  onCloseDialog
840
1533
  } = useSetupDialog(uiTypes.DialogTypes.soundingsLocations);
1534
+ const locationSelections = useSelector(getLocationSelections);
1535
+ const sourceSelections = useSelector(getSourceSelections);
1536
+ const selectedSourceIds = useMemo(() => Object.keys(sourceSelections).filter(key => sourceSelections[key].isChecked), [sourceSelections]);
841
1537
  const edrQueries = useMemo(() => soundingsCollections.map(collection => ({
842
1538
  queryKey: ['edr-locations', collection.id],
843
1539
  queryFn: async () => {
@@ -855,206 +1551,57 @@ const LocationsMenuConnect = ({
855
1551
  return jsx(LocationsMenu, {
856
1552
  isOpen: isDialogOpen,
857
1553
  onClose: onCloseDialog,
858
- services: services
1554
+ services: services.filter(service => selectedSourceIds.includes(service.id)),
1555
+ selectedLocations: locationSelections,
1556
+ updateSelectedLocation: (sourceId, locationId) => void dispatch(soundingsActions.updateSelectedLocation({
1557
+ sourceId,
1558
+ locationId
1559
+ }))
859
1560
  });
860
1561
  };
861
1562
 
862
- var SoundingFeatures = [
863
- {
864
- type: "Feature",
865
- geometry: {
866
- type: "Point",
867
- coordinates: [
868
- 24.9727,
869
- 60.3294
870
- ]
871
- },
872
- properties: {
873
- time: "2025-02-05T00:00:00Z",
874
- temp: -4.74961,
875
- dewpoint: -5.646749999999998,
876
- pressure: 1000,
877
- windSpeed: 9.291457267156751,
878
- windDirection: -18.803465916723095
879
- }
880
- },
881
- {
882
- type: "Feature",
883
- geometry: {
884
- type: "Point",
885
- coordinates: [
886
- 24.9727,
887
- 60.3294
888
- ]
889
- },
890
- properties: {
891
- time: "2025-02-05T00:00:00Z",
892
- temp: -5.72214,
893
- dewpoint: -5.740760000000001,
894
- pressure: 925,
895
- windSpeed: 11.482972536081412,
896
- windDirection: 18.797638010822038
897
- }
898
- },
899
- {
900
- type: "Feature",
901
- geometry: {
902
- type: "Point",
903
- coordinates: [
904
- 24.9727,
905
- 60.3294
906
- ]
907
- },
908
- properties: {
909
- time: "2025-02-05T00:00:00Z",
910
- temp: -5.94461,
911
- dewpoint: -6.284489999999999,
912
- pressure: 850,
913
- windSpeed: 10.355155067433804,
914
- windDirection: 46.95919904362452
915
- }
916
- },
917
- {
918
- type: "Feature",
919
- geometry: {
920
- type: "Point",
921
- coordinates: [
922
- 24.9727,
923
- 60.3294
924
- ]
925
- },
926
- properties: {
927
- time: "2025-02-05T00:00:00Z",
928
- temp: -7.85121,
929
- dewpoint: -7.903229999999999,
930
- pressure: 700,
931
- windSpeed: 10.216260991987234,
932
- windDirection: 125.33681135255785
933
- }
934
- },
935
- {
936
- type: "Feature",
937
- geometry: {
938
- type: "Point",
939
- coordinates: [
940
- 24.9727,
941
- 60.3294
942
- ]
943
- },
944
- properties: {
945
- time: "2025-02-05T00:00:00Z",
946
- temp: -20.9667,
947
- dewpoint: -22.12218,
948
- pressure: 500,
949
- windSpeed: 14.222681508365433,
950
- windDirection: 120.16238041101973
951
- }
952
- },
953
- {
954
- type: "Feature",
955
- geometry: {
956
- type: "Point",
957
- coordinates: [
958
- 24.9727,
959
- 60.3294
960
- ]
961
- },
962
- properties: {
963
- time: "2025-02-05T00:00:00Z",
964
- temp: -48.8678,
965
- dewpoint: -60.802800000000005,
966
- pressure: 300,
967
- windSpeed: 23.436991331013456,
968
- windDirection: 128.0547684118523
969
- }
970
- },
971
- {
972
- type: "Feature",
973
- geometry: {
974
- type: "Point",
975
- coordinates: [
976
- 24.9727,
977
- 60.3294
978
- ]
979
- },
980
- properties: {
981
- time: "2025-02-05T00:00:00Z",
982
- temp: -58.3,
983
- dewpoint: -69.68567999999999,
984
- pressure: 250,
985
- windSpeed: 28.155356371568097,
986
- windDirection: 133.50317473958455
987
- }
988
- },
989
- {
990
- type: "Feature",
991
- geometry: {
992
- type: "Point",
993
- coordinates: [
994
- 24.9727,
995
- 60.3294
996
- ]
997
- },
998
- properties: {
999
- time: "2025-02-05T00:00:00Z",
1000
- temp: -68.6166,
1001
- dewpoint: -71.9641,
1002
- pressure: 200,
1003
- windSpeed: 24.833678713593763,
1004
- windDirection: 137.492458365928
1005
- }
1006
- },
1007
- {
1008
- type: "Feature",
1009
- geometry: {
1010
- type: "Point",
1011
- coordinates: [
1012
- 24.9727,
1013
- 60.3294
1014
- ]
1015
- },
1016
- properties: {
1017
- time: "2025-02-05T00:00:00Z",
1018
- temp: -69.4935,
1019
- dewpoint: -87.827772,
1020
- pressure: 50,
1021
- windSpeed: 35.1450619746217,
1022
- windDirection: 129.8915278264372
1023
- }
1024
- }
1025
- ];
1026
-
1027
1563
  var MenuOptions;
1028
1564
  (function (MenuOptions) {
1029
1565
  MenuOptions["sources"] = "sources";
1030
1566
  MenuOptions["locations"] = "locations";
1031
1567
  })(MenuOptions || (MenuOptions = {}));
1568
+ const buttonStyle = {
1569
+ height: '10px !important',
1570
+ width: '130px'
1571
+ };
1032
1572
  const SoundingsMenuBar = ({
1033
1573
  handleClick,
1034
- isOpen
1574
+ isOpen,
1575
+ selectedSourcesAmount = 0
1035
1576
  }) => {
1036
1577
  const {
1037
1578
  t
1038
1579
  } = useSoundingsTranslation();
1039
- return jsxs(Box, {
1040
- display: "flex",
1041
- gap: 1,
1042
- children: [jsx(Button, {
1043
- variant: isOpen[MenuOptions.sources] ? 'primary' : 'tertiary',
1044
- disableRipple: true,
1045
- "aria-pressed": isOpen[MenuOptions.sources],
1580
+ return jsxs(Stack, {
1581
+ direction: "row",
1582
+ spacing: 1,
1583
+ sx: {
1584
+ marginBottom: '4px'
1585
+ },
1586
+ children: [jsxs(CustomToggleButton, {
1587
+ variant: "tool",
1588
+ selected: isOpen[MenuOptions.sources],
1046
1589
  "data-testid": "soundingsSourcesButton",
1047
1590
  onClick: () => handleClick(MenuOptions.sources),
1048
- startIcon: jsx(Sources, {}),
1049
1591
  size: "small",
1050
- children: t('sources')
1051
- }), jsx(Button, {
1052
- variant: isOpen[MenuOptions.locations] ? 'primary' : 'tertiary',
1053
- disableRipple: true,
1592
+ sx: buttonStyle,
1593
+ children: [jsx(Sources, {
1594
+ sx: {
1595
+ marginRight: '4px!important'
1596
+ }
1597
+ }), selectedSourcesAmount ? `${t('sources')} (${selectedSourcesAmount})` : t('sources')]
1598
+ }), jsx(CustomToggleButton, {
1599
+ variant: "tool",
1054
1600
  "data-testid": "soundingsLocationsButton",
1055
- "aria-pressed": isOpen[MenuOptions.locations],
1601
+ selected: isOpen[MenuOptions.locations],
1056
1602
  onClick: () => handleClick(MenuOptions.locations),
1057
1603
  size: "small",
1604
+ sx: buttonStyle,
1058
1605
  children: t('locations')
1059
1606
  })]
1060
1607
  });
@@ -1068,6 +1615,7 @@ const MenuBarConnect = ({
1068
1615
  const currentActiveMapId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
1069
1616
  const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
1070
1617
  const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.soundingsLocations));
1618
+ const sourcesAmount = useSelector(getSelectedSourcesAmount);
1071
1619
  const dispatchActiveMapId = React.useCallback((dialogType, isOpen) => void dispatch(uiActions.setActiveMapIdForDialog({
1072
1620
  type: dialogType,
1073
1621
  mapId: viewId,
@@ -1090,39 +1638,276 @@ const MenuBarConnect = ({
1090
1638
  };
1091
1639
  return jsx(SoundingsMenuBar, {
1092
1640
  isOpen: isOpen,
1093
- handleClick: handleClick
1641
+ handleClick: handleClick,
1642
+ selectedSourcesAmount: sourcesAmount
1094
1643
  });
1095
1644
  };
1096
1645
 
1646
+ /* *
1647
+ * Licensed under the Apache License, Version 2.0 (the "License");
1648
+ * you may not use this file except in compliance with the License.
1649
+ * You may obtain a copy of the License at
1650
+ *
1651
+ * http://www.apache.org/licenses/LICENSE-2.0
1652
+ *
1653
+ * Unless required by applicable law or agreed to in writing, software
1654
+ * distributed under the License is distributed on an "AS IS" BASIS,
1655
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1656
+ * See the License for the specific language governing permissions and
1657
+ * limitations under the License.
1658
+ *
1659
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1660
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1661
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1662
+ * */
1663
+ const epsilon = 0.622; // Ratio of molecular weights of water vapour and dry air
1664
+ const e0 = 611.2; // Reference vapor pressure [Pa]
1665
+ const aConstant = 17.67;
1666
+ const bConstant = 243.5; // Constants for water, b in Celsius
1667
+ const tKelvinOffset = 273.15; // To convert Celsius to Kelvin
1668
+ const gConstant = 461.5; // Gas constant ratio between water vapour and air [J/(K*kg)]
1669
+ const hConstant = 2.5e6; // Latent heat for water vaporization [J/kg]
1670
+ // Formula to use when dewpoint is not available
1671
+ // Known as Magnus Formula
1672
+ // - Calculate vapor pressure
1673
+ // - Use inverse of the Clausius-Clapeyron equation
1674
+ function dewpointFromSpecificHumidity(specificHumidity,
1675
+ // specific humidity [kg/kg]
1676
+ pressure) {
1677
+ // Step 1: Calculate vapor pressure (Pa)
1678
+ const vaporPressure = specificHumidity * pressure / (epsilon + (1 - epsilon) * specificHumidity);
1679
+ // Step 2: Calculate dew point in Celsius
1680
+ const lnRatio = Math.log(vaporPressure / e0);
1681
+ const dewpoint = bConstant * lnRatio / (aConstant - lnRatio);
1682
+ return dewpoint; // dew point in Celsius
1683
+ }
1684
+ // Formula to use when dewpoint is not available
1685
+ function dewpointFromRelativeHumidity(tCelsius,
1686
+ // air temperature [°C]
1687
+ relHumidity) {
1688
+ // Convert to Kelvin
1689
+ const tKelvin = tCelsius + tKelvinOffset;
1690
+ // Calculate dewpoint
1691
+ const dewpoint = tKelvin / (1 - tKelvin * Math.log(relHumidity * 0.01) * gConstant / hConstant);
1692
+ return dewpoint;
1693
+ }
1694
+
1695
+ /* *
1696
+ * Licensed under the Apache License, Version 2.0 (the "License");
1697
+ * you may not use this file except in compliance with the License.
1698
+ * You may obtain a copy of the License at
1699
+ *
1700
+ * http://www.apache.org/licenses/LICENSE-2.0
1701
+ *
1702
+ * Unless required by applicable law or agreed to in writing, software
1703
+ * distributed under the License is distributed on an "AS IS" BASIS,
1704
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1705
+ * See the License for the specific language governing permissions and
1706
+ * limitations under the License.
1707
+ *
1708
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1709
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1710
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1711
+ * */
1712
+ // Parsing and conversion function
1713
+ function parseCoverageCollection(response) {
1714
+ const features = [];
1715
+ const {
1716
+ t: tAxis,
1717
+ x: xAxis,
1718
+ y: yAxis,
1719
+ z: zAxis
1720
+ } = response.coverages[0]?.domain.axes || {};
1721
+ if (!tAxis?.values || !xAxis?.values || !yAxis?.values || !zAxis?.values) {
1722
+ return features;
1723
+ }
1724
+ response.coverages.forEach(coverage => {
1725
+ const {
1726
+ ranges
1727
+ } = coverage;
1728
+ const time = coverage.domain.axes.t?.values[0];
1729
+ const xCoord = coverage.domain.axes.x.values[0];
1730
+ const yCoord = coverage.domain.axes.y.values[0];
1731
+ const zValues = coverage.domain.axes.z.values;
1732
+ zValues.forEach((_, zIndex) => {
1733
+ const temp = parseFloat(ranges.temperature?.values[zIndex]);
1734
+ const windums = parseFloat(ranges.windums?.values[zIndex]);
1735
+ const windvms = parseFloat(ranges.windvms?.values[zIndex]);
1736
+ let dewpoint = parseFloat(ranges.dewpoint?.values[zIndex]);
1737
+ const relativeHumidity = parseFloat(ranges.relativehumidity?.values[zIndex]);
1738
+ const specificHumidity = parseFloat(ranges.specifichumidity?.values[zIndex]);
1739
+ let windSpeed = parseFloat(ranges.windSpeed?.values[zIndex]);
1740
+ // Fallback calculations
1741
+ if (Number.isNaN(windSpeed) && !Number.isNaN(windums) && !Number.isNaN(windvms)) {
1742
+ // Fallback calculation
1743
+ windSpeed = Math.sqrt(windums ** 2 + windvms ** 2);
1744
+ }
1745
+ let windDirection = parseFloat(ranges.windDirection?.values[zIndex]);
1746
+ if (Number.isNaN(windDirection) && !Number.isNaN(windums) && !Number.isNaN(windvms)) {
1747
+ // Fallback calculation
1748
+ windDirection = (Math.atan2(-windums, -windvms) * (180 / Math.PI) + 360) % 360;
1749
+ }
1750
+ const pressureFromRange = parseFloat(ranges.pressure?.values[zIndex]);
1751
+ const pressure = !Number.isNaN(pressureFromRange) ? pressureFromRange : parseFloat(zValues[zIndex]);
1752
+ if (dewpoint === null || Number.isNaN(dewpoint)) {
1753
+ // Use dewpoint fallback calculations if not present
1754
+ if (!Number.isNaN(specificHumidity)) {
1755
+ dewpoint = dewpointFromSpecificHumidity(specificHumidity / 1000,
1756
+ // from g/kg to kg/kg
1757
+ pressure * 100);
1758
+ } else if (!Number.isNaN(relativeHumidity)) {
1759
+ dewpoint = dewpointFromRelativeHumidity(temp, relativeHumidity);
1760
+ }
1761
+ }
1762
+ features.push({
1763
+ type: 'Feature',
1764
+ geometry: {
1765
+ type: 'Point',
1766
+ coordinates: [xCoord, yCoord]
1767
+ },
1768
+ properties: {
1769
+ time,
1770
+ temp,
1771
+ dewpoint,
1772
+ pressure,
1773
+ windSpeed,
1774
+ windDirection
1775
+ }
1776
+ });
1777
+ });
1778
+ });
1779
+ return features;
1780
+ }
1781
+
1782
+ /* *
1783
+ * Licensed under the Apache License, Version 2.0 (the "License");
1784
+ * you may not use this file except in compliance with the License.
1785
+ * You may obtain a copy of the License at
1786
+ *
1787
+ * http://www.apache.org/licenses/LICENSE-2.0
1788
+ *
1789
+ * Unless required by applicable law or agreed to in writing, software
1790
+ * distributed under the License is distributed on an "AS IS" BASIS,
1791
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1792
+ * See the License for the specific language governing permissions and
1793
+ * limitations under the License.
1794
+ *
1795
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1796
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1797
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1798
+ * */
1799
+ const sortSoundingFeatures = data => {
1800
+ return data.sort((featureA, featureB) => {
1801
+ // Sort by date (ascending)
1802
+ if (featureA.properties.time !== featureB.properties.time) {
1803
+ return featureA.properties.time > featureB.properties.time ? 1 : -1;
1804
+ }
1805
+ // Sort by pressure (descending)
1806
+ return featureB.properties.pressure - featureA.properties.pressure;
1807
+ });
1808
+ };
1809
+ const constructQueryParams = (collectionId, instanceId, location, datetime, parameters) => {
1810
+ if (typeof location === 'string') {
1811
+ // LOCATION call
1812
+ return {
1813
+ queryKey: ['edr-soundings-for-location', collectionId, instanceId || '', location, parameters.join(), datetime],
1814
+ queryFn: async () => {
1815
+ // No collection or location selected, don't do EDR call but return empty array
1816
+ if (collectionId === undefined || location === '') {
1817
+ return [];
1818
+ }
1819
+ try {
1820
+ const rawData = await getEdrCollectionForLocation(collectionId, instanceId, location, parameters, datetime);
1821
+ const parsedData = parseCoverageCollection(rawData);
1822
+ return sortSoundingFeatures(parsedData);
1823
+ } catch (err) {
1824
+ throw new Error('Error fetching EDR data collection by location');
1825
+ }
1826
+ }
1827
+ };
1828
+ }
1829
+ // POSITION call
1830
+ return {
1831
+ queryKey: ['edr-soundings-for-position', collectionId, location.join(), parameters.join(), datetime],
1832
+ queryFn: async () => {
1833
+ // No collection or position selected, don't do EDR call but return empty array
1834
+ if (collectionId === undefined || location === undefined) {
1835
+ return [];
1836
+ }
1837
+ try {
1838
+ const rawData = await getEdrCollectionForPosition(collectionId, instanceId, location, parameters, datetime);
1839
+ const parsedData = parseCoverageCollection(rawData);
1840
+ return sortSoundingFeatures(parsedData);
1841
+ } catch (err) {
1842
+ throw new Error('Error fetching EDR data collection by location');
1843
+ }
1844
+ }
1845
+ };
1846
+ };
1847
+ const useEdrCollectionForLocationPosition = (collectionId, instanceId, location, datetime, parameters = ['temperature', 'WindDirection', 'WindSpeedMS', 'windUMS', 'windVMS', 'Pressure', 'DewPoint', 'humidity', 'specifichumidity']) => {
1848
+ // TODO: Remove once we pass in the datetime coming from the timeslider!
1849
+ const {
1850
+ data
1851
+ } = useQuery({
1852
+ queryKey: ['temporary-instance-call', collectionId, instanceId],
1853
+ queryFn: async () => {
1854
+ return fetch(`https://opendata.fmi.fi/edr/collections/${collectionId}/instances/${instanceId}`, {
1855
+ headers: getHeaders()
1856
+ }).then(handleResponse);
1857
+ }
1858
+ });
1859
+ const datetime2 = collectionId === undefined || data === undefined ? '' : data.extent.temporal.interval[0][0];
1860
+ // Up until here can be removed
1861
+ // If position passed in, call positions hook
1862
+ const queryParams = constructQueryParams(collectionId, instanceId, location, datetime2, parameters);
1863
+ return useQuery(queryParams);
1864
+ };
1865
+
1097
1866
  const SoundingsConnect = ({
1098
1867
  viewId
1099
1868
  }) => {
1100
- return jsxs(Box, {
1101
- sx: {
1102
- padding: 2
1103
- },
1104
- children: [jsx(Box, {
1105
- paddingLeft: 5,
1106
- children: jsx(MenuBarConnect, {
1107
- viewId: viewId
1108
- })
1109
- }), jsx(Soundings
1110
- // TODO: use real data
1111
- , {
1112
- // TODO: use real data
1113
- data: {
1114
- features: SoundingFeatures
1869
+ const sourceSelections = useSelector(getSelectedSources);
1870
+ const locationSelections = useSelector(getLocationSelections);
1871
+ // TODO https://gitlab.com/opengeoweb/opengeoweb/-/issues/5916: Allow multiple sources to be selected
1872
+ // For now, only select the first one
1873
+ const {
1874
+ error,
1875
+ data: soundingData,
1876
+ isPending
1877
+ } = useEdrCollectionForLocationPosition(sourceSelections[0]?.collectionId, sourceSelections[0]?.instanceId, locationSelections[sourceSelections[0]?.collectionId] || '');
1878
+ return jsxs(Fragment$1, {
1879
+ children: [error && jsx(AlertBanner, {
1880
+ title: error.message,
1881
+ shouldClose: true
1882
+ }), isPending && jsx(LinearProgress, {
1883
+ color: "secondary"
1884
+ }), jsxs(Box, {
1885
+ sx: {
1886
+ padding: 2
1115
1887
  },
1116
- svgDimensions: {
1117
- width: 600,
1118
- height: 600,
1119
- margin: {
1120
- bottom: 0,
1121
- left: 0,
1122
- right: 0,
1123
- top: 0
1888
+ children: [jsx(Box, {
1889
+ paddingLeft: 5,
1890
+ children: jsx(MenuBarConnect, {
1891
+ viewId: viewId
1892
+ })
1893
+ }), jsx(Soundings
1894
+ // TODO: use real data
1895
+ , {
1896
+ // TODO: use real data
1897
+ data: {
1898
+ features: soundingData || []
1899
+ },
1900
+ svgDimensions: {
1901
+ width: 600,
1902
+ height: 600,
1903
+ margin: {
1904
+ bottom: 0,
1905
+ left: 0,
1906
+ right: 0,
1907
+ top: 0
1908
+ }
1124
1909
  }
1125
- }
1910
+ })]
1126
1911
  })]
1127
1912
  });
1128
1913
  };
@@ -1137,4 +1922,33 @@ const soundingsLookUp = payload => {
1137
1922
  return null;
1138
1923
  };
1139
1924
 
1140
- export { LocationsMenuConnect, Soundings, SoundingsSourceDialogConnect, soundingsComponentType, soundingsLookUp };
1925
+ /* *
1926
+ * Licensed under the Apache License, Version 2.0 (the "License");
1927
+ * you may not use this file except in compliance with the License.
1928
+ * You may obtain a copy of the License at
1929
+ *
1930
+ * http://www.apache.org/licenses/LICENSE-2.0
1931
+ *
1932
+ * Unless required by applicable law or agreed to in writing, software
1933
+ * distributed under the License is distributed on an "AS IS" BASIS,
1934
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1935
+ * See the License for the specific language governing permissions and
1936
+ * limitations under the License.
1937
+ *
1938
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1939
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1940
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1941
+ * */
1942
+ const soundingsReducersMap = {
1943
+ soundings: soundingsReducer,
1944
+ webmap: mapReducer,
1945
+ ui: uiReducer
1946
+ };
1947
+ const soundingsRootReuducer = combineReducers(soundingsReducersMap);
1948
+ const createMockStore = mockState => configureStore({
1949
+ reducer: soundingsRootReuducer,
1950
+ preloadedState: mockState,
1951
+ middleware: getDefaultMiddleware => getDefaultMiddleware()
1952
+ });
1953
+
1954
+ export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, Soundings, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };