@opengeoweb/soundings 12.1.2 → 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 (45) hide show
  1. package/index.esm.js +1950 -6
  2. package/package.json +13 -2
  3. package/src/index.d.ts +7 -1
  4. package/src/lib/api/edr-api/api.d.ts +6 -0
  5. package/src/lib/api/edr-api/crss-types.d.ts +26 -0
  6. package/src/lib/api/edr-api/fakeApi.d.ts +1 -0
  7. package/src/lib/api/edr-api/hooks.d.ts +5 -0
  8. package/src/lib/api/edr-api/types.d.ts +118 -0
  9. package/src/lib/components/DrawUtils.d.ts +39 -0
  10. package/src/lib/components/EDRFetchAndParse.stories.d.ts +7 -0
  11. package/src/lib/components/MenuBar/LocationsMenu.d.ts +13 -0
  12. package/src/lib/components/MenuBar/LocationsMenuConnect.d.ts +6 -0
  13. package/src/lib/components/MenuBar/MenuBar.d.ts +12 -0
  14. package/src/lib/components/MenuBar/MenuBar.stories.d.ts +20 -0
  15. package/src/lib/components/MenuBar/MenuBarConnect.d.ts +8 -0
  16. package/src/lib/components/MenuBar/MenuBarConnect.spec.d.ts +1 -0
  17. package/src/lib/components/MenuBar/SoundingsSourceDialogConnect.d.ts +7 -0
  18. package/src/lib/components/MenuBar/SoundingsSourceMenu.d.ts +12 -0
  19. package/src/lib/components/Providers/Providers.d.ts +12 -0
  20. package/src/lib/components/Soundings.d.ts +14 -0
  21. package/src/lib/components/Soundings.spec.d.ts +1 -0
  22. package/src/lib/components/Soundings.stories.d.ts +7 -0
  23. package/src/lib/components/SoundingsConnect.d.ts +6 -0
  24. package/src/lib/components/StaticLines/drawDryAdiabats.d.ts +11 -0
  25. package/src/lib/components/StaticLines/drawIsobars.d.ts +11 -0
  26. package/src/lib/components/StaticLines/drawIsohumes.d.ts +10 -0
  27. package/src/lib/components/StaticLines/drawIsotherms.d.ts +10 -0
  28. package/src/lib/components/StaticLines/drawMoistAdiabats.d.ts +16 -0
  29. package/src/lib/components/StaticLines/utils.d.ts +15 -0
  30. package/src/lib/components/WindBarbs/utils.d.ts +25 -0
  31. package/src/lib/components/WindBarbs/windsymbols.d.ts +5 -0
  32. package/src/lib/components/index.d.ts +3 -0
  33. package/src/lib/components/types.d.ts +5 -0
  34. package/src/lib/i18n.d.ts +7 -0
  35. package/src/lib/parser/coverages.d.ts +2 -0
  36. package/src/lib/store/reducer.d.ts +7 -0
  37. package/src/lib/store/selectors.d.ts +105 -0
  38. package/src/lib/store/selectors.spec.d.ts +1 -0
  39. package/src/lib/store/store.d.ts +9 -0
  40. package/src/lib/store/types.d.ts +25 -0
  41. package/src/lib/utils/conversions.d.ts +11 -0
  42. package/src/lib/utils/soundingsLookUp.d.ts +8 -0
  43. package/src/lib/components/Sounding.d.ts +0 -2
  44. package/src/lib/components/Sounding.spec.d.ts +0 -0
  45. package/src/lib/components/soundings.stories.d.ts +0 -9
package/index.esm.js CHANGED
@@ -1,10 +1,1954 @@
1
- import { jsx } from 'react/jsx-runtime';
1
+ import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
+ import React, { useRef, useEffect, useState, Fragment, useMemo } from 'react';
3
+ import * as d3 from 'd3';
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';
11
+ import { getHeaders, handleResponse } from '@opengeoweb/api';
12
+ import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
13
+ import { useQueries, useQuery } from '@tanstack/react-query';
14
+ import { ChevronDown, Sources } from '@opengeoweb/theme';
2
15
 
