@opengeoweb/soundings 12.3.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 +1073 -660
- package/package.json +9 -7
- package/src/index.d.ts +1 -1
- package/src/lib/api/edr-api/api.d.ts +2 -3
- package/src/lib/api/edr-api/hooks.d.ts +14 -2
- 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 +6 -3
- package/src/lib/components/MenuBar/MenuBarConnect.integration.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/SoundingsSourceDialogConnect.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/SoundingsSourceMenu.d.ts +6 -2
- package/src/lib/components/Providers/Providers.d.ts +1 -1
- 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} +6 -4
- 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/WindBarbs/utils.d.ts +2 -7
- package/src/lib/components/WindBarbs/utils.spec.d.ts +1 -0
- package/src/lib/components/index.d.ts +1 -1
- package/src/lib/store/reducer.d.ts +2 -1
- package/src/lib/store/selectors.d.ts +21 -0
- package/src/lib/store/store.d.ts +7 -1
- package/src/lib/store/types.d.ts +5 -0
- package/src/lib/utils/soundingsLookUp.spec.d.ts +1 -0
- package/src/lib/components/SoundingsConnect.d.ts +0 -6
- package/src/lib/utils/conversions.d.ts +0 -11
- /package/src/lib/{components/Soundings.spec.d.ts → api/edr-api/hooks.spec.d.ts} +0 -0
- /package/src/lib/components/{Soundings.stories.d.ts → Soundings/Soundings.stories.d.ts} +0 -0
- /package/src/lib/{i18n.d.ts → utils/i18n.d.ts} +0 -0
package/index.esm.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import { ToolContainerDraggable, StyledSelect, dateUtils, CustomToggleButton, AlertBanner } from '@opengeoweb/shared';
|
|
7
|
+
import { useQueries, useQuery } from '@tanstack/react-query';
|
|
8
|
+
import { getHeaders, handleResponse } from '@opengeoweb/api';
|
|
9
|
+
import { timesliderTranslations, TIMESLIDER_NAMESPACE, BackwardForwardStepButton } from '@opengeoweb/timeslider';
|
|
8
10
|
import i18n from 'i18next';
|
|
9
11
|
export { default as i18n } from 'i18next';
|
|
10
|
-
import { useTranslation
|
|
11
|
-
import { getHeaders, handleResponse } from '@opengeoweb/api';
|
|
12
|
+
import { useTranslation } from 'react-i18next';
|
|
12
13
|
import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { selectorMemoizationOptions, uiSelectors, uiTypes, uiActions, useSetupDialog, storeReducerMap, storeMiddlewares } from '@opengeoweb/store';
|
|
15
|
+
import { Sources } from '@opengeoweb/theme';
|
|
16
|
+
import { FixedSizeList } from 'react-window';
|
|
15
17
|
|
|
16
18
|
var en = {
|
|
17
19
|
sources: "Sources",
|
|
@@ -108,7 +110,7 @@ const drawIsobars = ({
|
|
|
108
110
|
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
109
111
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
110
112
|
* */
|
|
111
|
-
const SOUNDING_COLORS = ['#4487f3'];
|
|
113
|
+
const SOUNDING_COLORS = ['#4487f3', '#c3213c', '#509a00'];
|
|
112
114
|
// vfx code
|
|
113
115
|
const initializeSvg = ({
|
|
114
116
|
svgRef,
|
|
@@ -118,7 +120,7 @@ const initializeSvg = ({
|
|
|
118
120
|
}) => {
|
|
119
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})`);
|
|
120
122
|
svg.append('clipPath').attr('id', 'clip-skewTd3').append('rect').attr('width', width).attr('height', height).attr('x', 0).attr('y', 0);
|
|
121
|
-
svg.append('rect').attr('height', height).attr('width', width).attr('fill', '
|
|
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)');
|
|
122
124
|
return svg;
|
|
123
125
|
};
|
|
124
126
|
const drawAxes = ({
|
|
@@ -165,14 +167,139 @@ const plotSounding = ({
|
|
|
165
167
|
height,
|
|
166
168
|
svg
|
|
167
169
|
}) => {
|
|
170
|
+
const [filteredPressureValues, filteredTemperatureValues] = filterDomainRangeForParameter(pressureValues, temperatureValues);
|
|
168
171
|
// Map temp and pressure to pixels
|
|
169
|
-
const Px = d3.zip(
|
|
172
|
+
const Px = d3.zip(filteredPressureValues, filteredTemperatureValues).map(([pressure, temperature]) => [xScale(temperature), yScale(pressure)]);
|
|
170
173
|
// Skew/shear transformation for skew-t
|
|
171
174
|
const skewedPx = Px.map(([x, y]) => [x + height - y, y]);
|
|
172
175
|
const lineGenerator = d3.line();
|
|
173
176
|
const pathString = lineGenerator(skewedPx);
|
|
174
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)');
|
|
175
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
|
+
};
|
|
176
303
|
|
|
177
304
|
/* *
|
|
178
305
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -542,7 +669,7 @@ const drawWindBarbs = ({
|
|
|
542
669
|
const barbsSize = 25;
|
|
543
670
|
const scalingFactor = 0.5;
|
|
544
671
|
const horizontalBarbMargin = barbsSize * scalingFactor;
|
|
545
|
-
const visibleItems =
|
|
672
|
+
const visibleItems = removeItemsExponential(data.reverse()).filter(item => item.pressure >= 150);
|
|
546
673
|
// Measure width and height and make extra room for one column
|
|
547
674
|
// of windbarbs
|
|
548
675
|
const svg = d3.select(svgRef.current);
|
|
@@ -562,7 +689,7 @@ const drawWindBarbs = ({
|
|
|
562
689
|
const newD = extendXAxis(path, horizontalBarbMargin);
|
|
563
690
|
d3.select(this).attr('d', newD);
|
|
564
691
|
});
|
|
565
|
-
svg.attr('width', currentWidth + margin.
|
|
692
|
+
svg.attr('width', currentWidth + margin.left + horizontalBarbMargin);
|
|
566
693
|
const pressureScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]).clamp(true);
|
|
567
694
|
const barbGroup = svg.append('g').attr('class', 'wind-group').attr('transform',
|
|
568
695
|
// This translation should be revisited when implementing
|
|
@@ -600,45 +727,14 @@ const drawWindBarbs = ({
|
|
|
600
727
|
}
|
|
601
728
|
});
|
|
602
729
|
};
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
EXPONENTIAL: 'EXPONENTIAL',
|
|
606
|
-
CUBIC: 'CUBIC'
|
|
607
|
-
};
|
|
608
|
-
const removeItemsByGrowthType = (array, growthType = 'LINEAR', base = 2, initial = 1) => {
|
|
609
|
-
if (!Object.values(GrowthType).includes(growthType)) {
|
|
610
|
-
throw new Error('Unsupported growth type');
|
|
611
|
-
}
|
|
612
|
-
if (!Array.isArray(array) || array.length === 0) {
|
|
613
|
-
return [];
|
|
614
|
-
}
|
|
615
|
-
if (growthType === GrowthType.LINEAR && base === 0) {
|
|
616
|
-
throw new Error('Base must be greater than 0');
|
|
617
|
-
}
|
|
618
|
-
if (growthType === GrowthType.EXPONENTIAL && base <= 1) {
|
|
619
|
-
throw new Error('Base must be greater than 1');
|
|
620
|
-
}
|
|
621
|
-
if (growthType === GrowthType.CUBIC && base <= 0) {
|
|
622
|
-
throw new Error('Base must be greater than 0');
|
|
623
|
-
}
|
|
730
|
+
const removeItemsExponential = array => {
|
|
731
|
+
const base = 1.15;
|
|
624
732
|
const indicesToKeep = [];
|
|
625
733
|
let i = 0;
|
|
626
734
|
let nextIndex = 0;
|
|
627
735
|
while (nextIndex < array.length) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
nextIndex = initial + i * base;
|
|
631
|
-
break;
|
|
632
|
-
case GrowthType.EXPONENTIAL:
|
|
633
|
-
nextIndex = Math.floor(initial * base ** i);
|
|
634
|
-
break;
|
|
635
|
-
case GrowthType.CUBIC:
|
|
636
|
-
nextIndex = initial + Math.floor(i ** 3 * base);
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
if (nextIndex < array.length) {
|
|
640
|
-
indicesToKeep.push(nextIndex);
|
|
641
|
-
}
|
|
736
|
+
nextIndex = Math.floor(base ** i);
|
|
737
|
+
indicesToKeep.push(nextIndex);
|
|
642
738
|
i += 1;
|
|
643
739
|
}
|
|
644
740
|
return array.filter((_, index) => indicesToKeep.includes(index));
|
|
@@ -966,14 +1062,58 @@ const drawStaticLines = ({
|
|
|
966
1062
|
});
|
|
967
1063
|
};
|
|
968
1064
|
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
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'
|
|
972
1083
|
},
|
|
973
|
-
|
|
974
|
-
|
|
1084
|
+
dewPoint: {
|
|
1085
|
+
fontSize: '12px',
|
|
1086
|
+
fontWeight: 'bold'
|
|
1087
|
+
},
|
|
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
|
|
975
1109
|
}) => {
|
|
976
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
|
|
977
1117
|
const svgDimensionsOptions = {
|
|
978
1118
|
width: svgDimensions?.width || 812,
|
|
979
1119
|
height: svgDimensions?.height || 802,
|
|
@@ -985,12 +1125,7 @@ const Soundings = ({
|
|
|
985
1125
|
}
|
|
986
1126
|
};
|
|
987
1127
|
// Set SVG dimensions
|
|
988
|
-
const margin =
|
|
989
|
-
top: svgDimensionsOptions?.margin?.top,
|
|
990
|
-
right: svgDimensionsOptions?.margin?.right,
|
|
991
|
-
bottom: svgDimensionsOptions?.margin?.bottom,
|
|
992
|
-
left: svgDimensionsOptions?.margin?.left
|
|
993
|
-
};
|
|
1128
|
+
const margin = svgDimensionsOptions?.margin;
|
|
994
1129
|
const baseWidth = svgDimensionsOptions?.width;
|
|
995
1130
|
const baseHeight = svgDimensionsOptions?.height;
|
|
996
1131
|
const width = baseWidth - (margin.left + margin.right);
|
|
@@ -1006,18 +1141,7 @@ const Soundings = ({
|
|
|
1006
1141
|
height,
|
|
1007
1142
|
margin
|
|
1008
1143
|
});
|
|
1009
|
-
|
|
1010
|
-
const maxPressure = 1050;
|
|
1011
|
-
const minPressure = 150;
|
|
1012
|
-
const staticPressureArray = [maxPressure];
|
|
1013
|
-
const dp = -5;
|
|
1014
|
-
const minTemperature = -40;
|
|
1015
|
-
const maxTemperature = 40.6;
|
|
1016
|
-
let pressure = maxPressure;
|
|
1017
|
-
while (pressure > minPressure) {
|
|
1018
|
-
pressure += dp;
|
|
1019
|
-
staticPressureArray.push(pressure);
|
|
1020
|
-
}
|
|
1144
|
+
const svgRectangle = svgRef.current.getBoundingClientRect();
|
|
1021
1145
|
// Set scales for temperature and pressure
|
|
1022
1146
|
const xScale = d3.scaleLinear().domain([minTemperature, maxTemperature]).range([0, width]);
|
|
1023
1147
|
const yScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]);
|
|
@@ -1047,217 +1171,157 @@ const Soundings = ({
|
|
|
1047
1171
|
svg,
|
|
1048
1172
|
width
|
|
1049
1173
|
});
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
+
});
|
|
1089
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
|
+
}
|
|
1090
1239
|
const svgRefCur = svgRef.current;
|
|
1091
1240
|
// eslint-disable-next-line consistent-return
|
|
1092
1241
|
return () => {
|
|
1093
1242
|
d3.select(svgRefCur).selectAll('.wind-group').remove();
|
|
1094
1243
|
svg.selectAll('*').remove();
|
|
1095
|
-
|
|
1244
|
+
svg.on('mousemove', null);
|
|
1096
1245
|
};
|
|
1097
1246
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1098
|
-
}, [
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
/* *
|
|
1107
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1108
|
-
* you may not use this file except in compliance with the License.
|
|
1109
|
-
* You may obtain a copy of the License at
|
|
1110
|
-
*
|
|
1111
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1112
|
-
*
|
|
1113
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1114
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1115
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1116
|
-
* See the License for the specific language governing permissions and
|
|
1117
|
-
* limitations under the License.
|
|
1118
|
-
*
|
|
1119
|
-
* Copyright 2023 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1120
|
-
* Copyright 2023 - Finnish Meteorological Institute (FMI)
|
|
1121
|
-
* Copyright 2024 - The Norwegian Meteorological Institute (MET Norway)
|
|
1122
|
-
* */
|
|
1123
|
-
const SOUNDINGS_NAMESPACE = 'soundings';
|
|
1124
|
-
const defaultLanguage = 'en';
|
|
1125
|
-
const initSoundingsI18n = () => {
|
|
1126
|
-
void i18n.use(initReactI18next).init({
|
|
1127
|
-
lng: defaultLanguage,
|
|
1128
|
-
ns: SOUNDINGS_NAMESPACE,
|
|
1129
|
-
interpolation: {
|
|
1130
|
-
escapeValue: false
|
|
1131
|
-
},
|
|
1132
|
-
resources: {
|
|
1133
|
-
en: {
|
|
1134
|
-
[SOUNDINGS_NAMESPACE]: soundingsTranslations.en
|
|
1135
|
-
},
|
|
1136
|
-
nl: {
|
|
1137
|
-
[SOUNDINGS_NAMESPACE]: soundingsTranslations.nl
|
|
1138
|
-
},
|
|
1139
|
-
fi: {
|
|
1140
|
-
[SOUNDINGS_NAMESPACE]: soundingsTranslations.fi
|
|
1141
|
-
},
|
|
1142
|
-
no: {
|
|
1143
|
-
[SOUNDINGS_NAMESPACE]: soundingsTranslations.no
|
|
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';
|
|
1144
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);
|
|
1145
1266
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
isOpen = false,
|
|
1157
|
-
handleClose,
|
|
1158
|
-
updateCollection,
|
|
1159
|
-
sourceSelections
|
|
1160
|
-
}) => {
|
|
1161
|
-
const {
|
|
1162
|
-
t
|
|
1163
|
-
} = useSoundingsTranslation();
|
|
1164
|
-
const handleSelectionChange = (collectionId, update) => {
|
|
1165
|
-
updateCollection(collectionId, update);
|
|
1166
|
-
};
|
|
1167
|
-
const getDisplayValue = instanceId => {
|
|
1168
|
-
if (!instanceId) {
|
|
1169
|
-
return t('noInstancesAvailable');
|
|
1267
|
+
return () => {
|
|
1268
|
+
if (canvasElement) {
|
|
1269
|
+
canvasElement.removeEventListener('mouseleave', handleMouseLeave);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
}, []);
|
|
1273
|
+
useEffect(() => {
|
|
1274
|
+
// Reset the tooltips when filteredData changes
|
|
1275
|
+
if (pressureRef.current) {
|
|
1276
|
+
pressureRef.current.style.display = 'none';
|
|
1170
1277
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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'
|
|
1177
1293
|
},
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
bounds: "parent",
|
|
1183
|
-
children: jsx(List, {
|
|
1294
|
+
children: [jsx("svg", {
|
|
1295
|
+
ref: svgRef
|
|
1296
|
+
}), jsx(Box, {
|
|
1297
|
+
ref: pressureRef,
|
|
1184
1298
|
sx: {
|
|
1185
|
-
|
|
1299
|
+
...soundingsStyle.tooltip,
|
|
1300
|
+
...soundingsStyle.pressure
|
|
1186
1301
|
},
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
additionalStyling: {
|
|
1211
|
-
width: 'calc(100% - 24px)'
|
|
1212
|
-
},
|
|
1213
|
-
skipLocalStyling: false,
|
|
1214
|
-
value: selectedInstanceId,
|
|
1215
|
-
onChange: event => handleSelectionChange(collection.id, {
|
|
1216
|
-
instanceId: event.target.value
|
|
1217
|
-
}),
|
|
1218
|
-
renderValue: value => jsx(Box, {
|
|
1219
|
-
sx: {
|
|
1220
|
-
textOverflow: 'ellipsis',
|
|
1221
|
-
overflow: 'hidden'
|
|
1222
|
-
},
|
|
1223
|
-
children: jsxs(Box, {
|
|
1224
|
-
display: "flex",
|
|
1225
|
-
children: [jsx(Box, {
|
|
1226
|
-
sx: {
|
|
1227
|
-
minWidth: 200
|
|
1228
|
-
},
|
|
1229
|
-
children: collection.name
|
|
1230
|
-
}), jsx(Box, {
|
|
1231
|
-
sx: {
|
|
1232
|
-
textOverflow: 'ellipsis',
|
|
1233
|
-
overflow: 'hidden'
|
|
1234
|
-
},
|
|
1235
|
-
children: getDisplayValue(value)
|
|
1236
|
-
})]
|
|
1237
|
-
})
|
|
1238
|
-
}),
|
|
1239
|
-
children: [!hasInstances && jsx(MenuItem, {
|
|
1240
|
-
value: "",
|
|
1241
|
-
disabled: true,
|
|
1242
|
-
sx: {
|
|
1243
|
-
fontSize: 12
|
|
1244
|
-
},
|
|
1245
|
-
children: t('noInstancesAvailable')
|
|
1246
|
-
}), collection.instances.map(instance => jsx(MenuItem, {
|
|
1247
|
-
value: instance.id,
|
|
1248
|
-
children: getDisplayValue(instance.id)
|
|
1249
|
-
}, instance.id))]
|
|
1250
|
-
})]
|
|
1251
|
-
}, collection.id);
|
|
1252
|
-
}) : jsx(ListItem, {
|
|
1253
|
-
children: jsx(ListItemText, {
|
|
1254
|
-
primary: t('noServicesFound'),
|
|
1255
|
-
sx: {
|
|
1256
|
-
pl: 4
|
|
1257
|
-
}
|
|
1258
|
-
})
|
|
1259
|
-
})
|
|
1260
|
-
})
|
|
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
|
+
})]
|
|
1261
1325
|
});
|
|
1262
1326
|
};
|
|
1263
1327
|
|
|
@@ -1282,7 +1346,7 @@ const BASE_URL = 'https://opendata.fmi.fi/edr';
|
|
|
1282
1346
|
const getCollectionLocations = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/locations`, {
|
|
1283
1347
|
headers: getHeaders()
|
|
1284
1348
|
}).then(handleResponse);
|
|
1285
|
-
const getCollectionInstance =
|
|
1349
|
+
const getCollectionInstance = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/instances`, {
|
|
1286
1350
|
headers: getHeaders()
|
|
1287
1351
|
}).then(handleResponse);
|
|
1288
1352
|
const getEdrCollectionForLocation = (collectionId, instanceId, locationId, parameters, datetime) => {
|
|
@@ -1305,344 +1369,6 @@ const getEdrCollectionForPosition = (collectionId, instanceId, coordinates, para
|
|
|
1305
1369
|
}).then(handleResponse);
|
|
1306
1370
|
};
|
|
1307
1371
|
|
|
1308
|
-
/* *
|
|
1309
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1310
|
-
* you may not use this file except in compliance with the License.
|
|
1311
|
-
* You may obtain a copy of the License at
|
|
1312
|
-
*
|
|
1313
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1314
|
-
*
|
|
1315
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1316
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1317
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1318
|
-
* See the License for the specific language governing permissions and
|
|
1319
|
-
* limitations under the License.
|
|
1320
|
-
*
|
|
1321
|
-
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1322
|
-
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1323
|
-
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1324
|
-
* */
|
|
1325
|
-
const initialSoundingsState = {
|
|
1326
|
-
selectedSources: {},
|
|
1327
|
-
selectedLocations: {}
|
|
1328
|
-
};
|
|
1329
|
-
const slice = createSlice({
|
|
1330
|
-
initialState: initialSoundingsState,
|
|
1331
|
-
name: 'soundings',
|
|
1332
|
-
reducers: {
|
|
1333
|
-
updateSelectedSource: (draft, payloadAction) => {
|
|
1334
|
-
const {
|
|
1335
|
-
sourceId,
|
|
1336
|
-
value
|
|
1337
|
-
} = payloadAction.payload;
|
|
1338
|
-
draft.selectedSources[sourceId] = {
|
|
1339
|
-
...draft.selectedSources[sourceId],
|
|
1340
|
-
...value
|
|
1341
|
-
};
|
|
1342
|
-
},
|
|
1343
|
-
updateSelectedLocation: (draft, payloadAction) => {
|
|
1344
|
-
const {
|
|
1345
|
-
sourceId,
|
|
1346
|
-
locationId
|
|
1347
|
-
} = payloadAction.payload;
|
|
1348
|
-
draft.selectedLocations[sourceId] = locationId;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
const {
|
|
1353
|
-
reducer: soundingsReducer,
|
|
1354
|
-
actions: soundingsActions
|
|
1355
|
-
} = slice;
|
|
1356
|
-
|
|
1357
|
-
/* *
|
|
1358
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1359
|
-
* you may not use this file except in compliance with the License.
|
|
1360
|
-
* You may obtain a copy of the License at
|
|
1361
|
-
*
|
|
1362
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1363
|
-
*
|
|
1364
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1365
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1366
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1367
|
-
* See the License for the specific language governing permissions and
|
|
1368
|
-
* limitations under the License.
|
|
1369
|
-
*
|
|
1370
|
-
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1371
|
-
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1372
|
-
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1373
|
-
* */
|
|
1374
|
-
const getSoundingsStore = store => store?.soundings || initialSoundingsState;
|
|
1375
|
-
const getSourceSelections = createSelector(getSoundingsStore, store => {
|
|
1376
|
-
return store.selectedSources;
|
|
1377
|
-
}, selectorMemoizationOptions);
|
|
1378
|
-
const getLocationSelections = createSelector(getSoundingsStore, store => store.selectedLocations, selectorMemoizationOptions);
|
|
1379
|
-
const getSelectedSources = createSelector(getSoundingsStore, store => {
|
|
1380
|
-
const sources = store.selectedSources;
|
|
1381
|
-
const activeSources = Object.keys(sources).reduce((list, key) => {
|
|
1382
|
-
const value = sources[key];
|
|
1383
|
-
if (value.isChecked === true) {
|
|
1384
|
-
return list.concat({
|
|
1385
|
-
collectionId: key,
|
|
1386
|
-
instanceId: value.instanceId
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
return list;
|
|
1390
|
-
}, []);
|
|
1391
|
-
return activeSources;
|
|
1392
|
-
}, selectorMemoizationOptions);
|
|
1393
|
-
const getSelectedSourcesAmount = createSelector(getSelectedSources, activeSources => {
|
|
1394
|
-
return activeSources.length;
|
|
1395
|
-
}, selectorMemoizationOptions);
|
|
1396
|
-
|
|
1397
|
-
const SoundingsSourceDialogConnect = ({
|
|
1398
|
-
soundingsCollections
|
|
1399
|
-
}) => {
|
|
1400
|
-
const [soundingsCollectionsWithInstances, setSoundingsCollectionsWithInstances] = React.useState();
|
|
1401
|
-
const dispatch = useDispatch();
|
|
1402
|
-
const {
|
|
1403
|
-
isDialogOpen,
|
|
1404
|
-
onCloseDialog
|
|
1405
|
-
} = useSetupDialog(uiTypes.DialogTypes.SoundingsSources);
|
|
1406
|
-
const updateCollection = React.useCallback((sourceId, value) => {
|
|
1407
|
-
dispatch(soundingsActions.updateSelectedSource({
|
|
1408
|
-
sourceId,
|
|
1409
|
-
value
|
|
1410
|
-
}));
|
|
1411
|
-
}, [dispatch]);
|
|
1412
|
-
const sourceSelections = useSelector(getSourceSelections);
|
|
1413
|
-
React.useEffect(() => {
|
|
1414
|
-
if (soundingsCollections && isDialogOpen) {
|
|
1415
|
-
try {
|
|
1416
|
-
void Promise.all(soundingsCollections.map(async collection => {
|
|
1417
|
-
const data = await getCollectionInstance(collection.url, collection.id);
|
|
1418
|
-
return {
|
|
1419
|
-
...collection,
|
|
1420
|
-
instances: data.instances.reverse()
|
|
1421
|
-
}; // Reverse to get latest first
|
|
1422
|
-
})).then(results => setSoundingsCollectionsWithInstances(results));
|
|
1423
|
-
} catch (error) {
|
|
1424
|
-
console.error('Error fetching soundings instances:', error);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}, [soundingsCollections, isDialogOpen]);
|
|
1428
|
-
return jsx(SoundingsSourceMenu, {
|
|
1429
|
-
soundingsCollections: soundingsCollectionsWithInstances,
|
|
1430
|
-
isOpen: isDialogOpen,
|
|
1431
|
-
updateCollection: (sourceId, value) => updateCollection(sourceId, value),
|
|
1432
|
-
sourceSelections: sourceSelections,
|
|
1433
|
-
handleClose: () => onCloseDialog()
|
|
1434
|
-
});
|
|
1435
|
-
};
|
|
1436
|
-
|
|
1437
|
-
const LocationsMenu = ({
|
|
1438
|
-
services,
|
|
1439
|
-
selectedLocations,
|
|
1440
|
-
isOpen,
|
|
1441
|
-
onClose,
|
|
1442
|
-
updateSelectedLocation
|
|
1443
|
-
}) => {
|
|
1444
|
-
const [collapseMap, setCollapseMap] = useState({});
|
|
1445
|
-
const {
|
|
1446
|
-
t
|
|
1447
|
-
} = useSoundingsTranslation();
|
|
1448
|
-
const isChecked = (sourceId, locationId) => selectedLocations[sourceId] === locationId;
|
|
1449
|
-
return jsx(ToolContainerDraggable, {
|
|
1450
|
-
startSize: {
|
|
1451
|
-
width: 320,
|
|
1452
|
-
height: 500
|
|
1453
|
-
},
|
|
1454
|
-
title: t('locations'),
|
|
1455
|
-
isOpen: isOpen,
|
|
1456
|
-
onClose: () => onClose(),
|
|
1457
|
-
bounds: "parent",
|
|
1458
|
-
children: jsxs(List, {
|
|
1459
|
-
disablePadding: true,
|
|
1460
|
-
sx: {
|
|
1461
|
-
maxHeight: '100%',
|
|
1462
|
-
overflowY: 'auto'
|
|
1463
|
-
},
|
|
1464
|
-
children: [services.length === 0 && jsx(Typography, {
|
|
1465
|
-
sx: {
|
|
1466
|
-
marginLeft: 1
|
|
1467
|
-
},
|
|
1468
|
-
children: t('locations-menu-warning')
|
|
1469
|
-
}), services.map(service => jsxs(Fragment, {
|
|
1470
|
-
children: [jsx(ListSubheader, {
|
|
1471
|
-
disableGutters: true,
|
|
1472
|
-
sx: {
|
|
1473
|
-
borderBottom: '1px solid'
|
|
1474
|
-
},
|
|
1475
|
-
children: jsxs(ListItemButton, {
|
|
1476
|
-
onClick: () => setCollapseMap({
|
|
1477
|
-
...collapseMap,
|
|
1478
|
-
[service.id]: !collapseMap[service.id]
|
|
1479
|
-
}),
|
|
1480
|
-
children: [jsx(ListItemText, {
|
|
1481
|
-
children: service.id
|
|
1482
|
-
}), jsx(ListItemIcon, {
|
|
1483
|
-
children: jsx(ChevronDown, {})
|
|
1484
|
-
})]
|
|
1485
|
-
})
|
|
1486
|
-
}), jsx(Collapse, {
|
|
1487
|
-
in: collapseMap[service.id],
|
|
1488
|
-
children: jsx(List, {
|
|
1489
|
-
component: "div",
|
|
1490
|
-
disablePadding: true,
|
|
1491
|
-
sx: {
|
|
1492
|
-
overflowY: 'auto',
|
|
1493
|
-
overflow: 'hidden'
|
|
1494
|
-
},
|
|
1495
|
-
children: service.features?.map(location => jsx(ListItem, {
|
|
1496
|
-
disablePadding: true,
|
|
1497
|
-
disableGutters: true,
|
|
1498
|
-
children: jsxs(ListItemButton, {
|
|
1499
|
-
onClick: () => {
|
|
1500
|
-
updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
|
|
1501
|
-
},
|
|
1502
|
-
dense: true,
|
|
1503
|
-
children: [jsx(ListItemIcon, {
|
|
1504
|
-
children: jsx(Checkbox, {
|
|
1505
|
-
edge: "start",
|
|
1506
|
-
checked: isChecked(service.id, location.id),
|
|
1507
|
-
tabIndex: -1,
|
|
1508
|
-
disableRipple: true,
|
|
1509
|
-
inputProps: {
|
|
1510
|
-
'aria-labelledby': location.id
|
|
1511
|
-
}
|
|
1512
|
-
})
|
|
1513
|
-
}), jsx(ListItemText, {
|
|
1514
|
-
id: location.id,
|
|
1515
|
-
primary: location.properties.name
|
|
1516
|
-
})]
|
|
1517
|
-
})
|
|
1518
|
-
}, `${service.id}-${location.id}`))
|
|
1519
|
-
})
|
|
1520
|
-
})]
|
|
1521
|
-
}, service.id))]
|
|
1522
|
-
})
|
|
1523
|
-
});
|
|
1524
|
-
};
|
|
1525
|
-
|
|
1526
|
-
const LocationsMenuConnect = ({
|
|
1527
|
-
soundingsCollections
|
|
1528
|
-
}) => {
|
|
1529
|
-
const dispatch = useDispatch();
|
|
1530
|
-
const {
|
|
1531
|
-
isDialogOpen,
|
|
1532
|
-
onCloseDialog
|
|
1533
|
-
} = useSetupDialog(uiTypes.DialogTypes.soundingsLocations);
|
|
1534
|
-
const locationSelections = useSelector(getLocationSelections);
|
|
1535
|
-
const sourceSelections = useSelector(getSourceSelections);
|
|
1536
|
-
const selectedSourceIds = useMemo(() => Object.keys(sourceSelections).filter(key => sourceSelections[key].isChecked), [sourceSelections]);
|
|
1537
|
-
const edrQueries = useMemo(() => soundingsCollections.map(collection => ({
|
|
1538
|
-
queryKey: ['edr-locations', collection.id],
|
|
1539
|
-
queryFn: async () => {
|
|
1540
|
-
const locations = await getCollectionLocations(collection.id);
|
|
1541
|
-
return {
|
|
1542
|
-
...locations,
|
|
1543
|
-
id: collection.id
|
|
1544
|
-
};
|
|
1545
|
-
}
|
|
1546
|
-
})), [soundingsCollections]);
|
|
1547
|
-
const services = useQueries({
|
|
1548
|
-
queries: edrQueries,
|
|
1549
|
-
combine: results => results.map(result => result.data).filter(result => !!result)
|
|
1550
|
-
});
|
|
1551
|
-
return jsx(LocationsMenu, {
|
|
1552
|
-
isOpen: isDialogOpen,
|
|
1553
|
-
onClose: onCloseDialog,
|
|
1554
|
-
services: services.filter(service => selectedSourceIds.includes(service.id)),
|
|
1555
|
-
selectedLocations: locationSelections,
|
|
1556
|
-
updateSelectedLocation: (sourceId, locationId) => void dispatch(soundingsActions.updateSelectedLocation({
|
|
1557
|
-
sourceId,
|
|
1558
|
-
locationId
|
|
1559
|
-
}))
|
|
1560
|
-
});
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
var MenuOptions;
|
|
1564
|
-
(function (MenuOptions) {
|
|
1565
|
-
MenuOptions["sources"] = "sources";
|
|
1566
|
-
MenuOptions["locations"] = "locations";
|
|
1567
|
-
})(MenuOptions || (MenuOptions = {}));
|
|
1568
|
-
const buttonStyle = {
|
|
1569
|
-
height: '10px !important',
|
|
1570
|
-
width: '130px'
|
|
1571
|
-
};
|
|
1572
|
-
const SoundingsMenuBar = ({
|
|
1573
|
-
handleClick,
|
|
1574
|
-
isOpen,
|
|
1575
|
-
selectedSourcesAmount = 0
|
|
1576
|
-
}) => {
|
|
1577
|
-
const {
|
|
1578
|
-
t
|
|
1579
|
-
} = useSoundingsTranslation();
|
|
1580
|
-
return jsxs(Stack, {
|
|
1581
|
-
direction: "row",
|
|
1582
|
-
spacing: 1,
|
|
1583
|
-
sx: {
|
|
1584
|
-
marginBottom: '4px'
|
|
1585
|
-
},
|
|
1586
|
-
children: [jsxs(CustomToggleButton, {
|
|
1587
|
-
variant: "tool",
|
|
1588
|
-
selected: isOpen[MenuOptions.sources],
|
|
1589
|
-
"data-testid": "soundingsSourcesButton",
|
|
1590
|
-
onClick: () => handleClick(MenuOptions.sources),
|
|
1591
|
-
size: "small",
|
|
1592
|
-
sx: buttonStyle,
|
|
1593
|
-
children: [jsx(Sources, {
|
|
1594
|
-
sx: {
|
|
1595
|
-
marginRight: '4px!important'
|
|
1596
|
-
}
|
|
1597
|
-
}), selectedSourcesAmount ? `${t('sources')} (${selectedSourcesAmount})` : t('sources')]
|
|
1598
|
-
}), jsx(CustomToggleButton, {
|
|
1599
|
-
variant: "tool",
|
|
1600
|
-
"data-testid": "soundingsLocationsButton",
|
|
1601
|
-
selected: isOpen[MenuOptions.locations],
|
|
1602
|
-
onClick: () => handleClick(MenuOptions.locations),
|
|
1603
|
-
size: "small",
|
|
1604
|
-
sx: buttonStyle,
|
|
1605
|
-
children: t('locations')
|
|
1606
|
-
})]
|
|
1607
|
-
});
|
|
1608
|
-
};
|
|
1609
|
-
|
|
1610
|
-
const MenuBarConnect = ({
|
|
1611
|
-
viewId,
|
|
1612
|
-
source
|
|
1613
|
-
}) => {
|
|
1614
|
-
const dispatch = useDispatch();
|
|
1615
|
-
const currentActiveMapId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
|
|
1616
|
-
const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
|
|
1617
|
-
const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.soundingsLocations));
|
|
1618
|
-
const sourcesAmount = useSelector(getSelectedSourcesAmount);
|
|
1619
|
-
const dispatchActiveMapId = React.useCallback((dialogType, isOpen) => void dispatch(uiActions.setActiveMapIdForDialog({
|
|
1620
|
-
type: dialogType,
|
|
1621
|
-
mapId: viewId,
|
|
1622
|
-
setOpen: currentActiveMapId !== viewId ? true : !isOpen,
|
|
1623
|
-
source
|
|
1624
|
-
})), [dispatch, currentActiveMapId, viewId, source]);
|
|
1625
|
-
const handleClick = React.useCallback(menuType => {
|
|
1626
|
-
switch (menuType) {
|
|
1627
|
-
case MenuOptions.sources:
|
|
1628
|
-
dispatchActiveMapId(uiTypes.DialogTypes.SoundingsSources, isSourcesOpenInStore);
|
|
1629
|
-
break;
|
|
1630
|
-
case MenuOptions.locations:
|
|
1631
|
-
dispatchActiveMapId(uiTypes.DialogTypes.soundingsLocations, isLocationsOpenInStore);
|
|
1632
|
-
break;
|
|
1633
|
-
}
|
|
1634
|
-
}, [dispatchActiveMapId, isSourcesOpenInStore, isLocationsOpenInStore]);
|
|
1635
|
-
const isOpen = {
|
|
1636
|
-
[MenuOptions.sources]: currentActiveMapId === viewId && isSourcesOpenInStore,
|
|
1637
|
-
[MenuOptions.locations]: currentActiveMapId === viewId && isLocationsOpenInStore
|
|
1638
|
-
};
|
|
1639
|
-
return jsx(SoundingsMenuBar, {
|
|
1640
|
-
isOpen: isOpen,
|
|
1641
|
-
handleClick: handleClick,
|
|
1642
|
-
selectedSourcesAmount: sourcesAmount
|
|
1643
|
-
});
|
|
1644
|
-
};
|
|
1645
|
-
|
|
1646
1372
|
/* *
|
|
1647
1373
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1648
1374
|
* you may not use this file except in compliance with the License.
|
|
@@ -1691,24 +1417,6 @@ relHumidity) {
|
|
|
1691
1417
|
const dewpoint = tKelvin / (1 - tKelvin * Math.log(relHumidity * 0.01) * gConstant / hConstant);
|
|
1692
1418
|
return dewpoint;
|
|
1693
1419
|
}
|
|
1694
|
-
|
|
1695
|
-
/* *
|
|
1696
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1697
|
-
* you may not use this file except in compliance with the License.
|
|
1698
|
-
* You may obtain a copy of the License at
|
|
1699
|
-
*
|
|
1700
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1701
|
-
*
|
|
1702
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
1703
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1704
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1705
|
-
* See the License for the specific language governing permissions and
|
|
1706
|
-
* limitations under the License.
|
|
1707
|
-
*
|
|
1708
|
-
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
1709
|
-
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1710
|
-
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1711
|
-
* */
|
|
1712
1420
|
// Parsing and conversion function
|
|
1713
1421
|
function parseCoverageCollection(response) {
|
|
1714
1422
|
const features = [];
|
|
@@ -1839,64 +1547,438 @@ const constructQueryParams = (collectionId, instanceId, location, datetime, para
|
|
|
1839
1547
|
const parsedData = parseCoverageCollection(rawData);
|
|
1840
1548
|
return sortSoundingFeatures(parsedData);
|
|
1841
1549
|
} catch (err) {
|
|
1842
|
-
throw new Error('Error fetching EDR data collection by
|
|
1550
|
+
throw new Error('Error fetching EDR data collection by position');
|
|
1843
1551
|
}
|
|
1844
1552
|
}
|
|
1845
1553
|
};
|
|
1846
1554
|
};
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
const {
|
|
1850
|
-
data
|
|
1851
|
-
} = useQuery({
|
|
1555
|
+
const useFakeTime = (collectionId, instanceId) => {
|
|
1556
|
+
return useQuery({
|
|
1852
1557
|
queryKey: ['temporary-instance-call', collectionId, instanceId],
|
|
1853
1558
|
queryFn: async () => {
|
|
1854
1559
|
return fetch(`https://opendata.fmi.fi/edr/collections/${collectionId}/instances/${instanceId}`, {
|
|
1855
1560
|
headers: getHeaders()
|
|
1856
|
-
}).then(handleResponse);
|
|
1857
|
-
}
|
|
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 || [];
|
|
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
|
+
};
|
|
1599
|
+
}
|
|
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;
|
|
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
|
+
};
|
|
1628
|
+
}
|
|
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, {
|
|
1894
|
+
sx: {
|
|
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
|
|
1858
1944
|
});
|
|
1859
|
-
const datetime2 = collectionId === undefined || data === undefined ? '' : data.extent.temporal.interval[0][0];
|
|
1860
|
-
// Up until here can be removed
|
|
1861
|
-
// If position passed in, call positions hook
|
|
1862
|
-
const queryParams = constructQueryParams(collectionId, instanceId, location, datetime2, parameters);
|
|
1863
|
-
return useQuery(queryParams);
|
|
1864
1945
|
};
|
|
1865
1946
|
|
|
1866
1947
|
const SoundingsConnect = ({
|
|
1867
1948
|
viewId
|
|
1868
1949
|
}) => {
|
|
1950
|
+
// TODO: use in follow-up: https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
|
|
1951
|
+
// const selectedTime = useSelector(getSelectedTime);
|
|
1869
1952
|
const sourceSelections = useSelector(getSelectedSources);
|
|
1870
1953
|
const locationSelections = useSelector(getLocationSelections);
|
|
1871
|
-
// TODO https://gitlab.com/opengeoweb/opengeoweb/-/issues/5916: Allow multiple sources to be selected
|
|
1872
|
-
// For now, only select the first one
|
|
1873
1954
|
const {
|
|
1874
1955
|
error,
|
|
1875
|
-
data
|
|
1956
|
+
data,
|
|
1876
1957
|
isPending
|
|
1877
|
-
} =
|
|
1878
|
-
return jsxs(Fragment
|
|
1958
|
+
} = useMultipleSources(sourceSelections, locationSelections);
|
|
1959
|
+
return jsxs(Fragment, {
|
|
1879
1960
|
children: [error && jsx(AlertBanner, {
|
|
1880
1961
|
title: error.message,
|
|
1881
1962
|
shouldClose: true
|
|
1882
1963
|
}), isPending && jsx(LinearProgress, {
|
|
1883
|
-
color: "secondary"
|
|
1964
|
+
color: "secondary",
|
|
1965
|
+
sx: {
|
|
1966
|
+
position: 'absolute',
|
|
1967
|
+
width: '100%'
|
|
1968
|
+
}
|
|
1884
1969
|
}), jsxs(Box, {
|
|
1885
1970
|
sx: {
|
|
1886
|
-
padding: 2
|
|
1971
|
+
padding: 2,
|
|
1972
|
+
overflowY: 'scroll',
|
|
1973
|
+
height: '100%'
|
|
1887
1974
|
},
|
|
1888
1975
|
children: [jsx(Box, {
|
|
1889
1976
|
paddingLeft: 5,
|
|
1890
1977
|
children: jsx(MenuBarConnect, {
|
|
1891
1978
|
viewId: viewId
|
|
1892
1979
|
})
|
|
1893
|
-
}), jsx(Soundings
|
|
1894
|
-
|
|
1895
|
-
, {
|
|
1896
|
-
// TODO: use real data
|
|
1897
|
-
data: {
|
|
1898
|
-
features: soundingData || []
|
|
1899
|
-
},
|
|
1980
|
+
}), jsx(Soundings, {
|
|
1981
|
+
data: data,
|
|
1900
1982
|
svgDimensions: {
|
|
1901
1983
|
width: 600,
|
|
1902
1984
|
height: 600,
|
|
@@ -1907,11 +1989,343 @@ const SoundingsConnect = ({
|
|
|
1907
1989
|
top: 0
|
|
1908
1990
|
}
|
|
1909
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: []
|
|
1910
1997
|
})]
|
|
1911
1998
|
})]
|
|
1912
1999
|
});
|
|
1913
2000
|
};
|
|
1914
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;
|
|
2019
|
+
};
|
|
2020
|
+
return jsxs(ToolContainerDraggable, {
|
|
2021
|
+
startSize: {
|
|
2022
|
+
width: 320
|
|
2023
|
+
},
|
|
2024
|
+
startPosition: {
|
|
2025
|
+
top: 40,
|
|
2026
|
+
left: 50
|
|
2027
|
+
},
|
|
2028
|
+
minWidth: 300,
|
|
2029
|
+
title: t('sources'),
|
|
2030
|
+
isOpen: isOpen,
|
|
2031
|
+
onClose: handleClose,
|
|
2032
|
+
bounds: "parent",
|
|
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, {
|
|
2045
|
+
sx: {
|
|
2046
|
+
pr: 1
|
|
2047
|
+
},
|
|
2048
|
+
children: soundingsCollections && soundingsCollections.length > 0 ? soundingsCollections.map(collection => {
|
|
2049
|
+
const {
|
|
2050
|
+
isChecked,
|
|
2051
|
+
instanceId
|
|
2052
|
+
} = sourceSelections[collection.id] || {};
|
|
2053
|
+
const hasInstances = collection.instances.length > 0;
|
|
2054
|
+
const selectedInstanceId = instanceId || collection.instances[0]?.id || undefined;
|
|
2055
|
+
return jsxs(ListItem, {
|
|
2056
|
+
disablePadding: true,
|
|
2057
|
+
children: [jsx(ListItemIcon, {
|
|
2058
|
+
sx: {
|
|
2059
|
+
minWidth: 42
|
|
2060
|
+
},
|
|
2061
|
+
children: jsx(Checkbox, {
|
|
2062
|
+
checked: !!isChecked,
|
|
2063
|
+
onChange: event => updateCollection(collection.id, {
|
|
2064
|
+
isChecked: event.target.checked,
|
|
2065
|
+
instanceId: selectedInstanceId
|
|
2066
|
+
}),
|
|
2067
|
+
disableRipple: true
|
|
2068
|
+
})
|
|
2069
|
+
}), jsxs(StyledSelect, {
|
|
2070
|
+
isEnabled: true,
|
|
2071
|
+
additionalStyling: {
|
|
2072
|
+
width: 'calc(100% - 42px)'
|
|
2073
|
+
},
|
|
2074
|
+
skipLocalStyling: false,
|
|
2075
|
+
value: selectedInstanceId || t('noInstancesAvailable'),
|
|
2076
|
+
onChange: event => updateCollection(collection.id, {
|
|
2077
|
+
instanceId: event.target.value
|
|
2078
|
+
}),
|
|
2079
|
+
renderValue: value => jsx(Box, {
|
|
2080
|
+
sx: {
|
|
2081
|
+
textOverflow: 'ellipsis',
|
|
2082
|
+
overflow: 'hidden'
|
|
2083
|
+
},
|
|
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
|
+
})
|
|
2099
|
+
}),
|
|
2100
|
+
children: [!hasInstances && jsx(MenuItem, {
|
|
2101
|
+
value: t('noInstancesAvailable'),
|
|
2102
|
+
disabled: true,
|
|
2103
|
+
sx: {
|
|
2104
|
+
fontSize: 12
|
|
2105
|
+
},
|
|
2106
|
+
children: t('noInstancesAvailable')
|
|
2107
|
+
}), collection.instances.map(instance => jsx(MenuItem, {
|
|
2108
|
+
value: instance.id,
|
|
2109
|
+
children: getDisplayValue(instance.id)
|
|
2110
|
+
}, instance.id))]
|
|
2111
|
+
})]
|
|
2112
|
+
}, collection.id);
|
|
2113
|
+
}) : jsx(ListItem, {
|
|
2114
|
+
children: jsx(ListItemText, {
|
|
2115
|
+
primary: t('noServicesFound'),
|
|
2116
|
+
sx: {
|
|
2117
|
+
pl: 4
|
|
2118
|
+
}
|
|
2119
|
+
})
|
|
2120
|
+
})
|
|
2121
|
+
})]
|
|
2122
|
+
});
|
|
2123
|
+
};
|
|
2124
|
+
|
|
2125
|
+
const SoundingsSourceDialogConnect = ({
|
|
2126
|
+
soundingsCollections
|
|
2127
|
+
}) => {
|
|
2128
|
+
const dispatch = useDispatch();
|
|
2129
|
+
const {
|
|
2130
|
+
isDialogOpen,
|
|
2131
|
+
onCloseDialog,
|
|
2132
|
+
dialogOrder,
|
|
2133
|
+
setDialogOrder
|
|
2134
|
+
} = useSetupDialog(uiTypes.DialogTypes.SoundingsSources);
|
|
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);
|
|
2147
|
+
return jsx(SoundingsSourceMenu, {
|
|
2148
|
+
soundingsCollections: data,
|
|
2149
|
+
isOpen: isDialogOpen,
|
|
2150
|
+
updateCollection: updateCollection,
|
|
2151
|
+
sourceSelections: sourceSelections,
|
|
2152
|
+
handleClose: onCloseDialog,
|
|
2153
|
+
onMouseDown: setDialogOrder,
|
|
2154
|
+
order: dialogOrder,
|
|
2155
|
+
isLoading: isPending,
|
|
2156
|
+
error: error
|
|
2157
|
+
});
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
const LocationsMenu = ({
|
|
2161
|
+
services,
|
|
2162
|
+
selectedLocations,
|
|
2163
|
+
isOpen,
|
|
2164
|
+
onClose,
|
|
2165
|
+
updateSelectedLocation,
|
|
2166
|
+
onMouseDown = () => {},
|
|
2167
|
+
order = 0
|
|
2168
|
+
}) => {
|
|
2169
|
+
const [searchFilter, setSearchFilter] = useState('');
|
|
2170
|
+
const {
|
|
2171
|
+
t
|
|
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;
|
|
2185
|
+
return jsx(ToolContainerDraggable, {
|
|
2186
|
+
startSize: {
|
|
2187
|
+
width: 320,
|
|
2188
|
+
height: 500
|
|
2189
|
+
},
|
|
2190
|
+
minWidth: 264,
|
|
2191
|
+
title: t('locations'),
|
|
2192
|
+
isOpen: isOpen,
|
|
2193
|
+
startPosition: {
|
|
2194
|
+
top: 60,
|
|
2195
|
+
left: 200
|
|
2196
|
+
},
|
|
2197
|
+
onClose: onClose,
|
|
2198
|
+
bounds: "parent",
|
|
2199
|
+
onMouseDown: onMouseDown,
|
|
2200
|
+
order: order,
|
|
2201
|
+
children: services.length === 0 ? jsx(Typography, {
|
|
2202
|
+
sx: {
|
|
2203
|
+
margin: 1
|
|
2204
|
+
},
|
|
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,
|
|
2225
|
+
sx: {
|
|
2226
|
+
width: '100%',
|
|
2227
|
+
margin: '0 8px 2px 8px',
|
|
2228
|
+
'.MuiAccordionDetails-root': {
|
|
2229
|
+
padding: 0
|
|
2230
|
+
},
|
|
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
|
+
}
|
|
2280
|
+
})
|
|
2281
|
+
})
|
|
2282
|
+
}, service.id))]
|
|
2283
|
+
})
|
|
2284
|
+
});
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
const LocationsMenuConnect = ({
|
|
2288
|
+
soundingsCollections
|
|
2289
|
+
}) => {
|
|
2290
|
+
const dispatch = useDispatch();
|
|
2291
|
+
const {
|
|
2292
|
+
isDialogOpen,
|
|
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]);
|
|
2300
|
+
const edrQueries = useMemo(() => soundingsCollections.map(collection => ({
|
|
2301
|
+
queryKey: ['edr-locations', collection.id],
|
|
2302
|
+
queryFn: async () => {
|
|
2303
|
+
const locations = await getCollectionLocations(collection.id);
|
|
2304
|
+
return {
|
|
2305
|
+
...locations,
|
|
2306
|
+
id: collection.id,
|
|
2307
|
+
name: collection.name
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
})), [soundingsCollections]);
|
|
2311
|
+
const services = useQueries({
|
|
2312
|
+
queries: edrQueries,
|
|
2313
|
+
combine: results => results.map(result => result.data).filter(result => !!result)
|
|
2314
|
+
});
|
|
2315
|
+
return jsx(LocationsMenu, {
|
|
2316
|
+
isOpen: isDialogOpen,
|
|
2317
|
+
onClose: onCloseDialog,
|
|
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
|
|
2326
|
+
});
|
|
2327
|
+
};
|
|
2328
|
+
|
|
1915
2329
|
const soundingsComponentType = 'Soundings';
|
|
1916
2330
|
const soundingsLookUp = payload => {
|
|
1917
2331
|
if (payload.componentType === soundingsComponentType) {
|
|
@@ -1941,14 +2355,13 @@ const soundingsLookUp = payload => {
|
|
|
1941
2355
|
* */
|
|
1942
2356
|
const soundingsReducersMap = {
|
|
1943
2357
|
soundings: soundingsReducer,
|
|
1944
|
-
|
|
1945
|
-
ui: uiReducer
|
|
2358
|
+
...storeReducerMap
|
|
1946
2359
|
};
|
|
1947
|
-
const
|
|
2360
|
+
const soundingsRootReducer = combineReducers(soundingsReducersMap);
|
|
1948
2361
|
const createMockStore = mockState => configureStore({
|
|
1949
|
-
reducer:
|
|
2362
|
+
reducer: soundingsRootReducer,
|
|
1950
2363
|
preloadedState: mockState,
|
|
1951
|
-
middleware: getDefaultMiddleware => getDefaultMiddleware()
|
|
2364
|
+
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(storeMiddlewares)
|
|
1952
2365
|
});
|
|
1953
2366
|
|
|
1954
|
-
export { LocationsMenuConnect, SOUNDINGS_NAMESPACE,
|
|
2367
|
+
export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsConnect, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };
|