@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.
- package/index.esm.js +1950 -6
- package/package.json +13 -2
- package/src/index.d.ts +7 -1
- package/src/lib/api/edr-api/api.d.ts +6 -0
- package/src/lib/api/edr-api/crss-types.d.ts +26 -0
- package/src/lib/api/edr-api/fakeApi.d.ts +1 -0
- package/src/lib/api/edr-api/hooks.d.ts +5 -0
- package/src/lib/api/edr-api/types.d.ts +118 -0
- package/src/lib/components/DrawUtils.d.ts +39 -0
- package/src/lib/components/EDRFetchAndParse.stories.d.ts +7 -0
- package/src/lib/components/MenuBar/LocationsMenu.d.ts +13 -0
- package/src/lib/components/MenuBar/LocationsMenuConnect.d.ts +6 -0
- package/src/lib/components/MenuBar/MenuBar.d.ts +12 -0
- package/src/lib/components/MenuBar/MenuBar.stories.d.ts +20 -0
- package/src/lib/components/MenuBar/MenuBarConnect.d.ts +8 -0
- package/src/lib/components/MenuBar/MenuBarConnect.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/SoundingsSourceDialogConnect.d.ts +7 -0
- package/src/lib/components/MenuBar/SoundingsSourceMenu.d.ts +12 -0
- package/src/lib/components/Providers/Providers.d.ts +12 -0
- package/src/lib/components/Soundings.d.ts +14 -0
- package/src/lib/components/Soundings.spec.d.ts +1 -0
- package/src/lib/components/Soundings.stories.d.ts +7 -0
- package/src/lib/components/SoundingsConnect.d.ts +6 -0
- package/src/lib/components/StaticLines/drawDryAdiabats.d.ts +11 -0
- package/src/lib/components/StaticLines/drawIsobars.d.ts +11 -0
- package/src/lib/components/StaticLines/drawIsohumes.d.ts +10 -0
- package/src/lib/components/StaticLines/drawIsotherms.d.ts +10 -0
- package/src/lib/components/StaticLines/drawMoistAdiabats.d.ts +16 -0
- package/src/lib/components/StaticLines/utils.d.ts +15 -0
- package/src/lib/components/WindBarbs/utils.d.ts +25 -0
- package/src/lib/components/WindBarbs/windsymbols.d.ts +5 -0
- package/src/lib/components/index.d.ts +3 -0
- package/src/lib/components/types.d.ts +5 -0
- package/src/lib/i18n.d.ts +7 -0
- package/src/lib/parser/coverages.d.ts +2 -0
- package/src/lib/store/reducer.d.ts +7 -0
- package/src/lib/store/selectors.d.ts +105 -0
- package/src/lib/store/selectors.spec.d.ts +1 -0
- package/src/lib/store/store.d.ts +9 -0
- package/src/lib/store/types.d.ts +25 -0
- package/src/lib/utils/conversions.d.ts +11 -0
- package/src/lib/utils/soundingsLookUp.d.ts +8 -0
- package/src/lib/components/Sounding.d.ts +0 -2
- package/src/lib/components/Sounding.spec.d.ts +0 -0
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
1954
|
+
export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, Soundings, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };
|