@opengeoweb/soundings 12.3.0 → 12.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/index.esm.js +1073 -660
  2. package/package.json +9 -7
  3. package/src/index.d.ts +1 -1
  4. package/src/lib/api/edr-api/api.d.ts +2 -3
  5. package/src/lib/api/edr-api/hooks.d.ts +14 -2
  6. package/src/lib/api/edr-api/types.d.ts +0 -23
  7. package/src/lib/{parser/coverages.d.ts → api/edr-api/utils.d.ts} +1 -1
  8. package/src/lib/components/EDRFetchAndParse.stories.d.ts +12 -5
  9. package/src/lib/components/MenuBar/LocationsMenu.d.ts +6 -3
  10. package/src/lib/components/MenuBar/MenuBarConnect.integration.spec.d.ts +1 -0
  11. package/src/lib/components/MenuBar/SoundingsSourceDialogConnect.spec.d.ts +1 -0
  12. package/src/lib/components/MenuBar/SoundingsSourceMenu.d.ts +6 -2
  13. package/src/lib/components/Providers/Providers.d.ts +1 -1
  14. package/src/lib/components/{Soundings.d.ts → Soundings/Soundings.d.ts} +4 -5
  15. package/src/lib/components/Soundings/Soundings.spec.d.ts +1 -0
  16. package/src/lib/components/Soundings/SoundingsConnect.d.ts +6 -0
  17. package/src/lib/components/Soundings/SoundingsConnect.integration.spec.d.ts +1 -0
  18. package/src/lib/components/Soundings/SoundingsConnect.spec.d.ts +1 -0
  19. package/src/lib/components/Soundings/SoundingsConnect.stories.d.ts +35 -0
  20. package/src/lib/components/{DrawUtils.d.ts → Soundings/drawUtils.d.ts} +6 -4
  21. package/src/lib/components/Soundings/drawUtils.spec.d.ts +1 -0
  22. package/src/lib/components/{types.d.ts → Soundings/types.d.ts} +1 -1
  23. package/src/lib/components/SoundingsDemo.stories.d.ts +15 -0
  24. package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.d.ts +7 -0
  25. package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.spec.d.ts +1 -0
  26. package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.stories.d.ts +8 -0
  27. package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSliderConnect.d.ts +6 -0
  28. package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSliderConnect.spec.d.ts +1 -0
  29. package/src/lib/components/SoundingsTimeSlider/index.d.ts +2 -0
  30. package/src/lib/components/SoundingsTooltip/utils.d.ts +29 -0
  31. package/src/lib/components/SoundingsTooltip/utils.spec.d.ts +1 -0
  32. package/src/lib/components/WindBarbs/utils.d.ts +2 -7
  33. package/src/lib/components/WindBarbs/utils.spec.d.ts +1 -0
  34. package/src/lib/components/index.d.ts +1 -1
  35. package/src/lib/store/reducer.d.ts +2 -1
  36. package/src/lib/store/selectors.d.ts +21 -0
  37. package/src/lib/store/store.d.ts +7 -1
  38. package/src/lib/store/types.d.ts +5 -0
  39. package/src/lib/utils/soundingsLookUp.spec.d.ts +1 -0
  40. package/src/lib/components/SoundingsConnect.d.ts +0 -6
  41. package/src/lib/utils/conversions.d.ts +0 -11
  42. /package/src/lib/{components/Soundings.spec.d.ts → api/edr-api/hooks.spec.d.ts} +0 -0
  43. /package/src/lib/components/{Soundings.stories.d.ts → Soundings/Soundings.stories.d.ts} +0 -0
  44. /package/src/lib/{i18n.d.ts → utils/i18n.d.ts} +0 -0
package/index.esm.js CHANGED
@@ -1,17 +1,19 @@
1
- import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
- import React, { useRef, useEffect, useState, Fragment, useMemo } from 'react';
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 { Card, List, ListItem, ListItemIcon, Checkbox, MenuItem, Box, ListItemText, Typography, ListSubheader, ListItemButton, Collapse, Stack, LinearProgress } from '@mui/material';
5
- import { selectorMemoizationOptions, useSetupDialog, uiTypes, uiSelectors, uiActions, uiReducer, mapReducer } from '@opengeoweb/store';
6
- import { useDispatch, useSelector } from 'react-redux';
7
- import { ToolContainerDraggable, StyledSelect, dateUtils, CustomToggleButton, AlertBanner } from '@opengeoweb/shared';
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, initReactI18next } from 'react-i18next';
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 { useQueries, useQuery } from '@tanstack/react-query';
14
- import { ChevronDown, Sources } from '@opengeoweb/theme';
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', 'none').attr('opacity', 1).attr('id', 'skewTd3').attr('clip-path', 'url(#clip-skewTd3)');
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(pressureValues, temperatureValues).map(([pressure, temperature]) => [xScale(temperature), yScale(pressure)]);
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 = removeItemsByGrowthType(data.reverse(), GrowthType.EXPONENTIAL, 1.15, 1).filter(item => item.pressure >= 150);
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.right + margin.left + barbsSize);
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 GrowthType = {
604
- LINEAR: 'LINEAR',
605
- EXPONENTIAL: 'EXPONENTIAL',
606
- CUBIC: 'CUBIC'
607
- };
608
- const removeItemsByGrowthType = (array, growthType = 'LINEAR', base = 2, initial = 1) => {
609
- if (!Object.values(GrowthType).includes(growthType)) {
610
- throw new Error('Unsupported growth type');
611
- }
612
- if (!Array.isArray(array) || array.length === 0) {
613
- return [];
614
- }
615
- if (growthType === GrowthType.LINEAR && base === 0) {
616
- throw new Error('Base must be greater than 0');
617
- }
618
- if (growthType === GrowthType.EXPONENTIAL && base <= 1) {
619
- throw new Error('Base must be greater than 1');
620
- }
621
- if (growthType === GrowthType.CUBIC && base <= 0) {
622
- throw new Error('Base must be greater than 0');
623
- }
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
- switch (growthType) {
629
- case GrowthType.LINEAR:
630
- nextIndex = initial + i * base;
631
- break;
632
- case GrowthType.EXPONENTIAL:
633
- nextIndex = Math.floor(initial * base ** i);
634
- break;
635
- case GrowthType.CUBIC:
636
- nextIndex = initial + Math.floor(i ** 3 * base);
637
- break;
638
- }
639
- if (nextIndex < array.length) {
640
- indicesToKeep.push(nextIndex);
641
- }
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 Soundings = ({
970
- data: {
971
- features
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
- svgDimensions
974
- // tooltip
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
- // Define pressure and temperature values for static lines
1010
- const maxPressure = 1050;
1011
- const minPressure = 150;
1012
- const staticPressureArray = [maxPressure];
1013
- const dp = -5;
1014
- const minTemperature = -40;
1015
- const maxTemperature = 40.6;
1016
- let pressure = maxPressure;
1017
- while (pressure > minPressure) {
1018
- pressure += dp;
1019
- staticPressureArray.push(pressure);
1020
- }
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
- const temperatureValues = features.map(feature => feature.properties.temp);
1051
- const pressureValues = features.map(feature => feature.properties.pressure);
1052
- // Draw Temperature
1053
- plotSounding({
1054
- temperatureValues,
1055
- pressureValues,
1056
- color: SOUNDING_COLORS[0],
1057
- lineStyle: 'none',
1058
- xScale,
1059
- yScale,
1060
- height,
1061
- svg
1062
- });
1063
- const dewpointValues = features.map(feature => feature.properties.dewpoint);
1064
- // Dewpoint
1065
- plotSounding({
1066
- temperatureValues: dewpointValues,
1067
- color: SOUNDING_COLORS[0],
1068
- pressureValues,
1069
- lineStyle: '8, 2',
1070
- xScale,
1071
- yScale,
1072
- height,
1073
- svg
1074
- });
1075
- const windData = features.map(feature => ({
1076
- windDirection: feature.properties.windDirection,
1077
- windSpeed: feature.properties.windSpeed,
1078
- pressure: feature.properties.pressure
1079
- }));
1080
- drawWindBarbs({
1081
- svgRef,
1082
- data: windData,
1083
- width,
1084
- height,
1085
- margin,
1086
- minPressure,
1087
- maxPressure,
1088
- barbColor: SOUNDING_COLORS[0]
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
- // svg.on('mousemove', null)
1244
+ svg.on('mousemove', null);
1096
1245
  };
1097
1246
  // eslint-disable-next-line react-hooks/exhaustive-deps
1098
- }, [features]);
1099
- return jsx(Card, {
1100
- children: jsx("svg", {
1101
- ref: svgRef
1102
- })
1103
- });
1104
- };
1105
-
1106
- /* *
1107
- * Licensed under the Apache License, Version 2.0 (the "License");
1108
- * you may not use this file except in compliance with the License.
1109
- * You may obtain a copy of the License at
1110
- *
1111
- * http://www.apache.org/licenses/LICENSE-2.0
1112
- *
1113
- * Unless required by applicable law or agreed to in writing, software
1114
- * distributed under the License is distributed on an "AS IS" BASIS,
1115
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1116
- * See the License for the specific language governing permissions and
1117
- * limitations under the License.
1118
- *
1119
- * Copyright 2023 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
1120
- * Copyright 2023 - Finnish Meteorological Institute (FMI)
1121
- * Copyright 2024 - The Norwegian Meteorological Institute (MET Norway)
1122
- * */
1123
- const SOUNDINGS_NAMESPACE = 'soundings';
1124
- const defaultLanguage = 'en';
1125
- const initSoundingsI18n = () => {
1126
- void i18n.use(initReactI18next).init({
1127
- lng: defaultLanguage,
1128
- ns: SOUNDINGS_NAMESPACE,
1129
- interpolation: {
1130
- escapeValue: false
1131
- },
1132
- resources: {
1133
- en: {
1134
- [SOUNDINGS_NAMESPACE]: soundingsTranslations.en
1135
- },
1136
- nl: {
1137
- [SOUNDINGS_NAMESPACE]: soundingsTranslations.nl
1138
- },
1139
- fi: {
1140
- [SOUNDINGS_NAMESPACE]: soundingsTranslations.fi
1141
- },
1142
- no: {
1143
- [SOUNDINGS_NAMESPACE]: soundingsTranslations.no
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
- const translateKeyOutsideComponents = (key, params = undefined) => i18n.t(key, {
1149
- ns: [SOUNDINGS_NAMESPACE],
1150
- ...params
1151
- });
1152
- const useSoundingsTranslation = () => useTranslation(SOUNDINGS_NAMESPACE);
1153
-
1154
- const SoundingsSourceMenu = ({
1155
- soundingsCollections,
1156
- isOpen = false,
1157
- handleClose,
1158
- updateCollection,
1159
- sourceSelections
1160
- }) => {
1161
- const {
1162
- t
1163
- } = useSoundingsTranslation();
1164
- const handleSelectionChange = (collectionId, update) => {
1165
- updateCollection(collectionId, update);
1166
- };
1167
- const getDisplayValue = instanceId => {
1168
- if (!instanceId) {
1169
- return t('noInstancesAvailable');
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
- const instanceAsDate = dateUtils.isoStringToDate(instanceId);
1172
- return instanceAsDate ? dateUtils.dateToIsoString(instanceAsDate) : instanceId;
1173
- };
1174
- return jsx(ToolContainerDraggable, {
1175
- startSize: {
1176
- width: 320
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
- minWidth: 300,
1179
- title: t('sources'),
1180
- isOpen: isOpen,
1181
- onClose: () => handleClose(),
1182
- bounds: "parent",
1183
- children: jsx(List, {
1294
+ children: [jsx("svg", {
1295
+ ref: svgRef
1296
+ }), jsx(Box, {
1297
+ ref: pressureRef,
1184
1298
  sx: {
1185
- pr: 1
1299
+ ...soundingsStyle.tooltip,
1300
+ ...soundingsStyle.pressure
1186
1301
  },
1187
- children: soundingsCollections && soundingsCollections.length > 0 ? soundingsCollections.map(collection => {
1188
- const {
1189
- isChecked,
1190
- instanceId
1191
- } = sourceSelections[collection.id] || {};
1192
- const hasInstances = collection.instances.length > 0;
1193
- const selectedInstanceId = instanceId || collection.instances[0].id;
1194
- return jsxs(ListItem, {
1195
- disablePadding: true,
1196
- children: [jsx(ListItemIcon, {
1197
- sx: {
1198
- minWidth: 42
1199
- },
1200
- children: jsx(Checkbox, {
1201
- checked: !!isChecked,
1202
- onChange: event => handleSelectionChange(collection.id, {
1203
- isChecked: event.target.checked,
1204
- instanceId: selectedInstanceId
1205
- }),
1206
- disableRipple: true
1207
- })
1208
- }), jsxs(StyledSelect, {
1209
- isEnabled: true,
1210
- additionalStyling: {
1211
- width: 'calc(100% - 24px)'
1212
- },
1213
- skipLocalStyling: false,
1214
- value: selectedInstanceId,
1215
- onChange: event => handleSelectionChange(collection.id, {
1216
- instanceId: event.target.value
1217
- }),
1218
- renderValue: value => jsx(Box, {
1219
- sx: {
1220
- textOverflow: 'ellipsis',
1221
- overflow: 'hidden'
1222
- },
1223
- children: jsxs(Box, {
1224
- display: "flex",
1225
- children: [jsx(Box, {
1226
- sx: {
1227
- minWidth: 200
1228
- },
1229
- children: collection.name
1230
- }), jsx(Box, {
1231
- sx: {
1232
- textOverflow: 'ellipsis',
1233
- overflow: 'hidden'
1234
- },
1235
- children: getDisplayValue(value)
1236
- })]
1237
- })
1238
- }),
1239
- children: [!hasInstances && jsx(MenuItem, {
1240
- value: "",
1241
- disabled: true,
1242
- sx: {
1243
- fontSize: 12
1244
- },
1245
- children: t('noInstancesAvailable')
1246
- }), collection.instances.map(instance => jsx(MenuItem, {
1247
- value: instance.id,
1248
- children: getDisplayValue(instance.id)
1249
- }, instance.id))]
1250
- })]
1251
- }, collection.id);
1252
- }) : jsx(ListItem, {
1253
- children: jsx(ListItemText, {
1254
- primary: t('noServicesFound'),
1255
- sx: {
1256
- pl: 4
1257
- }
1258
- })
1259
- })
1260
- })
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 = (baseURL, collectionId) => fetch(`${baseURL}/collections/${collectionId}/instances`, {
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 location');
1550
+ throw new Error('Error fetching EDR data collection by position');
1843
1551
  }
1844
1552
  }
1845
1553
  };
1846
1554
  };
1847
- const useEdrCollectionForLocationPosition = (collectionId, instanceId, location, datetime, parameters = ['temperature', 'WindDirection', 'WindSpeedMS', 'windUMS', 'windVMS', 'Pressure', 'DewPoint', 'humidity', 'specifichumidity']) => {
1848
- // TODO: Remove once we pass in the datetime coming from the timeslider!
1849
- const {
1850
- data
1851
- } = useQuery({
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: soundingData,
1956
+ data,
1876
1957
  isPending
1877
- } = useEdrCollectionForLocationPosition(sourceSelections[0]?.collectionId, sourceSelections[0]?.instanceId, locationSelections[sourceSelections[0]?.collectionId] || '');
1878
- return jsxs(Fragment$1, {
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
- // TODO: use real data
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
- webmap: mapReducer,
1945
- ui: uiReducer
2358
+ ...storeReducerMap
1946
2359
  };
1947
- const soundingsRootReuducer = combineReducers(soundingsReducersMap);
2360
+ const soundingsRootReducer = combineReducers(soundingsReducersMap);
1948
2361
  const createMockStore = mockState => configureStore({
1949
- reducer: soundingsRootReuducer,
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, Soundings, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };
2367
+ export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsConnect, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingsComponentType, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateKeyOutsideComponents, useSoundingsTranslation };