@opengeoweb/soundings 15.1.0 → 15.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/index.esm.js +595 -134
- package/package.json +9 -9
- package/src/index.d.ts +1 -0
- package/src/lib/api/edr-api/utils.d.ts +2 -1
- package/src/lib/components/Hodograph/Hodograph.d.ts +6 -0
- package/src/lib/components/Hodograph/Hodograph.spec.d.ts +1 -0
- package/src/lib/components/Hodograph/Hodograph.stories.d.ts +7 -0
- package/src/lib/components/Hodograph/HodographCollection.d.ts +6 -0
- package/src/lib/components/Hodograph/HodographCollection.spec.d.ts +1 -0
- package/src/lib/components/Hodograph/HodographLegend.d.ts +2 -0
- package/src/lib/components/Hodograph/drawUtils.d.ts +53 -0
- package/src/lib/components/Hodograph/drawUtils.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/LocationsMenuConnect.d.ts +2 -1
- package/src/lib/components/SoundingPanelPlaceholder/SoundingPanelPlaceholder.d.ts +5 -0
- package/src/lib/components/SoundingPanelPlaceholder/SoundingPanelPlaceholder.spec.d.ts +1 -0
- package/src/lib/components/SoundingPanelPlaceholder/index.d.ts +1 -0
- package/src/lib/components/SoundingsAccordion/SoundingsAccordion.d.ts +1 -0
- package/src/lib/components/SoundingsManager/SoundingManagerListItem.d.ts +3 -1
- package/src/lib/components/SoundingsManager/SoundingsManager.d.ts +2 -1
- package/src/lib/store/reducer.d.ts +23 -14
- package/src/lib/store/selectors.d.ts +24 -23
- package/src/lib/store/store.d.ts +1 -1
- package/src/lib/store/types.d.ts +19 -2
- package/src/lib/store/utils.d.ts +10 -0
package/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { Menu, ListSubheader, Typography, MenuItem, Box,
|
|
2
|
+
import { Menu, ListSubheader, Typography, MenuItem, Box, Stack, Card, IconButton, Tooltip, LinearProgress, List, ListItem, ListItemIcon, Checkbox, ListItemText, Grid, ListItemButton } from '@mui/material';
|
|
3
3
|
import { range, minBy, isEqual } from 'lodash';
|
|
4
4
|
import React, { useRef, useEffect, useMemo, useState, Fragment as Fragment$1 } from 'react';
|
|
5
5
|
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
|
@@ -17,8 +17,8 @@ import { timesliderTranslations, TIME_SLIDER_NAMESPACE, BackwardForwardStepButto
|
|
|
17
17
|
import { useResizable } from 'react-resizable-layout';
|
|
18
18
|
import { useQueries } from '@tanstack/react-query';
|
|
19
19
|
import { getHeaders, handleResponse } from '@opengeoweb/api';
|
|
20
|
-
import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
|
|
21
|
-
import {
|
|
20
|
+
import { createSlice, createAction, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
|
|
21
|
+
import { List as List$1 } from 'react-window';
|
|
22
22
|
|
|
23
23
|
var en = {
|
|
24
24
|
sources: "Sources",
|
|
@@ -31,6 +31,7 @@ var en = {
|
|
|
31
31
|
"diagram-SKEWT": "Skew-T",
|
|
32
32
|
"diagram-BIJVOET": "Bijvoet",
|
|
33
33
|
"diagram-EMAGRAM": "Emagram",
|
|
34
|
+
"coming-soon": "Coming soon",
|
|
34
35
|
"soundings-toggle-location-markers-label": "Markers",
|
|
35
36
|
"soundings-toggle-location-markers-tooltip": "Show/hide location markers"
|
|
36
37
|
};
|
|
@@ -45,6 +46,7 @@ var no = {
|
|
|
45
46
|
"diagram-SKEWT": "Skew-T",
|
|
46
47
|
"diagram-BIJVOET": "Bijvoet",
|
|
47
48
|
"diagram-EMAGRAM": "Emagram",
|
|
49
|
+
"coming-soon": "Kommer snart",
|
|
48
50
|
"soundings-toggle-location-markers-label": "ikke oversatt",
|
|
49
51
|
"soundings-toggle-location-markers-tooltip": "ikke oversatt"
|
|
50
52
|
};
|
|
@@ -59,6 +61,7 @@ var nl = {
|
|
|
59
61
|
"diagram-SKEWT": "Skew-T",
|
|
60
62
|
"diagram-BIJVOET": "Bijvoet",
|
|
61
63
|
"diagram-EMAGRAM": "Emagram",
|
|
64
|
+
"coming-soon": "Binnenkort beschikbaar",
|
|
62
65
|
"soundings-toggle-location-markers-label": "Kaart locaties",
|
|
63
66
|
"soundings-toggle-location-markers-tooltip": "Toon/Verberg locatie punten"
|
|
64
67
|
};
|
|
@@ -73,6 +76,7 @@ var fi = {
|
|
|
73
76
|
"diagram-SKEWT": "Skew-T",
|
|
74
77
|
"diagram-BIJVOET": "Bijvoet",
|
|
75
78
|
"diagram-EMAGRAM": "Emagram",
|
|
79
|
+
"coming-soon": "Tulossa pian",
|
|
76
80
|
"soundings-toggle-location-markers-label": "Merkit",
|
|
77
81
|
"soundings-toggle-location-markers-tooltip": "Näytä/piilota kohdemerkit"
|
|
78
82
|
};
|
|
@@ -322,7 +326,7 @@ const getDiagramName = (t, diagramType) => {
|
|
|
322
326
|
}
|
|
323
327
|
};
|
|
324
328
|
// vfx code
|
|
325
|
-
const initializeSvg = ({
|
|
329
|
+
const initializeSvg$1 = ({
|
|
326
330
|
svgRef,
|
|
327
331
|
width,
|
|
328
332
|
height,
|
|
@@ -341,7 +345,7 @@ const initializeSvg = ({
|
|
|
341
345
|
svg.append('rect').attr('height', height).attr('width', width).attr('fill', '#ffffff00').attr('opacity', 1).attr('id', `${d3GraphId}-${diagramType}d3`).attr('clip-path', `url(#clip-${d3GraphId}-${diagramType}d3)`);
|
|
342
346
|
return svg;
|
|
343
347
|
};
|
|
344
|
-
const createScales = (diagramType, width, height, minPressure, maxPressure, minTemperature, maxTemperature) => {
|
|
348
|
+
const createScales$1 = (diagramType, width, height, minPressure, maxPressure, minTemperature, maxTemperature) => {
|
|
345
349
|
if (diagramType === 'BIJVOET') {
|
|
346
350
|
// Note, for Bijvoet, the x-axis is NOT temperature but theta_es
|
|
347
351
|
// Both axis aren't linear nor logarithmic, hence we need an interpolation function to determine (x,y) based on (theta_es,p)
|
|
@@ -354,7 +358,7 @@ const createScales = (diagramType, width, height, minPressure, maxPressure, minT
|
|
|
354
358
|
const yScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]);
|
|
355
359
|
return [xScale, yScale];
|
|
356
360
|
};
|
|
357
|
-
const drawAxes = ({
|
|
361
|
+
const drawAxes$1 = ({
|
|
358
362
|
svg,
|
|
359
363
|
xScale,
|
|
360
364
|
yScale,
|
|
@@ -693,7 +697,7 @@ const drawSoundingsTooltip = ({
|
|
|
693
697
|
// Wind
|
|
694
698
|
windRefd3?.style('display', 'block').style('left', `${width + margin.left + margin.right}px`).style('top', `${pressureY}px`).html(() => {
|
|
695
699
|
const windSpeedStrings = interpolatedData.map((dataSet, index) => {
|
|
696
|
-
return getParamHtml(dataSet.hoveredWindSpeed, index, '
|
|
700
|
+
return getParamHtml(dataSet.hoveredWindSpeed, index, 'kts', index === interpolatedData.length - 1, dataSet.color, true);
|
|
697
701
|
});
|
|
698
702
|
const windDirStrings = interpolatedData.map((dataSet, index) => {
|
|
699
703
|
return getParamHtml(dataSet.hoveredWindDirection, index, '°', index === interpolatedData.length - 1, dataSet.color, index === interpolatedData.length - 1);
|
|
@@ -925,7 +929,7 @@ const drawIsothermsBijvoet = ({
|
|
|
925
929
|
const pathString = lineGenerator(points);
|
|
926
930
|
const strID = `${temp}-isotherm-BIJVOET`;
|
|
927
931
|
svg.append('path').attr('id', strID).attr('d', pathString).style('fill', 'none').style('stroke-width', temp % 10 === 0 ? 2 : 1).style('stroke', temp === 0 ? '#D27D2D' : 'orange').style('stroke-opacity', 0.5).attr('clip-path', `url(#clip-${d3GraphId}-BIJVOETd3)`);
|
|
928
|
-
if (temp % 10 === 0 && points[0]
|
|
932
|
+
if (temp % 10 === 0 && points[0]?.[1] === height) {
|
|
929
933
|
svg.append('text').text(temp).attr('text-anchor', 'middle').attr('x', `${points[0][0]}px`).attr('y', `${height + 12}px`).attr('fill', temp === 0 ? '#D27D2D' : 'orange').attr('font-size', '10px');
|
|
930
934
|
}
|
|
931
935
|
}
|
|
@@ -1227,7 +1231,7 @@ const Soundings = ({
|
|
|
1227
1231
|
if (!svgRef.current) {
|
|
1228
1232
|
return;
|
|
1229
1233
|
}
|
|
1230
|
-
const svg = initializeSvg({
|
|
1234
|
+
const svg = initializeSvg$1({
|
|
1231
1235
|
svgRef,
|
|
1232
1236
|
width,
|
|
1233
1237
|
height,
|
|
@@ -1241,9 +1245,9 @@ const Soundings = ({
|
|
|
1241
1245
|
widthRef.current = width;
|
|
1242
1246
|
const minT = diagramType === 'EMAGRAM' ? minEmagramTemperature : minTemperature;
|
|
1243
1247
|
const maxT = maxTemperature;
|
|
1244
|
-
const [xScale, yScale] = createScales(diagramType, width, height, minPressure, maxPressure, minT, maxT);
|
|
1248
|
+
const [xScale, yScale] = createScales$1(diagramType, width, height, minPressure, maxPressure, minT, maxT);
|
|
1245
1249
|
yScaleRef.current = yScale;
|
|
1246
|
-
drawAxes({
|
|
1250
|
+
drawAxes$1({
|
|
1247
1251
|
svg,
|
|
1248
1252
|
xScale,
|
|
1249
1253
|
yScale,
|
|
@@ -1330,7 +1334,7 @@ const Soundings = ({
|
|
|
1330
1334
|
const dewpointRefd3 = d3.select(dewpointRef.current);
|
|
1331
1335
|
const minT = diagramType === 'EMAGRAM' ? minEmagramTemperature : minTemperature;
|
|
1332
1336
|
const maxT = maxTemperature;
|
|
1333
|
-
const [, yScale] = createScales(diagramType, width, height, minPressure, maxPressure, minT, maxT);
|
|
1337
|
+
const [, yScale] = createScales$1(diagramType, width, height, minPressure, maxPressure, minT, maxT);
|
|
1334
1338
|
drawSoundingsTooltip({
|
|
1335
1339
|
event: synthetic,
|
|
1336
1340
|
yScale,
|
|
@@ -1734,7 +1738,6 @@ const windSpeedRanges = [{
|
|
|
1734
1738
|
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1735
1739
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1736
1740
|
* */
|
|
1737
|
-
const metersPerSecondToKnots = metersPerSecond => metersPerSecond * 1.94384;
|
|
1738
1741
|
const extendIsoBar = (path, horizontalBarbMargin) => {
|
|
1739
1742
|
const match = /^M([\d.-]+),([\d.-]+)\s*L([\d.-]+),([\d.-]+)/.exec(path);
|
|
1740
1743
|
if (match) {
|
|
@@ -1842,11 +1845,10 @@ const removeItemsExponential = array => {
|
|
|
1842
1845
|
return array.filter((_, index) => indicesToKeep.includes(index));
|
|
1843
1846
|
};
|
|
1844
1847
|
const getWindSymbol = value => {
|
|
1845
|
-
const valueKts = metersPerSecondToKnots(value);
|
|
1846
1848
|
const matchedRange = windSpeedRanges.find(({
|
|
1847
1849
|
min,
|
|
1848
1850
|
max
|
|
1849
|
-
}) =>
|
|
1851
|
+
}) => value >= min && value < max);
|
|
1850
1852
|
// Out of range
|
|
1851
1853
|
if (!matchedRange) {
|
|
1852
1854
|
return null;
|
|
@@ -1925,7 +1927,7 @@ const WindBarbs = ({
|
|
|
1925
1927
|
}
|
|
1926
1928
|
const root = d3.select(svgRef.current);
|
|
1927
1929
|
root.selectAll('*').remove();
|
|
1928
|
-
const svg = initializeSvg({
|
|
1930
|
+
const svg = initializeSvg$1({
|
|
1929
1931
|
svgRef,
|
|
1930
1932
|
width,
|
|
1931
1933
|
height,
|
|
@@ -1943,7 +1945,7 @@ const WindBarbs = ({
|
|
|
1943
1945
|
widthRef.current = width;
|
|
1944
1946
|
const minT = diagramType === 'EMAGRAM' ? minEmagramTemperature : minTemperature;
|
|
1945
1947
|
const maxT = maxTemperature;
|
|
1946
|
-
const [xScale, yScale] = createScales(diagramType, width, height, minPressure, maxPressure, minT, maxT);
|
|
1948
|
+
const [xScale, yScale] = createScales$1(diagramType, width, height, minPressure, maxPressure, minT, maxT);
|
|
1947
1949
|
yScaleRef.current = yScale;
|
|
1948
1950
|
drawIsobars({
|
|
1949
1951
|
diagramType,
|
|
@@ -2036,6 +2038,257 @@ const WindBarbs = ({
|
|
|
2036
2038
|
});
|
|
2037
2039
|
};
|
|
2038
2040
|
|
|
2041
|
+
/* *
|
|
2042
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
2043
|
+
* you may not use this file except in compliance with the License.
|
|
2044
|
+
* You may obtain a copy of the License at
|
|
2045
|
+
*
|
|
2046
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
2047
|
+
*
|
|
2048
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
2049
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
2050
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
2051
|
+
* See the License for the specific language governing permissions and
|
|
2052
|
+
* limitations under the License.
|
|
2053
|
+
*
|
|
2054
|
+
* Copyright 2026 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
2055
|
+
* Copyright 2026 - Finnish Meteorological Institute (FMI)
|
|
2056
|
+
* Copyright 2026 - The Norwegian Meteorological Institute (MET Norway)
|
|
2057
|
+
* */
|
|
2058
|
+
const initializeSvg = ({
|
|
2059
|
+
svgRef,
|
|
2060
|
+
widthHeight
|
|
2061
|
+
}) => {
|
|
2062
|
+
const svg = d3.select(svgRef.current).attr('data-testid', 'hodograph').attr('width', '100%').attr('height', '100%').attr('viewBox', `0 0 ${widthHeight + 3} ${widthHeight + 3}`).attr('style', 'width: 100%; height: auto;');
|
|
2063
|
+
const gSel = svg.select('g.hodograph');
|
|
2064
|
+
if (gSel.empty()) {
|
|
2065
|
+
svg.append('g').attr('class', 'hodograph');
|
|
2066
|
+
}
|
|
2067
|
+
return svg;
|
|
2068
|
+
};
|
|
2069
|
+
const createScales = widthHeight => {
|
|
2070
|
+
const scaleRadius = d3.scaleLinear().domain([0, 100]).range([0, widthHeight / 2]);
|
|
2071
|
+
const scaleAngle = d3.scaleLinear().domain([0, 360]).range([0, 2 * Math.PI]);
|
|
2072
|
+
return [scaleRadius, scaleAngle];
|
|
2073
|
+
};
|
|
2074
|
+
const drawAxes = ({
|
|
2075
|
+
svg,
|
|
2076
|
+
scaleRadius,
|
|
2077
|
+
scaleAngle,
|
|
2078
|
+
widthHeight
|
|
2079
|
+
}) => {
|
|
2080
|
+
// Draw circles
|
|
2081
|
+
svg.select('g').selectAll('circle').data([10, 20, 30, 40, 50, 60, 70, 80, 90, 100]).join(enter => enter.append('circle').attr('cx', widthHeight / 2).attr('cy', widthHeight / 2).attr('fill', 'none').attr('stroke', 'gray').style('opacity', 0.6).attr('stroke-dasharray', data => data === 50 || data === 100 ? 'none' : '5, 4').attr('r', data => scaleRadius(data)));
|
|
2082
|
+
// Draw windspeed scale
|
|
2083
|
+
const speedTicks = scaleRadius.ticks(9);
|
|
2084
|
+
svg.select('g').append('g').attr('type', 'windspeed').attr('transform', `translate(${widthHeight / 2}, ${widthHeight / 2})`) // origin transformed to center
|
|
2085
|
+
.call(d3.axisBottom(scaleRadius).tickValues(speedTicks).tickSizeInner(0).tickSizeOuter(0))
|
|
2086
|
+
// Draw unit label
|
|
2087
|
+
.append('text').attr('fill', 'currentColor').attr('x', scaleRadius(90)).attr('y', '0').attr('font-size', '11px').attr('dy', '2.2em').attr('dx', '0.8em').attr('text-anchor', 'left').text('kts');
|
|
2088
|
+
};
|
|
2089
|
+
// Use this function to filter the pressure and parameter array so no NaN or null values are included
|
|
2090
|
+
const filterDomainRangeForHodograph = (pressureDomain, parameterDomain) => {
|
|
2091
|
+
const filteredPressureDomain = pressureDomain.filter((_value, index) => parameterDomain[index] !== null && !isNaN(parameterDomain[index][0]) && parameterDomain[index][0] !== null && !isNaN(parameterDomain[index][1]) && parameterDomain[index][1] !== null);
|
|
2092
|
+
const filteredParameterDomain = parameterDomain.filter(value => !isNaN(value[0]) && value[0] !== null && !isNaN(value[1]) && value[1] !== null);
|
|
2093
|
+
// If first pressure value is smallest, reverse arrays to ensure we draw starting from surface
|
|
2094
|
+
if (filteredPressureDomain[0] < filteredPressureDomain[filteredPressureDomain.length - 1]) {
|
|
2095
|
+
return [filteredPressureDomain.reverse(), filteredParameterDomain.reverse()];
|
|
2096
|
+
}
|
|
2097
|
+
return [filteredPressureDomain, filteredParameterDomain];
|
|
2098
|
+
};
|
|
2099
|
+
const T0_SEA = 288.15; // Standard temperature in Kelvin (15 degrees Celsius)
|
|
2100
|
+
const P0_SEA = 101325; // Reference pressure (Pa)
|
|
2101
|
+
const R_GAS = 8.314462618; // Universal gas constant (J/mol K)
|
|
2102
|
+
const L_TEMP = 0.0065; // Temperature gradient (K/m) at sea level
|
|
2103
|
+
const G_ACC = 9.80665; // The gravitational acceleration in units of geopotential height (m/s2)
|
|
2104
|
+
const M_AIR = 0.0289647; // Mean molar mass of air at sea level (kg/mol)
|
|
2105
|
+
const exponent = R_GAS * L_TEMP / (G_ACC * M_AIR); // ≈ 0.1903
|
|
2106
|
+
// This function returns the altitude above MSL using the barometric formula in m
|
|
2107
|
+
// Pressure passed in is in hPa, which is converted to Pa (pressure * 100)
|
|
2108
|
+
const altitudeBarometricFromPressure = pressure => {
|
|
2109
|
+
const altitude = T0_SEA / L_TEMP * ((P0_SEA / (pressure * 100)) ** exponent - 1);
|
|
2110
|
+
return altitude;
|
|
2111
|
+
};
|
|
2112
|
+
const generateSegmentsBasedOnLevel = (pressure, windValues) => {
|
|
2113
|
+
const segments = [[], [], [], [], []];
|
|
2114
|
+
pressure.forEach((pressureVal, index) => {
|
|
2115
|
+
// Ensure we ignore the last value
|
|
2116
|
+
if (pressure[index + 1]) {
|
|
2117
|
+
const heightInMeters = altitudeBarometricFromPressure(pressureVal);
|
|
2118
|
+
// index 0, red line
|
|
2119
|
+
if (heightInMeters < 1000) {
|
|
2120
|
+
segments[0].push(windValues[index]);
|
|
2121
|
+
segments[0].push(windValues[index + 1]);
|
|
2122
|
+
}
|
|
2123
|
+
// index 1, green line
|
|
2124
|
+
if (heightInMeters >= 1000 && heightInMeters < 3000) {
|
|
2125
|
+
segments[1].push(windValues[index]);
|
|
2126
|
+
segments[1].push(windValues[index + 1]);
|
|
2127
|
+
}
|
|
2128
|
+
// index 2, yellow line
|
|
2129
|
+
if (heightInMeters >= 3000 && heightInMeters < 6000) {
|
|
2130
|
+
segments[2].push(windValues[index]);
|
|
2131
|
+
segments[2].push(windValues[index + 1]);
|
|
2132
|
+
}
|
|
2133
|
+
// index 3, purple line
|
|
2134
|
+
if (heightInMeters >= 6000 && heightInMeters < 9000) {
|
|
2135
|
+
segments[3].push(windValues[index]);
|
|
2136
|
+
segments[3].push(windValues[index + 1]);
|
|
2137
|
+
}
|
|
2138
|
+
// index 4, black line
|
|
2139
|
+
if (heightInMeters >= 9000 && heightInMeters <= 19000) {
|
|
2140
|
+
segments[4].push(windValues[index]);
|
|
2141
|
+
segments[4].push(windValues[index + 1]);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
return segments;
|
|
2146
|
+
};
|
|
2147
|
+
const HODOGRAPH_COLORS = ['red', 'olivedrab', 'orange', 'darkviolet', 'black'];
|
|
2148
|
+
const getColorForSegment = index => {
|
|
2149
|
+
if (HODOGRAPH_COLORS[index]) {
|
|
2150
|
+
return HODOGRAPH_COLORS[index];
|
|
2151
|
+
}
|
|
2152
|
+
return 'black';
|
|
2153
|
+
};
|
|
2154
|
+
const plotHodograph = ({
|
|
2155
|
+
windValues,
|
|
2156
|
+
pressureValues,
|
|
2157
|
+
scaleAngle,
|
|
2158
|
+
scaleRadius,
|
|
2159
|
+
widthHeight,
|
|
2160
|
+
svg,
|
|
2161
|
+
index
|
|
2162
|
+
}) => {
|
|
2163
|
+
// Filter out any NaN or null values
|
|
2164
|
+
const [filteredPressureValues, filteredWindValues] = filterDomainRangeForHodograph(pressureValues, windValues);
|
|
2165
|
+
// Construct path segments to be able to give each segment it's own color
|
|
2166
|
+
const pathSegments = generateSegmentsBasedOnLevel(filteredPressureValues, filteredWindValues);
|
|
2167
|
+
// index 0 = wind direction, index 1 = wind speed
|
|
2168
|
+
const lineGenerator = d3.lineRadial().angle(dataPoint => scaleAngle(dataPoint[0])).radius(dataPoint => scaleRadius(dataPoint[1]));
|
|
2169
|
+
svg.select('g.hodograph').selectAll(`path.dataline-${index}`).data(pathSegments).enter().append('path').classed(`dataline-${index}`, true).attr('data-testid', `hodograph-dataline-${index}`).attr('transform', `translate(${widthHeight / 2}, ${widthHeight / 2})`).attr('fill', 'none').attr('stroke', (data, segmentIndex) => getColorForSegment(segmentIndex)).attr('stroke-width', 2).style('stroke-opacity', 1).attr('d', data => lineGenerator(data));
|
|
2170
|
+
// Adding a dot for the very first data point in the color used in the soundings graph
|
|
2171
|
+
const firstDataPoint = filteredWindValues[0];
|
|
2172
|
+
if (firstDataPoint) {
|
|
2173
|
+
const color = SOUNDING_COLORS[index] || SOUNDING_COLORS[0];
|
|
2174
|
+
svg.select('g.hodograph').append('g').attr('transform', `translate(${widthHeight / 2}, ${widthHeight / 2})`) // origin transformed to center
|
|
2175
|
+
.attr('fill', color).attr('stroke', 'none').attr('data-testid', `hodograph-datastart-${index}`).selectAll().data([firstDataPoint]).join('circle').attr('cx', dataPoint => {
|
|
2176
|
+
const angle = scaleAngle(dataPoint[0]);
|
|
2177
|
+
const radius = scaleRadius(dataPoint[1]);
|
|
2178
|
+
return radius * Math.sin(angle);
|
|
2179
|
+
}).attr('cy', dataPoint => {
|
|
2180
|
+
const angle = scaleAngle(dataPoint[0]);
|
|
2181
|
+
const radius = scaleRadius(dataPoint[1]);
|
|
2182
|
+
return radius * -1 * Math.cos(angle);
|
|
2183
|
+
}).attr('r', 3);
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2187
|
+
const Hodograph = ({
|
|
2188
|
+
data
|
|
2189
|
+
}) => {
|
|
2190
|
+
const svgRef = useRef(null);
|
|
2191
|
+
const widthHeight = 400;
|
|
2192
|
+
useEffect(() => {
|
|
2193
|
+
if (!svgRef.current) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
// Define svg
|
|
2197
|
+
const svg = initializeSvg({
|
|
2198
|
+
svgRef,
|
|
2199
|
+
widthHeight
|
|
2200
|
+
});
|
|
2201
|
+
// Set scales for temperature and pressure
|
|
2202
|
+
const [scaleRadius, scaleAngle] = createScales(widthHeight);
|
|
2203
|
+
// Add axis'
|
|
2204
|
+
drawAxes({
|
|
2205
|
+
svg,
|
|
2206
|
+
scaleRadius,
|
|
2207
|
+
scaleAngle,
|
|
2208
|
+
widthHeight
|
|
2209
|
+
});
|
|
2210
|
+
data.forEach((features, index) => {
|
|
2211
|
+
const windValues = features?.map(feature => {
|
|
2212
|
+
// Transform windDirectionFrom to windDirectionTo
|
|
2213
|
+
const windDirectionTo = ((feature.properties.windDirection + 180) % 360 + 360) % 360;
|
|
2214
|
+
return [windDirectionTo, feature.properties.windSpeed];
|
|
2215
|
+
});
|
|
2216
|
+
const pressureValues = features.map(feature => feature.properties.pressure);
|
|
2217
|
+
// Draw Temperature
|
|
2218
|
+
plotHodograph({
|
|
2219
|
+
windValues,
|
|
2220
|
+
pressureValues,
|
|
2221
|
+
scaleAngle,
|
|
2222
|
+
scaleRadius,
|
|
2223
|
+
widthHeight,
|
|
2224
|
+
svg,
|
|
2225
|
+
index
|
|
2226
|
+
});
|
|
2227
|
+
});
|
|
2228
|
+
// Allow zooming and panning
|
|
2229
|
+
const zoomBehavior = d3.zoom().scaleExtent([1, 7]).translateExtent([[-0.2 * widthHeight, -0.2 * widthHeight], [1.2 * widthHeight, 1.2 * widthHeight]]).on('zoom', event => {
|
|
2230
|
+
svg.select('g.hodograph').attr('transform', event.transform);
|
|
2231
|
+
});
|
|
2232
|
+
svg.call(zoomBehavior);
|
|
2233
|
+
// eslint-disable-next-line consistent-return
|
|
2234
|
+
return () => {
|
|
2235
|
+
svg.on('zoom', null);
|
|
2236
|
+
svg.select('g.hodograph').selectAll('*').remove();
|
|
2237
|
+
};
|
|
2238
|
+
}, [data, widthHeight]);
|
|
2239
|
+
return jsx("svg", {
|
|
2240
|
+
height: "100%",
|
|
2241
|
+
ref: svgRef,
|
|
2242
|
+
width: "100%"
|
|
2243
|
+
});
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
const getLegendItem = (color, text) => {
|
|
2247
|
+
return jsxs(Typography, {
|
|
2248
|
+
sx: {
|
|
2249
|
+
color: 'geowebColors.typographyAndIcons.text',
|
|
2250
|
+
fontSize: '9px'
|
|
2251
|
+
},
|
|
2252
|
+
variant: "caption",
|
|
2253
|
+
children: [jsx(Box, {
|
|
2254
|
+
sx: {
|
|
2255
|
+
width: '16px',
|
|
2256
|
+
height: '4px',
|
|
2257
|
+
backgroundColor: color,
|
|
2258
|
+
display: 'inline-block',
|
|
2259
|
+
marginRight: '4px',
|
|
2260
|
+
marginBottom: '2px'
|
|
2261
|
+
}
|
|
2262
|
+
}), text]
|
|
2263
|
+
});
|
|
2264
|
+
};
|
|
2265
|
+
const HodographLegend = () => {
|
|
2266
|
+
return jsxs(Stack, {
|
|
2267
|
+
direction: "column",
|
|
2268
|
+
justifyContent: "center",
|
|
2269
|
+
sx: {
|
|
2270
|
+
flexWrap: 'wrap',
|
|
2271
|
+
paddingLeft: 2,
|
|
2272
|
+
paddingTop: 0.5
|
|
2273
|
+
},
|
|
2274
|
+
useFlexGap: true,
|
|
2275
|
+
children: [getLegendItem(HODOGRAPH_COLORS[0], '0 - 1 km'), getLegendItem(HODOGRAPH_COLORS[1], '1 - 3 km'), getLegendItem(HODOGRAPH_COLORS[2], '3 - 6 km'), getLegendItem(HODOGRAPH_COLORS[3], '6 - 9 km'), getLegendItem(HODOGRAPH_COLORS[4], '9 - 19 km')]
|
|
2276
|
+
});
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
const HodographCollection = ({
|
|
2280
|
+
data
|
|
2281
|
+
}) => {
|
|
2282
|
+
return jsxs(Box, {
|
|
2283
|
+
sx: {
|
|
2284
|
+
padding: '5px'
|
|
2285
|
+
},
|
|
2286
|
+
children: [jsx(Hodograph, {
|
|
2287
|
+
data: data
|
|
2288
|
+
}), jsx(HodographLegend, {})]
|
|
2289
|
+
});
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2039
2292
|
const ACCORDION_MIN_WIDTH = 24;
|
|
2040
2293
|
const SoundingsAccordion = React.forwardRef(({
|
|
2041
2294
|
name,
|
|
@@ -2047,7 +2300,8 @@ const SoundingsAccordion = React.forwardRef(({
|
|
|
2047
2300
|
children,
|
|
2048
2301
|
setSoundingsIsClosed,
|
|
2049
2302
|
setPreviousHeight,
|
|
2050
|
-
size
|
|
2303
|
+
size,
|
|
2304
|
+
isDisplayed
|
|
2051
2305
|
}, aspectRatioRef) => {
|
|
2052
2306
|
const containerRef = React.useRef(null);
|
|
2053
2307
|
const widthRef = React.useRef(null);
|
|
@@ -2164,6 +2418,7 @@ const SoundingsAccordion = React.forwardRef(({
|
|
|
2164
2418
|
};
|
|
2165
2419
|
}, [autoSizing, minWidth, setContainerWidth]);
|
|
2166
2420
|
const cardStyleWhenAuto = {
|
|
2421
|
+
display: isDisplayed ? 'block' : 'none',
|
|
2167
2422
|
width: 'auto',
|
|
2168
2423
|
flex: '1 1 100%',
|
|
2169
2424
|
minWidth: 0,
|
|
@@ -2171,12 +2426,14 @@ const SoundingsAccordion = React.forwardRef(({
|
|
|
2171
2426
|
boxSizing: 'border-box'
|
|
2172
2427
|
};
|
|
2173
2428
|
const cardStyleWhenLocked = {
|
|
2429
|
+
display: isDisplayed ? 'block' : 'none',
|
|
2174
2430
|
width: containerWidth,
|
|
2175
2431
|
flex: '0 1 auto',
|
|
2176
2432
|
minWidth,
|
|
2177
2433
|
maxWidth: maxWidth || '100%',
|
|
2178
2434
|
boxSizing: 'border-box'
|
|
2179
2435
|
};
|
|
2436
|
+
//
|
|
2180
2437
|
return jsx(Card, {
|
|
2181
2438
|
className: "sortable-item",
|
|
2182
2439
|
"data-autosizing": autoSizing ? 'true' : 'false',
|
|
@@ -2291,7 +2548,7 @@ const SortableAccordions = ({
|
|
|
2291
2548
|
}) => {
|
|
2292
2549
|
const [soundingsIsClosed, setSoundingsIsClosed] = React.useState(false);
|
|
2293
2550
|
const [previousHeight, setPreviousHeight] = React.useState(size);
|
|
2294
|
-
const createPanel = React.useCallback(id => {
|
|
2551
|
+
const createPanel = React.useCallback((id, isDisplayed) => {
|
|
2295
2552
|
const normalizedId = id.toLowerCase().replace(/\s+/g, '');
|
|
2296
2553
|
switch (normalizedId) {
|
|
2297
2554
|
case 'soundings':
|
|
@@ -2301,6 +2558,7 @@ const SortableAccordions = ({
|
|
|
2301
2558
|
diagramType: chosenDiagramType,
|
|
2302
2559
|
setDiagramType: setChosenDiagramType
|
|
2303
2560
|
}),
|
|
2561
|
+
isDisplayed: isDisplayed,
|
|
2304
2562
|
name: "Soundings",
|
|
2305
2563
|
ref: viewRef,
|
|
2306
2564
|
setPreviousHeight: setPreviousHeight,
|
|
@@ -2324,6 +2582,7 @@ const SortableAccordions = ({
|
|
|
2324
2582
|
});
|
|
2325
2583
|
case 'windbarbs':
|
|
2326
2584
|
return jsx(SoundingsAccordion, {
|
|
2585
|
+
isDisplayed: isDisplayed,
|
|
2327
2586
|
maxWidth: 200,
|
|
2328
2587
|
name: "Wind Barbs",
|
|
2329
2588
|
children: jsx(WindBarbs, {
|
|
@@ -2343,6 +2602,14 @@ const SortableAccordions = ({
|
|
|
2343
2602
|
}
|
|
2344
2603
|
})
|
|
2345
2604
|
});
|
|
2605
|
+
case 'hodograph':
|
|
2606
|
+
return jsx(SoundingsAccordion, {
|
|
2607
|
+
maxWidth: 500,
|
|
2608
|
+
name: "Hodograph",
|
|
2609
|
+
children: jsx(HodographCollection, {
|
|
2610
|
+
data: data
|
|
2611
|
+
})
|
|
2612
|
+
});
|
|
2346
2613
|
default:
|
|
2347
2614
|
return null;
|
|
2348
2615
|
}
|
|
@@ -2378,16 +2645,18 @@ const SortableAccordions = ({
|
|
|
2378
2645
|
width: '100%'
|
|
2379
2646
|
},
|
|
2380
2647
|
tag: "div",
|
|
2381
|
-
children: soundingPanelsInOrder.
|
|
2382
|
-
id
|
|
2648
|
+
children: soundingPanelsInOrder.flatMap(({
|
|
2649
|
+
id,
|
|
2650
|
+
isDisplayed
|
|
2383
2651
|
}) => {
|
|
2384
|
-
const panel = createPanel(id);
|
|
2652
|
+
const panel = createPanel(id, isDisplayed);
|
|
2385
2653
|
if (!panel) {
|
|
2386
2654
|
return [];
|
|
2387
2655
|
}
|
|
2388
2656
|
return panel ? jsx(React.Fragment, {
|
|
2389
2657
|
children: React.cloneElement(panel, {
|
|
2390
2658
|
accordionId: id,
|
|
2659
|
+
isDisplayed,
|
|
2391
2660
|
handleButtonComponent: jsx(IconButton, {
|
|
2392
2661
|
"aria-label": "drag-handle",
|
|
2393
2662
|
className: "drag-handle",
|
|
@@ -2546,6 +2815,7 @@ const aConstant = 17.67;
|
|
|
2546
2815
|
const bConstant = 243.5; // Constants for water, b in Celsius
|
|
2547
2816
|
const gConstant = 461.5; // Gas constant ratio between water vapour and air [J/(K*kg)]
|
|
2548
2817
|
const hConstant = 2.5e6; // Latent heat for water vaporization [J/kg]
|
|
2818
|
+
const ktsConstant = 1.94384;
|
|
2549
2819
|
// Formula to use when dewpoint is not available
|
|
2550
2820
|
// Known as Magnus Formula
|
|
2551
2821
|
// - Calculate vapor pressure
|
|
@@ -2570,8 +2840,15 @@ relHumidity) {
|
|
|
2570
2840
|
const dewpoint = tKelvin / (1 - tKelvin * Math.log(relHumidity * 0.01) * gConstant / hConstant);
|
|
2571
2841
|
return dewpoint;
|
|
2572
2842
|
}
|
|
2843
|
+
const getWindSpeedInKts = (windSpeed, windSpeedUnit) => {
|
|
2844
|
+
if (windSpeedUnit === 'kts') {
|
|
2845
|
+
return windSpeed;
|
|
2846
|
+
}
|
|
2847
|
+
// Formula to convert from m/s to kts
|
|
2848
|
+
return windSpeed * ktsConstant;
|
|
2849
|
+
};
|
|
2573
2850
|
// Parsing and conversion function
|
|
2574
|
-
function parseCoverageCollection(response, parameterMappings) {
|
|
2851
|
+
function parseCoverageCollection(response, parameterMappings, windSpeedUnit) {
|
|
2575
2852
|
const features = [];
|
|
2576
2853
|
const pressureArray = [];
|
|
2577
2854
|
const tempArray = [];
|
|
@@ -2675,13 +2952,14 @@ function parseCoverageCollection(response, parameterMappings) {
|
|
|
2675
2952
|
dewpoint = dewpointFromRelativeHumidity(tempArray[index], relativeHumidity);
|
|
2676
2953
|
}
|
|
2677
2954
|
// Extract wind speed and direction - use fallback calculations if no speed and direction in parameter mappings
|
|
2955
|
+
// Convert to kts if retrieved in m/s
|
|
2678
2956
|
let windSpeed = NaN;
|
|
2679
2957
|
if (parameterMappings.windSpeed) {
|
|
2680
|
-
windSpeed = collectionData.windSpeedArray[index];
|
|
2958
|
+
windSpeed = getWindSpeedInKts(collectionData.windSpeedArray[index], windSpeedUnit);
|
|
2681
2959
|
} else if (parameterMappings.windU && parameterMappings.windV) {
|
|
2682
2960
|
const windU = collectionData.windUArray[index];
|
|
2683
2961
|
const windV = collectionData.windVArray[index];
|
|
2684
|
-
windSpeed = Math.sqrt(windU ** 2 + windV ** 2);
|
|
2962
|
+
windSpeed = getWindSpeedInKts(Math.sqrt(windU ** 2 + windV ** 2), windSpeedUnit);
|
|
2685
2963
|
}
|
|
2686
2964
|
let windDirection = NaN;
|
|
2687
2965
|
if (parameterMappings.windDirection) {
|
|
@@ -2790,7 +3068,7 @@ const sortSoundingFeatures = data => {
|
|
|
2790
3068
|
return featureB.properties.pressure - featureA.properties.pressure;
|
|
2791
3069
|
});
|
|
2792
3070
|
};
|
|
2793
|
-
const constructQueryParams = (collectionId, baseUrl, instanceId, location, datetime, parameterMappings, verticalRange) => {
|
|
3071
|
+
const constructQueryParams = (collectionId, baseUrl, instanceId, location, datetime, parameterMappings, windSpeedUnit, verticalRange) => {
|
|
2794
3072
|
const parameters = getParameterMappings(parameterMappings);
|
|
2795
3073
|
if (typeof location === 'string') {
|
|
2796
3074
|
// LOCATION call
|
|
@@ -2803,7 +3081,7 @@ const constructQueryParams = (collectionId, baseUrl, instanceId, location, datet
|
|
|
2803
3081
|
}
|
|
2804
3082
|
try {
|
|
2805
3083
|
const rawData = await getEdrCollectionForLocation(collectionId, baseUrl, instanceId, location, parameters, datetime, verticalRange);
|
|
2806
|
-
const parsedData = parseCoverageCollection(rawData, parameterMappings);
|
|
3084
|
+
const parsedData = parseCoverageCollection(rawData, parameterMappings, windSpeedUnit);
|
|
2807
3085
|
return sortSoundingFeatures(parsedData);
|
|
2808
3086
|
} catch (_err) {
|
|
2809
3087
|
throw new Error('Error fetching EDR data collection by location');
|
|
@@ -2821,7 +3099,7 @@ const constructQueryParams = (collectionId, baseUrl, instanceId, location, datet
|
|
|
2821
3099
|
}
|
|
2822
3100
|
try {
|
|
2823
3101
|
const rawData = await getEdrCollectionForPosition(collectionId, baseUrl, instanceId, location, parameters, datetime, verticalRange);
|
|
2824
|
-
const parsedData = parseCoverageCollection(rawData, parameterMappings);
|
|
3102
|
+
const parsedData = parseCoverageCollection(rawData, parameterMappings, windSpeedUnit);
|
|
2825
3103
|
return sortSoundingFeatures(parsedData);
|
|
2826
3104
|
} catch (_err) {
|
|
2827
3105
|
throw new Error('Error fetching EDR data collection by position');
|
|
@@ -2856,6 +3134,7 @@ const useMultipleSources = (sources, locations, datetime) => {
|
|
|
2856
3134
|
url,
|
|
2857
3135
|
instanceId,
|
|
2858
3136
|
parameterMappings,
|
|
3137
|
+
windSpeedUnit,
|
|
2859
3138
|
verticalRange
|
|
2860
3139
|
}, index) => {
|
|
2861
3140
|
// Find the best matching timestep for this source
|
|
@@ -2880,7 +3159,7 @@ const useMultipleSources = (sources, locations, datetime) => {
|
|
|
2880
3159
|
dateToUse = nearestTimestep;
|
|
2881
3160
|
}
|
|
2882
3161
|
}
|
|
2883
|
-
const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dateToUse || '', parameterMappings || defaultParameterMappings, verticalRange || '');
|
|
3162
|
+
const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dateToUse || '', parameterMappings || defaultParameterMappings, windSpeedUnit || 'ms', verticalRange || '');
|
|
2884
3163
|
return queryParams;
|
|
2885
3164
|
});
|
|
2886
3165
|
}, [datetime, locations, sources, timesteps]);
|
|
@@ -3061,6 +3340,48 @@ const useGetCollectionLocations = (collections, enabled) => {
|
|
|
3061
3340
|
});
|
|
3062
3341
|
};
|
|
3063
3342
|
|
|
3343
|
+
/* *
|
|
3344
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3345
|
+
* you may not use this file except in compliance with the License.
|
|
3346
|
+
* You may obtain a copy of the License at
|
|
3347
|
+
*
|
|
3348
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
3349
|
+
*
|
|
3350
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
3351
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
3352
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
3353
|
+
* See the License for the specific language governing permissions and
|
|
3354
|
+
* limitations under the License.
|
|
3355
|
+
*
|
|
3356
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
3357
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
3358
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
3359
|
+
* */
|
|
3360
|
+
const emptyPanelIds = [];
|
|
3361
|
+
const toStringArray = value => {
|
|
3362
|
+
if (!Array.isArray(value)) {
|
|
3363
|
+
return emptyPanelIds;
|
|
3364
|
+
}
|
|
3365
|
+
return value.filter(item => typeof item === 'string');
|
|
3366
|
+
};
|
|
3367
|
+
const getRegisteredPanelIds = state => {
|
|
3368
|
+
if (!state) {
|
|
3369
|
+
return emptyPanelIds;
|
|
3370
|
+
}
|
|
3371
|
+
const registeredPanelIds = toStringArray(state.registeredPanelIds);
|
|
3372
|
+
if (registeredPanelIds.length > 0) {
|
|
3373
|
+
return registeredPanelIds;
|
|
3374
|
+
}
|
|
3375
|
+
return Object.keys(state.byId);
|
|
3376
|
+
};
|
|
3377
|
+
const isRegisteredPanelId = (state, panelId) => {
|
|
3378
|
+
if (!panelId) {
|
|
3379
|
+
return false;
|
|
3380
|
+
}
|
|
3381
|
+
return getRegisteredPanelIds(state).includes(panelId);
|
|
3382
|
+
};
|
|
3383
|
+
const getPanelIdFromAction = action => action.payload?.panelId;
|
|
3384
|
+
|
|
3064
3385
|
/* *
|
|
3065
3386
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3066
3387
|
* you may not use this file except in compliance with the License.
|
|
@@ -3084,7 +3405,7 @@ const initialSoundingsState = {
|
|
|
3084
3405
|
selectedTime: new Date().toISOString(),
|
|
3085
3406
|
diagramType: 'SKEWT',
|
|
3086
3407
|
soundingPanels: {
|
|
3087
|
-
order: ['soundings', 'windbarbs'],
|
|
3408
|
+
order: ['soundings', 'windbarbs', 'hodograph'],
|
|
3088
3409
|
entities: {
|
|
3089
3410
|
soundings: {
|
|
3090
3411
|
id: 'soundings',
|
|
@@ -3095,17 +3416,31 @@ const initialSoundingsState = {
|
|
|
3095
3416
|
id: 'windbarbs',
|
|
3096
3417
|
name: 'Wind Barbs',
|
|
3097
3418
|
isDisplayed: true
|
|
3419
|
+
},
|
|
3420
|
+
hodograph: {
|
|
3421
|
+
id: 'hodograph',
|
|
3422
|
+
name: 'Hodograph',
|
|
3423
|
+
isDisplayed: true
|
|
3098
3424
|
}
|
|
3099
3425
|
}
|
|
3100
3426
|
},
|
|
3101
3427
|
shouldHideLocationMarkers: false
|
|
3102
3428
|
};
|
|
3429
|
+
const initialState = {
|
|
3430
|
+
byId: {},
|
|
3431
|
+
registeredPanelIds: [],
|
|
3432
|
+
soundingsCollections: []
|
|
3433
|
+
};
|
|
3103
3434
|
const slice = createSlice({
|
|
3104
3435
|
initialState: initialSoundingsState,
|
|
3105
3436
|
name: 'soundings',
|
|
3106
3437
|
reducers: {
|
|
3438
|
+
registerSoundingsPanel: (draft, _payloadAction) => {
|
|
3439
|
+
},
|
|
3107
3440
|
registerSoundingsPreset: (draft, payloadAction) => {
|
|
3108
|
-
const
|
|
3441
|
+
const {
|
|
3442
|
+
preset
|
|
3443
|
+
} = payloadAction.payload;
|
|
3109
3444
|
if (preset) {
|
|
3110
3445
|
draft.selectedSources = preset.selectedSources ?? {};
|
|
3111
3446
|
draft.selectedLocations = preset.selectedLocations ?? {};
|
|
@@ -3151,7 +3486,7 @@ const slice = createSlice({
|
|
|
3151
3486
|
const {
|
|
3152
3487
|
order,
|
|
3153
3488
|
entities
|
|
3154
|
-
} = payloadAction.payload;
|
|
3489
|
+
} = payloadAction.payload.panels;
|
|
3155
3490
|
draft.soundingPanels.order = order;
|
|
3156
3491
|
draft.soundingPanels.entities = entities;
|
|
3157
3492
|
},
|
|
@@ -3160,13 +3495,86 @@ const slice = createSlice({
|
|
|
3160
3495
|
newOrderOfIds
|
|
3161
3496
|
} = payloadAction.payload;
|
|
3162
3497
|
draft.soundingPanels.order = newOrderOfIds;
|
|
3498
|
+
},
|
|
3499
|
+
toggleSoundingPanelVisibility: (draft, payloadAction) => {
|
|
3500
|
+
const {
|
|
3501
|
+
id
|
|
3502
|
+
} = payloadAction.payload;
|
|
3503
|
+
const panel = draft.soundingPanels.entities[id];
|
|
3504
|
+
if (panel) {
|
|
3505
|
+
panel.isDisplayed = !panel.isDisplayed;
|
|
3506
|
+
}
|
|
3163
3507
|
}
|
|
3164
3508
|
}
|
|
3165
3509
|
});
|
|
3166
|
-
const
|
|
3167
|
-
|
|
3168
|
-
actions
|
|
3169
|
-
|
|
3510
|
+
const setSoundingsCollections = createAction('soundings/setCollections');
|
|
3511
|
+
const soundingsActions = {
|
|
3512
|
+
...slice.actions,
|
|
3513
|
+
setSoundingsCollections
|
|
3514
|
+
};
|
|
3515
|
+
const shouldHandlePanelAction = (state, action) => {
|
|
3516
|
+
return isRegisteredPanelId(state, getPanelIdFromAction(action));
|
|
3517
|
+
};
|
|
3518
|
+
const normalizeSoundingsState = state => {
|
|
3519
|
+
if ('byId' in state) {
|
|
3520
|
+
return state;
|
|
3521
|
+
}
|
|
3522
|
+
return {
|
|
3523
|
+
...initialState,
|
|
3524
|
+
byId: {
|
|
3525
|
+
...initialState.byId,
|
|
3526
|
+
default: state
|
|
3527
|
+
},
|
|
3528
|
+
registeredPanelIds: ['default']
|
|
3529
|
+
};
|
|
3530
|
+
};
|
|
3531
|
+
const soundingsReducer = (state, action) => {
|
|
3532
|
+
const normalizedState = normalizeSoundingsState(state ?? initialState);
|
|
3533
|
+
if (action.type === setSoundingsCollections.type) {
|
|
3534
|
+
const collections = action.payload;
|
|
3535
|
+
if (collections === normalizedState.soundingsCollections) {
|
|
3536
|
+
return normalizedState;
|
|
3537
|
+
}
|
|
3538
|
+
return {
|
|
3539
|
+
...normalizedState,
|
|
3540
|
+
soundingsCollections: collections
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
if (action.type === slice.actions.registerSoundingsPanel.type) {
|
|
3544
|
+
const {
|
|
3545
|
+
panelId
|
|
3546
|
+
} = action.payload;
|
|
3547
|
+
if (!panelId) {
|
|
3548
|
+
return normalizedState;
|
|
3549
|
+
}
|
|
3550
|
+
if (normalizedState.registeredPanelIds?.includes(panelId)) {
|
|
3551
|
+
return normalizedState;
|
|
3552
|
+
}
|
|
3553
|
+
return {
|
|
3554
|
+
...normalizedState,
|
|
3555
|
+
registeredPanelIds: [...(normalizedState.registeredPanelIds ?? []), panelId]
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
if (!shouldHandlePanelAction(normalizedState, action)) {
|
|
3559
|
+
return normalizedState;
|
|
3560
|
+
}
|
|
3561
|
+
const panelId = getPanelIdFromAction(action);
|
|
3562
|
+
if (!panelId) {
|
|
3563
|
+
return normalizedState;
|
|
3564
|
+
}
|
|
3565
|
+
const currentPanelState = normalizedState.byId[panelId];
|
|
3566
|
+
const nextPanelState = slice.reducer(currentPanelState ?? initialSoundingsState, action);
|
|
3567
|
+
if (nextPanelState === currentPanelState) {
|
|
3568
|
+
return normalizedState;
|
|
3569
|
+
}
|
|
3570
|
+
return {
|
|
3571
|
+
...normalizedState,
|
|
3572
|
+
byId: {
|
|
3573
|
+
...normalizedState.byId,
|
|
3574
|
+
[panelId]: nextPanelState
|
|
3575
|
+
}
|
|
3576
|
+
};
|
|
3577
|
+
};
|
|
3170
3578
|
|
|
3171
3579
|
/* *
|
|
3172
3580
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -3185,7 +3593,16 @@ const {
|
|
|
3185
3593
|
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
3186
3594
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
3187
3595
|
* */
|
|
3188
|
-
const
|
|
3596
|
+
const emptySoundingsCollections = [];
|
|
3597
|
+
const getSoundingsStore = (store, panelId) => {
|
|
3598
|
+
const soundingsState = store?.soundings;
|
|
3599
|
+
if (!panelId || !isRegisteredPanelId(soundingsState, panelId)) {
|
|
3600
|
+
return initialSoundingsState;
|
|
3601
|
+
}
|
|
3602
|
+
const panelStore = soundingsState?.byId[panelId];
|
|
3603
|
+
return panelStore ?? initialSoundingsState;
|
|
3604
|
+
};
|
|
3605
|
+
const getSoundingsCollections = store => store?.soundings?.soundingsCollections || emptySoundingsCollections;
|
|
3189
3606
|
const getSourceSelections = createSelector(getSoundingsStore, store => {
|
|
3190
3607
|
return store.selectedSources;
|
|
3191
3608
|
}, selectorMemoizationOptions);
|
|
@@ -3204,6 +3621,7 @@ const getSelectedSources = createSelector(getSoundingsStore, store => {
|
|
|
3204
3621
|
instanceId: value.instanceId,
|
|
3205
3622
|
verticalRange: value.verticalRange,
|
|
3206
3623
|
parameterMappings: value.parameterMappings,
|
|
3624
|
+
windSpeedUnit: value.windSpeedUnit ?? 'ms',
|
|
3207
3625
|
name: value.name ?? ''
|
|
3208
3626
|
});
|
|
3209
3627
|
}
|
|
@@ -3238,14 +3656,15 @@ const getSoundingsPreset = createSelector(getSoundingsStore, soundingsStore => {
|
|
|
3238
3656
|
selectedSources: filteredSources
|
|
3239
3657
|
};
|
|
3240
3658
|
}, selectorMemoizationOptions);
|
|
3659
|
+
const EMPTY_SOUDINGS_PANELS = {
|
|
3660
|
+
order: [],
|
|
3661
|
+
entities: {}
|
|
3662
|
+
};
|
|
3241
3663
|
const getSoundingPanelsInOrder = createSelector(getSoundingsStore, store => {
|
|
3242
3664
|
const {
|
|
3243
3665
|
order = [],
|
|
3244
3666
|
entities = {}
|
|
3245
|
-
} = store.soundingPanels ||
|
|
3246
|
-
order: [],
|
|
3247
|
-
entities: {}
|
|
3248
|
-
};
|
|
3667
|
+
} = store.soundingPanels || EMPTY_SOUDINGS_PANELS;
|
|
3249
3668
|
return order.map(id => entities[id]);
|
|
3250
3669
|
}, selectorMemoizationOptions);
|
|
3251
3670
|
|
|
@@ -3259,6 +3678,7 @@ var selectors = /*#__PURE__*/Object.freeze({
|
|
|
3259
3678
|
getSelectedTime: getSelectedTime,
|
|
3260
3679
|
getShouldHideLocationMarkers: getShouldHideLocationMarkers,
|
|
3261
3680
|
getSoundingPanelsInOrder: getSoundingPanelsInOrder,
|
|
3681
|
+
getSoundingsCollections: getSoundingsCollections,
|
|
3262
3682
|
getSoundingsPreset: getSoundingsPreset,
|
|
3263
3683
|
getSourceSelections: getSourceSelections
|
|
3264
3684
|
});
|
|
@@ -3413,14 +3833,19 @@ const MenuBarConnect = ({
|
|
|
3413
3833
|
source
|
|
3414
3834
|
}) => {
|
|
3415
3835
|
const dispatch = useDispatch();
|
|
3836
|
+
React.useEffect(() => {
|
|
3837
|
+
dispatch(soundingsActions.registerSoundingsPanel({
|
|
3838
|
+
panelId: viewId
|
|
3839
|
+
}));
|
|
3840
|
+
}, [dispatch, viewId]);
|
|
3416
3841
|
const currentActiveMapIdSources = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
|
|
3417
3842
|
const currentActiveMapIdLocations = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsLocations));
|
|
3418
3843
|
const currentActiveMapIdsoundingsManager = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsManager));
|
|
3419
3844
|
const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
|
|
3420
3845
|
const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsLocations));
|
|
3421
3846
|
const isSoundingsManagerOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsManager));
|
|
3422
|
-
const sourcesAmount = useSelector(getSelectedSourcesAmount);
|
|
3423
|
-
const shouldHideLocationMarkers = useSelector(getShouldHideLocationMarkers);
|
|
3847
|
+
const sourcesAmount = useSelector(state => getSelectedSourcesAmount(state, viewId));
|
|
3848
|
+
const shouldHideLocationMarkers = useSelector(state => getShouldHideLocationMarkers(state, viewId));
|
|
3424
3849
|
const toggleLocationMarkers = React.useCallback(shouldHide => {
|
|
3425
3850
|
dispatch(soundingsActions.toggleLocationMarkers({
|
|
3426
3851
|
shouldHideLocationMarkers: shouldHide,
|
|
@@ -3450,7 +3875,7 @@ const MenuBarConnect = ({
|
|
|
3450
3875
|
const isOpen = {
|
|
3451
3876
|
[MenuOptions.sources]: currentActiveMapIdSources === viewId && isSourcesOpenInStore,
|
|
3452
3877
|
[MenuOptions.locations]: currentActiveMapIdLocations === viewId && isLocationsOpenInStore,
|
|
3453
|
-
[MenuOptions.soundingsManager]: isSoundingsManagerOpenInStore
|
|
3878
|
+
[MenuOptions.soundingsManager]: currentActiveMapIdsoundingsManager === viewId && isSoundingsManagerOpenInStore
|
|
3454
3879
|
};
|
|
3455
3880
|
return jsx(SoundingsMenuBar, {
|
|
3456
3881
|
handleClick: handleClick,
|
|
@@ -3600,8 +4025,8 @@ const roundToClosestTimestep = (timeToRound, availableSteps) => {
|
|
|
3600
4025
|
const SoundingsTimeSliderConnect = ({
|
|
3601
4026
|
viewId
|
|
3602
4027
|
}) => {
|
|
3603
|
-
const selectedTime = useSelector(getSelectedTime);
|
|
3604
|
-
const sourceSelections = useSelector(getSelectedSources);
|
|
4028
|
+
const selectedTime = useSelector(state => getSelectedTime(state, viewId));
|
|
4029
|
+
const sourceSelections = useSelector(state => getSelectedSources(state, viewId));
|
|
3605
4030
|
const dispatch = useDispatch();
|
|
3606
4031
|
const updateSelectedTime = React.useCallback((selectedTime, isInitialCorrection = false) => {
|
|
3607
4032
|
dispatch(soundingsActions.updateSelectedTime({
|
|
@@ -3673,12 +4098,18 @@ const SoundingsConnect = ({
|
|
|
3673
4098
|
const initialLocationsRef = React.useRef({});
|
|
3674
4099
|
const previousLocationsRef = React.useRef({});
|
|
3675
4100
|
React.useLayoutEffect(() => {
|
|
4101
|
+
dispatch(soundingsActions.registerSoundingsPanel({
|
|
4102
|
+
panelId: viewId
|
|
4103
|
+
}));
|
|
3676
4104
|
if (soundingsPreset && !isEqual(soundingsPreset, soundingsPresetRef.current)) {
|
|
3677
4105
|
soundingsPresetRef.current = soundingsPreset;
|
|
3678
4106
|
presetJustLoadedRef.current = true;
|
|
3679
4107
|
initialLocationsRef.current = soundingsPreset.selectedLocations || {};
|
|
3680
4108
|
previousLocationsRef.current = {};
|
|
3681
|
-
dispatch(soundingsActions.registerSoundingsPreset(
|
|
4109
|
+
dispatch(soundingsActions.registerSoundingsPreset({
|
|
4110
|
+
panelId: viewId,
|
|
4111
|
+
preset: soundingsPreset
|
|
4112
|
+
}));
|
|
3682
4113
|
setTimeout(() => {
|
|
3683
4114
|
presetJustLoadedRef.current = false;
|
|
3684
4115
|
initialLocationsRef.current = {};
|
|
@@ -3689,11 +4120,11 @@ const SoundingsConnect = ({
|
|
|
3689
4120
|
previousLocationsRef.current = {};
|
|
3690
4121
|
}
|
|
3691
4122
|
}, [dispatch, soundingsPreset, viewId]);
|
|
3692
|
-
const selectedTime = useSelector(getSelectedTime);
|
|
3693
|
-
const sourceSelections = useSelector(getSelectedSources, shallowEqual);
|
|
3694
|
-
const locationSelections = useSelector(getLocationSelections, shallowEqual);
|
|
3695
|
-
const diagramType = useSelector(getDiagramType);
|
|
3696
|
-
const shouldHideLocationMarkers = useSelector(getShouldHideLocationMarkers);
|
|
4123
|
+
const selectedTime = useSelector(state => getSelectedTime(state, viewId));
|
|
4124
|
+
const sourceSelections = useSelector(state => getSelectedSources(state, viewId), shallowEqual);
|
|
4125
|
+
const locationSelections = useSelector(state => getLocationSelections(state, viewId), shallowEqual);
|
|
4126
|
+
const diagramType = useSelector(state => getDiagramType(state, viewId));
|
|
4127
|
+
const shouldHideLocationMarkers = useSelector(state => getShouldHideLocationMarkers(state, viewId));
|
|
3697
4128
|
const setChosenDiagramType = React.useCallback(diagramType => {
|
|
3698
4129
|
dispatch(soundingsActions.updateDiagramType({
|
|
3699
4130
|
diagramType,
|
|
@@ -3925,11 +4356,13 @@ const SoundingsConnect = ({
|
|
|
3925
4356
|
data,
|
|
3926
4357
|
isPending
|
|
3927
4358
|
} = useMultipleSources(sourceSelections, locationSelections, selectedTime);
|
|
3928
|
-
const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store));
|
|
4359
|
+
const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store, viewId));
|
|
3929
4360
|
const changeOrder = newOrder => {
|
|
3930
4361
|
const orderedIds = newOrder.map(collection => collection.id);
|
|
3931
4362
|
dispatch(soundingsActions.changeSoundingPanelOrder({
|
|
3932
|
-
|
|
4363
|
+
panelId: viewId,
|
|
4364
|
+
newOrderOfIds: orderedIds,
|
|
4365
|
+
origin: SoundingsActionOrigin.soundingsComponent
|
|
3933
4366
|
}));
|
|
3934
4367
|
};
|
|
3935
4368
|
return jsxs(Fragment, {
|
|
@@ -4182,10 +4615,17 @@ const SoundingsSourceDialogConnect = ({
|
|
|
4182
4615
|
origin: SoundingsActionOrigin.soundingsComponent
|
|
4183
4616
|
}));
|
|
4184
4617
|
}, [dispatch, panelId]);
|
|
4185
|
-
const
|
|
4618
|
+
const soundingsCollectionsFromStore = useSelector(state => getSoundingsCollections(state));
|
|
4619
|
+
const collections = soundingsCollections ?? soundingsCollectionsFromStore;
|
|
4620
|
+
const sourceSelections = useSelector(state => getSourceSelections(state, panelId));
|
|
4186
4621
|
// Set mappings for each collection
|
|
4187
4622
|
React.useEffect(() => {
|
|
4188
|
-
soundingsCollections
|
|
4623
|
+
if (soundingsCollections && soundingsCollections.length > 0) {
|
|
4624
|
+
dispatch(soundingsActions.setSoundingsCollections(soundingsCollections));
|
|
4625
|
+
}
|
|
4626
|
+
}, [dispatch, soundingsCollections]);
|
|
4627
|
+
React.useEffect(() => {
|
|
4628
|
+
collections?.forEach(collection => {
|
|
4189
4629
|
const currentSelection = sourceSelections[collection.id];
|
|
4190
4630
|
if (currentSelection?.url && currentSelection?.parameterMappings) {
|
|
4191
4631
|
return;
|
|
@@ -4194,6 +4634,7 @@ const SoundingsSourceDialogConnect = ({
|
|
|
4194
4634
|
url: collection.url,
|
|
4195
4635
|
isChecked: currentSelection?.isChecked || false,
|
|
4196
4636
|
parameterMappings: collection.parameterMappings,
|
|
4637
|
+
windSpeedUnit: collection.windSpeedUnit,
|
|
4197
4638
|
...(collection.name && collection.name !== '' ? {
|
|
4198
4639
|
name: collection.name
|
|
4199
4640
|
} : {})
|
|
@@ -4201,16 +4642,17 @@ const SoundingsSourceDialogConnect = ({
|
|
|
4201
4642
|
dispatch(soundingsActions.updateSelectedSource({
|
|
4202
4643
|
sourceId: collection.id,
|
|
4203
4644
|
value: newValue,
|
|
4645
|
+
panelId,
|
|
4204
4646
|
origin: SoundingsActionOrigin.soundingsManager
|
|
4205
4647
|
}));
|
|
4206
4648
|
});
|
|
4207
4649
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4208
|
-
}, [dispatch,
|
|
4650
|
+
}, [dispatch, collections, panelId]);
|
|
4209
4651
|
const {
|
|
4210
4652
|
data,
|
|
4211
4653
|
isPending,
|
|
4212
4654
|
error
|
|
4213
|
-
} = useGetCollectionsInstances(
|
|
4655
|
+
} = useGetCollectionsInstances(collections, isDialogOpen);
|
|
4214
4656
|
return jsx(SoundingsSourceMenu, {
|
|
4215
4657
|
error: error,
|
|
4216
4658
|
handleClose: onCloseDialog,
|
|
@@ -4224,6 +4666,44 @@ const SoundingsSourceDialogConnect = ({
|
|
|
4224
4666
|
});
|
|
4225
4667
|
};
|
|
4226
4668
|
|
|
4669
|
+
const RowComponent = ({
|
|
4670
|
+
index,
|
|
4671
|
+
style,
|
|
4672
|
+
service,
|
|
4673
|
+
isChecked,
|
|
4674
|
+
updateSelectedLocation
|
|
4675
|
+
}) => {
|
|
4676
|
+
const location = service.features[index];
|
|
4677
|
+
return jsx("div", {
|
|
4678
|
+
style: style,
|
|
4679
|
+
children: jsx(ListItem, {
|
|
4680
|
+
disableGutters: true,
|
|
4681
|
+
disablePadding: true,
|
|
4682
|
+
children: jsxs(ListItemButton, {
|
|
4683
|
+
dense: true,
|
|
4684
|
+
onClick: () => {
|
|
4685
|
+
updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
|
|
4686
|
+
},
|
|
4687
|
+
children: [jsx(ListItemIcon, {
|
|
4688
|
+
children: jsx(Checkbox, {
|
|
4689
|
+
checked: isChecked(service.id, location.id),
|
|
4690
|
+
disableRipple: true,
|
|
4691
|
+
edge: "start",
|
|
4692
|
+
slotProps: {
|
|
4693
|
+
input: {
|
|
4694
|
+
'aria-labelledby': location.id
|
|
4695
|
+
}
|
|
4696
|
+
},
|
|
4697
|
+
tabIndex: -1
|
|
4698
|
+
})
|
|
4699
|
+
}), jsx(ListItemText, {
|
|
4700
|
+
id: location.id,
|
|
4701
|
+
primary: location.properties.name
|
|
4702
|
+
})]
|
|
4703
|
+
})
|
|
4704
|
+
})
|
|
4705
|
+
}, `${service.id}-${location.id}`);
|
|
4706
|
+
};
|
|
4227
4707
|
const LocationsMenu = ({
|
|
4228
4708
|
services,
|
|
4229
4709
|
selectedSourcesAmount,
|
|
@@ -4315,45 +4795,18 @@ const LocationsMenu = ({
|
|
|
4315
4795
|
}
|
|
4316
4796
|
},
|
|
4317
4797
|
title: service.name,
|
|
4318
|
-
children: jsx(
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
}
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
children: jsx(ListItem, {
|
|
4331
|
-
disableGutters: true,
|
|
4332
|
-
disablePadding: true,
|
|
4333
|
-
children: jsxs(ListItemButton, {
|
|
4334
|
-
dense: true,
|
|
4335
|
-
onClick: () => {
|
|
4336
|
-
updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
|
|
4337
|
-
},
|
|
4338
|
-
children: [jsx(ListItemIcon, {
|
|
4339
|
-
children: jsx(Checkbox, {
|
|
4340
|
-
checked: isChecked(service.id, location.id),
|
|
4341
|
-
disableRipple: true,
|
|
4342
|
-
edge: "start",
|
|
4343
|
-
slotProps: {
|
|
4344
|
-
input: {
|
|
4345
|
-
'aria-labelledby': location.id
|
|
4346
|
-
}
|
|
4347
|
-
},
|
|
4348
|
-
tabIndex: -1
|
|
4349
|
-
})
|
|
4350
|
-
}), jsx(ListItemText, {
|
|
4351
|
-
id: location.id,
|
|
4352
|
-
primary: location.properties.name
|
|
4353
|
-
})]
|
|
4354
|
-
})
|
|
4355
|
-
})
|
|
4356
|
-
}, `${service.id}-${location.id}`);
|
|
4798
|
+
children: jsx(List$1, {
|
|
4799
|
+
rowComponent: RowComponent,
|
|
4800
|
+
rowCount: service.features.length,
|
|
4801
|
+
rowHeight: 40,
|
|
4802
|
+
rowProps: {
|
|
4803
|
+
service,
|
|
4804
|
+
isChecked,
|
|
4805
|
+
updateSelectedLocation
|
|
4806
|
+
},
|
|
4807
|
+
style: {
|
|
4808
|
+
height: 320,
|
|
4809
|
+
width: '100%'
|
|
4357
4810
|
}
|
|
4358
4811
|
})
|
|
4359
4812
|
})
|
|
@@ -4372,14 +4825,22 @@ const LocationsMenuConnect = ({
|
|
|
4372
4825
|
dialogOrder,
|
|
4373
4826
|
setDialogOrder
|
|
4374
4827
|
} = useSetupDialog(uiTypes.DialogTypes.SoundingsLocations);
|
|
4375
|
-
const
|
|
4376
|
-
const
|
|
4377
|
-
const
|
|
4828
|
+
const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsLocations));
|
|
4829
|
+
const locationSelections = useSelector(state => getLocationSelections(state, panelId));
|
|
4830
|
+
const selectedSourceIds = useSelector(state => getSelectedSourcesIds(state, panelId));
|
|
4831
|
+
const sourcesAmount = useSelector(state => getSelectedSourcesAmount(state, panelId));
|
|
4832
|
+
const soundingsCollectionsFromStore = useSelector(state => getSoundingsCollections(state));
|
|
4833
|
+
const collections = soundingsCollections ?? soundingsCollectionsFromStore;
|
|
4834
|
+
React.useEffect(() => {
|
|
4835
|
+
if (soundingsCollections && soundingsCollections.length > 0) {
|
|
4836
|
+
dispatch(soundingsActions.setSoundingsCollections(soundingsCollections));
|
|
4837
|
+
}
|
|
4838
|
+
}, [dispatch, soundingsCollections]);
|
|
4378
4839
|
const {
|
|
4379
4840
|
data,
|
|
4380
4841
|
isPending,
|
|
4381
4842
|
error
|
|
4382
|
-
} = useGetCollectionLocations(
|
|
4843
|
+
} = useGetCollectionLocations(collections, isDialogOpen);
|
|
4383
4844
|
const services = useMemo(() => {
|
|
4384
4845
|
return data?.filter(service => selectedSourceIds.includes(service.id)) || [];
|
|
4385
4846
|
}, [data, selectedSourceIds]);
|
|
@@ -4396,37 +4857,18 @@ const LocationsMenuConnect = ({
|
|
|
4396
4857
|
updateSelectedLocation: (sourceId, locationId) => {
|
|
4397
4858
|
dispatch(soundingsActions.updateSelectedLocation({
|
|
4398
4859
|
sourceId,
|
|
4399
|
-
location: locationId
|
|
4860
|
+
location: locationId,
|
|
4861
|
+
panelId
|
|
4400
4862
|
}));
|
|
4401
4863
|
}
|
|
4402
4864
|
});
|
|
4403
4865
|
};
|
|
4404
4866
|
|
|
4405
|
-
var SoundingManagerDummyPreset = {
|
|
4406
|
-
order: [
|
|
4407
|
-
"soundings",
|
|
4408
|
-
"windbarbs"
|
|
4409
|
-
],
|
|
4410
|
-
entities: {
|
|
4411
|
-
soundings: {
|
|
4412
|
-
id: "soundings",
|
|
4413
|
-
name: "Soundings",
|
|
4414
|
-
isDisplayed: true
|
|
4415
|
-
},
|
|
4416
|
-
windbarbs: {
|
|
4417
|
-
id: "windbarbs",
|
|
4418
|
-
name: "Wind Barbs",
|
|
4419
|
-
isDisplayed: true
|
|
4420
|
-
}
|
|
4421
|
-
}
|
|
4422
|
-
};
|
|
4423
|
-
var preset = {
|
|
4424
|
-
SoundingManagerDummyPreset: SoundingManagerDummyPreset
|
|
4425
|
-
};
|
|
4426
|
-
|
|
4427
4867
|
const SoundingManagerListItem = ({
|
|
4868
|
+
id,
|
|
4428
4869
|
name,
|
|
4429
|
-
isDisplayed
|
|
4870
|
+
isDisplayed,
|
|
4871
|
+
onToggleVisibility
|
|
4430
4872
|
}) => {
|
|
4431
4873
|
return jsx(Card, {
|
|
4432
4874
|
className: "sortable-item",
|
|
@@ -4460,7 +4902,16 @@ const SoundingManagerListItem = ({
|
|
|
4460
4902
|
display: 'flex',
|
|
4461
4903
|
alignItems: 'center'
|
|
4462
4904
|
},
|
|
4463
|
-
children:
|
|
4905
|
+
children: jsx(IconButton, {
|
|
4906
|
+
"aria-label": "toggle-visibility",
|
|
4907
|
+
onClick: () => onToggleVisibility(id),
|
|
4908
|
+
size: "small",
|
|
4909
|
+
sx: {
|
|
4910
|
+
width: '24px',
|
|
4911
|
+
height: '24px'
|
|
4912
|
+
},
|
|
4913
|
+
children: isDisplayed ? jsx(Visibility, {}) : jsx(VisibilityOff, {})
|
|
4914
|
+
})
|
|
4464
4915
|
}), jsxs(Box, {
|
|
4465
4916
|
sx: {
|
|
4466
4917
|
paddingRight: '8px',
|
|
@@ -4478,7 +4929,8 @@ const SoundingsManager = ({
|
|
|
4478
4929
|
onMouseDown,
|
|
4479
4930
|
order,
|
|
4480
4931
|
soundingPanelsInOrder,
|
|
4481
|
-
onOrderChange
|
|
4932
|
+
onOrderChange,
|
|
4933
|
+
onToggleVisibility
|
|
4482
4934
|
}) => {
|
|
4483
4935
|
const sortableList = React.useMemo(() => soundingPanelsInOrder.map(collection => ({
|
|
4484
4936
|
...collection
|
|
@@ -4506,8 +4958,10 @@ const SoundingsManager = ({
|
|
|
4506
4958
|
name,
|
|
4507
4959
|
isDisplayed
|
|
4508
4960
|
}) => jsx(SoundingManagerListItem, {
|
|
4961
|
+
id: id,
|
|
4509
4962
|
isDisplayed: isDisplayed,
|
|
4510
|
-
name: name
|
|
4963
|
+
name: name,
|
|
4964
|
+
onToggleVisibility: onToggleVisibility
|
|
4511
4965
|
}, id))
|
|
4512
4966
|
})
|
|
4513
4967
|
});
|
|
@@ -4521,15 +4975,21 @@ const SoundingsManagerConnect = () => {
|
|
|
4521
4975
|
setDialogOrder
|
|
4522
4976
|
} = useSetupDialog(uiTypes.DialogTypes.SoundingsManager);
|
|
4523
4977
|
const dispatch = useDispatch();
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
dispatch(soundingsActions.addSoundingPanelsFromPreset(data));
|
|
4527
|
-
}, [dispatch]);
|
|
4528
|
-
const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store));
|
|
4978
|
+
const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsManager));
|
|
4979
|
+
const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store, panelId));
|
|
4529
4980
|
const changeOrder = newOrder => {
|
|
4530
4981
|
const orderedIds = newOrder.map(collection => collection.id);
|
|
4531
4982
|
dispatch(soundingsActions.changeSoundingPanelOrder({
|
|
4532
|
-
|
|
4983
|
+
panelId,
|
|
4984
|
+
newOrderOfIds: orderedIds,
|
|
4985
|
+
origin: 'soundingsManager'
|
|
4986
|
+
}));
|
|
4987
|
+
};
|
|
4988
|
+
const toggleVisibility = id => {
|
|
4989
|
+
dispatch(soundingsActions.toggleSoundingPanelVisibility({
|
|
4990
|
+
id,
|
|
4991
|
+
panelId,
|
|
4992
|
+
origin: 'soundingsManager'
|
|
4533
4993
|
}));
|
|
4534
4994
|
};
|
|
4535
4995
|
return jsx(SoundingsManager, {
|
|
@@ -4537,6 +4997,7 @@ const SoundingsManagerConnect = () => {
|
|
|
4537
4997
|
onClose: onCloseDialog,
|
|
4538
4998
|
onMouseDown: setDialogOrder,
|
|
4539
4999
|
onOrderChange: changeOrder,
|
|
5000
|
+
onToggleVisibility: toggleVisibility,
|
|
4540
5001
|
order: dialogOrder,
|
|
4541
5002
|
soundingPanelsInOrder: soundingPanelsInOrder
|
|
4542
5003
|
});
|