3
- const Sounding = () => {
4
- // eslint-disable-next-line i18next/no-literal-string
5
- return jsx("div", {
6
- children: "This is an example component"
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
+ };
93
+
94
+ /* *
95
+ * Licensed under the Apache License, Version 2.0 (the "License");
96
+ * you may not use this file except in compliance with the License.
97
+ * You may obtain a copy of the License at
98
+ *
99
+ * http://www.apache.org/licenses/LICENSE-2.0
100
+ *
101
+ * Unless required by applicable law or agreed to in writing, software
102
+ * distributed under the License is distributed on an "AS IS" BASIS,
103
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
104
+ * See the License for the specific language governing permissions and
105
+ * limitations under the License.
106
+ *
107
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
108
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
109
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
110
+ * */
111
+ const SOUNDING_COLORS = ['#4487f3'];
112
+ // vfx code
113
+ const initializeSvg = ({
114
+ svgRef,
115
+ width,
116
+ height,
117
+ margin
118
+ }) => {
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})`);
120
+ svg.append('clipPath').attr('id', 'clip-skewTd3').append('rect').attr('width', width).attr('height', height).attr('x', 0).attr('y', 0);
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)');
122
+ return svg;
123
+ };
124
+ const drawAxes = ({
125
+ svg,
126
+ xScale,
127
+ yScale,
128
+ height
129
+ }) => {
130
+ // Create x-axis with intervals
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
+ });
144
+ // Create y-axis with intervals
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');
148
+ };
149
+ const addLegends = ({
150
+ svg,
151
+ width
152
+ }) => {
153
+ // Add a legend for the temperature line
154
+ svg.append('text').attr('x', width - 160).attr('y', 20).attr('fill', SOUNDING_COLORS[0]).text('Temperature (°C)');
155
+ // Add a legend for the dewpoint line
156
+ svg.append('text').attr('x', width - 137).attr('y', 40).attr('fill', SOUNDING_COLORS[0]).text('Dewpoint (°C)');
157
+ };
158
+ const plotSounding = ({
159
+ temperatureValues,
160
+ pressureValues,
161
+ color,
162
+ lineStyle,
163
+ xScale,
164
+ yScale,
165
+ height,
166
+ svg
167
+ }) => {
168
+ // Map temp and pressure to pixels
169
+ const Px = d3.zip(pressureValues, temperatureValues).map(([pressure, temperature]) => [xScale(temperature), yScale(pressure)]);
170
+ // Skew/shear transformation for skew-t
171
+ const skewedPx = Px.map(([x, y]) => [x + height - y, y]);
172
+ const lineGenerator = d3.line();
173
+ const pathString = lineGenerator(skewedPx);
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)');
175
+ };
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
+
664
+ /* *
665
+ * Licensed under the Apache License, Version 2.0 (the "License");
666
+ * you may not use this file except in compliance with the License.
667
+ * You may obtain a copy of the License at
668
+ *
669
+ * http://www.apache.org/licenses/LICENSE-2.0
670
+ *
671
+ * Unless required by applicable law or agreed to in writing, software
672
+ * distributed under the License is distributed on an "AS IS" BASIS,
673
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
674
+ * See the License for the specific language governing permissions and
675
+ * limitations under the License.
676
+ *
677
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
678
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
679
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
680
+ * */
681
+ const CP$1 = 1.03e3;
682
+ const Po = 1000.0;
683
+ const dryAdiabaticLapserate = (theta, pressure, temperature, dp) => {
684
+ const Tt = (theta + 273.15) * (Po / pressure) ** (-287 / CP$1) - 273.15;
685
+ const dt = Tt - temperature;
686
+ return [dp, dt];
687
+ };
688
+ const drawDryAdiabats = ({
689
+ staticPressureArray,
690
+ height,
691
+ xScale,
692
+ yScale,
693
+ svg,
694
+ dp
695
+ }) => {
696
+ for (let Tbase = -40; Tbase < 240; Tbase += 10) {
697
+ let Pnew = staticPressureArray[0];
698
+ let Tnew = Tbase;
699
+ const Px = [];
700
+ for (const pressure of staticPressureArray) {
701
+ const DPDT = dryAdiabaticLapserate(Tbase, Pnew, Tnew, dp);
702
+ Tnew += DPDT[1];
703
+ Pnew += DPDT[0];
704
+ Px.push([xScale(Tnew), yScale(pressure)]);
705
+ }
706
+ // Skew/shear transformation for skew-t
707
+ for (const PxElement of Px) {
708
+ PxElement[0] = PxElement[0] + height - PxElement[1];
709
+ }
710
+ const lineGenerator = d3.line();
711
+ const daPathString = lineGenerator(Px);
712
+ svg.append('path').attr('d', daPathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'green').style('opacity', 0.3).attr('clip-path', 'url(#clip-skewTd3)');
713
+ }
714
+ };
715
+
716
+ /* *
717
+ * Licensed under the Apache License, Version 2.0 (the "License");
718
+ * you may not use this file except in compliance with the License.
719
+ * You may obtain a copy of the License at
720
+ *
721
+ * http://www.apache.org/licenses/LICENSE-2.0
722
+ *
723
+ * Unless required by applicable law or agreed to in writing, software
724
+ * distributed under the License is distributed on an "AS IS" BASIS,
725
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
726
+ * See the License for the specific language governing permissions and
727
+ * limitations under the License.
728
+ *
729
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
730
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
731
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
732
+ * */
733
+ const isohumeValues = [0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 5.0, 8.0, 12.0, 16.0, 20.0];
734
+ const drawIsohumes = ({
735
+ staticPressureArray,
736
+ height,
737
+ xScale,
738
+ yScale,
739
+ svg
740
+ }) => {
741
+ for (const isohumeVal of isohumeValues) {
742
+ const Px = [];
743
+ for (const pressureVal of staticPressureArray) {
744
+ const es = pressureVal * isohumeVal / (isohumeVal + 622.0);
745
+ const logthing = Math.log(es / 6.11);
746
+ const temp = 237.3 * logthing / (17.269 - logthing);
747
+ Px.push([xScale(temp), yScale(pressureVal)]);
748
+ }
749
+ // Skew/shear transformation for skew-t
750
+ for (const PxElement of Px) {
751
+ PxElement[0] = PxElement[0] + height - PxElement[1];
752
+ }
753
+ const lineGenerator = d3.line();
754
+ const pathStringQ = lineGenerator(Px);
755
+ const strID = `${isohumeVal}isohume`;
756
+ svg.append('path').attr('id', strID).attr('d', pathStringQ).style('fill', 'none').style('stroke-width', 1).style('stroke-dasharray', '15, 8').style('stroke', '#5267A9').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
757
+ svg.append('text').text(isohumeVal).attr('text-anchor', 'middle').attr('x', `${Px[0][0] + 20}px`).attr('y', `${height - 10}px`).attr('fill', '#5267A9').attr('font-size', '10px');
758
+ }
759
+ };
760
+
761
+ /* *
762
+ * Licensed under the Apache License, Version 2.0 (the "License");
763
+ * you may not use this file except in compliance with the License.
764
+ * You may obtain a copy of the License at
765
+ *
766
+ * http://www.apache.org/licenses/LICENSE-2.0
767
+ *
768
+ * Unless required by applicable law or agreed to in writing, software
769
+ * distributed under the License is distributed on an "AS IS" BASIS,
770
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
771
+ * See the License for the specific language governing permissions and
772
+ * limitations under the License.
773
+ *
774
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
775
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
776
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
777
+ * */
778
+ const drawIsotherms = ({
779
+ staticPressureArray,
780
+ height,
781
+ xScale,
782
+ yScale,
783
+ svg
784
+ }) => {
785
+ // lines of constant temperature (10 degree interval from -80...+40C)
786
+ for (let temp = -80; temp <= 40; temp += 10) {
787
+ const points = [];
788
+ for (const pressure of staticPressureArray) {
789
+ const Ppx = yScale(pressure);
790
+ const Tpx = xScale(temp);
791
+ const skewedX = Tpx + (height - Ppx);
792
+ points.push([skewedX, Ppx]);
793
+ }
794
+ const lineGenerator = d3.line();
795
+ const pathString = lineGenerator(points);
796
+ svg.append('path').attr('d', pathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'orange').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
797
+ }
798
+ };
799
+
800
+ /* *
801
+ * Licensed under the Apache License, Version 2.0 (the "License");
802
+ * you may not use this file except in compliance with the License.
803
+ * You may obtain a copy of the License at
804
+ *
805
+ * http://www.apache.org/licenses/LICENSE-2.0
806
+ *
807
+ * Unless required by applicable law or agreed to in writing, software
808
+ * distributed under the License is distributed on an "AS IS" BASIS,
809
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
810
+ * See the License for the specific language governing permissions and
811
+ * limitations under the License.
812
+ *
813
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
814
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
815
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
816
+ * */
817
+ const CP = 1.03e3;
818
+ const KELVIN = 273.15;
819
+ const L_CONST = 2.5e6;
820
+ const RD = 287.0;
821
+ const RV = 461.0;
822
+ const moistAdiabaticLapserate = (minTemp, pressure, temperature, dp) => {
823
+ const kelvin = temperature + KELVIN;
824
+ const lsbc = L_CONST / RV * (1.0 / KELVIN - 1.0 / kelvin);
825
+ const rw = 6.11 * Math.exp(lsbc) * (0.622 / pressure);
826
+ const lrwbt = L_CONST * rw / (RD * kelvin);
827
+ const nume = RD * kelvin / (CP * pressure) * (1.0 + lrwbt);
828
+ const deno = 1.0 + lrwbt * (0.622 * L_CONST / (CP * kelvin));
829
+ const gradi = nume / deno;
830
+ const dt = dp * gradi;
831
+ return [dp, dt];
832
+ };
833
+ const drawMoistAdiabats = ({
834
+ staticPressureArray,
835
+ minTemperature,
836
+ maxTemperature,
837
+ minPressure,
838
+ maxPressure,
839
+ width,
840
+ height,
841
+ xScale,
842
+ yScale,
843
+ svg,
844
+ dp
845
+ }) => {
846
+ /* We need the moist adiabat to cross the isotherm at 1000hPa
847
+ Therefore we start the calculations from 1000hPa and work upwards (towards 0)
848
+ then draw a separate line starting from 1000hPa working down (towards defined maxPressure) */
849
+ for (let Tbase = -40; Tbase < 240; Tbase += 10) {
850
+ let Pnew = 1000;
851
+ let Tnew = Tbase;
852
+ const Px = [];
853
+ // First calculate position of moist adiabat at a pressure of 1000hPa
854
+ const Temp = Tnew + -1 * minTemperature;
855
+ const Tpx1000 = width * Temp / (maxTemperature - minTemperature);
856
+ const Ppx1000 = height * (Math.log(Pnew) - Math.log(minPressure)) / (Math.log(maxPressure) - Math.log(minPressure));
857
+ Px.push([Tpx1000, Ppx1000]);
858
+ // Then calculate the rest of the moist adiabat for pressures <1000hPa
859
+ for (const pressure of staticPressureArray) {
860
+ if (pressure < 1000) {
861
+ const DPDT = moistAdiabaticLapserate(-80, Pnew, Tnew, dp);
862
+ Tnew += DPDT[1];
863
+ Pnew += DPDT[0];
864
+ Px.push([xScale(Tnew), yScale(pressure)]);
865
+ }
866
+ }
867
+ for (const PxElement of Px) {
868
+ PxElement[0] = PxElement[0] + height - PxElement[1];
869
+ }
870
+ const lineGenerator = d3.line();
871
+ const maPathString = lineGenerator(Px);
872
+ // Draw this first section of the line
873
+ svg.append('path').attr('d', maPathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'purple').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
874
+ // Now work backwards from 1000hPa to calculate most adiabat until the bottom of the plot
875
+ const PxBottom = [];
876
+ PxBottom.push([Tpx1000, Ppx1000]);
877
+ let PnewBottom = 1000;
878
+ let TnewBottom = Tbase;
879
+ const startIndex = staticPressureArray.findIndex(value => value === 1000);
880
+ for (let i = startIndex - 1; i >= 0; i -= 1) {
881
+ const DPDT = moistAdiabaticLapserate(-80, PnewBottom, TnewBottom, 5);
882
+ TnewBottom += DPDT[1];
883
+ PnewBottom += DPDT[0];
884
+ PxBottom.push([xScale(TnewBottom), yScale(staticPressureArray[i])]);
885
+ }
886
+ // Skew/shear transformation for skew-t
887
+ for (const PxElement of PxBottom) {
888
+ PxElement[0] = PxElement[0] + height - PxElement[1];
889
+ }
890
+ const maPathStringBottom = lineGenerator(PxBottom);
891
+ svg.append('path').attr('d', maPathStringBottom).style('fill', 'none').style('stroke-width', 1).style('stroke', 'purple').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
892
+ }
893
+ };
894
+
895
+ /* *
896
+ * Licensed under the Apache License, Version 2.0 (the "License");
897
+ * you may not use this file except in compliance with the License.
898
+ * You may obtain a copy of the License at
899
+ *
900
+ * http://www.apache.org/licenses/LICENSE-2.0
901
+ *
902
+ * Unless required by applicable law or agreed to in writing, software
903
+ * distributed under the License is distributed on an "AS IS" BASIS,
904
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
905
+ * See the License for the specific language governing permissions and
906
+ * limitations under the License.
907
+ *
908
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
909
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
910
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
911
+ * */
912
+ const drawStaticLines = ({
913
+ dp,
914
+ staticPressureArray,
915
+ minTemperature,
916
+ maxTemperature,
917
+ height,
918
+ width,
919
+ xScale,
920
+ yScale,
921
+ minPressure,
922
+ maxPressure,
923
+ svg
924
+ }) => {
925
+ drawIsobars({
926
+ minTemperature,
927
+ maxTemperature,
928
+ xScale,
929
+ yScale,
930
+ svg
931
+ });
932
+ drawIsotherms({
933
+ staticPressureArray,
934
+ height,
935
+ xScale,
936
+ yScale,
937
+ svg
938
+ });
939
+ drawIsohumes({
940
+ staticPressureArray,
941
+ height,
942
+ xScale,
943
+ yScale,
944
+ svg
945
+ });
946
+ drawDryAdiabats({
947
+ staticPressureArray,
948
+ height,
949
+ xScale,
950
+ yScale,
951
+ svg,
952
+ dp
953
+ });
954
+ drawMoistAdiabats({
955
+ staticPressureArray,
956
+ minTemperature,
957
+ maxTemperature,
958
+ minPressure,
959
+ maxPressure,
960
+ width,
961
+ height,
962
+ xScale,
963
+ yScale,
964
+ svg,
965
+ dp
966
+ });
967
+ };
968
+
969
+ const Soundings = ({
970
+ data: {
971
+ features
972
+ },
973
+ svgDimensions
974
+ // tooltip
975
+ }) => {
976
+ const svgRef = useRef(null);
977
+ const svgDimensionsOptions = {
978
+ width: svgDimensions?.width || 812,
979
+ height: svgDimensions?.height || 802,
980
+ margin: {
981
+ top: svgDimensions?.margin?.top || 12,
982
+ right: svgDimensions?.margin?.right || 20,
983
+ bottom: svgDimensions?.margin?.bottom || 25,
984
+ left: svgDimensions?.margin?.left || 20
985
+ }
986
+ };
987
+ // Set SVG dimensions
988
+ const margin = {
989
+ top: svgDimensionsOptions?.margin?.top,
990
+ right: svgDimensionsOptions?.margin?.right,
991
+ bottom: svgDimensionsOptions?.margin?.bottom,
992
+ left: svgDimensionsOptions?.margin?.left
993
+ };
994
+ const baseWidth = svgDimensionsOptions?.width;
995
+ const baseHeight = svgDimensionsOptions?.height;
996
+ const width = baseWidth - (margin.left + margin.right);
997
+ const height = baseHeight - (margin.top + margin.bottom);
998
+ useEffect(() => {
999
+ if (!svgRef.current) {
1000
+ return;
1001
+ }
1002
+ // Define svg
1003
+ const svg = initializeSvg({
1004
+ svgRef,
1005
+ width,
1006
+ height,
1007
+ margin
1008
+ });
1009
+ // Define pressure and temperature values for static lines
1010
+ const maxPressure = 1050;
1011
+ const minPressure = 150;
1012
+ const staticPressureArray = [maxPressure];
1013
+ const dp = -5;
1014
+ const minTemperature = -40;
1015
+ const maxTemperature = 40.6;
1016
+ let pressure = maxPressure;
1017
+ while (pressure > minPressure) {
1018
+ pressure += dp;
1019
+ staticPressureArray.push(pressure);
1020
+ }
1021
+ // Set scales for temperature and pressure
1022
+ const xScale = d3.scaleLinear().domain([minTemperature, maxTemperature]).range([0, width]);
1023
+ const yScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]);
1024
+ // Add axis'
1025
+ drawAxes({
1026
+ svg,
1027
+ xScale,
1028
+ yScale,
1029
+ height
1030
+ });
1031
+ // Draw static lines
1032
+ drawStaticLines({
1033
+ dp,
1034
+ staticPressureArray,
1035
+ minTemperature,
1036
+ maxTemperature,
1037
+ height,
1038
+ width,
1039
+ xScale,
1040
+ yScale,
1041
+ minPressure,
1042
+ maxPressure,
1043
+ svg
1044
+ });
1045
+ // Add legends
1046
+ addLegends({
1047
+ svg,
1048
+ width
1049
+ });
1050
+ const temperatureValues = features.map(feature => feature.properties.temp);
1051
+ const pressureValues = features.map(feature => feature.properties.pressure);
1052
+ // Draw Temperature
1053
+ plotSounding({
1054
+ temperatureValues,
1055
+ pressureValues,
1056
+ color: SOUNDING_COLORS[0],
1057
+ lineStyle: 'none',
1058
+ xScale,
1059
+ yScale,
1060
+ height,
1061
+ svg
1062
+ });
1063
+ const dewpointValues = features.map(feature => feature.properties.dewpoint);
1064
+ // Dewpoint
1065
+ plotSounding({
1066
+ temperatureValues: dewpointValues,
1067
+ color: SOUNDING_COLORS[0],
1068
+ pressureValues,
1069
+ lineStyle: '8, 2',
1070
+ xScale,
1071
+ yScale,
1072
+ height,
1073
+ svg
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;
1091
+ // eslint-disable-next-line consistent-return
1092
+ return () => {
1093
+ d3.select(svgRefCur).selectAll('.wind-group').remove();
1094
+ svg.selectAll('*').remove();
1095
+ // svg.on('mousemove', null)
1096
+ };
1097
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1098
+ }, [features]);
1099
+ return jsx(Card, {
1100
+ children: jsx("svg", {
1101
+ ref: svgRef
1102
+ })
1103
+ });
1104
+ };
1105
+
1106
+ /* *
1107
+ * Licensed under the Apache License, Version 2.0 (the "License");
1108
+ * you may not use this file except in compliance with the License.
1109
+ * You may obtain a copy of the License at
1110
+ *
1111
+ * http://www.apache.org/licenses/LICENSE-2.0
1112
+ *
1113
+ * Unless required by applicable law or agreed to in writing, software
1114
+ * distributed under the License is distributed on an "AS IS" BASIS,
1115
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1116
+ * See the License for the specific language governing permissions and
1117
+ * limitations under the License.
1118
+ *
1119
+ * Copyright 2023 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1120
+ * Copyright 2023 - Finnish Meteorological Institute (FMI)
1121
+ * Copyright 2024 - The Norwegian Meteorological Institute (MET Norway)
1122
+ * */
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
+ });
1152
+ const useSoundingsTranslation = () => useTranslation(SOUNDINGS_NAMESPACE);
1153
+
1154
+ const SoundingsSourceMenu = ({
1155
+ soundingsCollections,
1156
+ isOpen = false,
1157
+ handleClose,
1158
+ updateCollection,
1159
+ sourceSelections
1160
+ }) => {
1161
+ const {
1162
+ t
1163
+ } = useSoundingsTranslation();
1164
+ const handleSelectionChange = (collectionId, update) => {
1165
+ updateCollection(collectionId, update);
1166
+ };
1167
+ const getDisplayValue = instanceId => {
1168
+ if (!instanceId) {
1169
+ return t('noInstancesAvailable');
1170
+ }
1171
+ const instanceAsDate = dateUtils.isoStringToDate(instanceId);
1172
+ return instanceAsDate ? dateUtils.dateToIsoString(instanceAsDate) : instanceId;
1173
+ };
1174
+ return jsx(ToolContainerDraggable, {
1175
+ startSize: {
1176
+ width: 320
1177
+ },
1178
+ minWidth: 300,
1179
+ title: t('sources'),
1180
+ isOpen: isOpen,
1181
+ onClose: () => handleClose(),
1182
+ bounds: "parent",
1183
+ children: jsx(List, {
1184
+ sx: {
1185
+ pr: 1
1186
+ },
1187
+ children: soundingsCollections && soundingsCollections.length > 0 ? soundingsCollections.map(collection => {
1188
+ const {
1189
+ isChecked,
1190
+ instanceId
1191
+ } = sourceSelections[collection.id] || {};
1192
+ const hasInstances = collection.instances.length > 0;
1193
+ const selectedInstanceId = instanceId || collection.instances[0].id;
1194
+ return jsxs(ListItem, {
1195
+ disablePadding: true,
1196
+ children: [jsx(ListItemIcon, {
1197
+ sx: {
1198
+ minWidth: 42
1199
+ },
1200
+ children: jsx(Checkbox, {
1201
+ checked: !!isChecked,
1202
+ onChange: event => handleSelectionChange(collection.id, {
1203
+ isChecked: event.target.checked,
1204
+ instanceId: selectedInstanceId
1205
+ }),
1206
+ disableRipple: true
1207
+ })
1208
+ }), jsxs(StyledSelect, {
1209
+ isEnabled: true,
1210
+ additionalStyling: {
1211
+ width: 'calc(100% - 24px)'
1212
+ },
1213
+ skipLocalStyling: false,
1214
+ value: selectedInstanceId,
1215
+ onChange: event => handleSelectionChange(collection.id, {
1216
+ instanceId: event.target.value
1217
+ }),
1218
+ renderValue: value => jsx(Box, {
1219
+ sx: {
1220
+ textOverflow: 'ellipsis',
1221
+ overflow: 'hidden'
1222
+ },
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
+ })
1238
+ }),
1239
+ children: [!hasInstances && jsx(MenuItem, {
1240
+ value: "",
1241
+ disabled: true,
1242
+ sx: {
1243
+ fontSize: 12
1244
+ },
1245
+ children: t('noInstancesAvailable')
1246
+ }), collection.instances.map(instance => jsx(MenuItem, {
1247
+ value: instance.id,
1248
+ children: getDisplayValue(instance.id)
1249
+ }, instance.id))]
1250
+ })]
1251
+ }, collection.id);
1252
+ }) : jsx(ListItem, {
1253
+ children: jsx(ListItemText, {
1254
+ primary: t('noServicesFound'),
1255
+ sx: {
1256
+ pl: 4
1257
+ }
1258
+ })
1259
+ })
1260
+ })
1261
+ });
1262
+ };
1263
+
1264
+ /* *
1265
+ * Licensed under the Apache License, Version 2.0 (the "License");
1266
+ * you may not use this file except in compliance with the License.
1267
+ * You may obtain a copy of the License at
1268
+ *
1269
+ * http://www.apache.org/licenses/LICENSE-2.0
1270
+ *
1271
+ * Unless required by applicable law or agreed to in writing, software
1272
+ * distributed under the License is distributed on an "AS IS" BASIS,
1273
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1274
+ * See the License for the specific language governing permissions and
1275
+ * limitations under the License.
1276
+ *
1277
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1278
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
1279
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
1280
+ * */
1281
+ const BASE_URL = 'https://opendata.fmi.fi/edr';
1282
+ const getCollectionLocations = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/locations`, {
1283
+ headers: getHeaders()
1284
+ }).then(handleResponse);
1285
+ const getCollectionInstance = (baseURL, collectionId) => fetch(`${baseURL}/collections/${collectionId}/instances`, {
1286
+ headers: getHeaders()
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);
1396
+
1397
+ const SoundingsSourceDialogConnect = ({
1398
+ soundingsCollections
1399
+ }) => {
1400
+ const [soundingsCollectionsWithInstances, setSoundingsCollectionsWithInstances] = React.useState();
1401
+ const dispatch = useDispatch();
1402
+ const {
1403
+ isDialogOpen,
1404
+ onCloseDialog
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);
1413
+ React.useEffect(() => {
1414
+ if (soundingsCollections && isDialogOpen) {
1415
+ try {
1416
+ void Promise.all(soundingsCollections.map(async collection => {
1417
+ const data = await getCollectionInstance(collection.url, collection.id);
1418
+ return {
1419
+ ...collection,
1420
+ instances: data.instances.reverse()
1421
+ }; // Reverse to get latest first
1422
+ })).then(results => setSoundingsCollectionsWithInstances(results));
1423
+ } catch (error) {
1424
+ console.error('Error fetching soundings instances:', error);
1425
+ }
1426
+ }
1427
+ }, [soundingsCollections, isDialogOpen]);
1428
+ return jsx(SoundingsSourceMenu, {
1429
+ soundingsCollections: soundingsCollectionsWithInstances,
1430
+ isOpen: isDialogOpen,
1431
+ updateCollection: (sourceId, value) => updateCollection(sourceId, value),
1432
+ sourceSelections: sourceSelections,
1433
+ handleClose: () => onCloseDialog()
1434
+ });
1435
+ };
1436
+
1437
+ const LocationsMenu = ({
1438
+ services,
1439
+ selectedLocations,
1440
+ isOpen,
1441
+ onClose,
1442
+ updateSelectedLocation
1443
+ }) => {
1444
+ const [collapseMap, setCollapseMap] = useState({});
1445
+ const {
1446
+ t
1447
+ } = useSoundingsTranslation();
1448
+ const isChecked = (sourceId, locationId) => selectedLocations[sourceId] === locationId;
1449
+ return jsx(ToolContainerDraggable, {
1450
+ startSize: {
1451
+ width: 320,
1452
+ height: 500
1453
+ },
1454
+ title: t('locations'),
1455
+ isOpen: isOpen,
1456
+ onClose: () => onClose(),
1457
+ bounds: "parent",
1458
+ children: jsxs(List, {
1459
+ disablePadding: true,
1460
+ sx: {
1461
+ maxHeight: '100%',
1462
+ overflowY: 'auto'
1463
+ },
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, {
1470
+ children: [jsx(ListSubheader, {
1471
+ disableGutters: true,
1472
+ sx: {
1473
+ borderBottom: '1px solid'
1474
+ },
1475
+ children: jsxs(ListItemButton, {
1476
+ onClick: () => setCollapseMap({
1477
+ ...collapseMap,
1478
+ [service.id]: !collapseMap[service.id]
1479
+ }),
1480
+ children: [jsx(ListItemText, {
1481
+ children: service.id
1482
+ }), jsx(ListItemIcon, {
1483
+ children: jsx(ChevronDown, {})
1484
+ })]
1485
+ })
1486
+ }), jsx(Collapse, {
1487
+ in: collapseMap[service.id],
1488
+ children: jsx(List, {
1489
+ component: "div",
1490
+ disablePadding: true,
1491
+ sx: {
1492
+ overflowY: 'auto',
1493
+ overflow: 'hidden'
1494
+ },
1495
+ children: service.features?.map(location => jsx(ListItem, {
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
+ })
1518
+ }, `${service.id}-${location.id}`))
1519
+ })
1520
+ })]
1521
+ }, service.id))]
1522
+ })
1523
+ });
1524
+ };
1525
+
1526
+ const LocationsMenuConnect = ({
1527
+ soundingsCollections
1528
+ }) => {
1529
+ const dispatch = useDispatch();
1530
+ const {
1531
+ isDialogOpen,
1532
+ onCloseDialog
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]);
1537
+ const edrQueries = useMemo(() => soundingsCollections.map(collection => ({
1538
+ queryKey: ['edr-locations', collection.id],
1539
+ queryFn: async () => {
1540
+ const locations = await getCollectionLocations(collection.id);
1541
+ return {
1542
+ ...locations,
1543
+ id: collection.id
1544
+ };
1545
+ }
1546
+ })), [soundingsCollections]);
1547
+ const services = useQueries({
1548
+ queries: edrQueries,
1549
+ combine: results => results.map(result => result.data).filter(result => !!result)
1550
+ });
1551
+ return jsx(LocationsMenu, {
1552
+ isOpen: isDialogOpen,
1553
+ onClose: onCloseDialog,
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
+ }))
1560
+ });
1561
+ };
1562
+
1563
+ var MenuOptions;
1564
+ (function (MenuOptions) {
1565
+ MenuOptions["sources"] = "sources";
1566
+ MenuOptions["locations"] = "locations";
1567
+ })(MenuOptions || (MenuOptions = {}));
1568
+ const buttonStyle = {
1569
+ height: '10px !important',
1570
+ width: '130px'
1571
+ };
1572
+ const SoundingsMenuBar = ({
1573
+ handleClick,
1574
+ isOpen,
1575
+ selectedSourcesAmount = 0
1576
+ }) => {
1577
+ const {
1578
+ t
1579
+ } = useSoundingsTranslation();
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],
1589
+ "data-testid": "soundingsSourcesButton",
1590
+ onClick: () => handleClick(MenuOptions.sources),
1591
+ size: "small",
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",
1600
+ "data-testid": "soundingsLocationsButton",
1601
+ selected: isOpen[MenuOptions.locations],
1602
+ onClick: () => handleClick(MenuOptions.locations),
1603
+ size: "small",
1604
+ sx: buttonStyle,
1605
+ children: t('locations')
1606
+ })]
1607
+ });
1608
+ };
1609
+
1610
+ const MenuBarConnect = ({
1611
+ viewId,
1612
+ source
1613
+ }) => {
1614
+ const dispatch = useDispatch();
1615
+ const currentActiveMapId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
1616
+ const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
1617
+ const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.soundingsLocations));
1618
+ const sourcesAmount = useSelector(getSelectedSourcesAmount);
1619
+ const dispatchActiveMapId = React.useCallback((dialogType, isOpen) => void dispatch(uiActions.setActiveMapIdForDialog({
1620
+ type: dialogType,
1621
+ mapId: viewId,
1622
+ setOpen: currentActiveMapId !== viewId ? true : !isOpen,
1623
+ source
1624
+ })), [dispatch, currentActiveMapId, viewId, source]);
1625
+ const handleClick = React.useCallback(menuType => {
1626
+ switch (menuType) {
1627
+ case MenuOptions.sources:
1628
+ dispatchActiveMapId(uiTypes.DialogTypes.SoundingsSources, isSourcesOpenInStore);
1629
+ break;
1630
+ case MenuOptions.locations:
1631
+ dispatchActiveMapId(uiTypes.DialogTypes.soundingsLocations, isLocationsOpenInStore);
1632
+ break;
1633
+ }
1634
+ }, [dispatchActiveMapId, isSourcesOpenInStore, isLocationsOpenInStore]);
1635
+ const isOpen = {
1636
+ [MenuOptions.sources]: currentActiveMapId === viewId && isSourcesOpenInStore,
1637
+ [MenuOptions.locations]: currentActiveMapId === viewId && isLocationsOpenInStore
1638
+ };
1639
+ return jsx(SoundingsMenuBar, {
1640
+ isOpen: isOpen,
1641
+ handleClick: handleClick,
1642
+ selectedSourcesAmount: sourcesAmount
1643
+ });
1644
+ };
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
+ }
7
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
+
1866
+ const SoundingsConnect = ({
1867
+ viewId
1868
+ }) => {
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
1887
+ },
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
+ }
1909
+ }
1910
+ })]
1911
+ })]
1912
+ });
1913
+ };
1914
+
1915
+ const soundingsComponentType = 'Soundings';
1916
+ const soundingsLookUp = payload => {
1917
+ if (payload.componentType === soundingsComponentType) {
1918
+ return jsx(SoundingsConnect, {
1919
+ viewId: payload.id
1920
+ });
1921
+ }
1922
+ return null;
1923
+ };
1924
+
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
8
1946
  };
1947
+ const soundingsRootReuducer = combineReducers(soundingsReducersMap);
1948
+ const createMockStore = mockState => configureStore({
1949
+ reducer: soundingsRootReuducer,
1950
+ preloadedState: mockState,
1951
+ middleware: getDefaultMiddleware => getDefaultMiddleware()
1952
+ });
9
1953
 
10
- export { Sounding };
1954
+ export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, Soundings, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };