@opengeoweb/soundings 12.2.0 → 12.4.1
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 +1781 -554
- package/package.json +11 -9
- package/src/index.d.ts +4 -0
- package/src/lib/api/edr-api/api.d.ts +4 -5
- package/src/lib/api/edr-api/hooks.d.ts +16 -3
- package/src/lib/api/edr-api/types.d.ts +0 -23
- package/src/lib/{parser/coverages.d.ts → api/edr-api/utils.d.ts} +1 -1
- package/src/lib/components/EDRFetchAndParse.stories.d.ts +12 -5
- package/src/lib/components/MenuBar/LocationsMenu.d.ts +16 -0
- package/src/lib/components/{SoundingsMenuBar → MenuBar}/MenuBar.d.ts +2 -1
- package/src/lib/components/MenuBar/MenuBar.stories.d.ts +20 -0
- package/src/lib/components/MenuBar/MenuBarConnect.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/SoundingsSourceDialogConnect.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/SoundingsSourceMenu.d.ts +16 -0
- package/src/lib/components/Providers/Providers.d.ts +12 -0
- package/src/lib/components/{Soundings.d.ts → Soundings/Soundings.d.ts} +4 -5
- package/src/lib/components/Soundings/Soundings.spec.d.ts +1 -0
- package/src/lib/components/Soundings/SoundingsConnect.d.ts +6 -0
- package/src/lib/components/Soundings/SoundingsConnect.integration.spec.d.ts +1 -0
- package/src/lib/components/Soundings/SoundingsConnect.spec.d.ts +1 -0
- package/src/lib/components/Soundings/SoundingsConnect.stories.d.ts +35 -0
- package/src/lib/components/{DrawUtils.d.ts → Soundings/drawUtils.d.ts} +7 -6
- package/src/lib/components/Soundings/drawUtils.spec.d.ts +1 -0
- package/src/lib/components/{types.d.ts → Soundings/types.d.ts} +1 -1
- package/src/lib/components/SoundingsDemo.stories.d.ts +15 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.d.ts +7 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.spec.d.ts +1 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.stories.d.ts +8 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSliderConnect.d.ts +6 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSliderConnect.spec.d.ts +1 -0
- package/src/lib/components/SoundingsTimeSlider/index.d.ts +2 -0
- package/src/lib/components/SoundingsTooltip/utils.d.ts +29 -0
- package/src/lib/components/SoundingsTooltip/utils.spec.d.ts +1 -0
- package/src/lib/components/StaticLines/drawIsobars.d.ts +1 -0
- package/src/lib/components/WindBarbs/utils.d.ts +20 -0
- package/src/lib/components/WindBarbs/utils.spec.d.ts +1 -0
- package/src/lib/components/WindBarbs/windsymbols.d.ts +5 -0
- package/src/lib/components/index.d.ts +3 -3
- package/src/lib/store/reducer.d.ts +8 -0
- package/src/lib/store/selectors.d.ts +126 -0
- package/src/lib/store/selectors.spec.d.ts +1 -0
- package/src/lib/store/store.d.ts +15 -0
- package/src/lib/store/types.d.ts +30 -0
- package/src/lib/{i18n.d.ts → utils/i18n.d.ts} +1 -0
- package/src/lib/utils/soundingsLookUp.spec.d.ts +1 -0
- package/src/lib/components/SoundingsConnect.d.ts +0 -5
- package/src/lib/components/SoundingsMenuBar/LocationsMenu.d.ts +0 -11
- package/src/lib/components/SoundingsMenuBar/MenuBar.stories.d.ts +0 -26
- package/src/lib/components/SoundingsSourceMenu.d.ts +0 -9
- package/src/lib/utils/conversions.d.ts +0 -11
- /package/src/lib/{utils/testHandlers.d.ts → api/edr-api/fakeApi.d.ts} +0 -0
- /package/src/lib/{components/Soundings.spec.d.ts → api/edr-api/hooks.spec.d.ts} +0 -0
- /package/src/lib/components/{SoundingsMenuBar → MenuBar}/LocationsMenuConnect.d.ts +0 -0
- /package/src/lib/components/{SoundingsMenuBar → MenuBar}/MenuBarConnect.d.ts +0 -0
- /package/src/lib/components/{SoundingsMenuBar/MenuBarConnect.spec.d.ts → MenuBar/MenuBarConnect.integration.spec.d.ts} +0 -0
- /package/src/lib/components/{SoundingsSourceDialogConnect.d.ts → MenuBar/SoundingsSourceDialogConnect.d.ts} +0 -0
- /package/src/lib/components/{Soundings.stories.d.ts → Soundings/Soundings.stories.d.ts} +0 -0
package/index.esm.js
CHANGED
|
@@ -1,15 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
3
|
+
import { Card, Box, Stack, LinearProgress, List, ListItem, ListItemIcon, Checkbox, MenuItem, ListItemText, Typography, Grid, ListItemButton } from '@mui/material';
|
|
4
|
+
import { safelyInitI18nForTestsAndStories, sharedTranslations, SHARED_NAMESPACE, dateUtils, CustomSlider, CustomToggleButton, AlertBanner, ToolContainerDraggable, StyledSelect, SearchField, CustomAccordion } from '@opengeoweb/shared';
|
|
5
|
+
import React, { useRef, useEffect, useMemo, useState, Fragment as Fragment$1 } from 'react';
|
|
3
6
|
import * as d3 from 'd3';
|
|
4
|
-
import {
|
|
5
|
-
import { useSetupDialog, uiTypes, uiSelectors, uiActions } from '@opengeoweb/store';
|
|
6
|
-
import { ToolContainerDraggable, StyledSelect, dateUtils } from '@opengeoweb/shared';
|
|
7
|
-
import 'i18next';
|
|
8
|
-
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { useQueries, useQuery } from '@tanstack/react-query';
|
|
9
8
|
import { getHeaders, handleResponse } from '@opengeoweb/api';
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
9
|
+
import { timesliderTranslations, TIMESLIDER_NAMESPACE, BackwardForwardStepButton } from '@opengeoweb/timeslider';
|
|
10
|
+
import i18n from 'i18next';
|
|
11
|
+
export { default as i18n } from 'i18next';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
|
|
14
|
+
import { selectorMemoizationOptions, uiSelectors, uiTypes, uiActions, useSetupDialog, storeReducerMap, storeMiddlewares } from '@opengeoweb/store';
|
|
15
|
+
import { Sources } from '@opengeoweb/theme';
|
|
16
|
+
import { FixedSizeList } from 'react-window';
|
|
17
|
+
|
|
18
|
+
var en = {
|
|
19
|
+
sources: "Sources",
|
|
20
|
+
latest: "Latest",
|
|
21
|
+
noInstancesAvailable: "No instances found",
|
|
22
|
+
noServicesFound: "No source available",
|
|
23
|
+
locations: "Locations",
|
|
24
|
+
"locations-menu-warning": "You have to choose your sources in the sources menu"
|
|
25
|
+
};
|
|
26
|
+
var no = {
|
|
27
|
+
sources: "Kilder",
|
|
28
|
+
latest: "Siste",
|
|
29
|
+
noInstancesAvailable: "Ingen instanser funnet",
|
|
30
|
+
noServicesFound: "Ingen tjenester funnet",
|
|
31
|
+
locations: "Lokasjoner",
|
|
32
|
+
"locations-menu-warning": "Du må velge kilder i kilde-menyen"
|
|
33
|
+
};
|
|
34
|
+
var nl = {
|
|
35
|
+
sources: "Bronnen",
|
|
36
|
+
latest: "Nieuwste",
|
|
37
|
+
noInstancesAvailable: "Geen exemplaren gevonden",
|
|
38
|
+
noServicesFound: "Geen bron beschikbaar",
|
|
39
|
+
locations: "Locaties",
|
|
40
|
+
"locations-menu-warning": "Selecteer eerst een bron in het bron menu"
|
|
41
|
+
};
|
|
42
|
+
var fi = {
|
|
43
|
+
sources: "Lähteet",
|
|
44
|
+
latest: "Viimeisin",
|
|
45
|
+
noInstancesAvailable: "Tapauksia ei löytynyt",
|
|
46
|
+
noServicesFound: "Lähdettä ei ole saatavilla",
|
|
47
|
+
locations: "Locations",
|
|
48
|
+
"locations-menu-warning": "Valitse ensin lähde lähdevalikosta"
|
|
49
|
+
};
|
|
50
|
+
var soundingsTranslations = {
|
|
51
|
+
en: en,
|
|
52
|
+
no: no,
|
|
53
|
+
nl: nl,
|
|
54
|
+
fi: fi
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/* *
|
|
58
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
59
|
+
* you may not use this file except in compliance with the License.
|
|
60
|
+
* You may obtain a copy of the License at
|
|
61
|
+
*
|
|
62
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
63
|
+
*
|
|
64
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
65
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
66
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
67
|
+
* See the License for the specific language governing permissions and
|
|
68
|
+
* limitations under the License.
|
|
69
|
+
*
|
|
70
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
71
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
72
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
73
|
+
* */
|
|
74
|
+
const isobarValues = [200, 300, 400, 500, 700, 850, 925, 1000];
|
|
75
|
+
const drawIsobars = ({
|
|
76
|
+
minTemperature,
|
|
77
|
+
maxTemperature,
|
|
78
|
+
xScale,
|
|
79
|
+
yScale,
|
|
80
|
+
svg
|
|
81
|
+
}) => {
|
|
82
|
+
// Calculate X and Y coordinates for each pressure level
|
|
83
|
+
for (const pressureVal of isobarValues) {
|
|
84
|
+
const Px = [];
|
|
85
|
+
const tempBounds = [minTemperature, maxTemperature];
|
|
86
|
+
for (const bound of tempBounds) {
|
|
87
|
+
Px.push([xScale(bound), yScale(pressureVal)]);
|
|
88
|
+
}
|
|
89
|
+
const lineGenerator = d3.line();
|
|
90
|
+
const isoPathString = lineGenerator(Px);
|
|
91
|
+
const strID = `${pressureVal}textId`;
|
|
92
|
+
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);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
13
95
|
|
|
14
96
|
/* *
|
|
15
97
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -28,7 +110,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|
|
28
110
|
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
29
111
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
30
112
|
* */
|
|
31
|
-
const SOUNDING_COLORS = ['#4487f3'];
|
|
113
|
+
const SOUNDING_COLORS = ['#4487f3', '#c3213c', '#509a00'];
|
|
32
114
|
// vfx code
|
|
33
115
|
const initializeSvg = ({
|
|
34
116
|
svgRef,
|
|
@@ -38,20 +120,33 @@ const initializeSvg = ({
|
|
|
38
120
|
}) => {
|
|
39
121
|
const svg = d3.select(svgRef.current).attr('width', width + (margin.left + margin.right)).attr('height', height + (margin.top + margin.bottom)).attr('id', 'skewT').attr('data-testid', 'skewT').append('g').attr('transform', `translate(${margin.left + margin.right},${margin.top})`);
|
|
40
122
|
svg.append('clipPath').attr('id', 'clip-skewTd3').append('rect').attr('width', width).attr('height', height).attr('x', 0).attr('y', 0);
|
|
41
|
-
svg.append('rect').attr('height', height).attr('width', width).attr('fill', '
|
|
123
|
+
svg.append('rect').attr('height', height).attr('width', width).attr('fill', '#ffffff00').attr('opacity', 1).attr('id', 'skewTd3').attr('clip-path', 'url(#clip-skewTd3)');
|
|
42
124
|
return svg;
|
|
43
125
|
};
|
|
44
126
|
const drawAxes = ({
|
|
45
127
|
svg,
|
|
46
128
|
xScale,
|
|
47
129
|
yScale,
|
|
48
|
-
height
|
|
49
|
-
width
|
|
130
|
+
height
|
|
50
131
|
}) => {
|
|
51
132
|
// Create x-axis with intervals
|
|
52
|
-
|
|
133
|
+
const xTicks = xScale.ticks(10);
|
|
134
|
+
const xMiddleValue = xTicks[Math.floor(xTicks.length / 2)];
|
|
135
|
+
svg.append('g').attr('type', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickValues(xTicks).tickSizeInner(0).tickSizeOuter(0));
|
|
136
|
+
// Reformat in the case of the middle tick label
|
|
137
|
+
// to add the unit part
|
|
138
|
+
svg.selectAll('g[type="x-axis"] .tick').filter(function () {
|
|
139
|
+
return parseInt(d3.select(this).select('text').text(), 10) === xMiddleValue;
|
|
140
|
+
}).each(function () {
|
|
141
|
+
const unitPart = '°C';
|
|
142
|
+
d3.select(this).select('text')
|
|
143
|
+
// tspan necessary to use a different font-size
|
|
144
|
+
.append('tspan').text(unitPart).attr('font-size', '11px').attr('dy', '0.04em');
|
|
145
|
+
});
|
|
53
146
|
// Create y-axis with intervals
|
|
54
|
-
|
|
147
|
+
// Make sure all zeroes are printed (1000 vs 1k)
|
|
148
|
+
const formatTicks = d3.format('.0f');
|
|
149
|
+
svg.append('g').call(d3.axisLeft(yScale).tickValues(isobarValues).tickFormat(formatTicks).tickSizeInner(0).tickSizeOuter(0)).call(g => g.select('.domain').remove()).append('text').attr('fill', 'currentColor').attr('x', '0').attr('y', yScale(400)).attr('font-size', '11px').attr('dy', '-0.9em').attr('text-anchor', 'left').text('hPa');
|
|
55
150
|
};
|
|
56
151
|
const addLegends = ({
|
|
57
152
|
svg,
|
|
@@ -72,14 +167,595 @@ const plotSounding = ({
|
|
|
72
167
|
height,
|
|
73
168
|
svg
|
|
74
169
|
}) => {
|
|
170
|
+
const [filteredPressureValues, filteredTemperatureValues] = filterDomainRangeForParameter(pressureValues, temperatureValues);
|
|
75
171
|
// Map temp and pressure to pixels
|
|
76
|
-
const Px = d3.zip(
|
|
172
|
+
const Px = d3.zip(filteredPressureValues, filteredTemperatureValues).map(([pressure, temperature]) => [xScale(temperature), yScale(pressure)]);
|
|
77
173
|
// Skew/shear transformation for skew-t
|
|
78
174
|
const skewedPx = Px.map(([x, y]) => [x + height - y, y]);
|
|
79
175
|
const lineGenerator = d3.line();
|
|
80
176
|
const pathString = lineGenerator(skewedPx);
|
|
81
177
|
svg.append('path').attr('d', pathString).attr('x', 0).attr('y', 0).style('fill', 'none').style('stroke-width', 2).style('stroke', color).style('stroke-opacity', 1).style('stroke-dasharray', lineStyle).attr('clip-path', 'url(#clip-skewTd3)');
|
|
82
178
|
};
|
|
179
|
+
// Use this function to filter the pressure and parameter array so no NaN or null values are included
|
|
180
|
+
const filterDomainRangeForParameter = (pressureDomain, parameterDomain) => {
|
|
181
|
+
const filteredPressureDomain = pressureDomain.filter((value, index) => !isNaN(parameterDomain[index]) && parameterDomain[index] !== null);
|
|
182
|
+
const filteredParameterDomain = parameterDomain.filter(value => !isNaN(value) && value !== null);
|
|
183
|
+
return [filteredPressureDomain, filteredParameterDomain];
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/* *
|
|
187
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
188
|
+
* you may not use this file except in compliance with the License.
|
|
189
|
+
* You may obtain a copy of the License at
|
|
190
|
+
*
|
|
191
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
192
|
+
*
|
|
193
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
194
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
195
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
196
|
+
* See the License for the specific language governing permissions and
|
|
197
|
+
* limitations under the License.
|
|
198
|
+
*
|
|
199
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
200
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
201
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
202
|
+
* */
|
|
203
|
+
// Check if the value is outside the domain, if so, return NaN
|
|
204
|
+
// Otherwise, return the interpolated value
|
|
205
|
+
const createClampedInterpolationFunc = (interpolationFunc, domain) => {
|
|
206
|
+
return value => {
|
|
207
|
+
if (value < domain[domain.length - 1] || value > domain[0]) {
|
|
208
|
+
return NaN;
|
|
209
|
+
}
|
|
210
|
+
return interpolationFunc(value);
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
// Construct interpolation functions to use when hovering between data points
|
|
214
|
+
// Ensure NaN/null values are removed and cut the domain to the upper and lower pressure bounds of the parameter
|
|
215
|
+
const constructTooltipData = data => {
|
|
216
|
+
const tooltipData = data.map(features => {
|
|
217
|
+
const pressureData = features.map(feature => feature.properties.pressure);
|
|
218
|
+
const tempData = features.map(feature => feature.properties.temp);
|
|
219
|
+
const dewpointData = features.map(feature => feature.properties.dewpoint);
|
|
220
|
+
const windDirData = features.map(feature => feature.properties.windDirection);
|
|
221
|
+
const windSpeedData = features.map(feature => feature.properties.windSpeed);
|
|
222
|
+
const interpolatePressure = d3.scaleLinear().domain(pressureData).range(pressureData).clamp(true);
|
|
223
|
+
const [tempPressure, filteredTempData] = filterDomainRangeForParameter(pressureData, tempData);
|
|
224
|
+
const interpolateTemperature = d3.scaleLinear().domain(tempPressure).range(filteredTempData).clamp(true);
|
|
225
|
+
const [dewpointPressure, filteredDewpointData] = filterDomainRangeForParameter(pressureData, dewpointData);
|
|
226
|
+
const interpolateDewpoint = d3.scaleLinear().domain(dewpointPressure).range(filteredDewpointData).clamp(true);
|
|
227
|
+
const [windSpeedPressure, filteredWindSpeedData] = filterDomainRangeForParameter(pressureData, windSpeedData);
|
|
228
|
+
const interpolateWindSpeed = d3.scaleLinear().domain(windSpeedPressure).range(filteredWindSpeedData).clamp(true);
|
|
229
|
+
const [windDirPressure, filteredWindDirData] = filterDomainRangeForParameter(pressureData, windDirData);
|
|
230
|
+
const interpolateWindDirection = d3.scaleLinear().domain(windDirPressure).range(filteredWindDirData).clamp(true);
|
|
231
|
+
return {
|
|
232
|
+
pressureData,
|
|
233
|
+
interpolatePressure: createClampedInterpolationFunc(interpolatePressure, pressureData),
|
|
234
|
+
interpolateTemperature: createClampedInterpolationFunc(interpolateTemperature, tempPressure),
|
|
235
|
+
interpolateDewpoint: createClampedInterpolationFunc(interpolateDewpoint, dewpointPressure),
|
|
236
|
+
interpolateWindSpeed: createClampedInterpolationFunc(interpolateWindSpeed, windSpeedPressure),
|
|
237
|
+
interpolateWindDirection: createClampedInterpolationFunc(interpolateWindDirection, windDirPressure)
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
return tooltipData.filter(data => data.pressureData.length > 0);
|
|
241
|
+
};
|
|
242
|
+
const getParamHtml = (paramValue, colorIndex, unit, isLastElement, noDistance = false) => {
|
|
243
|
+
if (colorIndex === 0) {
|
|
244
|
+
return `<p style="${noDistance || isLastElement ? 'margin: 0;' : 'margin-top: 0;'} color: ${SOUNDING_COLORS[colorIndex]}">${!isNaN(paramValue) && paramValue !== null ? `${paramValue.toFixed(1)}${unit}` : '-'}${noDistance ? '' : '<br/>'}</p>`;
|
|
245
|
+
}
|
|
246
|
+
if (isLastElement) {
|
|
247
|
+
return `<p style="${noDistance ? 'margin: 0;' : 'margin-bottom: 0;'} color: ${SOUNDING_COLORS[colorIndex]}">${!isNaN(paramValue) && paramValue !== null ? `${paramValue.toFixed(1)}${unit}` : '-'}</p>`;
|
|
248
|
+
}
|
|
249
|
+
return `<p style="${noDistance ? 'margin: 0;' : 'margin-top: 0;'} color: ${SOUNDING_COLORS[colorIndex]}">${!isNaN(paramValue) && paramValue !== null ? `${paramValue.toFixed(1)}${unit}` : '-'}${noDistance ? '' : '<br/>'}</p>`;
|
|
250
|
+
};
|
|
251
|
+
const drawSoundingsTooltip = ({
|
|
252
|
+
event,
|
|
253
|
+
yScale,
|
|
254
|
+
svgRectangle,
|
|
255
|
+
pressureRefd3,
|
|
256
|
+
tempRefd3,
|
|
257
|
+
dewpointRefd3,
|
|
258
|
+
windRefd3,
|
|
259
|
+
margin,
|
|
260
|
+
width,
|
|
261
|
+
tooltipData
|
|
262
|
+
}) => {
|
|
263
|
+
if (tooltipData.length === 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const [, y] = d3.pointer(event);
|
|
267
|
+
const pressureY = event.clientY - svgRectangle.top - 10;
|
|
268
|
+
const pressure = yScale.invert(y);
|
|
269
|
+
const interpolatedData = tooltipData.map(dataSet => {
|
|
270
|
+
return {
|
|
271
|
+
hoveredPressure: dataSet.interpolatePressure(pressure),
|
|
272
|
+
hoveredTemp: dataSet.interpolateTemperature(pressure),
|
|
273
|
+
hoveredDewpoint: dataSet.interpolateDewpoint(pressure),
|
|
274
|
+
hoveredWindDirection: dataSet.interpolateWindDirection(pressure),
|
|
275
|
+
hoveredWindSpeed: dataSet.interpolateWindSpeed(pressure)
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
// Pressure
|
|
279
|
+
pressureRefd3.style('display', 'block').style('left', `${margin.left + margin.right + 4}px`) // 4 to move it slightly away from y-axis
|
|
280
|
+
.style('top', `${pressureY}px`).html(`${Math.round(pressure)}hPa`);
|
|
281
|
+
// Temperature
|
|
282
|
+
tempRefd3.style('display', 'block').style('left', `${0.75 * width + margin.left}px`).style('top', `${pressureY}px`).html(() => interpolatedData.map((dataSet, index) => {
|
|
283
|
+
return getParamHtml(dataSet.hoveredTemp, index, '°C', index === interpolatedData.length - 1);
|
|
284
|
+
}).join(''));
|
|
285
|
+
// Dewpoint temperature
|
|
286
|
+
dewpointRefd3.style('display', 'block').style('left', `${width / 3 + margin.left}px`).style('top', `${pressureY}px`).html(interpolatedData.map((dataSet, index) => {
|
|
287
|
+
return getParamHtml(dataSet.hoveredDewpoint, index, '°C', index === interpolatedData.length - 1);
|
|
288
|
+
}).join(''));
|
|
289
|
+
// Wind
|
|
290
|
+
windRefd3.style('display', 'block').style('left', `${width + margin.left + margin.right - 55}px`) // -55 to account for width of element
|
|
291
|
+
.style('top', `${pressureY}px`).html(() => {
|
|
292
|
+
const windSpeedStrings = interpolatedData.map((dataSet, index) => {
|
|
293
|
+
return getParamHtml(dataSet.hoveredWindSpeed, index, 'm/s', index === interpolatedData.length - 1, true);
|
|
294
|
+
});
|
|
295
|
+
const windDirStrings = interpolatedData.map((dataSet, index) => {
|
|
296
|
+
return getParamHtml(dataSet.hoveredWindDirection, index, '°', index === interpolatedData.length - 1, index === interpolatedData.length - 1);
|
|
297
|
+
});
|
|
298
|
+
return windSpeedStrings.map((windSpeedString, index) => {
|
|
299
|
+
return `${windSpeedString}${windDirStrings[index]}`;
|
|
300
|
+
}).join('');
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/* *
|
|
305
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
306
|
+
* you may not use this file except in compliance with the License.
|
|
307
|
+
* You may obtain a copy of the License at
|
|
308
|
+
*
|
|
309
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
310
|
+
*
|
|
311
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
312
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
313
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
314
|
+
* See the License for the specific language governing permissions and
|
|
315
|
+
* limitations under the License.
|
|
316
|
+
*
|
|
317
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
318
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
319
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
320
|
+
* */
|
|
321
|
+
const windSymbolDefinitions = {
|
|
322
|
+
calm: `<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
323
|
+
<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"/>
|
|
324
|
+
</svg>
|
|
325
|
+
`,
|
|
326
|
+
lightAir: `<svg width="14" height="48" viewBox="0 0 14 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
327
|
+
<path d="M7.22656 4.89307V48.0001M7.22656 43.0262V44.6842" stroke="black" stroke-width="1.8"/>
|
|
328
|
+
</svg>
|
|
329
|
+
`,
|
|
330
|
+
fiveKnots: `<svg width="15" height="49" viewBox="0 0 15 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
331
|
+
<path d="M8.26953 5.57324V48.608M8.26953 40.5757L0.269531 42.2309" stroke="black" stroke-width="1.8"/>
|
|
332
|
+
</svg>`,
|
|
333
|
+
tenKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
334
|
+
<path d="M17.0781 4.64502V44.9031L1.07812 47.9999" stroke="black" stroke-width="1.8"/>
|
|
335
|
+
</svg>`,
|
|
336
|
+
fifteenKnots: `<svg width="23" height="50" viewBox="0 0 23 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
337
|
+
<path d="M16.4727 5.55469V45.8128L0.472656 48.9095M16.4727 38.0708L9.61551 39.6192" stroke="black" stroke-width="1.8"/>
|
|
338
|
+
</svg>
|
|
339
|
+
`,
|
|
340
|
+
twentyKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
341
|
+
<path d="M16.8555 4.71924V44.9773L0.855469 48.0741M16.8555 38.4093L0.855469 41.5061" stroke="black" stroke-width="1.8"/>
|
|
342
|
+
</svg>`,
|
|
343
|
+
twentyFiveKnots: `<svg width="23" height="51" viewBox="0 0 23 51" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
344
|
+
<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"/>
|
|
345
|
+
</svg>`,
|
|
346
|
+
thirtyKnots: `<svg width="23" height="50" viewBox="0 0 23 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
347
|
+
<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"/>
|
|
348
|
+
</svg>`,
|
|
349
|
+
thirtyFiveKnots: `<svg width="19" height="50" viewBox="0 0 19 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
350
|
+
<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"/>
|
|
351
|
+
</svg>`,
|
|
352
|
+
fortyKnots: `<svg width="24" height="50" viewBox="0 0 24 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
353
|
+
<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"/>
|
|
354
|
+
</svg>`,
|
|
355
|
+
fortyFiveKnots: `<svg width="23" height="50" viewBox="0 0 23 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
356
|
+
<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"/>
|
|
357
|
+
</svg>`,
|
|
358
|
+
fiftyKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
359
|
+
<path d="M16.9062 5.27002V45.5281L0.90625 48.6249L16.9062 37.7861" fill="black"/>
|
|
360
|
+
<path d="M16.9053 5.27002V45.5281L4.35132 47.2337L16.9053 37.7861" stroke="black" stroke-width="1.8"/>
|
|
361
|
+
</svg>
|
|
362
|
+
`,
|
|
363
|
+
fiftyFiveKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
364
|
+
<path d="M16.728 5.55371V45.8118L0.894531 48.9085L16.728 38.0698" fill="black"/>
|
|
365
|
+
<path d="M16.73 5.55371V45.8118L4.33984 47.5245L16.73 38.0698" stroke="black" stroke-width="1.8"/>
|
|
366
|
+
<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"/>
|
|
367
|
+
</svg>`,
|
|
368
|
+
sixtyKnots: `<svg width="23" height="49" viewBox="0 0 23 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
369
|
+
<path d="M16.2891 5.27051V45.5286L0.289062 48.6253L16.2891 37.7866" fill="black"/>
|
|
370
|
+
<path d="M16.2897 5.27051V45.5286L3.7422 47.2478L16.2897 37.7866" stroke="black" stroke-width="1.8"/>
|
|
371
|
+
<path d="M16.2891 34.6899L0.289062 37.7867" stroke="black" stroke-width="1.8"/>
|
|
372
|
+
</svg>`,
|
|
373
|
+
sixtyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
374
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
375
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
376
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L9.5 28.5" stroke="black" stroke-width="1.8"/>
|
|
377
|
+
</svg>`,
|
|
378
|
+
seventyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
379
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
380
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
381
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
|
|
382
|
+
</svg>`,
|
|
383
|
+
seventyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
384
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
385
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
386
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
|
|
387
|
+
<path d="M16.9167 22C13.8251 22.6048 9 23.5484 9 23.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
388
|
+
</svg>`,
|
|
389
|
+
eightyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
390
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
391
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
392
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
|
|
393
|
+
<path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
394
|
+
</svg>`,
|
|
395
|
+
eightyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
396
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
397
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
398
|
+
<path d="M16.9167 17C13.8251 17.6048 9 18.5484 9 18.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
399
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
|
|
400
|
+
<path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
401
|
+
</svg>
|
|
402
|
+
`,
|
|
403
|
+
ninetyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
404
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
405
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
406
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
|
|
407
|
+
<path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
408
|
+
<path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
409
|
+
</svg>
|
|
410
|
+
`,
|
|
411
|
+
ninetyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
412
|
+
<path d="M17 3.26807V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
413
|
+
<path d="M16.9984 3.26807V43.5261L4.35938 45.2985L16.9984 35.7842" stroke="black" stroke-width="1.8"/>
|
|
414
|
+
<path d="M17 32.0788L1 35.1756M17 26.8862L1 29.983" stroke="black" stroke-width="1.8"/>
|
|
415
|
+
<path d="M17 22C10.7518 23.1718 1 25 1 25" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
416
|
+
<path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
417
|
+
<path d="M16.9167 12C13.8251 12.6048 9 13.5484 9 13.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
418
|
+
</svg>`,
|
|
419
|
+
hundredKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
420
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
421
|
+
<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"/>
|
|
422
|
+
</svg>
|
|
423
|
+
`,
|
|
424
|
+
hundredFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
425
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
426
|
+
<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"/>
|
|
427
|
+
<path d="M16.9167 25C13.8251 25.6048 9 26.5484 9 26.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
428
|
+
</svg>
|
|
429
|
+
`,
|
|
430
|
+
hundredTenKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
431
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
432
|
+
<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"/>
|
|
433
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
434
|
+
</svg>`,
|
|
435
|
+
hundredFifteenKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
436
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
437
|
+
<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"/>
|
|
438
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
439
|
+
<path d="M16.9167 21C13.8251 21.6048 9 22.5484 9 22.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
440
|
+
</svg>`,
|
|
441
|
+
hundredTwentyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
442
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
443
|
+
<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"/>
|
|
444
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
445
|
+
<path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
446
|
+
</svg>`,
|
|
447
|
+
hundredTwentyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
448
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
449
|
+
<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"/>
|
|
450
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
451
|
+
<path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
452
|
+
<path d="M16.9167 17C13.8251 17.6048 9 18.5484 9 18.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
453
|
+
</svg>
|
|
454
|
+
`,
|
|
455
|
+
hundredThirtyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
456
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
457
|
+
<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"/>
|
|
458
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
459
|
+
<path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
460
|
+
<path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
461
|
+
</svg>`,
|
|
462
|
+
hundredThirtyFiveKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
463
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
464
|
+
<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"/>
|
|
465
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
466
|
+
<path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
467
|
+
<path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
468
|
+
<path d="M16.9167 13C13.8251 13.6048 9 14.5484 9 14.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
469
|
+
</svg>`,
|
|
470
|
+
hundredFortyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
471
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
472
|
+
<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"/>
|
|
473
|
+
<path d="M17 25C10.7518 26.1718 1 28 1 28" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
474
|
+
<path d="M17 21C10.7518 22.1718 1 24 1 24" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
475
|
+
<path d="M17 17C10.7518 18.1718 1 20 1 20" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
476
|
+
<path d="M17 13C10.7518 14.1718 1 16 1 16" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
477
|
+
</svg>`,
|
|
478
|
+
hundredFortyFiveKnots: `<svg width="24" height="49" viewBox="0 0 24 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
479
|
+
<path d="M17 5.26807V25.3971V45.5261L1 48.6229L17 37.7842" fill="black"/>
|
|
480
|
+
<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"/>
|
|
481
|
+
<path d="M16.9167 12C13.8251 12.6048 9 13.5484 9 13.5484" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
482
|
+
<path d="M17 27C10.7518 28.1718 1 30 1 30" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
483
|
+
<path d="M17 23C10.7518 24.1718 1 26 1 26" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
484
|
+
<path d="M17 19C10.7518 20.1718 1 22 1 22" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
485
|
+
<path d="M17 15C10.7518 16.1718 1 18 1 18" stroke="black" stroke-width="1.8" stroke-miterlimit="16"/>
|
|
486
|
+
</svg>`,
|
|
487
|
+
hundredFiftyKnots: `<svg width="24" height="47" viewBox="0 0 24 47" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
488
|
+
<path d="M17 3.26807V23.3971V43.5261L1 46.6229L17 35.7842" fill="black"/>
|
|
489
|
+
<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"/>
|
|
490
|
+
</svg>`
|
|
491
|
+
};
|
|
492
|
+
const windSpeedRanges = [{
|
|
493
|
+
min: 0,
|
|
494
|
+
max: 1,
|
|
495
|
+
symbol: windSymbolDefinitions.calm
|
|
496
|
+
}, {
|
|
497
|
+
min: 1,
|
|
498
|
+
max: 4.5,
|
|
499
|
+
symbol: windSymbolDefinitions.lightAir
|
|
500
|
+
}, {
|
|
501
|
+
min: 4.5,
|
|
502
|
+
max: 7.5,
|
|
503
|
+
symbol: windSymbolDefinitions.fiveKnots
|
|
504
|
+
}, {
|
|
505
|
+
min: 7.5,
|
|
506
|
+
max: 12.5,
|
|
507
|
+
symbol: windSymbolDefinitions.tenKnots
|
|
508
|
+
}, {
|
|
509
|
+
min: 12.5,
|
|
510
|
+
max: 17.5,
|
|
511
|
+
symbol: windSymbolDefinitions.fifteenKnots
|
|
512
|
+
}, {
|
|
513
|
+
min: 17.5,
|
|
514
|
+
max: 22.5,
|
|
515
|
+
symbol: windSymbolDefinitions.twentyKnots
|
|
516
|
+
}, {
|
|
517
|
+
min: 22.5,
|
|
518
|
+
max: 27.5,
|
|
519
|
+
symbol: windSymbolDefinitions.twentyFiveKnots
|
|
520
|
+
}, {
|
|
521
|
+
min: 27.5,
|
|
522
|
+
max: 32.5,
|
|
523
|
+
symbol: windSymbolDefinitions.thirtyKnots
|
|
524
|
+
}, {
|
|
525
|
+
min: 32.5,
|
|
526
|
+
max: 37.5,
|
|
527
|
+
symbol: windSymbolDefinitions.thirtyFiveKnots
|
|
528
|
+
}, {
|
|
529
|
+
min: 37.5,
|
|
530
|
+
max: 42.5,
|
|
531
|
+
symbol: windSymbolDefinitions.fortyKnots
|
|
532
|
+
}, {
|
|
533
|
+
min: 42.5,
|
|
534
|
+
max: 47.5,
|
|
535
|
+
symbol: windSymbolDefinitions.fortyFiveKnots
|
|
536
|
+
}, {
|
|
537
|
+
min: 47.5,
|
|
538
|
+
max: 52.5,
|
|
539
|
+
symbol: windSymbolDefinitions.fiftyKnots
|
|
540
|
+
}, {
|
|
541
|
+
min: 52.5,
|
|
542
|
+
max: 57.5,
|
|
543
|
+
symbol: windSymbolDefinitions.fiftyFiveKnots
|
|
544
|
+
}, {
|
|
545
|
+
min: 57.5,
|
|
546
|
+
max: 62.5,
|
|
547
|
+
symbol: windSymbolDefinitions.sixtyKnots
|
|
548
|
+
}, {
|
|
549
|
+
min: 62.5,
|
|
550
|
+
max: 67.5,
|
|
551
|
+
symbol: windSymbolDefinitions.sixtyFiveKnots
|
|
552
|
+
}, {
|
|
553
|
+
min: 67.5,
|
|
554
|
+
max: 72.5,
|
|
555
|
+
symbol: windSymbolDefinitions.seventyKnots
|
|
556
|
+
}, {
|
|
557
|
+
min: 72.5,
|
|
558
|
+
max: 77.5,
|
|
559
|
+
symbol: windSymbolDefinitions.seventyFiveKnots
|
|
560
|
+
}, {
|
|
561
|
+
min: 77.5,
|
|
562
|
+
max: 82.5,
|
|
563
|
+
symbol: windSymbolDefinitions.eightyKnots
|
|
564
|
+
}, {
|
|
565
|
+
min: 82.5,
|
|
566
|
+
max: 87.5,
|
|
567
|
+
symbol: windSymbolDefinitions.eightyFiveKnots
|
|
568
|
+
}, {
|
|
569
|
+
min: 87.5,
|
|
570
|
+
max: 92.5,
|
|
571
|
+
symbol: windSymbolDefinitions.ninetyKnots
|
|
572
|
+
}, {
|
|
573
|
+
min: 92.5,
|
|
574
|
+
max: 97.5,
|
|
575
|
+
symbol: windSymbolDefinitions.ninetyFiveKnots
|
|
576
|
+
}, {
|
|
577
|
+
min: 97.5,
|
|
578
|
+
max: 102.5,
|
|
579
|
+
symbol: windSymbolDefinitions.hundredKnots
|
|
580
|
+
}, {
|
|
581
|
+
min: 102.5,
|
|
582
|
+
max: 107.5,
|
|
583
|
+
symbol: windSymbolDefinitions.hundredFiveKnots
|
|
584
|
+
}, {
|
|
585
|
+
min: 107.5,
|
|
586
|
+
max: 112.5,
|
|
587
|
+
symbol: windSymbolDefinitions.hundredTenKnots
|
|
588
|
+
}, {
|
|
589
|
+
min: 112.5,
|
|
590
|
+
max: 117.5,
|
|
591
|
+
symbol: windSymbolDefinitions.hundredFifteenKnots
|
|
592
|
+
}, {
|
|
593
|
+
min: 117.5,
|
|
594
|
+
max: 122.5,
|
|
595
|
+
symbol: windSymbolDefinitions.hundredTwentyKnots
|
|
596
|
+
}, {
|
|
597
|
+
min: 122.5,
|
|
598
|
+
max: 127.5,
|
|
599
|
+
symbol: windSymbolDefinitions.hundredTwentyFiveKnots
|
|
600
|
+
}, {
|
|
601
|
+
min: 127.5,
|
|
602
|
+
max: 132.5,
|
|
603
|
+
symbol: windSymbolDefinitions.hundredThirtyKnots
|
|
604
|
+
}, {
|
|
605
|
+
min: 132.5,
|
|
606
|
+
max: 137.5,
|
|
607
|
+
symbol: windSymbolDefinitions.hundredThirtyFiveKnots
|
|
608
|
+
}, {
|
|
609
|
+
min: 137.5,
|
|
610
|
+
max: 142.5,
|
|
611
|
+
symbol: windSymbolDefinitions.hundredFortyKnots
|
|
612
|
+
}, {
|
|
613
|
+
min: 142.5,
|
|
614
|
+
max: 147.5,
|
|
615
|
+
symbol: windSymbolDefinitions.hundredFortyFiveKnots
|
|
616
|
+
}, {
|
|
617
|
+
min: 147.5,
|
|
618
|
+
max: 152,
|
|
619
|
+
symbol: windSymbolDefinitions.hundredFiftyKnots
|
|
620
|
+
}];
|
|
621
|
+
|
|
622
|
+
/* *
|
|
623
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
624
|
+
* you may not use this file except in compliance with the License.
|
|
625
|
+
* You may obtain a copy of the License at
|
|
626
|
+
*
|
|
627
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
628
|
+
*
|
|
629
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
630
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
631
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
632
|
+
* See the License for the specific language governing permissions and
|
|
633
|
+
* limitations under the License.
|
|
634
|
+
*
|
|
635
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
636
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
637
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
638
|
+
* */
|
|
639
|
+
const metersPerSecondToKnots = metersPerSecond => metersPerSecond * 1.94384;
|
|
640
|
+
const extendIsoBar = (path, horizontalBarbMargin) => {
|
|
641
|
+
const match = path.match(/^M([\d.-]+),([\d.-]+)\s*L([\d.-]+),([\d.-]+)/);
|
|
642
|
+
if (match) {
|
|
643
|
+
const startX = parseFloat(match[1]);
|
|
644
|
+
const startY = parseFloat(match[2]);
|
|
645
|
+
const endX = parseFloat(match[3]);
|
|
646
|
+
const endY = parseFloat(match[4]);
|
|
647
|
+
return `M${startX},${startY}L${endX + horizontalBarbMargin * 3},${endY}`;
|
|
648
|
+
}
|
|
649
|
+
return path;
|
|
650
|
+
};
|
|
651
|
+
const extendXAxis = (path, horizontalBarbMargin) => {
|
|
652
|
+
const match = path.match(/^M([\d.-]+),([\d.-]+)H([\d.-]+)/);
|
|
653
|
+
if (match) {
|
|
654
|
+
const endX = parseFloat(match[3]);
|
|
655
|
+
return path.replace(/H[\d.-]+/, `H${endX + horizontalBarbMargin * 3}`);
|
|
656
|
+
}
|
|
657
|
+
return path;
|
|
658
|
+
};
|
|
659
|
+
const drawWindBarbs = ({
|
|
660
|
+
svgRef,
|
|
661
|
+
data,
|
|
662
|
+
width,
|
|
663
|
+
height,
|
|
664
|
+
margin,
|
|
665
|
+
minPressure,
|
|
666
|
+
maxPressure,
|
|
667
|
+
barbColor = 'black'
|
|
668
|
+
}) => {
|
|
669
|
+
const barbsSize = 25;
|
|
670
|
+
const scalingFactor = 0.5;
|
|
671
|
+
const horizontalBarbMargin = barbsSize * scalingFactor;
|
|
672
|
+
const visibleItems = removeItemsExponential(data.reverse()).filter(item => item.pressure >= 150);
|
|
673
|
+
// Measure width and height and make extra room for one column
|
|
674
|
+
// of windbarbs
|
|
675
|
+
const svg = d3.select(svgRef.current);
|
|
676
|
+
const currentWidth = svg.node()?.getBoundingClientRect().width;
|
|
677
|
+
if (!currentWidth) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
// Add extra width to all isobars needed for the windbarbs
|
|
681
|
+
d3.selectAll('path[type="isobar"]').each(function () {
|
|
682
|
+
const path = d3.select(this).attr('d');
|
|
683
|
+
const newD = extendIsoBar(path, horizontalBarbMargin);
|
|
684
|
+
d3.select(this).attr('d', newD).attr('clip-path', null);
|
|
685
|
+
});
|
|
686
|
+
// Add extra width to x-axis needed for the windbarbs
|
|
687
|
+
d3.select('g[type="x-axis"] path.domain').each(function () {
|
|
688
|
+
const path = d3.select(this).attr('d');
|
|
689
|
+
const newD = extendXAxis(path, horizontalBarbMargin);
|
|
690
|
+
d3.select(this).attr('d', newD);
|
|
691
|
+
});
|
|
692
|
+
svg.attr('width', currentWidth + margin.left + horizontalBarbMargin);
|
|
693
|
+
const pressureScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]).clamp(true);
|
|
694
|
+
const barbGroup = svg.append('g').attr('class', 'wind-group').attr('transform',
|
|
695
|
+
// This translation should be revisited when implementing
|
|
696
|
+
// the plotting of several datasets at once
|
|
697
|
+
`translate(${(currentWidth ?? width) + horizontalBarbMargin}, ${margin.top})`);
|
|
698
|
+
barbGroup.selectAll('g').data(visibleItems).enter().append('g').attr('id', 'wind-barb').each(function handleBarb(data) {
|
|
699
|
+
const group = d3.select(this);
|
|
700
|
+
const windSymbol = getWindSymbol(data.windSpeed);
|
|
701
|
+
if (windSymbol) {
|
|
702
|
+
// Inner group will have rotation and scaling applied to it
|
|
703
|
+
const innerGroup = group.append('g');
|
|
704
|
+
innerGroup.node()?.appendChild(windSymbol);
|
|
705
|
+
// Update stroke color
|
|
706
|
+
innerGroup.selectAll('*').attr('stroke', barbColor);
|
|
707
|
+
// Only update the fill for the paths that already used a fill
|
|
708
|
+
// in their original svg form.
|
|
709
|
+
innerGroup.selectAll('*').each(function () {
|
|
710
|
+
const el = d3.select(this);
|
|
711
|
+
const currentFill = el.attr('fill');
|
|
712
|
+
if (currentFill && currentFill !== 'none') {
|
|
713
|
+
el.attr('fill', barbColor);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
const bbox = windSymbol.getBBox();
|
|
717
|
+
// Central point
|
|
718
|
+
const cx = bbox.x + bbox.width / 2;
|
|
719
|
+
const cy = bbox.y + bbox.height / 2;
|
|
720
|
+
const rotation = 180 + data.windDirection;
|
|
721
|
+
const y = pressureScale(data.pressure);
|
|
722
|
+
// Place items in the correct vertical position
|
|
723
|
+
// Correcting for barbSize
|
|
724
|
+
group.attr('transform', `translate(0, ${y - barbsSize * scalingFactor / 8})`);
|
|
725
|
+
// Rotate accordingly, and scale
|
|
726
|
+
innerGroup.attr('transform', `rotate(${rotation}) scale(${scalingFactor}) translate(${-cx}, ${-cy})`);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
const removeItemsExponential = array => {
|
|
731
|
+
const base = 1.15;
|
|
732
|
+
const indicesToKeep = [];
|
|
733
|
+
let i = 0;
|
|
734
|
+
let nextIndex = 0;
|
|
735
|
+
while (nextIndex < array.length) {
|
|
736
|
+
nextIndex = Math.floor(base ** i);
|
|
737
|
+
indicesToKeep.push(nextIndex);
|
|
738
|
+
i += 1;
|
|
739
|
+
}
|
|
740
|
+
return array.filter((_, index) => indicesToKeep.includes(index));
|
|
741
|
+
};
|
|
742
|
+
const getWindSymbol = value => {
|
|
743
|
+
const valueKts = metersPerSecondToKnots(value);
|
|
744
|
+
const matchedRange = windSpeedRanges.find(({
|
|
745
|
+
min,
|
|
746
|
+
max
|
|
747
|
+
}) => valueKts >= min && valueKts < max);
|
|
748
|
+
// Out of range
|
|
749
|
+
if (!matchedRange) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
// SVG string to actual SVG element
|
|
753
|
+
const parser = new DOMParser();
|
|
754
|
+
const svgDoc = parser.parseFromString(matchedRange.symbol, 'image/svg+xml');
|
|
755
|
+
const svgNode = svgDoc.documentElement;
|
|
756
|
+
// Return the parsed SVG node
|
|
757
|
+
return svgNode;
|
|
758
|
+
};
|
|
83
759
|
|
|
84
760
|
/* *
|
|
85
761
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -133,48 +809,6 @@ const drawDryAdiabats = ({
|
|
|
133
809
|
}
|
|
134
810
|
};
|
|
135
811
|
|
|
136
|
-
/* *
|
|
137
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
138
|
-
* you may not use this file except in compliance with the License.
|
|
139
|
-
* You may obtain a copy of the License at
|
|
140
|
-
*
|
|
141
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
142
|
-
*
|
|
143
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
144
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
145
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
146
|
-
* See the License for the specific language governing permissions and
|
|
147
|
-
* limitations under the License.
|
|
148
|
-
*
|
|
149
|
-
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
150
|
-
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
151
|
-
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
152
|
-
* */
|
|
153
|
-
const isobarValues = [200, 300, 400, 500, 700, 850, 925, 1000];
|
|
154
|
-
const drawIsobars = ({
|
|
155
|
-
minTemperature,
|
|
156
|
-
maxTemperature,
|
|
157
|
-
xScale,
|
|
158
|
-
yScale,
|
|
159
|
-
svg
|
|
160
|
-
}) => {
|
|
161
|
-
// Calculate X and Y coordinates for each pressure level
|
|
162
|
-
for (const pressureVal of isobarValues) {
|
|
163
|
-
const Px = [];
|
|
164
|
-
const tempBounds = [minTemperature, maxTemperature];
|
|
165
|
-
const Ppx = yScale(pressureVal);
|
|
166
|
-
for (const bound of tempBounds) {
|
|
167
|
-
Px.push([xScale(bound), yScale(pressureVal)]);
|
|
168
|
-
}
|
|
169
|
-
const lineGenerator = d3.line();
|
|
170
|
-
const isoPathString = lineGenerator(Px);
|
|
171
|
-
const strID = `${pressureVal}textId`;
|
|
172
|
-
svg.append('path').attr('id', strID).attr('d', isoPathString).style('fill', 'none').style('stroke-width', 1.5).style('stroke', '#B5B51F').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
|
|
173
|
-
svg.append('text').text(pressureVal).attr('text-anchor', 'middle').attr('x', `${ -24}px`).attr('y', Ppx + 5) // Y-coordinate of the label
|
|
174
|
-
.attr('fill', '#000').attr('font-size', '12px');
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
812
|
/* *
|
|
179
813
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
180
814
|
* you may not use this file except in compliance with the License.
|
|
@@ -428,14 +1062,58 @@ const drawStaticLines = ({
|
|
|
428
1062
|
});
|
|
429
1063
|
};
|
|
430
1064
|
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
1065
|
+
const shadow = '0 1px 5px 0 rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.14)';
|
|
1066
|
+
const soundingsStyle = {
|
|
1067
|
+
tooltip: {
|
|
1068
|
+
display: 'block',
|
|
1069
|
+
position: 'absolute',
|
|
1070
|
+
borderRadius: '1.5px',
|
|
1071
|
+
backgroundColor: 'geowebColors.soundings.tooltip.background',
|
|
1072
|
+
boxShadow: shadow,
|
|
1073
|
+
padding: '2px 4px',
|
|
1074
|
+
pointerEvents: 'none',
|
|
1075
|
+
zIndex: 500
|
|
1076
|
+
},
|
|
1077
|
+
pressure: {
|
|
1078
|
+
fontSize: '10px'
|
|
1079
|
+
},
|
|
1080
|
+
temperature: {
|
|
1081
|
+
fontSize: '12px',
|
|
1082
|
+
fontWeight: 'bold'
|
|
1083
|
+
},
|
|
1084
|
+
dewPoint: {
|
|
1085
|
+
fontSize: '12px',
|
|
1086
|
+
fontWeight: 'bold'
|
|
434
1087
|
},
|
|
435
|
-
|
|
436
|
-
|
|
1088
|
+
wind: {
|
|
1089
|
+
fontSize: '12px',
|
|
1090
|
+
fontWeight: 'bold'
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
// Define pressure and temperature values for static lines
|
|
1094
|
+
const maxPressure = 1050;
|
|
1095
|
+
const minPressure = 150;
|
|
1096
|
+
const staticPressureArray = [maxPressure];
|
|
1097
|
+
const dp = -5;
|
|
1098
|
+
const minTemperature = -40;
|
|
1099
|
+
const maxTemperature = 40.6;
|
|
1100
|
+
let pressure = maxPressure;
|
|
1101
|
+
while (pressure > minPressure) {
|
|
1102
|
+
pressure += dp;
|
|
1103
|
+
staticPressureArray.push(pressure);
|
|
1104
|
+
}
|
|
1105
|
+
const Soundings = ({
|
|
1106
|
+
data,
|
|
1107
|
+
svgDimensions,
|
|
1108
|
+
showTooltip = true
|
|
437
1109
|
}) => {
|
|
438
1110
|
const svgRef = useRef(null);
|
|
1111
|
+
const canvasRef = useRef(null);
|
|
1112
|
+
const pressureRef = useRef(null);
|
|
1113
|
+
const tempRef = useRef(null);
|
|
1114
|
+
const dewpointRef = useRef(null);
|
|
1115
|
+
const windRef = useRef(null);
|
|
1116
|
+
// set default values
|
|
439
1117
|
const svgDimensionsOptions = {
|
|
440
1118
|
width: svgDimensions?.width || 812,
|
|
441
1119
|
height: svgDimensions?.height || 802,
|
|
@@ -447,12 +1125,7 @@ const Soundings = ({
|
|
|
447
1125
|
}
|
|
448
1126
|
};
|
|
449
1127
|
// Set SVG dimensions
|
|
450
|
-
const margin =
|
|
451
|
-
top: svgDimensionsOptions?.margin?.top,
|
|
452
|
-
right: svgDimensionsOptions?.margin?.right,
|
|
453
|
-
bottom: svgDimensionsOptions?.margin?.bottom,
|
|
454
|
-
left: svgDimensionsOptions?.margin?.left
|
|
455
|
-
};
|
|
1128
|
+
const margin = svgDimensionsOptions?.margin;
|
|
456
1129
|
const baseWidth = svgDimensionsOptions?.width;
|
|
457
1130
|
const baseHeight = svgDimensionsOptions?.height;
|
|
458
1131
|
const width = baseWidth - (margin.left + margin.right);
|
|
@@ -468,18 +1141,7 @@ const Soundings = ({
|
|
|
468
1141
|
height,
|
|
469
1142
|
margin
|
|
470
1143
|
});
|
|
471
|
-
|
|
472
|
-
const maxPressure = 1050;
|
|
473
|
-
const minPressure = 150;
|
|
474
|
-
const staticPressureArray = [maxPressure];
|
|
475
|
-
const dp = -5;
|
|
476
|
-
const minTemperature = -40;
|
|
477
|
-
const maxTemperature = 40;
|
|
478
|
-
let pressure = maxPressure;
|
|
479
|
-
while (pressure > minPressure) {
|
|
480
|
-
pressure += dp;
|
|
481
|
-
staticPressureArray.push(pressure);
|
|
482
|
-
}
|
|
1144
|
+
const svgRectangle = svgRef.current.getBoundingClientRect();
|
|
483
1145
|
// Set scales for temperature and pressure
|
|
484
1146
|
const xScale = d3.scaleLinear().domain([minTemperature, maxTemperature]).range([0, width]);
|
|
485
1147
|
const yScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]);
|
|
@@ -488,8 +1150,7 @@ const Soundings = ({
|
|
|
488
1150
|
svg,
|
|
489
1151
|
xScale,
|
|
490
1152
|
yScale,
|
|
491
|
-
height
|
|
492
|
-
width
|
|
1153
|
+
height
|
|
493
1154
|
});
|
|
494
1155
|
// Draw static lines
|
|
495
1156
|
drawStaticLines({
|
|
@@ -510,42 +1171,157 @@ const Soundings = ({
|
|
|
510
1171
|
svg,
|
|
511
1172
|
width
|
|
512
1173
|
});
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1174
|
+
data.forEach((features, index) => {
|
|
1175
|
+
const color = SOUNDING_COLORS[index] || SOUNDING_COLORS[0];
|
|
1176
|
+
const temperatureValues = features.map(feature => feature.properties.temp);
|
|
1177
|
+
const pressureValues = features.map(feature => feature.properties.pressure);
|
|
1178
|
+
// Draw Temperature
|
|
1179
|
+
plotSounding({
|
|
1180
|
+
temperatureValues,
|
|
1181
|
+
pressureValues,
|
|
1182
|
+
color,
|
|
1183
|
+
lineStyle: 'none',
|
|
1184
|
+
xScale,
|
|
1185
|
+
yScale,
|
|
1186
|
+
height,
|
|
1187
|
+
svg
|
|
1188
|
+
});
|
|
1189
|
+
const dewpointValues = features.map(feature => feature.properties.dewpoint);
|
|
1190
|
+
// Dewpoint
|
|
1191
|
+
plotSounding({
|
|
1192
|
+
temperatureValues: dewpointValues,
|
|
1193
|
+
color,
|
|
1194
|
+
pressureValues,
|
|
1195
|
+
lineStyle: '8, 2',
|
|
1196
|
+
xScale,
|
|
1197
|
+
yScale,
|
|
1198
|
+
height,
|
|
1199
|
+
svg
|
|
1200
|
+
});
|
|
1201
|
+
const windData = features.map(feature => ({
|
|
1202
|
+
windDirection: feature.properties.windDirection,
|
|
1203
|
+
windSpeed: feature.properties.windSpeed,
|
|
1204
|
+
pressure: feature.properties.pressure
|
|
1205
|
+
}));
|
|
1206
|
+
drawWindBarbs({
|
|
1207
|
+
svgRef,
|
|
1208
|
+
data: windData,
|
|
1209
|
+
width,
|
|
1210
|
+
height,
|
|
1211
|
+
margin,
|
|
1212
|
+
minPressure,
|
|
1213
|
+
maxPressure,
|
|
1214
|
+
barbColor: color
|
|
1215
|
+
});
|
|
537
1216
|
});
|
|
1217
|
+
// Attach mousemove event listener to update current coordinates
|
|
1218
|
+
if (showTooltip) {
|
|
1219
|
+
const pressureRefd3 = d3.select(pressureRef.current);
|
|
1220
|
+
const tempRefd3 = d3.select(tempRef.current);
|
|
1221
|
+
const dewpointRefd3 = d3.select(dewpointRef.current);
|
|
1222
|
+
const windRefd3 = d3.select(windRef.current);
|
|
1223
|
+
const tooltipData = constructTooltipData(data);
|
|
1224
|
+
svg.on('mousemove', event => {
|
|
1225
|
+
drawSoundingsTooltip({
|
|
1226
|
+
event,
|
|
1227
|
+
yScale,
|
|
1228
|
+
svgRectangle,
|
|
1229
|
+
margin,
|
|
1230
|
+
width,
|
|
1231
|
+
pressureRefd3,
|
|
1232
|
+
tempRefd3,
|
|
1233
|
+
dewpointRefd3,
|
|
1234
|
+
windRefd3,
|
|
1235
|
+
tooltipData
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
const svgRefCur = svgRef.current;
|
|
538
1240
|
// eslint-disable-next-line consistent-return
|
|
539
1241
|
return () => {
|
|
1242
|
+
d3.select(svgRefCur).selectAll('.wind-group').remove();
|
|
540
1243
|
svg.selectAll('*').remove();
|
|
541
|
-
|
|
1244
|
+
svg.on('mousemove', null);
|
|
542
1245
|
};
|
|
543
1246
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1247
|
+
}, [showTooltip, data]);
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
const handleMouseLeave = () => {
|
|
1250
|
+
if (pressureRef.current) {
|
|
1251
|
+
pressureRef.current.style.display = 'none';
|
|
1252
|
+
}
|
|
1253
|
+
if (tempRef.current) {
|
|
1254
|
+
tempRef.current.style.display = 'none';
|
|
1255
|
+
}
|
|
1256
|
+
if (dewpointRef.current) {
|
|
1257
|
+
dewpointRef.current.style.display = 'none';
|
|
1258
|
+
}
|
|
1259
|
+
if (windRef.current) {
|
|
1260
|
+
windRef.current.style.display = 'none';
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
const canvasElement = canvasRef.current;
|
|
1264
|
+
if (canvasElement) {
|
|
1265
|
+
canvasElement.addEventListener('mouseleave', handleMouseLeave);
|
|
1266
|
+
}
|
|
1267
|
+
return () => {
|
|
1268
|
+
if (canvasElement) {
|
|
1269
|
+
canvasElement.removeEventListener('mouseleave', handleMouseLeave);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
544
1272
|
}, []);
|
|
545
|
-
|
|
546
|
-
|
|
1273
|
+
useEffect(() => {
|
|
1274
|
+
// Reset the tooltips when filteredData changes
|
|
1275
|
+
if (pressureRef.current) {
|
|
1276
|
+
pressureRef.current.style.display = 'none';
|
|
1277
|
+
}
|
|
1278
|
+
if (tempRef.current) {
|
|
1279
|
+
tempRef.current.style.display = 'none';
|
|
1280
|
+
}
|
|
1281
|
+
if (dewpointRef.current) {
|
|
1282
|
+
dewpointRef.current.style.display = 'none';
|
|
1283
|
+
}
|
|
1284
|
+
if (windRef.current) {
|
|
1285
|
+
windRef.current.style.display = 'none';
|
|
1286
|
+
}
|
|
1287
|
+
}, [data]);
|
|
1288
|
+
return jsxs(Card, {
|
|
1289
|
+
ref: canvasRef,
|
|
1290
|
+
sx: {
|
|
1291
|
+
position: 'relative',
|
|
1292
|
+
paddingBottom: '20px'
|
|
1293
|
+
},
|
|
1294
|
+
children: [jsx("svg", {
|
|
547
1295
|
ref: svgRef
|
|
548
|
-
})
|
|
1296
|
+
}), jsx(Box, {
|
|
1297
|
+
ref: pressureRef,
|
|
1298
|
+
sx: {
|
|
1299
|
+
...soundingsStyle.tooltip,
|
|
1300
|
+
...soundingsStyle.pressure
|
|
1301
|
+
},
|
|
1302
|
+
"data-testid": "pressure-tooltip"
|
|
1303
|
+
}), jsx(Box, {
|
|
1304
|
+
ref: tempRef,
|
|
1305
|
+
sx: {
|
|
1306
|
+
...soundingsStyle.tooltip,
|
|
1307
|
+
...soundingsStyle.temperature
|
|
1308
|
+
},
|
|
1309
|
+
"data-testid": "temperature-tooltip"
|
|
1310
|
+
}), jsx(Box, {
|
|
1311
|
+
ref: dewpointRef,
|
|
1312
|
+
sx: {
|
|
1313
|
+
...soundingsStyle.tooltip,
|
|
1314
|
+
...soundingsStyle.dewPoint
|
|
1315
|
+
},
|
|
1316
|
+
"data-testid": "dewpoint-tooltip"
|
|
1317
|
+
}), jsx(Box, {
|
|
1318
|
+
ref: windRef,
|
|
1319
|
+
sx: {
|
|
1320
|
+
...soundingsStyle.tooltip,
|
|
1321
|
+
...soundingsStyle.wind
|
|
1322
|
+
},
|
|
1323
|
+
"data-testid": "wind-tooltip"
|
|
1324
|
+
})]
|
|
549
1325
|
});
|
|
550
1326
|
};
|
|
551
1327
|
|
|
@@ -562,104 +1338,720 @@ const Soundings = ({
|
|
|
562
1338
|
* See the License for the specific language governing permissions and
|
|
563
1339
|
* limitations under the License.
|
|
564
1340
|
*
|
|
565
|
-
* Copyright
|
|
566
|
-
* Copyright
|
|
567
|
-
* Copyright
|
|
1341
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1342
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1343
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
568
1344
|
* */
|
|
569
|
-
const
|
|
570
|
-
const
|
|
1345
|
+
const BASE_URL = 'https://opendata.fmi.fi/edr';
|
|
1346
|
+
const getCollectionLocations = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/locations`, {
|
|
1347
|
+
headers: getHeaders()
|
|
1348
|
+
}).then(handleResponse);
|
|
1349
|
+
const getCollectionInstance = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/instances`, {
|
|
1350
|
+
headers: getHeaders()
|
|
1351
|
+
}).then(handleResponse);
|
|
1352
|
+
const getEdrCollectionForLocation = (collectionId, instanceId, locationId, parameters, datetime) => {
|
|
1353
|
+
const url = new URL(`${BASE_URL}/collections/${collectionId}${instanceId ? `/instances/${instanceId}` : ''}/locations/${locationId}`);
|
|
1354
|
+
url.searchParams.append('parameter-name', parameters.join(','));
|
|
1355
|
+
url.searchParams.append('datetime', datetime);
|
|
1356
|
+
url.searchParams.append('f', 'CoverageJSON');
|
|
1357
|
+
return fetch(url.toString(), {
|
|
1358
|
+
headers: getHeaders()
|
|
1359
|
+
}).then(handleResponse);
|
|
1360
|
+
};
|
|
1361
|
+
const getEdrCollectionForPosition = (collectionId, instanceId, coordinates, parameters, datetime) => {
|
|
1362
|
+
const url = new URL(`${BASE_URL}/collections/${collectionId}${instanceId ? `/instances/${instanceId}` : ''}/position`);
|
|
1363
|
+
url.searchParams.append('coords', `POINT(${coordinates[0]} ${coordinates[1]})`);
|
|
1364
|
+
url.searchParams.append('parameter-name', parameters.join(','));
|
|
1365
|
+
url.searchParams.append('datetime', datetime);
|
|
1366
|
+
url.searchParams.append('f', 'CoverageJSON');
|
|
1367
|
+
return fetch(url.toString(), {
|
|
1368
|
+
headers: getHeaders()
|
|
1369
|
+
}).then(handleResponse);
|
|
1370
|
+
};
|
|
571
1371
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1372
|
+
/* *
|
|
1373
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1374
|
+
* you may not use this file except in compliance with the License.
|
|
1375
|
+
* You may obtain a copy of the License at
|
|
1376
|
+
*
|
|
1377
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1378
|
+
*
|
|
1379
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1380
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1381
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1382
|
+
* See the License for the specific language governing permissions and
|
|
1383
|
+
* limitations under the License.
|
|
1384
|
+
*
|
|
1385
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1386
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1387
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1388
|
+
* */
|
|
1389
|
+
const epsilon = 0.622; // Ratio of molecular weights of water vapour and dry air
|
|
1390
|
+
const e0 = 611.2; // Reference vapor pressure [Pa]
|
|
1391
|
+
const aConstant = 17.67;
|
|
1392
|
+
const bConstant = 243.5; // Constants for water, b in Celsius
|
|
1393
|
+
const tKelvinOffset = 273.15; // To convert Celsius to Kelvin
|
|
1394
|
+
const gConstant = 461.5; // Gas constant ratio between water vapour and air [J/(K*kg)]
|
|
1395
|
+
const hConstant = 2.5e6; // Latent heat for water vaporization [J/kg]
|
|
1396
|
+
// Formula to use when dewpoint is not available
|
|
1397
|
+
// Known as Magnus Formula
|
|
1398
|
+
// - Calculate vapor pressure
|
|
1399
|
+
// - Use inverse of the Clausius-Clapeyron equation
|
|
1400
|
+
function dewpointFromSpecificHumidity(specificHumidity,
|
|
1401
|
+
// specific humidity [kg/kg]
|
|
1402
|
+
pressure) {
|
|
1403
|
+
// Step 1: Calculate vapor pressure (Pa)
|
|
1404
|
+
const vaporPressure = specificHumidity * pressure / (epsilon + (1 - epsilon) * specificHumidity);
|
|
1405
|
+
// Step 2: Calculate dew point in Celsius
|
|
1406
|
+
const lnRatio = Math.log(vaporPressure / e0);
|
|
1407
|
+
const dewpoint = bConstant * lnRatio / (aConstant - lnRatio);
|
|
1408
|
+
return dewpoint; // dew point in Celsius
|
|
1409
|
+
}
|
|
1410
|
+
// Formula to use when dewpoint is not available
|
|
1411
|
+
function dewpointFromRelativeHumidity(tCelsius,
|
|
1412
|
+
// air temperature [°C]
|
|
1413
|
+
relHumidity) {
|
|
1414
|
+
// Convert to Kelvin
|
|
1415
|
+
const tKelvin = tCelsius + tKelvinOffset;
|
|
1416
|
+
// Calculate dewpoint
|
|
1417
|
+
const dewpoint = tKelvin / (1 - tKelvin * Math.log(relHumidity * 0.01) * gConstant / hConstant);
|
|
1418
|
+
return dewpoint;
|
|
1419
|
+
}
|
|
1420
|
+
// Parsing and conversion function
|
|
1421
|
+
function parseCoverageCollection(response) {
|
|
1422
|
+
const features = [];
|
|
1423
|
+
const {
|
|
1424
|
+
t: tAxis,
|
|
1425
|
+
x: xAxis,
|
|
1426
|
+
y: yAxis,
|
|
1427
|
+
z: zAxis
|
|
1428
|
+
} = response.coverages[0]?.domain.axes || {};
|
|
1429
|
+
if (!tAxis?.values || !xAxis?.values || !yAxis?.values || !zAxis?.values) {
|
|
1430
|
+
return features;
|
|
1431
|
+
}
|
|
1432
|
+
response.coverages.forEach(coverage => {
|
|
1433
|
+
const {
|
|
1434
|
+
ranges
|
|
1435
|
+
} = coverage;
|
|
1436
|
+
const time = coverage.domain.axes.t?.values[0];
|
|
1437
|
+
const xCoord = coverage.domain.axes.x.values[0];
|
|
1438
|
+
const yCoord = coverage.domain.axes.y.values[0];
|
|
1439
|
+
const zValues = coverage.domain.axes.z.values;
|
|
1440
|
+
zValues.forEach((_, zIndex) => {
|
|
1441
|
+
const temp = parseFloat(ranges.temperature?.values[zIndex]);
|
|
1442
|
+
const windums = parseFloat(ranges.windums?.values[zIndex]);
|
|
1443
|
+
const windvms = parseFloat(ranges.windvms?.values[zIndex]);
|
|
1444
|
+
let dewpoint = parseFloat(ranges.dewpoint?.values[zIndex]);
|
|
1445
|
+
const relativeHumidity = parseFloat(ranges.relativehumidity?.values[zIndex]);
|
|
1446
|
+
const specificHumidity = parseFloat(ranges.specifichumidity?.values[zIndex]);
|
|
1447
|
+
let windSpeed = parseFloat(ranges.windSpeed?.values[zIndex]);
|
|
1448
|
+
// Fallback calculations
|
|
1449
|
+
if (Number.isNaN(windSpeed) && !Number.isNaN(windums) && !Number.isNaN(windvms)) {
|
|
1450
|
+
// Fallback calculation
|
|
1451
|
+
windSpeed = Math.sqrt(windums ** 2 + windvms ** 2);
|
|
1452
|
+
}
|
|
1453
|
+
let windDirection = parseFloat(ranges.windDirection?.values[zIndex]);
|
|
1454
|
+
if (Number.isNaN(windDirection) && !Number.isNaN(windums) && !Number.isNaN(windvms)) {
|
|
1455
|
+
// Fallback calculation
|
|
1456
|
+
windDirection = (Math.atan2(-windums, -windvms) * (180 / Math.PI) + 360) % 360;
|
|
1457
|
+
}
|
|
1458
|
+
const pressureFromRange = parseFloat(ranges.pressure?.values[zIndex]);
|
|
1459
|
+
const pressure = !Number.isNaN(pressureFromRange) ? pressureFromRange : parseFloat(zValues[zIndex]);
|
|
1460
|
+
if (dewpoint === null || Number.isNaN(dewpoint)) {
|
|
1461
|
+
// Use dewpoint fallback calculations if not present
|
|
1462
|
+
if (!Number.isNaN(specificHumidity)) {
|
|
1463
|
+
dewpoint = dewpointFromSpecificHumidity(specificHumidity / 1000,
|
|
1464
|
+
// from g/kg to kg/kg
|
|
1465
|
+
pressure * 100);
|
|
1466
|
+
} else if (!Number.isNaN(relativeHumidity)) {
|
|
1467
|
+
dewpoint = dewpointFromRelativeHumidity(temp, relativeHumidity);
|
|
601
1468
|
}
|
|
602
|
-
}
|
|
1469
|
+
}
|
|
1470
|
+
features.push({
|
|
1471
|
+
type: 'Feature',
|
|
1472
|
+
geometry: {
|
|
1473
|
+
type: 'Point',
|
|
1474
|
+
coordinates: [xCoord, yCoord]
|
|
1475
|
+
},
|
|
1476
|
+
properties: {
|
|
1477
|
+
time,
|
|
1478
|
+
temp,
|
|
1479
|
+
dewpoint,
|
|
1480
|
+
pressure,
|
|
1481
|
+
windSpeed,
|
|
1482
|
+
windDirection
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
603
1485
|
});
|
|
1486
|
+
});
|
|
1487
|
+
return features;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/* *
|
|
1491
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1492
|
+
* you may not use this file except in compliance with the License.
|
|
1493
|
+
* You may obtain a copy of the License at
|
|
1494
|
+
*
|
|
1495
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1496
|
+
*
|
|
1497
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1498
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1499
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1500
|
+
* See the License for the specific language governing permissions and
|
|
1501
|
+
* limitations under the License.
|
|
1502
|
+
*
|
|
1503
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1504
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1505
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1506
|
+
* */
|
|
1507
|
+
const sortSoundingFeatures = data => {
|
|
1508
|
+
return data.sort((featureA, featureB) => {
|
|
1509
|
+
// Sort by date (ascending)
|
|
1510
|
+
if (featureA.properties.time !== featureB.properties.time) {
|
|
1511
|
+
return featureA.properties.time > featureB.properties.time ? 1 : -1;
|
|
1512
|
+
}
|
|
1513
|
+
// Sort by pressure (descending)
|
|
1514
|
+
return featureB.properties.pressure - featureA.properties.pressure;
|
|
1515
|
+
});
|
|
1516
|
+
};
|
|
1517
|
+
const constructQueryParams = (collectionId, instanceId, location, datetime, parameters) => {
|
|
1518
|
+
if (typeof location === 'string') {
|
|
1519
|
+
// LOCATION call
|
|
1520
|
+
return {
|
|
1521
|
+
queryKey: ['edr-soundings-for-location', collectionId, instanceId || '', location, parameters.join(), datetime],
|
|
1522
|
+
queryFn: async () => {
|
|
1523
|
+
// No collection or location selected, don't do EDR call but return empty array
|
|
1524
|
+
if (collectionId === undefined || location === '') {
|
|
1525
|
+
return [];
|
|
1526
|
+
}
|
|
1527
|
+
try {
|
|
1528
|
+
const rawData = await getEdrCollectionForLocation(collectionId, instanceId, location, parameters, datetime);
|
|
1529
|
+
const parsedData = parseCoverageCollection(rawData);
|
|
1530
|
+
return sortSoundingFeatures(parsedData);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
throw new Error('Error fetching EDR data collection by location');
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
// POSITION call
|
|
1538
|
+
return {
|
|
1539
|
+
queryKey: ['edr-soundings-for-position', collectionId, location.join(), parameters.join(), datetime],
|
|
1540
|
+
queryFn: async () => {
|
|
1541
|
+
// No collection or position selected, don't do EDR call but return empty array
|
|
1542
|
+
if (collectionId === undefined || location === undefined) {
|
|
1543
|
+
return [];
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
const rawData = await getEdrCollectionForPosition(collectionId, instanceId, location, parameters, datetime);
|
|
1547
|
+
const parsedData = parseCoverageCollection(rawData);
|
|
1548
|
+
return sortSoundingFeatures(parsedData);
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
throw new Error('Error fetching EDR data collection by position');
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
604
1553
|
};
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1554
|
+
};
|
|
1555
|
+
const useFakeTime = (collectionId, instanceId) => {
|
|
1556
|
+
return useQuery({
|
|
1557
|
+
queryKey: ['temporary-instance-call', collectionId, instanceId],
|
|
1558
|
+
queryFn: async () => {
|
|
1559
|
+
return fetch(`https://opendata.fmi.fi/edr/collections/${collectionId}/instances/${instanceId}`, {
|
|
1560
|
+
headers: getHeaders()
|
|
1561
|
+
}).then(handleResponse).then(data => collectionId === undefined || data === undefined ? '' : data.extent.temporal.interval[0][0]);
|
|
1562
|
+
},
|
|
1563
|
+
enabled: !!collectionId && !!instanceId
|
|
1564
|
+
});
|
|
1565
|
+
};
|
|
1566
|
+
const parameters = ['temperature', 'WindDirection', 'WindSpeedMS', 'windUMS', 'windVMS', 'Pressure', 'DewPoint', 'humidity', 'specifichumidity'];
|
|
1567
|
+
const useMultipleSources = (sources, locations, datetime) => {
|
|
1568
|
+
// TODO: Remove once we pass in the datetime coming from the timeslider!
|
|
1569
|
+
const {
|
|
1570
|
+
data: date0
|
|
1571
|
+
} = useFakeTime(sources[0]?.collectionId, sources[0]?.instanceId);
|
|
1572
|
+
const {
|
|
1573
|
+
data: date1
|
|
1574
|
+
} = useFakeTime(sources[1]?.collectionId, sources[1]?.instanceId);
|
|
1575
|
+
const {
|
|
1576
|
+
data: date2
|
|
1577
|
+
} = useFakeTime(sources[2]?.collectionId, sources[2]?.instanceId);
|
|
1578
|
+
const dates = useMemo(() => [date0, date1, date2], [date0, date1, date2]);
|
|
1579
|
+
const queries = useMemo(() => sources.map(({
|
|
1580
|
+
collectionId,
|
|
1581
|
+
instanceId
|
|
1582
|
+
}, index) => {
|
|
1583
|
+
const queryParams = constructQueryParams(collectionId, instanceId, locations[collectionId] || '', dates[index] || '', parameters);
|
|
1584
|
+
return queryParams;
|
|
1585
|
+
}), [dates, datetime, locations, sources]);
|
|
1586
|
+
return useQueries({
|
|
1587
|
+
queries,
|
|
1588
|
+
combine: queryResults => {
|
|
1589
|
+
const data = queryResults.map(result => {
|
|
1590
|
+
return result.data || [];
|
|
609
1591
|
});
|
|
1592
|
+
const isPending = queryResults.some(result => result.isPending);
|
|
1593
|
+
const error = queryResults.find(result => result?.error)?.error;
|
|
1594
|
+
return {
|
|
1595
|
+
data,
|
|
1596
|
+
isPending,
|
|
1597
|
+
error
|
|
1598
|
+
};
|
|
610
1599
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1600
|
+
});
|
|
1601
|
+
};
|
|
1602
|
+
const useGetCollectionsInstances = (collections, isDialogOpen) => {
|
|
1603
|
+
const collectionIds = useMemo(() => collections?.map(collection => collection.id) || [], [collections]);
|
|
1604
|
+
const queries = useMemo(() => collectionIds.map(id => {
|
|
1605
|
+
return {
|
|
1606
|
+
queryKey: ['edr-soundings-instances', id],
|
|
1607
|
+
queryFn: async () => {
|
|
1608
|
+
const data = await getCollectionInstance(id);
|
|
1609
|
+
return data;
|
|
1610
|
+
},
|
|
1611
|
+
enabled: isDialogOpen
|
|
1612
|
+
};
|
|
1613
|
+
}), [collectionIds, isDialogOpen]);
|
|
1614
|
+
const results = useQueries({
|
|
1615
|
+
queries,
|
|
1616
|
+
combine: queryResults => {
|
|
1617
|
+
const data = queryResults.map(result => {
|
|
1618
|
+
const instances = result.data?.instances.sort((featureA, featureB) => featureB.id.localeCompare(featureA.id)) || []; // Sort to get latest first
|
|
1619
|
+
return instances;
|
|
627
1620
|
});
|
|
1621
|
+
const isPending = queryResults.some(result => result.isPending);
|
|
1622
|
+
const error = queryResults.find(result => result?.error)?.error;
|
|
1623
|
+
return {
|
|
1624
|
+
data,
|
|
1625
|
+
isPending,
|
|
1626
|
+
error
|
|
1627
|
+
};
|
|
628
1628
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1629
|
+
});
|
|
1630
|
+
const dataWithCollectionInfo = collections?.map((collection, index) => {
|
|
1631
|
+
return {
|
|
1632
|
+
...collection,
|
|
1633
|
+
instances: results.data[index] || []
|
|
1634
|
+
};
|
|
1635
|
+
});
|
|
1636
|
+
return {
|
|
1637
|
+
...results,
|
|
1638
|
+
data: dataWithCollectionInfo
|
|
1639
|
+
};
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
/* *
|
|
1643
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1644
|
+
* you may not use this file except in compliance with the License.
|
|
1645
|
+
* You may obtain a copy of the License at
|
|
1646
|
+
*
|
|
1647
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1648
|
+
*
|
|
1649
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1650
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1651
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1652
|
+
* See the License for the specific language governing permissions and
|
|
1653
|
+
* limitations under the License.
|
|
1654
|
+
*
|
|
1655
|
+
* Copyright 2023 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1656
|
+
* Copyright 2023 - Finnish Meteorological Institute (FMI)
|
|
1657
|
+
* Copyright 2024 - The Norwegian Meteorological Institute (MET Norway)
|
|
1658
|
+
* */
|
|
1659
|
+
const SOUNDINGS_NAMESPACE = 'soundings';
|
|
1660
|
+
const initSoundingsI18n = () => {
|
|
1661
|
+
safelyInitI18nForTestsAndStories(i18n, SOUNDINGS_NAMESPACE, [{
|
|
1662
|
+
ns: SOUNDINGS_NAMESPACE,
|
|
1663
|
+
translations: soundingsTranslations
|
|
1664
|
+
}, {
|
|
1665
|
+
ns: TIMESLIDER_NAMESPACE,
|
|
1666
|
+
translations: timesliderTranslations
|
|
1667
|
+
}, {
|
|
1668
|
+
ns: SHARED_NAMESPACE,
|
|
1669
|
+
translations: sharedTranslations
|
|
1670
|
+
}]);
|
|
1671
|
+
};
|
|
1672
|
+
const ns = [SOUNDINGS_NAMESPACE, TIMESLIDER_NAMESPACE, SHARED_NAMESPACE];
|
|
1673
|
+
const translateKeyOutsideComponents = (key, params = undefined) => i18n.t(key, {
|
|
1674
|
+
ns,
|
|
1675
|
+
...params
|
|
1676
|
+
});
|
|
1677
|
+
const useSoundingsTranslation = () => useTranslation(ns);
|
|
1678
|
+
|
|
1679
|
+
const SoundingsTimeSlider = ({
|
|
1680
|
+
timesteps,
|
|
1681
|
+
selectedTime,
|
|
1682
|
+
updateSelectedTime
|
|
1683
|
+
}) => {
|
|
1684
|
+
const {
|
|
1685
|
+
i18n
|
|
1686
|
+
} = useSoundingsTranslation();
|
|
1687
|
+
const marks = timesteps.filter(date => dateUtils.isValidIsoDateString(date)).map(date => ({
|
|
1688
|
+
value: dateUtils.unix(dateUtils.getUtcFromString(date)),
|
|
1689
|
+
label: dateUtils.dateToString(dateUtils.getUtcFromString(date), 'iii d. HH:mm', true, i18n.language)
|
|
1690
|
+
}));
|
|
1691
|
+
const onStepBackward = React.useCallback(() => {
|
|
1692
|
+
const currentIndex = timesteps.indexOf(selectedTime);
|
|
1693
|
+
const precedingIndex = Math.max(currentIndex - 1, 0);
|
|
1694
|
+
updateSelectedTime(timesteps[precedingIndex]);
|
|
1695
|
+
}, [selectedTime, updateSelectedTime, timesteps]);
|
|
1696
|
+
const onStepForward = React.useCallback(() => {
|
|
1697
|
+
const currentIndex = timesteps.indexOf(selectedTime);
|
|
1698
|
+
const followingIndex = Math.min(currentIndex + 1, timesteps.length - 1);
|
|
1699
|
+
updateSelectedTime(timesteps[followingIndex]);
|
|
1700
|
+
}, [selectedTime, updateSelectedTime, timesteps]);
|
|
1701
|
+
return jsxs("div", {
|
|
1702
|
+
style: {
|
|
1703
|
+
display: 'flex',
|
|
1704
|
+
marginTop: '17px'
|
|
1705
|
+
},
|
|
1706
|
+
children: [jsxs("div", {
|
|
1707
|
+
style: {
|
|
1708
|
+
minWidth: '60px',
|
|
1709
|
+
marginTop: '2px'
|
|
1710
|
+
},
|
|
1711
|
+
children: [jsx(BackwardForwardStepButton, {
|
|
1712
|
+
isForwardStep: false,
|
|
1713
|
+
onClickBFButton: onStepBackward,
|
|
1714
|
+
isDisabled: marks.length === 0
|
|
1715
|
+
}), jsx(BackwardForwardStepButton, {
|
|
1716
|
+
isForwardStep: true,
|
|
1717
|
+
onClickBFButton: onStepForward,
|
|
1718
|
+
isDisabled: marks.length === 0
|
|
1719
|
+
})]
|
|
1720
|
+
}), marks.length === 0 ? jsx(CustomSlider, {
|
|
1721
|
+
min: 0,
|
|
1722
|
+
max: 1,
|
|
1723
|
+
value: 0.5,
|
|
1724
|
+
orientation: "horizontal",
|
|
1725
|
+
disabled: true
|
|
1726
|
+
}) : jsx(CustomSlider, {
|
|
1727
|
+
orientation: "horizontal",
|
|
1728
|
+
valueLabelDisplay: "auto",
|
|
1729
|
+
valueLabelFormat: value => dateUtils.dateToString(dateUtils.fromUnix(value), "yyyy iii dd MMM HH:mm 'UTC'", true, i18n.language),
|
|
1730
|
+
marks: marks,
|
|
1731
|
+
step: null,
|
|
1732
|
+
min: marks[0].value,
|
|
1733
|
+
max: marks[marks.length - 1].value,
|
|
1734
|
+
value: dateUtils.unix(dateUtils.getUtcFromString(selectedTime)),
|
|
1735
|
+
onChange: (_, value) => {
|
|
1736
|
+
updateSelectedTime(dateUtils.fromUnix(value).toISOString());
|
|
1737
|
+
},
|
|
1738
|
+
extraStyles: {
|
|
1739
|
+
margin: '0px 20px 0px 20px',
|
|
1740
|
+
'.MuiSlider-markLabel': {
|
|
1741
|
+
rotate: '-45deg',
|
|
1742
|
+
translate: '-41%'
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
})]
|
|
1746
|
+
});
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
/* *
|
|
1750
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1751
|
+
* you may not use this file except in compliance with the License.
|
|
1752
|
+
* You may obtain a copy of the License at
|
|
1753
|
+
*
|
|
1754
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1755
|
+
*
|
|
1756
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1757
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1758
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1759
|
+
* See the License for the specific language governing permissions and
|
|
1760
|
+
* limitations under the License.
|
|
1761
|
+
*
|
|
1762
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1763
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1764
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1765
|
+
* */
|
|
1766
|
+
const initialSoundingsState = {
|
|
1767
|
+
selectedSources: {},
|
|
1768
|
+
selectedLocations: {},
|
|
1769
|
+
selectedTime: new Date().toISOString()
|
|
1770
|
+
};
|
|
1771
|
+
const slice = createSlice({
|
|
1772
|
+
initialState: initialSoundingsState,
|
|
1773
|
+
name: 'soundings',
|
|
1774
|
+
reducers: {
|
|
1775
|
+
updateSelectedSource: (draft, payloadAction) => {
|
|
1776
|
+
const {
|
|
1777
|
+
sourceId,
|
|
1778
|
+
value
|
|
1779
|
+
} = payloadAction.payload;
|
|
1780
|
+
draft.selectedSources[sourceId] = {
|
|
1781
|
+
...draft.selectedSources[sourceId],
|
|
1782
|
+
...value
|
|
1783
|
+
};
|
|
1784
|
+
},
|
|
1785
|
+
updateSelectedLocation: (draft, payloadAction) => {
|
|
1786
|
+
const {
|
|
1787
|
+
sourceId,
|
|
1788
|
+
locationId
|
|
1789
|
+
} = payloadAction.payload;
|
|
1790
|
+
draft.selectedLocations[sourceId] = locationId;
|
|
1791
|
+
},
|
|
1792
|
+
updateSelectedTime: (draft, payloadAction) => {
|
|
1793
|
+
const {
|
|
1794
|
+
selectedTime
|
|
1795
|
+
} = payloadAction.payload;
|
|
1796
|
+
draft.selectedTime = selectedTime;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
const {
|
|
1801
|
+
reducer: soundingsReducer,
|
|
1802
|
+
actions: soundingsActions
|
|
1803
|
+
} = slice;
|
|
1804
|
+
|
|
1805
|
+
/* *
|
|
1806
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1807
|
+
* you may not use this file except in compliance with the License.
|
|
1808
|
+
* You may obtain a copy of the License at
|
|
1809
|
+
*
|
|
1810
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1811
|
+
*
|
|
1812
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1813
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1814
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1815
|
+
* See the License for the specific language governing permissions and
|
|
1816
|
+
* limitations under the License.
|
|
1817
|
+
*
|
|
1818
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1819
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1820
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1821
|
+
* */
|
|
1822
|
+
const getSoundingsStore = store => store?.soundings || initialSoundingsState;
|
|
1823
|
+
const getSourceSelections = createSelector(getSoundingsStore, store => {
|
|
1824
|
+
return store.selectedSources;
|
|
1825
|
+
}, selectorMemoizationOptions);
|
|
1826
|
+
const getLocationSelections = createSelector(getSoundingsStore, store => store.selectedLocations, selectorMemoizationOptions);
|
|
1827
|
+
const getSelectedTime = createSelector(getSoundingsStore, store => store.selectedTime, selectorMemoizationOptions);
|
|
1828
|
+
const getSelectedSources = createSelector(getSoundingsStore, store => {
|
|
1829
|
+
const sources = store.selectedSources;
|
|
1830
|
+
const activeSources = Object.keys(sources).reduce((list, key) => {
|
|
1831
|
+
const value = sources[key];
|
|
1832
|
+
if (value.isChecked === true) {
|
|
1833
|
+
return list.concat({
|
|
1834
|
+
collectionId: key,
|
|
1835
|
+
instanceId: value.instanceId
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
return list;
|
|
1839
|
+
}, []);
|
|
1840
|
+
return activeSources;
|
|
1841
|
+
}, selectorMemoizationOptions);
|
|
1842
|
+
const getSelectedSourcesAmount = createSelector(getSelectedSources, activeSources => {
|
|
1843
|
+
return activeSources.length;
|
|
1844
|
+
}, selectorMemoizationOptions);
|
|
1845
|
+
|
|
1846
|
+
const SoundingsTimeSliderConnect = ({
|
|
1847
|
+
timesteps
|
|
1848
|
+
}) => {
|
|
1849
|
+
const selectedTime = useSelector(getSelectedTime);
|
|
1850
|
+
const dispatch = useDispatch();
|
|
1851
|
+
const updateSelectedTime = React.useCallback(selectedTime => {
|
|
1852
|
+
dispatch(soundingsActions.updateSelectedTime({
|
|
1853
|
+
selectedTime
|
|
1854
|
+
}));
|
|
1855
|
+
}, [dispatch]);
|
|
1856
|
+
return jsx(SoundingsTimeSlider, {
|
|
1857
|
+
timesteps: timesteps,
|
|
1858
|
+
selectedTime: selectedTime,
|
|
1859
|
+
updateSelectedTime: updateSelectedTime
|
|
1860
|
+
});
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
var MenuOptions;
|
|
1864
|
+
(function (MenuOptions) {
|
|
1865
|
+
MenuOptions["sources"] = "sources";
|
|
1866
|
+
MenuOptions["locations"] = "locations";
|
|
1867
|
+
})(MenuOptions || (MenuOptions = {}));
|
|
1868
|
+
const buttonStyle = {
|
|
1869
|
+
height: '10px !important',
|
|
1870
|
+
width: '130px'
|
|
1871
|
+
};
|
|
1872
|
+
const SoundingsMenuBar = ({
|
|
1873
|
+
handleClick,
|
|
1874
|
+
isOpen,
|
|
1875
|
+
selectedSourcesAmount = 0
|
|
1876
|
+
}) => {
|
|
1877
|
+
const {
|
|
1878
|
+
t
|
|
1879
|
+
} = useSoundingsTranslation();
|
|
1880
|
+
return jsxs(Stack, {
|
|
1881
|
+
direction: "row",
|
|
1882
|
+
spacing: 1,
|
|
1883
|
+
sx: {
|
|
1884
|
+
marginBottom: '4px'
|
|
1885
|
+
},
|
|
1886
|
+
children: [jsxs(CustomToggleButton, {
|
|
1887
|
+
variant: "tool",
|
|
1888
|
+
selected: isOpen[MenuOptions.sources],
|
|
1889
|
+
"data-testid": "soundingsSourcesButton",
|
|
1890
|
+
onClick: () => handleClick(MenuOptions.sources),
|
|
1891
|
+
size: "small",
|
|
1892
|
+
sx: buttonStyle,
|
|
1893
|
+
children: [jsx(Sources, {
|
|
637
1894
|
sx: {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1895
|
+
marginRight: '4px!important'
|
|
1896
|
+
}
|
|
1897
|
+
}), selectedSourcesAmount ? `${t('sources')} (${selectedSourcesAmount})` : t('sources')]
|
|
1898
|
+
}), jsx(CustomToggleButton, {
|
|
1899
|
+
variant: "tool",
|
|
1900
|
+
"data-testid": "soundingsLocationsButton",
|
|
1901
|
+
selected: isOpen[MenuOptions.locations],
|
|
1902
|
+
onClick: () => handleClick(MenuOptions.locations),
|
|
1903
|
+
size: "small",
|
|
1904
|
+
sx: buttonStyle,
|
|
1905
|
+
children: t('locations')
|
|
1906
|
+
})]
|
|
1907
|
+
});
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
const MenuBarConnect = ({
|
|
1911
|
+
viewId,
|
|
1912
|
+
source
|
|
1913
|
+
}) => {
|
|
1914
|
+
const dispatch = useDispatch();
|
|
1915
|
+
const currentActiveMapIdSources = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
|
|
1916
|
+
const currentActiveMapIdLocations = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsLocations));
|
|
1917
|
+
const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
|
|
1918
|
+
const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsLocations));
|
|
1919
|
+
const sourcesAmount = useSelector(getSelectedSourcesAmount);
|
|
1920
|
+
const dispatchActiveMapId = React.useCallback((dialogType, isOpen, activeMapId) => void dispatch(uiActions.setActiveMapIdForDialog({
|
|
1921
|
+
type: dialogType,
|
|
1922
|
+
mapId: viewId,
|
|
1923
|
+
setOpen: activeMapId !== viewId ? true : !isOpen,
|
|
1924
|
+
source
|
|
1925
|
+
})), [dispatch, viewId, source]);
|
|
1926
|
+
const handleClick = React.useCallback(menuType => {
|
|
1927
|
+
switch (menuType) {
|
|
1928
|
+
case MenuOptions.sources:
|
|
1929
|
+
dispatchActiveMapId(uiTypes.DialogTypes.SoundingsSources, isSourcesOpenInStore, currentActiveMapIdSources);
|
|
1930
|
+
break;
|
|
1931
|
+
case MenuOptions.locations:
|
|
1932
|
+
dispatchActiveMapId(uiTypes.DialogTypes.SoundingsLocations, isLocationsOpenInStore, currentActiveMapIdLocations);
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
}, [dispatchActiveMapId, isSourcesOpenInStore, currentActiveMapIdSources, isLocationsOpenInStore, currentActiveMapIdLocations]);
|
|
1936
|
+
const isOpen = {
|
|
1937
|
+
[MenuOptions.sources]: currentActiveMapIdSources === viewId && isSourcesOpenInStore,
|
|
1938
|
+
[MenuOptions.locations]: currentActiveMapIdLocations === viewId && isLocationsOpenInStore
|
|
1939
|
+
};
|
|
1940
|
+
return jsx(SoundingsMenuBar, {
|
|
1941
|
+
isOpen: isOpen,
|
|
1942
|
+
handleClick: handleClick,
|
|
1943
|
+
selectedSourcesAmount: sourcesAmount
|
|
1944
|
+
});
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
const SoundingsConnect = ({
|
|
1948
|
+
viewId
|
|
1949
|
+
}) => {
|
|
1950
|
+
// TODO: use in follow-up: https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
|
|
1951
|
+
// const selectedTime = useSelector(getSelectedTime);
|
|
1952
|
+
const sourceSelections = useSelector(getSelectedSources);
|
|
1953
|
+
const locationSelections = useSelector(getLocationSelections);
|
|
1954
|
+
const {
|
|
1955
|
+
error,
|
|
1956
|
+
data,
|
|
1957
|
+
isPending
|
|
1958
|
+
} = useMultipleSources(sourceSelections, locationSelections);
|
|
1959
|
+
return jsxs(Fragment, {
|
|
1960
|
+
children: [error && jsx(AlertBanner, {
|
|
1961
|
+
title: error.message,
|
|
1962
|
+
shouldClose: true
|
|
1963
|
+
}), isPending && jsx(LinearProgress, {
|
|
1964
|
+
color: "secondary",
|
|
1965
|
+
sx: {
|
|
1966
|
+
position: 'absolute',
|
|
1967
|
+
width: '100%'
|
|
1968
|
+
}
|
|
1969
|
+
}), jsxs(Box, {
|
|
1970
|
+
sx: {
|
|
1971
|
+
padding: 2,
|
|
1972
|
+
overflowY: 'scroll',
|
|
1973
|
+
height: '100%'
|
|
1974
|
+
},
|
|
1975
|
+
children: [jsx(Box, {
|
|
1976
|
+
paddingLeft: 5,
|
|
1977
|
+
children: jsx(MenuBarConnect, {
|
|
1978
|
+
viewId: viewId
|
|
1979
|
+
})
|
|
1980
|
+
}), jsx(Soundings, {
|
|
1981
|
+
data: data,
|
|
1982
|
+
svgDimensions: {
|
|
1983
|
+
width: 600,
|
|
1984
|
+
height: 600,
|
|
1985
|
+
margin: {
|
|
1986
|
+
bottom: 0,
|
|
1987
|
+
left: 0,
|
|
1988
|
+
right: 0,
|
|
1989
|
+
top: 0
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}), jsx(SoundingsTimeSliderConnect
|
|
1993
|
+
// TODO: use real timesteps https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
|
|
1994
|
+
, {
|
|
1995
|
+
// TODO: use real timesteps https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
|
|
1996
|
+
timesteps: []
|
|
642
1997
|
})]
|
|
643
|
-
})
|
|
1998
|
+
})]
|
|
1999
|
+
});
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
const SoundingsSourceMenu = ({
|
|
2003
|
+
soundingsCollections,
|
|
2004
|
+
isOpen = false,
|
|
2005
|
+
handleClose,
|
|
2006
|
+
updateCollection,
|
|
2007
|
+
sourceSelections,
|
|
2008
|
+
onMouseDown = () => {},
|
|
2009
|
+
order = 0,
|
|
2010
|
+
isLoading = false,
|
|
2011
|
+
error
|
|
2012
|
+
}) => {
|
|
2013
|
+
const {
|
|
2014
|
+
t
|
|
2015
|
+
} = useSoundingsTranslation();
|
|
2016
|
+
const getDisplayValue = instanceId => {
|
|
2017
|
+
const instanceAsDate = dateUtils.isoStringToDate(instanceId);
|
|
2018
|
+
return instanceAsDate ? dateUtils.dateToIsoString(instanceAsDate) : instanceId;
|
|
644
2019
|
};
|
|
645
|
-
return
|
|
2020
|
+
return jsxs(ToolContainerDraggable, {
|
|
646
2021
|
startSize: {
|
|
647
2022
|
width: 320
|
|
648
2023
|
},
|
|
2024
|
+
startPosition: {
|
|
2025
|
+
top: 40,
|
|
2026
|
+
left: 50
|
|
2027
|
+
},
|
|
649
2028
|
minWidth: 300,
|
|
650
2029
|
title: t('sources'),
|
|
651
2030
|
isOpen: isOpen,
|
|
652
|
-
onClose:
|
|
2031
|
+
onClose: handleClose,
|
|
653
2032
|
bounds: "parent",
|
|
654
|
-
|
|
2033
|
+
onMouseDown: onMouseDown,
|
|
2034
|
+
order: order,
|
|
2035
|
+
className: "source-menu",
|
|
2036
|
+
children: [isLoading && jsx(LinearProgress, {
|
|
2037
|
+
sx: {
|
|
2038
|
+
width: '100%',
|
|
2039
|
+
position: 'absolute'
|
|
2040
|
+
}
|
|
2041
|
+
}), error && jsx(AlertBanner, {
|
|
2042
|
+
title: error.message,
|
|
2043
|
+
shouldClose: true
|
|
2044
|
+
}), jsx(List, {
|
|
655
2045
|
sx: {
|
|
656
2046
|
pr: 1
|
|
657
2047
|
},
|
|
658
2048
|
children: soundingsCollections && soundingsCollections.length > 0 ? soundingsCollections.map(collection => {
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
2049
|
+
const {
|
|
2050
|
+
isChecked,
|
|
2051
|
+
instanceId
|
|
2052
|
+
} = sourceSelections[collection.id] || {};
|
|
662
2053
|
const hasInstances = collection.instances.length > 0;
|
|
2054
|
+
const selectedInstanceId = instanceId || collection.instances[0]?.id || undefined;
|
|
663
2055
|
return jsxs(ListItem, {
|
|
664
2056
|
disablePadding: true,
|
|
665
2057
|
children: [jsx(ListItemIcon, {
|
|
@@ -667,36 +2059,54 @@ const SoundingsSourceMenu = ({
|
|
|
667
2059
|
minWidth: 42
|
|
668
2060
|
},
|
|
669
2061
|
children: jsx(Checkbox, {
|
|
670
|
-
checked: isChecked,
|
|
671
|
-
onChange: event =>
|
|
672
|
-
isChecked: event.target.checked
|
|
2062
|
+
checked: !!isChecked,
|
|
2063
|
+
onChange: event => updateCollection(collection.id, {
|
|
2064
|
+
isChecked: event.target.checked,
|
|
2065
|
+
instanceId: selectedInstanceId
|
|
673
2066
|
}),
|
|
674
2067
|
disableRipple: true
|
|
675
2068
|
})
|
|
676
2069
|
}), jsxs(StyledSelect, {
|
|
677
2070
|
isEnabled: true,
|
|
2071
|
+
additionalStyling: {
|
|
2072
|
+
width: 'calc(100% - 42px)'
|
|
2073
|
+
},
|
|
678
2074
|
skipLocalStyling: false,
|
|
679
|
-
value: selectedInstanceId,
|
|
680
|
-
onChange: event =>
|
|
681
|
-
|
|
2075
|
+
value: selectedInstanceId || t('noInstancesAvailable'),
|
|
2076
|
+
onChange: event => updateCollection(collection.id, {
|
|
2077
|
+
instanceId: event.target.value
|
|
682
2078
|
}),
|
|
683
2079
|
renderValue: value => jsx(Box, {
|
|
684
2080
|
sx: {
|
|
685
2081
|
textOverflow: 'ellipsis',
|
|
686
2082
|
overflow: 'hidden'
|
|
687
2083
|
},
|
|
688
|
-
children:
|
|
2084
|
+
children: jsxs(Box, {
|
|
2085
|
+
display: "flex",
|
|
2086
|
+
children: [jsx(Box, {
|
|
2087
|
+
sx: {
|
|
2088
|
+
minWidth: 200
|
|
2089
|
+
},
|
|
2090
|
+
children: collection.name
|
|
2091
|
+
}), jsx(Box, {
|
|
2092
|
+
sx: {
|
|
2093
|
+
textOverflow: 'ellipsis',
|
|
2094
|
+
overflow: 'hidden'
|
|
2095
|
+
},
|
|
2096
|
+
children: getDisplayValue(value)
|
|
2097
|
+
})]
|
|
2098
|
+
})
|
|
689
2099
|
}),
|
|
690
2100
|
children: [!hasInstances && jsx(MenuItem, {
|
|
691
|
-
value:
|
|
2101
|
+
value: t('noInstancesAvailable'),
|
|
692
2102
|
disabled: true,
|
|
693
2103
|
sx: {
|
|
694
2104
|
fontSize: 12
|
|
695
2105
|
},
|
|
696
2106
|
children: t('noInstancesAvailable')
|
|
697
|
-
}), collection.instances.map(
|
|
2107
|
+
}), collection.instances.map(instance => jsx(MenuItem, {
|
|
698
2108
|
value: instance.id,
|
|
699
|
-
children:
|
|
2109
|
+
children: getDisplayValue(instance.id)
|
|
700
2110
|
}, instance.id))]
|
|
701
2111
|
})]
|
|
702
2112
|
}, collection.id);
|
|
@@ -708,125 +2118,168 @@ const SoundingsSourceMenu = ({
|
|
|
708
2118
|
}
|
|
709
2119
|
})
|
|
710
2120
|
})
|
|
711
|
-
})
|
|
2121
|
+
})]
|
|
712
2122
|
});
|
|
713
2123
|
};
|
|
714
2124
|
|
|
715
|
-
/* *
|
|
716
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
717
|
-
* you may not use this file except in compliance with the License.
|
|
718
|
-
* You may obtain a copy of the License at
|
|
719
|
-
*
|
|
720
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
721
|
-
*
|
|
722
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
723
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
724
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
725
|
-
* See the License for the specific language governing permissions and
|
|
726
|
-
* limitations under the License.
|
|
727
|
-
*
|
|
728
|
-
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
729
|
-
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
730
|
-
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
731
|
-
* */
|
|
732
|
-
const BASE_URL = 'https://opendata.fmi.fi/edr';
|
|
733
|
-
const getCollectionLocations = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/locations`, {
|
|
734
|
-
headers: getHeaders()
|
|
735
|
-
}).then(handleResponse);
|
|
736
|
-
const getCollectionInstance = (baseURL, collectionId) => fetch(`${baseURL}/collections/${collectionId}/instances`, {
|
|
737
|
-
headers: getHeaders()
|
|
738
|
-
}).then(handleResponse);
|
|
739
|
-
|
|
740
2125
|
const SoundingsSourceDialogConnect = ({
|
|
741
2126
|
soundingsCollections
|
|
742
2127
|
}) => {
|
|
743
|
-
const
|
|
2128
|
+
const dispatch = useDispatch();
|
|
744
2129
|
const {
|
|
745
2130
|
isDialogOpen,
|
|
746
|
-
onCloseDialog
|
|
2131
|
+
onCloseDialog,
|
|
2132
|
+
dialogOrder,
|
|
2133
|
+
setDialogOrder
|
|
747
2134
|
} = useSetupDialog(uiTypes.DialogTypes.SoundingsSources);
|
|
748
|
-
React.
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}, [soundingsCollections, isDialogOpen]);
|
|
2135
|
+
const updateCollection = React.useCallback((sourceId, value) => {
|
|
2136
|
+
dispatch(soundingsActions.updateSelectedSource({
|
|
2137
|
+
sourceId,
|
|
2138
|
+
value
|
|
2139
|
+
}));
|
|
2140
|
+
}, [dispatch]);
|
|
2141
|
+
const sourceSelections = useSelector(getSourceSelections);
|
|
2142
|
+
const {
|
|
2143
|
+
data,
|
|
2144
|
+
isPending,
|
|
2145
|
+
error
|
|
2146
|
+
} = useGetCollectionsInstances(soundingsCollections, isDialogOpen);
|
|
763
2147
|
return jsx(SoundingsSourceMenu, {
|
|
764
|
-
soundingsCollections:
|
|
2148
|
+
soundingsCollections: data,
|
|
765
2149
|
isOpen: isDialogOpen,
|
|
766
|
-
|
|
2150
|
+
updateCollection: updateCollection,
|
|
2151
|
+
sourceSelections: sourceSelections,
|
|
2152
|
+
handleClose: onCloseDialog,
|
|
2153
|
+
onMouseDown: setDialogOrder,
|
|
2154
|
+
order: dialogOrder,
|
|
2155
|
+
isLoading: isPending,
|
|
2156
|
+
error: error
|
|
767
2157
|
});
|
|
768
2158
|
};
|
|
769
2159
|
|
|
770
2160
|
const LocationsMenu = ({
|
|
771
2161
|
services,
|
|
2162
|
+
selectedLocations,
|
|
772
2163
|
isOpen,
|
|
773
|
-
onClose
|
|
2164
|
+
onClose,
|
|
2165
|
+
updateSelectedLocation,
|
|
2166
|
+
onMouseDown = () => {},
|
|
2167
|
+
order = 0
|
|
774
2168
|
}) => {
|
|
775
|
-
const [
|
|
2169
|
+
const [searchFilter, setSearchFilter] = useState('');
|
|
776
2170
|
const {
|
|
777
2171
|
t
|
|
778
2172
|
} = useSoundingsTranslation();
|
|
2173
|
+
const filteredResults = useMemo(() => {
|
|
2174
|
+
return services?.map(service => {
|
|
2175
|
+
const filteredFeatures = service.features ? service.features.filter(feature => {
|
|
2176
|
+
return feature.properties?.name.toLowerCase().includes(searchFilter.toLowerCase());
|
|
2177
|
+
}) : [];
|
|
2178
|
+
return {
|
|
2179
|
+
...service,
|
|
2180
|
+
features: filteredFeatures
|
|
2181
|
+
};
|
|
2182
|
+
});
|
|
2183
|
+
}, [services, searchFilter]);
|
|
2184
|
+
const isChecked = (sourceId, locationId) => selectedLocations[sourceId] === locationId;
|
|
779
2185
|
return jsx(ToolContainerDraggable, {
|
|
780
2186
|
startSize: {
|
|
781
2187
|
width: 320,
|
|
782
2188
|
height: 500
|
|
783
2189
|
},
|
|
2190
|
+
minWidth: 264,
|
|
784
2191
|
title: t('locations'),
|
|
785
|
-
headerSize: "medium",
|
|
786
2192
|
isOpen: isOpen,
|
|
787
|
-
|
|
2193
|
+
startPosition: {
|
|
2194
|
+
top: 60,
|
|
2195
|
+
left: 200
|
|
2196
|
+
},
|
|
2197
|
+
onClose: onClose,
|
|
788
2198
|
bounds: "parent",
|
|
789
|
-
|
|
790
|
-
|
|
2199
|
+
onMouseDown: onMouseDown,
|
|
2200
|
+
order: order,
|
|
2201
|
+
children: services.length === 0 ? jsx(Typography, {
|
|
791
2202
|
sx: {
|
|
792
|
-
|
|
793
|
-
overflowY: 'auto'
|
|
2203
|
+
margin: 1
|
|
794
2204
|
},
|
|
795
|
-
children:
|
|
796
|
-
|
|
797
|
-
|
|
2205
|
+
children: t('locations-menu-warning')
|
|
2206
|
+
}) : jsxs(Grid, {
|
|
2207
|
+
container: true,
|
|
2208
|
+
children: [jsx(Box, {
|
|
2209
|
+
sx: {
|
|
2210
|
+
width: '100%',
|
|
2211
|
+
padding: 1,
|
|
2212
|
+
position: 'sticky',
|
|
2213
|
+
top: 0,
|
|
2214
|
+
zIndex: 1,
|
|
2215
|
+
backgroundColor: 'geowebColors.background.surfaceApp'
|
|
2216
|
+
},
|
|
2217
|
+
children: jsx(SearchField, {
|
|
2218
|
+
searchFilter: searchFilter,
|
|
2219
|
+
setSearchFilter: setSearchFilter,
|
|
2220
|
+
small: true
|
|
2221
|
+
})
|
|
2222
|
+
}), filteredResults.map(service => jsx(Fragment$1, {
|
|
2223
|
+
children: jsx(CustomAccordion, {
|
|
2224
|
+
title: service.name,
|
|
798
2225
|
sx: {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
...collapseMap,
|
|
804
|
-
[service.id]: !collapseMap[service.id]
|
|
805
|
-
}),
|
|
806
|
-
children: [jsx(ListItemText, {
|
|
807
|
-
children: service.id
|
|
808
|
-
}), jsx(ListItemIcon, {
|
|
809
|
-
children: jsx(ChevronDown, {})
|
|
810
|
-
})]
|
|
811
|
-
})
|
|
812
|
-
}), jsx(Collapse, {
|
|
813
|
-
in: collapseMap[service.id],
|
|
814
|
-
children: jsx(List, {
|
|
815
|
-
component: "div",
|
|
816
|
-
disablePadding: true,
|
|
817
|
-
sx: {
|
|
818
|
-
overflowY: 'auto',
|
|
819
|
-
overflow: 'hidden'
|
|
2226
|
+
width: '100%',
|
|
2227
|
+
margin: '0 8px 2px 8px',
|
|
2228
|
+
'.MuiAccordionDetails-root': {
|
|
2229
|
+
padding: 0
|
|
820
2230
|
},
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
2231
|
+
'.MuiAccordionSummary-root': {
|
|
2232
|
+
minHeight: '40px'
|
|
2233
|
+
},
|
|
2234
|
+
'.MuiAccordionSummary-content': {
|
|
2235
|
+
paddingLeft: 3
|
|
2236
|
+
}
|
|
2237
|
+
},
|
|
2238
|
+
amountOfItems: service.features.length,
|
|
2239
|
+
defaultExpanded: false,
|
|
2240
|
+
children: jsx(FixedSizeList, {
|
|
2241
|
+
itemSize: 40,
|
|
2242
|
+
height: 320,
|
|
2243
|
+
width: "100%",
|
|
2244
|
+
itemCount: service.features.length,
|
|
2245
|
+
children: ({
|
|
2246
|
+
index,
|
|
2247
|
+
style
|
|
2248
|
+
}) => {
|
|
2249
|
+
const location = service.features[index];
|
|
2250
|
+
return jsx("div", {
|
|
2251
|
+
style: style,
|
|
2252
|
+
children: jsx(ListItem, {
|
|
2253
|
+
disablePadding: true,
|
|
2254
|
+
disableGutters: true,
|
|
2255
|
+
children: jsxs(ListItemButton, {
|
|
2256
|
+
onClick: () => {
|
|
2257
|
+
updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
|
|
2258
|
+
},
|
|
2259
|
+
dense: true,
|
|
2260
|
+
children: [jsx(ListItemIcon, {
|
|
2261
|
+
children: jsx(Checkbox, {
|
|
2262
|
+
edge: "start",
|
|
2263
|
+
checked: isChecked(service.id, location.id),
|
|
2264
|
+
tabIndex: -1,
|
|
2265
|
+
disableRipple: true,
|
|
2266
|
+
slotProps: {
|
|
2267
|
+
input: {
|
|
2268
|
+
'aria-labelledby': location.id
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
})
|
|
2272
|
+
}), jsx(ListItemText, {
|
|
2273
|
+
id: location.id,
|
|
2274
|
+
primary: location.properties.name
|
|
2275
|
+
})]
|
|
2276
|
+
})
|
|
2277
|
+
})
|
|
2278
|
+
}, `${service.id}-${location.id}`);
|
|
2279
|
+
}
|
|
827
2280
|
})
|
|
828
|
-
})
|
|
829
|
-
}, service.id))
|
|
2281
|
+
})
|
|
2282
|
+
}, service.id))]
|
|
830
2283
|
})
|
|
831
2284
|
});
|
|
832
2285
|
};
|
|
@@ -834,17 +2287,24 @@ const LocationsMenu = ({
|
|
|
834
2287
|
const LocationsMenuConnect = ({
|
|
835
2288
|
soundingsCollections
|
|
836
2289
|
}) => {
|
|
2290
|
+
const dispatch = useDispatch();
|
|
837
2291
|
const {
|
|
838
2292
|
isDialogOpen,
|
|
839
|
-
onCloseDialog
|
|
840
|
-
|
|
2293
|
+
onCloseDialog,
|
|
2294
|
+
dialogOrder,
|
|
2295
|
+
setDialogOrder
|
|
2296
|
+
} = useSetupDialog(uiTypes.DialogTypes.SoundingsLocations);
|
|
2297
|
+
const locationSelections = useSelector(getLocationSelections);
|
|
2298
|
+
const sourceSelections = useSelector(getSourceSelections);
|
|
2299
|
+
const selectedSourceIds = useMemo(() => Object.keys(sourceSelections).filter(key => sourceSelections[key].isChecked), [sourceSelections]);
|
|
841
2300
|
const edrQueries = useMemo(() => soundingsCollections.map(collection => ({
|
|
842
2301
|
queryKey: ['edr-locations', collection.id],
|
|
843
2302
|
queryFn: async () => {
|
|
844
2303
|
const locations = await getCollectionLocations(collection.id);
|
|
845
2304
|
return {
|
|
846
2305
|
...locations,
|
|
847
|
-
id: collection.id
|
|
2306
|
+
id: collection.id,
|
|
2307
|
+
name: collection.name
|
|
848
2308
|
};
|
|
849
2309
|
}
|
|
850
2310
|
})), [soundingsCollections]);
|
|
@@ -855,275 +2315,14 @@ const LocationsMenuConnect = ({
|
|
|
855
2315
|
return jsx(LocationsMenu, {
|
|
856
2316
|
isOpen: isDialogOpen,
|
|
857
2317
|
onClose: onCloseDialog,
|
|
858
|
-
services: services
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
type: "Point",
|
|
867
|
-
coordinates: [
|
|
868
|
-
24.9727,
|
|
869
|
-
60.3294
|
|
870
|
-
]
|
|
871
|
-
},
|
|
872
|
-
properties: {
|
|
873
|
-
time: "2025-02-05T00:00:00Z",
|
|
874
|
-
temp: -4.74961,
|
|
875
|
-
dewpoint: -5.646749999999998,
|
|
876
|
-
pressure: 1000,
|
|
877
|
-
windSpeed: 9.291457267156751,
|
|
878
|
-
windDirection: -18.803465916723095
|
|
879
|
-
}
|
|
880
|
-
},
|
|
881
|
-
{
|
|
882
|
-
type: "Feature",
|
|
883
|
-
geometry: {
|
|
884
|
-
type: "Point",
|
|
885
|
-
coordinates: [
|
|
886
|
-
24.9727,
|
|
887
|
-
60.3294
|
|
888
|
-
]
|
|
889
|
-
},
|
|
890
|
-
properties: {
|
|
891
|
-
time: "2025-02-05T00:00:00Z",
|
|
892
|
-
temp: -5.72214,
|
|
893
|
-
dewpoint: -5.740760000000001,
|
|
894
|
-
pressure: 925,
|
|
895
|
-
windSpeed: 11.482972536081412,
|
|
896
|
-
windDirection: 18.797638010822038
|
|
897
|
-
}
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
type: "Feature",
|
|
901
|
-
geometry: {
|
|
902
|
-
type: "Point",
|
|
903
|
-
coordinates: [
|
|
904
|
-
24.9727,
|
|
905
|
-
60.3294
|
|
906
|
-
]
|
|
907
|
-
},
|
|
908
|
-
properties: {
|
|
909
|
-
time: "2025-02-05T00:00:00Z",
|
|
910
|
-
temp: -5.94461,
|
|
911
|
-
dewpoint: -6.284489999999999,
|
|
912
|
-
pressure: 850,
|
|
913
|
-
windSpeed: 10.355155067433804,
|
|
914
|
-
windDirection: 46.95919904362452
|
|
915
|
-
}
|
|
916
|
-
},
|
|
917
|
-
{
|
|
918
|
-
type: "Feature",
|
|
919
|
-
geometry: {
|
|
920
|
-
type: "Point",
|
|
921
|
-
coordinates: [
|
|
922
|
-
24.9727,
|
|
923
|
-
60.3294
|
|
924
|
-
]
|
|
925
|
-
},
|
|
926
|
-
properties: {
|
|
927
|
-
time: "2025-02-05T00:00:00Z",
|
|
928
|
-
temp: -7.85121,
|
|
929
|
-
dewpoint: -7.903229999999999,
|
|
930
|
-
pressure: 700,
|
|
931
|
-
windSpeed: 10.216260991987234,
|
|
932
|
-
windDirection: 125.33681135255785
|
|
933
|
-
}
|
|
934
|
-
},
|
|
935
|
-
{
|
|
936
|
-
type: "Feature",
|
|
937
|
-
geometry: {
|
|
938
|
-
type: "Point",
|
|
939
|
-
coordinates: [
|
|
940
|
-
24.9727,
|
|
941
|
-
60.3294
|
|
942
|
-
]
|
|
943
|
-
},
|
|
944
|
-
properties: {
|
|
945
|
-
time: "2025-02-05T00:00:00Z",
|
|
946
|
-
temp: -20.9667,
|
|
947
|
-
dewpoint: -22.12218,
|
|
948
|
-
pressure: 500,
|
|
949
|
-
windSpeed: 14.222681508365433,
|
|
950
|
-
windDirection: 120.16238041101973
|
|
951
|
-
}
|
|
952
|
-
},
|
|
953
|
-
{
|
|
954
|
-
type: "Feature",
|
|
955
|
-
geometry: {
|
|
956
|
-
type: "Point",
|
|
957
|
-
coordinates: [
|
|
958
|
-
24.9727,
|
|
959
|
-
60.3294
|
|
960
|
-
]
|
|
961
|
-
},
|
|
962
|
-
properties: {
|
|
963
|
-
time: "2025-02-05T00:00:00Z",
|
|
964
|
-
temp: -48.8678,
|
|
965
|
-
dewpoint: -60.802800000000005,
|
|
966
|
-
pressure: 300,
|
|
967
|
-
windSpeed: 23.436991331013456,
|
|
968
|
-
windDirection: 128.0547684118523
|
|
969
|
-
}
|
|
970
|
-
},
|
|
971
|
-
{
|
|
972
|
-
type: "Feature",
|
|
973
|
-
geometry: {
|
|
974
|
-
type: "Point",
|
|
975
|
-
coordinates: [
|
|
976
|
-
24.9727,
|
|
977
|
-
60.3294
|
|
978
|
-
]
|
|
979
|
-
},
|
|
980
|
-
properties: {
|
|
981
|
-
time: "2025-02-05T00:00:00Z",
|
|
982
|
-
temp: -58.3,
|
|
983
|
-
dewpoint: -69.68567999999999,
|
|
984
|
-
pressure: 250,
|
|
985
|
-
windSpeed: 28.155356371568097,
|
|
986
|
-
windDirection: 133.50317473958455
|
|
987
|
-
}
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
type: "Feature",
|
|
991
|
-
geometry: {
|
|
992
|
-
type: "Point",
|
|
993
|
-
coordinates: [
|
|
994
|
-
24.9727,
|
|
995
|
-
60.3294
|
|
996
|
-
]
|
|
997
|
-
},
|
|
998
|
-
properties: {
|
|
999
|
-
time: "2025-02-05T00:00:00Z",
|
|
1000
|
-
temp: -68.6166,
|
|
1001
|
-
dewpoint: -71.9641,
|
|
1002
|
-
pressure: 200,
|
|
1003
|
-
windSpeed: 24.833678713593763,
|
|
1004
|
-
windDirection: 137.492458365928
|
|
1005
|
-
}
|
|
1006
|
-
},
|
|
1007
|
-
{
|
|
1008
|
-
type: "Feature",
|
|
1009
|
-
geometry: {
|
|
1010
|
-
type: "Point",
|
|
1011
|
-
coordinates: [
|
|
1012
|
-
24.9727,
|
|
1013
|
-
60.3294
|
|
1014
|
-
]
|
|
1015
|
-
},
|
|
1016
|
-
properties: {
|
|
1017
|
-
time: "2025-02-05T00:00:00Z",
|
|
1018
|
-
temp: -69.4935,
|
|
1019
|
-
dewpoint: -87.827772,
|
|
1020
|
-
pressure: 50,
|
|
1021
|
-
windSpeed: 35.1450619746217,
|
|
1022
|
-
windDirection: 129.8915278264372
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
];
|
|
1026
|
-
|
|
1027
|
-
var MenuOptions;
|
|
1028
|
-
(function (MenuOptions) {
|
|
1029
|
-
MenuOptions["sources"] = "sources";
|
|
1030
|
-
MenuOptions["locations"] = "locations";
|
|
1031
|
-
})(MenuOptions || (MenuOptions = {}));
|
|
1032
|
-
const SoundingsMenuBar = ({
|
|
1033
|
-
handleClick,
|
|
1034
|
-
isOpen
|
|
1035
|
-
}) => {
|
|
1036
|
-
const {
|
|
1037
|
-
t
|
|
1038
|
-
} = useSoundingsTranslation();
|
|
1039
|
-
return jsxs(Box, {
|
|
1040
|
-
display: "flex",
|
|
1041
|
-
gap: 1,
|
|
1042
|
-
children: [jsx(Button, {
|
|
1043
|
-
variant: isOpen[MenuOptions.sources] ? 'primary' : 'tertiary',
|
|
1044
|
-
disableRipple: true,
|
|
1045
|
-
"aria-pressed": isOpen[MenuOptions.sources],
|
|
1046
|
-
"data-testid": "soundingsSourcesButton",
|
|
1047
|
-
onClick: () => handleClick(MenuOptions.sources),
|
|
1048
|
-
startIcon: jsx(Sources, {}),
|
|
1049
|
-
size: "small",
|
|
1050
|
-
children: t('sources')
|
|
1051
|
-
}), jsx(Button, {
|
|
1052
|
-
variant: isOpen[MenuOptions.locations] ? 'primary' : 'tertiary',
|
|
1053
|
-
disableRipple: true,
|
|
1054
|
-
"data-testid": "soundingsLocationsButton",
|
|
1055
|
-
"aria-pressed": isOpen[MenuOptions.locations],
|
|
1056
|
-
onClick: () => handleClick(MenuOptions.locations),
|
|
1057
|
-
size: "small",
|
|
1058
|
-
children: t('locations')
|
|
1059
|
-
})]
|
|
1060
|
-
});
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
const MenuBarConnect = ({
|
|
1064
|
-
viewId,
|
|
1065
|
-
source
|
|
1066
|
-
}) => {
|
|
1067
|
-
const dispatch = useDispatch();
|
|
1068
|
-
const currentActiveMapId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
|
|
1069
|
-
const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
|
|
1070
|
-
const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.soundingsLocations));
|
|
1071
|
-
const dispatchActiveMapId = React.useCallback((dialogType, isOpen) => void dispatch(uiActions.setActiveMapIdForDialog({
|
|
1072
|
-
type: dialogType,
|
|
1073
|
-
mapId: viewId,
|
|
1074
|
-
setOpen: currentActiveMapId !== viewId ? true : !isOpen,
|
|
1075
|
-
source
|
|
1076
|
-
})), [dispatch, currentActiveMapId, viewId, source]);
|
|
1077
|
-
const handleClick = React.useCallback(menuType => {
|
|
1078
|
-
switch (menuType) {
|
|
1079
|
-
case MenuOptions.sources:
|
|
1080
|
-
dispatchActiveMapId(uiTypes.DialogTypes.SoundingsSources, isSourcesOpenInStore);
|
|
1081
|
-
break;
|
|
1082
|
-
case MenuOptions.locations:
|
|
1083
|
-
dispatchActiveMapId(uiTypes.DialogTypes.soundingsLocations, isLocationsOpenInStore);
|
|
1084
|
-
break;
|
|
1085
|
-
}
|
|
1086
|
-
}, [dispatchActiveMapId, isSourcesOpenInStore, isLocationsOpenInStore]);
|
|
1087
|
-
const isOpen = {
|
|
1088
|
-
[MenuOptions.sources]: currentActiveMapId === viewId && isSourcesOpenInStore,
|
|
1089
|
-
[MenuOptions.locations]: currentActiveMapId === viewId && isLocationsOpenInStore
|
|
1090
|
-
};
|
|
1091
|
-
return jsx(SoundingsMenuBar, {
|
|
1092
|
-
isOpen: isOpen,
|
|
1093
|
-
handleClick: handleClick
|
|
1094
|
-
});
|
|
1095
|
-
};
|
|
1096
|
-
|
|
1097
|
-
const SoundingsConnect = ({
|
|
1098
|
-
viewId
|
|
1099
|
-
}) => {
|
|
1100
|
-
return jsxs(Box, {
|
|
1101
|
-
sx: {
|
|
1102
|
-
padding: 2
|
|
1103
|
-
},
|
|
1104
|
-
children: [jsx(Box, {
|
|
1105
|
-
paddingLeft: 5,
|
|
1106
|
-
children: jsx(MenuBarConnect, {
|
|
1107
|
-
viewId: viewId
|
|
1108
|
-
})
|
|
1109
|
-
}), jsx(Soundings
|
|
1110
|
-
// TODO: use real data
|
|
1111
|
-
, {
|
|
1112
|
-
// TODO: use real data
|
|
1113
|
-
data: {
|
|
1114
|
-
features: SoundingFeatures
|
|
1115
|
-
},
|
|
1116
|
-
svgDimensions: {
|
|
1117
|
-
width: 600,
|
|
1118
|
-
height: 600,
|
|
1119
|
-
margin: {
|
|
1120
|
-
bottom: 0,
|
|
1121
|
-
left: 0,
|
|
1122
|
-
right: 0,
|
|
1123
|
-
top: 0
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
})]
|
|
2318
|
+
services: services.filter(service => selectedSourceIds.includes(service.id)),
|
|
2319
|
+
selectedLocations: locationSelections,
|
|
2320
|
+
updateSelectedLocation: (sourceId, locationId) => void dispatch(soundingsActions.updateSelectedLocation({
|
|
2321
|
+
sourceId,
|
|
2322
|
+
locationId
|
|
2323
|
+
})),
|
|
2324
|
+
onMouseDown: setDialogOrder,
|
|
2325
|
+
order: dialogOrder
|
|
1127
2326
|
});
|
|
1128
2327
|
};
|
|
1129
2328
|
|
|
@@ -1137,4 +2336,32 @@ const soundingsLookUp = payload => {
|
|
|
1137
2336
|
return null;
|
|
1138
2337
|
};
|
|
1139
2338
|
|
|
1140
|
-
|
|
2339
|
+
/* *
|
|
2340
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2341
|
+
* you may not use this file except in compliance with the License.
|
|
2342
|
+
* You may obtain a copy of the License at
|
|
2343
|
+
*
|
|
2344
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2345
|
+
*
|
|
2346
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2347
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2348
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2349
|
+
* See the License for the specific language governing permissions and
|
|
2350
|
+
* limitations under the License.
|
|
2351
|
+
*
|
|
2352
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
2353
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
2354
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
2355
|
+
* */
|
|
2356
|
+
const soundingsReducersMap = {
|
|
2357
|
+
soundings: soundingsReducer,
|
|
2358
|
+
...storeReducerMap
|
|
2359
|
+
};
|
|
2360
|
+
const soundingsRootReducer = combineReducers(soundingsReducersMap);
|
|
2361
|
+
const createMockStore = mockState => configureStore({
|
|
2362
|
+
reducer: soundingsRootReducer,
|
|
2363
|
+
preloadedState: mockState,
|
|
2364
|
+
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(storeMiddlewares)
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsConnect, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };
|