@opengeoweb/soundings 14.5.2 → 15.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.esm.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { Menu, ListSubheader, Typography, MenuItem, Box, Card, Stack, IconButton, Tooltip, LinearProgress, List, ListItem, ListItemIcon, Checkbox, ListItemText, Grid, ListItemButton } from '@mui/material';
3
+ import { range, minBy, isEqual } from 'lodash';
3
4
  import React, { useRef, useEffect, useMemo, useState, Fragment as Fragment$1 } from 'react';
4
- import { useDispatch, useSelector } from 'react-redux';
5
- import { safelyInitI18nForTestsAndStories, sharedTranslations, SHARED_NAMESPACE, CustomToggleButton, dateUtils, CustomSlider, AlertBanner, ToolContainerDraggable, StyledSelect, SearchField, CustomAccordion } from '@opengeoweb/shared';
5
+ import { useDispatch, useSelector, shallowEqual } from 'react-redux';
6
+ import { safelyInitI18nForTestsAndStories, sharedTranslations, SHARED_NAMESPACE, CustomToggleButton, dateUtils, SwitchButton, CustomSlider, AlertBanner, ToolContainerDraggable, StyledSelect, SearchField, CustomAccordion } from '@opengeoweb/shared';
6
7
  import { selectorMemoizationOptions, uiSelectors, uiTypes, uiActions, genericSelectors, mapSelectors, genericActions, syncGroupsActions, mapActions, useSetupDialog, storeReducerMap, storeMiddlewares } from '@opengeoweb/store';
7
8
  import { ParseISOTimeRangeDuration, parseISO8601IntervalToDateInterval, webmapUtils } from '@opengeoweb/webmap';
8
9
  import { MapFeatureClass } from '@opengeoweb/webmap-react';
9
10
  import { ReactSortable } from 'react-sortablejs';
10
- import { ArrowDropDownDown, DragHandle, Layers, Sources } from '@opengeoweb/theme';
11
+ import { ArrowDropDownDown, DragHandle, Layers, Sources, Visibility, VisibilityOff } from '@opengeoweb/theme';
11
12
  import * as d3 from 'd3';
12
13
  import i18n from 'i18next';
13
14
  export { default as i18n } from 'i18next';
@@ -16,7 +17,6 @@ import { timesliderTranslations, TIME_SLIDER_NAMESPACE, BackwardForwardStepButto
16
17
  import { useResizable } from 'react-resizable-layout';
17
18
  import { useQueries } from '@tanstack/react-query';
18
19
  import { getHeaders, handleResponse } from '@opengeoweb/api';
19
- import { range, minBy } from 'lodash';
20
20
  import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
21
21
  import { FixedSizeList } from 'react-window';
22
22
 
@@ -30,7 +30,9 @@ var en = {
30
30
  options: "Options",
31
31
  "diagram-SKEWT": "Skew-T",
32
32
  "diagram-BIJVOET": "Bijvoet",
33
- "diagram-EMAGRAM": "Emagram"
33
+ "diagram-EMAGRAM": "Emagram",
34
+ "soundings-toggle-location-markers-label": "Markers",
35
+ "soundings-toggle-location-markers-tooltip": "Show/hide location markers"
34
36
  };
35
37
  var no = {
36
38
  sources: "Kilder",
@@ -42,7 +44,9 @@ var no = {
42
44
  options: "Valg",
43
45
  "diagram-SKEWT": "Skew-T",
44
46
  "diagram-BIJVOET": "Bijvoet",
45
- "diagram-EMAGRAM": "Emagram"
47
+ "diagram-EMAGRAM": "Emagram",
48
+ "soundings-toggle-location-markers-label": "ikke oversatt",
49
+ "soundings-toggle-location-markers-tooltip": "ikke oversatt"
46
50
  };
47
51
  var nl = {
48
52
  sources: "Bronnen",
@@ -54,7 +58,9 @@ var nl = {
54
58
  options: "Opties",
55
59
  "diagram-SKEWT": "Skew-T",
56
60
  "diagram-BIJVOET": "Bijvoet",
57
- "diagram-EMAGRAM": "Emagram"
61
+ "diagram-EMAGRAM": "Emagram",
62
+ "soundings-toggle-location-markers-label": "Kaart locaties",
63
+ "soundings-toggle-location-markers-tooltip": "Toon/Verberg locatie punten"
58
64
  };
59
65
  var fi = {
60
66
  sources: "Lähteet",
@@ -66,7 +72,9 @@ var fi = {
66
72
  options: "Valinnat",
67
73
  "diagram-SKEWT": "Skew-T",
68
74
  "diagram-BIJVOET": "Bijvoet",
69
- "diagram-EMAGRAM": "Emagram"
75
+ "diagram-EMAGRAM": "Emagram",
76
+ "soundings-toggle-location-markers-label": "Merkit",
77
+ "soundings-toggle-location-markers-tooltip": "Näytä/piilota kohdemerkit"
70
78
  };
71
79
  var soundingsTranslations = {
72
80
  en: en,
@@ -683,8 +691,7 @@ const drawSoundingsTooltip = ({
683
691
  return getParamHtml(dataSet.hoveredDewpoint, index, '°C', index === interpolatedData.length - 1, dataSet.color);
684
692
  }).join(''));
685
693
  // Wind
686
- windRefd3?.style('display', 'block').style('left', `${width + margin.left + margin.right - 55}px`) // -55 to account for width of element
687
- .style('top', `${pressureY}px`).html(() => {
694
+ windRefd3?.style('display', 'block').style('left', `${width + margin.left + margin.right}px`).style('top', `${pressureY}px`).html(() => {
688
695
  const windSpeedStrings = interpolatedData.map((dataSet, index) => {
689
696
  return getParamHtml(dataSet.hoveredWindSpeed, index, 'm/s', index === interpolatedData.length - 1, dataSet.color, true);
690
697
  });
@@ -1870,6 +1877,19 @@ const soundingsStyle = {
1870
1877
  }
1871
1878
  };
1872
1879
  const WIND_BARB_OFFSET_X = 35;
1880
+ /**
1881
+ * Check if the sounding data is effectively empty.
1882
+ * Data can be empty in two ways:
1883
+ * 1. No sources selected: data = []
1884
+ * 2. No locations selected: data = [[], []] (sources exist but all have empty arrays)
1885
+ */
1886
+ const isDataEmpty = data => {
1887
+ if (!data || data.length === 0) {
1888
+ return true;
1889
+ }
1890
+ // Check if all inner arrays are empty (no locations selected)
1891
+ return data.every(features => features.length === 0);
1892
+ };
1873
1893
  const WindBarbs = ({
1874
1894
  d3GraphId,
1875
1895
  data,
@@ -1951,7 +1971,8 @@ const WindBarbs = ({
1951
1971
  columnOffsetX: index * WIND_BARB_OFFSET_X
1952
1972
  });
1953
1973
  });
1954
- tooltipDataRef.current = constructTooltipData(data);
1974
+ // Clear or update tooltip data based on whether we have actual data
1975
+ tooltipDataRef.current = isDataEmpty(data) ? null : constructTooltipData(data);
1955
1976
  // Update previous inner height after drawing so animation uses the prior value
1956
1977
  prevInnerHeightRef.current = height;
1957
1978
  // eslint-disable-next-line consistent-return, @typescript-eslint/explicit-function-return-type
@@ -1964,6 +1985,11 @@ const WindBarbs = ({
1964
1985
  windRef.current && (windRef.current.style.display = 'none');
1965
1986
  return;
1966
1987
  }
1988
+ // Hide tooltip when data is empty (all sources/locations removed)
1989
+ if (isDataEmpty(data)) {
1990
+ windRef.current.style.display = 'none';
1991
+ return;
1992
+ }
1967
1993
  const rect = svgRectRef.current ?? svgRef.current.getBoundingClientRect();
1968
1994
  if (clientY == null || clientY < rect.top || clientY > rect.bottom) {
1969
1995
  windRef.current.style.display = 'none';
@@ -1991,15 +2017,12 @@ const WindBarbs = ({
1991
2017
  tooltipData: tooltipDataRef.current,
1992
2018
  diagramType
1993
2019
  });
1994
- }, [clientY, showTooltip, diagramType]);
1995
- useEffect(() => {
1996
- // Reset the tooltips when filteredData changes
1997
- if (windRef.current) {
1998
- windRef.current.style.display = 'none';
1999
- }
2000
- }, [data]);
2020
+ }, [clientY, showTooltip, diagramType, data]);
2001
2021
  return jsxs(Box, {
2002
2022
  ref: canvasRef,
2023
+ sx: {
2024
+ position: 'relative'
2025
+ },
2003
2026
  children: [jsx("svg", {
2004
2027
  ref: svgRef
2005
2028
  }), jsx(Box, {
@@ -2258,21 +2281,19 @@ const SoundingsAccordion = React.forwardRef(({
2258
2281
  });
2259
2282
 
2260
2283
  const SortableAccordions = ({
2284
+ soundingPanelsInOrder,
2261
2285
  data,
2262
2286
  size,
2263
2287
  chosenDiagramType,
2264
2288
  setChosenDiagramType,
2265
- viewRef
2289
+ viewRef,
2290
+ onOrderChange
2266
2291
  }) => {
2267
- const [accordions, setAccordions] = React.useState([{
2268
- id: 'soundings'
2269
- }, {
2270
- id: 'windBarbs'
2271
- }]);
2272
2292
  const [soundingsIsClosed, setSoundingsIsClosed] = React.useState(false);
2273
2293
  const [previousHeight, setPreviousHeight] = React.useState(size);
2274
2294
  const createPanel = React.useCallback(id => {
2275
- switch (id) {
2295
+ const normalizedId = id.toLowerCase().replace(/\s+/g, '');
2296
+ switch (normalizedId) {
2276
2297
  case 'soundings':
2277
2298
  return jsx(SoundingsAccordion, {
2278
2299
  accordionIsOpen: true,
@@ -2286,7 +2307,7 @@ const SortableAccordions = ({
2286
2307
  setSoundingsIsClosed: setSoundingsIsClosed,
2287
2308
  size: size,
2288
2309
  children: jsx(Soundings, {
2289
- d3GraphId: id,
2310
+ d3GraphId: normalizedId,
2290
2311
  data: data,
2291
2312
  diagramType: chosenDiagramType,
2292
2313
  svgDimensions: {
@@ -2301,13 +2322,13 @@ const SortableAccordions = ({
2301
2322
  }
2302
2323
  })
2303
2324
  });
2304
- case 'windBarbs':
2325
+ case 'windbarbs':
2305
2326
  return jsx(SoundingsAccordion, {
2306
2327
  maxWidth: 200,
2307
2328
  name: "Wind Barbs",
2308
2329
  children: jsx(WindBarbs, {
2309
2330
  animateResize: Boolean(soundingsIsClosed && previousHeight),
2310
- d3GraphId: id,
2331
+ d3GraphId: normalizedId,
2311
2332
  data: data,
2312
2333
  diagramType: chosenDiagramType,
2313
2334
  svgDimensions: {
@@ -2339,18 +2360,31 @@ const SortableAccordions = ({
2339
2360
  direction: "horizontal",
2340
2361
  draggable: ".sortable-item",
2341
2362
  handle: ".drag-handle",
2342
- list: accordions,
2343
- setList: setAccordions,
2363
+ list: soundingPanelsInOrder.map(panel => ({
2364
+ ...panel
2365
+ })),
2366
+ setList: list => onOrderChange(list.map(({
2367
+ id,
2368
+ name,
2369
+ isDisplayed
2370
+ }) => ({
2371
+ id,
2372
+ name,
2373
+ isDisplayed
2374
+ }))),
2344
2375
  style: {
2345
2376
  display: 'flex',
2346
2377
  gap: 4,
2347
2378
  width: '100%'
2348
2379
  },
2349
2380
  tag: "div",
2350
- children: accordions.map(({
2381
+ children: soundingPanelsInOrder.filter(entry => entry.isDisplayed).flatMap(({
2351
2382
  id
2352
2383
  }) => {
2353
2384
  const panel = createPanel(id);
2385
+ if (!panel) {
2386
+ return [];
2387
+ }
2354
2388
  return panel ? jsx(React.Fragment, {
2355
2389
  children: React.cloneElement(panel, {
2356
2390
  accordionId: id,
@@ -2824,10 +2858,28 @@ const useMultipleSources = (sources, locations, datetime) => {
2824
2858
  parameterMappings,
2825
2859
  verticalRange
2826
2860
  }, index) => {
2827
- // If acceptance time logic is needed, it could come here.
2828
- const dateToUse = datetime &&
2829
- // Only use selected time if exact match if found
2830
- timesteps[index].includes(new Date(datetime).toISOString()) ? datetime : '';
2861
+ // Find the best matching timestep for this source
2862
+ let dateToUse = '';
2863
+ if (datetime && timesteps[index] && timesteps[index].length > 0) {
2864
+ const requestedTime = new Date(datetime).getTime();
2865
+ const availableTimesteps = timesteps[index];
2866
+ // Check for exact match first
2867
+ if (availableTimesteps.includes(new Date(datetime).toISOString())) {
2868
+ dateToUse = datetime;
2869
+ } else {
2870
+ // Find nearest timestep (prefer earlier timesteps if equidistant)
2871
+ let nearestTimestep = availableTimesteps[0];
2872
+ let minDiff = Math.abs(new Date(availableTimesteps[0]).getTime() - requestedTime);
2873
+ for (const timestep of availableTimesteps) {
2874
+ const diff = Math.abs(new Date(timestep).getTime() - requestedTime);
2875
+ if (diff < minDiff) {
2876
+ minDiff = diff;
2877
+ nearestTimestep = timestep;
2878
+ }
2879
+ }
2880
+ dateToUse = nearestTimestep;
2881
+ }
2882
+ }
2831
2883
  const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dateToUse || '', parameterMappings || defaultParameterMappings, verticalRange || '');
2832
2884
  return queryParams;
2833
2885
  });
@@ -3029,12 +3081,40 @@ const useGetCollectionLocations = (collections, enabled) => {
3029
3081
  const initialSoundingsState = {
3030
3082
  selectedSources: {},
3031
3083
  selectedLocations: {},
3032
- selectedTime: new Date().toISOString()
3084
+ selectedTime: new Date().toISOString(),
3085
+ diagramType: 'SKEWT',
3086
+ soundingPanels: {
3087
+ order: ['soundings', 'windbarbs'],
3088
+ entities: {
3089
+ soundings: {
3090
+ id: 'soundings',
3091
+ name: 'Soundings',
3092
+ isDisplayed: true
3093
+ },
3094
+ windbarbs: {
3095
+ id: 'windbarbs',
3096
+ name: 'Wind Barbs',
3097
+ isDisplayed: true
3098
+ }
3099
+ }
3100
+ },
3101
+ shouldHideLocationMarkers: false
3033
3102
  };
3034
3103
  const slice = createSlice({
3035
3104
  initialState: initialSoundingsState,
3036
3105
  name: 'soundings',
3037
3106
  reducers: {
3107
+ registerSoundingsPreset: (draft, payloadAction) => {
3108
+ const preset = payloadAction.payload;
3109
+ if (preset) {
3110
+ draft.selectedSources = preset.selectedSources ?? {};
3111
+ draft.selectedLocations = preset.selectedLocations ?? {};
3112
+ draft.selectedTime = preset.selectedTime ?? new Date().toISOString();
3113
+ draft.diagramType = preset.diagramType ?? 'SKEWT';
3114
+ draft.soundingPanels = preset.soundingPanels ?? initialSoundingsState.soundingPanels;
3115
+ draft.shouldHideLocationMarkers = preset.shouldHideLocationMarkers ?? false;
3116
+ }
3117
+ },
3038
3118
  updateSelectedSource: (draft, payloadAction) => {
3039
3119
  const {
3040
3120
  sourceId,
@@ -3057,6 +3137,29 @@ const slice = createSlice({
3057
3137
  selectedTime
3058
3138
  } = payloadAction.payload;
3059
3139
  draft.selectedTime = selectedTime;
3140
+ },
3141
+ updateDiagramType: (draft, payloadAction) => {
3142
+ draft.diagramType = payloadAction.payload.diagramType;
3143
+ },
3144
+ toggleLocationMarkers: (draft, payloadAction) => {
3145
+ const {
3146
+ shouldHideLocationMarkers
3147
+ } = payloadAction.payload;
3148
+ draft.shouldHideLocationMarkers = shouldHideLocationMarkers;
3149
+ },
3150
+ addSoundingPanelsFromPreset: (draft, payloadAction) => {
3151
+ const {
3152
+ order,
3153
+ entities
3154
+ } = payloadAction.payload;
3155
+ draft.soundingPanels.order = order;
3156
+ draft.soundingPanels.entities = entities;
3157
+ },
3158
+ changeSoundingPanelOrder: (draft, payloadAction) => {
3159
+ const {
3160
+ newOrderOfIds
3161
+ } = payloadAction.payload;
3162
+ draft.soundingPanels.order = newOrderOfIds;
3060
3163
  }
3061
3164
  }
3062
3165
  });
@@ -3087,7 +3190,9 @@ const getSourceSelections = createSelector(getSoundingsStore, store => {
3087
3190
  return store.selectedSources;
3088
3191
  }, selectorMemoizationOptions);
3089
3192
  const getLocationSelections = createSelector(getSoundingsStore, store => store.selectedLocations, selectorMemoizationOptions);
3090
- const getSelectedTime = createSelector(getSoundingsStore, store => store.selectedTime, selectorMemoizationOptions);
3193
+ const getSelectedTime = createSelector(getSoundingsStore, store => store.selectedTime ?? new Date().toISOString(), selectorMemoizationOptions);
3194
+ const getDiagramType = createSelector(getSoundingsStore, store => store.diagramType ?? 'SKEWT', selectorMemoizationOptions);
3195
+ const getShouldHideLocationMarkers = createSelector(getSoundingsStore, store => store.shouldHideLocationMarkers ?? false, selectorMemoizationOptions);
3091
3196
  const getSelectedSources = createSelector(getSoundingsStore, store => {
3092
3197
  const sources = store.selectedSources;
3093
3198
  const activeSources = Object.keys(sources).reduce((list, key) => {
@@ -3095,11 +3200,11 @@ const getSelectedSources = createSelector(getSoundingsStore, store => {
3095
3200
  if (value.isChecked === true) {
3096
3201
  return list.concat({
3097
3202
  collectionId: key,
3098
- url: value.url,
3203
+ url: value.url ?? '',
3099
3204
  instanceId: value.instanceId,
3100
3205
  verticalRange: value.verticalRange,
3101
3206
  parameterMappings: value.parameterMappings,
3102
- name: value.name || ''
3207
+ name: value.name ?? ''
3103
3208
  });
3104
3209
  }
3105
3210
  return list;
@@ -3112,6 +3217,135 @@ const getSelectedSourcesAmount = createSelector(getSelectedSources, activeSource
3112
3217
  const getSelectedSourcesIds = createSelector(getSelectedSources, activeSources => {
3113
3218
  return activeSources.map(source => source.collectionId);
3114
3219
  }, selectorMemoizationOptions);
3220
+ const getSoundingsPreset = createSelector(getSoundingsStore, soundingsStore => {
3221
+ const {
3222
+ selectedTime,
3223
+ selectedSources,
3224
+ ...rest
3225
+ } = soundingsStore;
3226
+ const filteredSources = Object.entries(selectedSources).reduce((acc, [key, value]) => {
3227
+ const {
3228
+ instanceId,
3229
+ ...sourceWithoutInstance
3230
+ } = value;
3231
+ return {
3232
+ ...acc,
3233
+ [key]: sourceWithoutInstance
3234
+ };
3235
+ }, {});
3236
+ return {
3237
+ ...rest,
3238
+ selectedSources: filteredSources
3239
+ };
3240
+ }, selectorMemoizationOptions);
3241
+ const getSoundingPanelsInOrder = createSelector(getSoundingsStore, store => {
3242
+ const {
3243
+ order = [],
3244
+ entities = {}
3245
+ } = store.soundingPanels || {
3246
+ order: [],
3247
+ entities: {}
3248
+ };
3249
+ return order.map(id => entities[id]);
3250
+ }, selectorMemoizationOptions);
3251
+
3252
+ var selectors = /*#__PURE__*/Object.freeze({
3253
+ __proto__: null,
3254
+ getDiagramType: getDiagramType,
3255
+ getLocationSelections: getLocationSelections,
3256
+ getSelectedSources: getSelectedSources,
3257
+ getSelectedSourcesAmount: getSelectedSourcesAmount,
3258
+ getSelectedSourcesIds: getSelectedSourcesIds,
3259
+ getSelectedTime: getSelectedTime,
3260
+ getShouldHideLocationMarkers: getShouldHideLocationMarkers,
3261
+ getSoundingPanelsInOrder: getSoundingPanelsInOrder,
3262
+ getSoundingsPreset: getSoundingsPreset,
3263
+ getSourceSelections: getSourceSelections
3264
+ });
3265
+
3266
+ /* *
3267
+ * Licensed under the Apache License, Version 2.0 (the "License");
3268
+ * you may not use this file except in compliance with the License.
3269
+ * You may obtain a copy of the License at
3270
+ *
3271
+ * http://www.apache.org/licenses/LICENSE-2.0
3272
+ *
3273
+ * Unless required by applicable law or agreed to in writing, software
3274
+ * distributed under the License is distributed on an "AS IS" BASIS,
3275
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3276
+ * See the License for the specific language governing permissions and
3277
+ * limitations under the License.
3278
+ *
3279
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
3280
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
3281
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
3282
+ * */
3283
+ var SoundingsActionOrigin;
3284
+ (function (SoundingsActionOrigin) {
3285
+ SoundingsActionOrigin["soundingsManager"] = "soundingsManager";
3286
+ SoundingsActionOrigin["soundingsComponent"] = "soundingsComponent";
3287
+ })(SoundingsActionOrigin || (SoundingsActionOrigin = {}));
3288
+
3289
+ var types = /*#__PURE__*/Object.freeze({
3290
+ __proto__: null,
3291
+ get SoundingsActionOrigin () { return SoundingsActionOrigin; }
3292
+ });
3293
+
3294
+ const SoundingsMarkersSwitch = ({
3295
+ shouldHideMarkers,
3296
+ toggleMarkers
3297
+ }) => {
3298
+ const {
3299
+ t
3300
+ } = useSoundingsTranslation();
3301
+ return jsxs(Box, {
3302
+ sx: {
3303
+ display: 'flex',
3304
+ flexWrap: 'wrap',
3305
+ alignItems: 'flex-start',
3306
+ 'label:first-of-type': {
3307
+ marginLeft: '5px',
3308
+ marginRight: '-5px'
3309
+ },
3310
+ 'label:last-of-type': {
3311
+ marginLeft: '-5px'
3312
+ },
3313
+ '.switch-button label': {
3314
+ display: 'none'
3315
+ },
3316
+ '.markers-label': {
3317
+ display: 'none'
3318
+ },
3319
+ '@media (min-width: 720px)': {
3320
+ minWidth: '146px',
3321
+ '.markers-label, .switch-button label': {
3322
+ display: 'initial'
3323
+ }
3324
+ }
3325
+ },
3326
+ children: [jsx(Typography, {
3327
+ className: "markers-label",
3328
+ sx: {
3329
+ marginTop: '4px'
3330
+ },
3331
+ variant: "body2",
3332
+ children: t('soundings-toggle-location-markers-label')
3333
+ }), jsx(Box, {
3334
+ sx: {
3335
+ marginTop: '-5px'
3336
+ },
3337
+ children: jsx(SwitchButton, {
3338
+ activeLabel: "ON",
3339
+ checked: !shouldHideMarkers,
3340
+ inActiveLabel: "OFF",
3341
+ onChange: event => {
3342
+ toggleMarkers(!event.target.checked);
3343
+ },
3344
+ tooltip: t('soundings-toggle-location-markers-tooltip')
3345
+ })
3346
+ })]
3347
+ });
3348
+ };
3115
3349
 
3116
3350
  var MenuOptions;
3117
3351
  (function (MenuOptions) {
@@ -3126,7 +3360,9 @@ const buttonStyle = {
3126
3360
  const SoundingsMenuBar = ({
3127
3361
  handleClick,
3128
3362
  isOpen,
3129
- selectedSourcesAmount = 0
3363
+ selectedSourcesAmount = 0,
3364
+ shouldHideMarkers,
3365
+ toggleMarkers
3130
3366
  }) => {
3131
3367
  const {
3132
3368
  t
@@ -3165,6 +3401,9 @@ const SoundingsMenuBar = ({
3165
3401
  sx: buttonStyle,
3166
3402
  variant: "tool",
3167
3403
  children: t('locations')
3404
+ }), jsx(SoundingsMarkersSwitch, {
3405
+ shouldHideMarkers: shouldHideMarkers,
3406
+ toggleMarkers: toggleMarkers
3168
3407
  })]
3169
3408
  });
3170
3409
  };
@@ -3181,6 +3420,14 @@ const MenuBarConnect = ({
3181
3420
  const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsLocations));
3182
3421
  const isSoundingsManagerOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsManager));
3183
3422
  const sourcesAmount = useSelector(getSelectedSourcesAmount);
3423
+ const shouldHideLocationMarkers = useSelector(getShouldHideLocationMarkers);
3424
+ const toggleLocationMarkers = React.useCallback(shouldHide => {
3425
+ dispatch(soundingsActions.toggleLocationMarkers({
3426
+ shouldHideLocationMarkers: shouldHide,
3427
+ panelId: viewId,
3428
+ origin: SoundingsActionOrigin.soundingsComponent
3429
+ }));
3430
+ }, [dispatch, viewId]);
3184
3431
  const dispatchActiveMapId = React.useCallback((dialogType, isOpen, activeMapId) => void dispatch(uiActions.setActiveMapIdForDialog({
3185
3432
  type: dialogType,
3186
3433
  mapId: viewId,
@@ -3208,7 +3455,9 @@ const MenuBarConnect = ({
3208
3455
  return jsx(SoundingsMenuBar, {
3209
3456
  handleClick: handleClick,
3210
3457
  isOpen: isOpen,
3211
- selectedSourcesAmount: sourcesAmount
3458
+ selectedSourcesAmount: sourcesAmount,
3459
+ shouldHideMarkers: shouldHideLocationMarkers,
3460
+ toggleMarkers: toggleLocationMarkers
3212
3461
  });
3213
3462
  };
3214
3463
 
@@ -3348,24 +3597,33 @@ const roundToClosestTimestep = (timeToRound, availableSteps) => {
3348
3597
  }
3349
3598
  return timeToRound;
3350
3599
  };
3351
- const SoundingsTimeSliderConnect = () => {
3600
+ const SoundingsTimeSliderConnect = ({
3601
+ viewId
3602
+ }) => {
3352
3603
  const selectedTime = useSelector(getSelectedTime);
3353
3604
  const sourceSelections = useSelector(getSelectedSources);
3354
3605
  const dispatch = useDispatch();
3355
- const updateSelectedTime = React.useCallback(selectedTime => {
3606
+ const updateSelectedTime = React.useCallback((selectedTime, isInitialCorrection = false) => {
3356
3607
  dispatch(soundingsActions.updateSelectedTime({
3357
- selectedTime
3608
+ selectedTime,
3609
+ panelId: viewId,
3610
+ origin: isInitialCorrection ? SoundingsActionOrigin.soundingsManager : SoundingsActionOrigin.soundingsComponent
3358
3611
  }));
3359
- }, [dispatch]);
3612
+ }, [dispatch, viewId]);
3360
3613
  const {
3361
3614
  data: timesteps
3362
3615
  } = useTimesteps(sourceSelections);
3363
3616
  const mergedTimesteps = [...new Set(timesteps.flat())].sort();
3617
+ const hasInitializedTime = React.useRef(false);
3364
3618
  // Ensure that the selected time is always one of the available timesteps
3365
3619
  const validSelectedTime = roundToClosestTimestep(selectedTime, mergedTimesteps);
3366
3620
  useEffect(() => {
3367
3621
  if (selectedTime !== validSelectedTime) {
3368
- updateSelectedTime(validSelectedTime);
3622
+ const isInitial = !hasInitializedTime.current;
3623
+ updateSelectedTime(validSelectedTime, isInitial);
3624
+ if (isInitial) {
3625
+ hasInitializedTime.current = true;
3626
+ }
3369
3627
  }
3370
3628
  }, [selectedTime, updateSelectedTime, validSelectedTime]);
3371
3629
  return jsx(SoundingsTimeSlider, {
@@ -3404,33 +3662,80 @@ const useGetSize = viewRef => {
3404
3662
  };
3405
3663
  const SoundingsConnect = ({
3406
3664
  viewId,
3407
- diagramType = 'SKEWT'
3665
+ soundingsPreset
3408
3666
  }) => {
3409
3667
  const viewRef = React.useRef(null);
3410
3668
  const size = useGetSize(viewRef);
3411
- const [chosenDiagramType, setChosenDiagramType] = React.useState(diagramType);
3412
3669
  const geojsonLayerId = React.useRef(webmapUtils.generateLayerId()).current;
3413
3670
  const dispatch = useDispatch();
3671
+ const soundingsPresetRef = React.useRef();
3672
+ const presetJustLoadedRef = React.useRef(false);
3673
+ const initialLocationsRef = React.useRef({});
3674
+ const previousLocationsRef = React.useRef({});
3675
+ React.useLayoutEffect(() => {
3676
+ if (soundingsPreset && !isEqual(soundingsPreset, soundingsPresetRef.current)) {
3677
+ soundingsPresetRef.current = soundingsPreset;
3678
+ presetJustLoadedRef.current = true;
3679
+ initialLocationsRef.current = soundingsPreset.selectedLocations || {};
3680
+ previousLocationsRef.current = {};
3681
+ dispatch(soundingsActions.registerSoundingsPreset(soundingsPreset));
3682
+ setTimeout(() => {
3683
+ presetJustLoadedRef.current = false;
3684
+ initialLocationsRef.current = {};
3685
+ }, 500);
3686
+ } else if (!soundingsPreset) {
3687
+ soundingsPresetRef.current = undefined;
3688
+ initialLocationsRef.current = {};
3689
+ previousLocationsRef.current = {};
3690
+ }
3691
+ }, [dispatch, soundingsPreset, viewId]);
3414
3692
  const selectedTime = useSelector(getSelectedTime);
3415
- const sourceSelections = useSelector(getSelectedSources);
3416
- const locationSelections = useSelector(getLocationSelections);
3417
- const selectedFeature = useSelector(state => genericSelectors.getSelectedFeature(state.syncGroups, viewId));
3418
- const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, viewId));
3693
+ const sourceSelections = useSelector(getSelectedSources, shallowEqual);
3694
+ const locationSelections = useSelector(getLocationSelections, shallowEqual);
3695
+ const diagramType = useSelector(getDiagramType);
3696
+ const shouldHideLocationMarkers = useSelector(getShouldHideLocationMarkers);
3697
+ const setChosenDiagramType = React.useCallback(diagramType => {
3698
+ dispatch(soundingsActions.updateDiagramType({
3699
+ diagramType,
3700
+ panelId: viewId,
3701
+ origin: SoundingsActionOrigin.soundingsComponent
3702
+ }));
3703
+ }, [dispatch, viewId]);
3704
+ const selectedFeature = useSelector(state => genericSelectors.getSelectedFeature(state.syncGroups, viewId), shallowEqual);
3705
+ const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, viewId), shallowEqual);
3706
+ const firstMapId = selectedMapIds[0];
3419
3707
  const selectedMapPin = useSelector(state =>
3420
3708
  // mapPins are synced so we can arbitrarily choose the first linked map
3421
- mapSelectors.getPinLocation(state, selectedMapIds[0]));
3422
- const collections = useMemo(() => sourceSelections.map(({
3423
- collectionId,
3424
- url,
3425
- name
3426
- }) => ({
3427
- id: collectionId,
3428
- url,
3429
- name: name || collectionId
3430
- })), [sourceSelections]);
3709
+ mapSelectors.getPinLocation(state, firstMapId), shallowEqual);
3710
+ const collectionsRef = React.useRef();
3711
+ const collections = useMemo(() => {
3712
+ const newCollections = sourceSelections.map(({
3713
+ collectionId,
3714
+ url,
3715
+ name
3716
+ }) => ({
3717
+ id: collectionId,
3718
+ url,
3719
+ name: name || collectionId
3720
+ }));
3721
+ if (isEqual(newCollections, collectionsRef.current)) {
3722
+ return collectionsRef.current;
3723
+ }
3724
+ collectionsRef.current = newCollections;
3725
+ return newCollections;
3726
+ }, [sourceSelections]);
3431
3727
  const {
3432
- data: locationData
3728
+ data: rawLocationData
3433
3729
  } = useGetCollectionLocations(collections, true);
3730
+ // Stabilize locationData to prevent infinite loops caused by new array references
3731
+ const locationDataRef = React.useRef();
3732
+ const locationData = useMemo(() => {
3733
+ if (isEqual(rawLocationData, locationDataRef.current)) {
3734
+ return locationDataRef.current;
3735
+ }
3736
+ locationDataRef.current = rawLocationData;
3737
+ return rawLocationData;
3738
+ }, [rawLocationData]);
3434
3739
  /* Make sure the map pin is visible when linking new maps */
3435
3740
  useEffect(() => {
3436
3741
  const toggleMapPinVisibility = displayMapPin => {
@@ -3448,25 +3753,26 @@ const SoundingsConnect = ({
3448
3753
  };
3449
3754
  }, [dispatch, selectedMapIds]);
3450
3755
  // construct and send geojson to map for drawing locations
3451
- useEffect(() => {
3452
- const geoJsonForMap = {
3453
- type: 'FeatureCollection',
3454
- features: (locationData || []).flatMap(({
3455
- id: serviceId,
3456
- features
3457
- }) => features.map(feature => ({
3458
- type: 'Feature',
3459
- geometry: feature.geometry,
3756
+ const geoJsonForMap = useMemo(() => ({
3757
+ type: 'FeatureCollection',
3758
+ features: (locationData || []).flatMap(({
3759
+ id: serviceId,
3760
+ features
3761
+ }) => features.map(feature => ({
3762
+ type: 'Feature',
3763
+ geometry: feature.geometry,
3764
+ id: feature.id,
3765
+ properties: {
3766
+ name: feature.properties.name,
3767
+ mapFeatureClass: MapFeatureClass.ObservationStation,
3768
+ highlightSelectedFeature: selectedFeature?.id === feature.id,
3460
3769
  id: feature.id,
3461
- properties: {
3462
- name: feature.properties.name,
3463
- mapFeatureClass: MapFeatureClass.ObservationStation,
3464
- highlightSelectedFeature: selectedFeature?.id === feature.id,
3465
- id: feature.id,
3466
- serviceId
3467
- }
3468
- })))
3469
- };
3770
+ serviceId,
3771
+ hidden: shouldHideLocationMarkers
3772
+ }
3773
+ })))
3774
+ }), [locationData, selectedFeature?.id, shouldHideLocationMarkers]);
3775
+ useEffect(() => {
3470
3776
  dispatch(genericActions.addSharedData({
3471
3777
  panelId: viewId,
3472
3778
  data: {
@@ -3476,7 +3782,7 @@ const SoundingsConnect = ({
3476
3782
  }]
3477
3783
  }
3478
3784
  }));
3479
- }, [dispatch, geojsonLayerId, locationData, selectedFeature?.id, viewId]);
3785
+ }, [dispatch, geojsonLayerId, geoJsonForMap, viewId]);
3480
3786
  useEffect(() => () => {
3481
3787
  if (viewId) {
3482
3788
  dispatch(syncGroupsActions.deleteSharedData({
@@ -3487,31 +3793,145 @@ const SoundingsConnect = ({
3487
3793
  // Map location selection has changed. Update soundings location
3488
3794
  // since the location menu may be out of sync
3489
3795
  useEffect(() => {
3796
+ if (presetJustLoadedRef.current) {
3797
+ return;
3798
+ }
3490
3799
  const serviceId = selectedFeature?.serviceId;
3491
- if (serviceId) {
3492
- dispatch(soundingsActions.updateSelectedLocation({
3493
- sourceId: serviceId,
3494
- location: selectedFeature?.id
3495
- }));
3800
+ const featureId = selectedFeature?.id;
3801
+ if (serviceId && featureId) {
3802
+ const currentLocation = locationSelectionsRef.current[serviceId];
3803
+ const initialLocation = initialLocationsRef.current[serviceId];
3804
+ if (currentLocation !== featureId && initialLocation !== featureId) {
3805
+ dispatch(soundingsActions.updateSelectedLocation({
3806
+ sourceId: serviceId,
3807
+ location: featureId,
3808
+ panelId: viewId,
3809
+ origin: SoundingsActionOrigin.soundingsComponent
3810
+ }));
3811
+ }
3496
3812
  }
3497
- }, [dispatch, selectedFeature?.id, selectedFeature?.serviceId]);
3498
- // Map pin change on its own implies position selection
3813
+ }, [dispatch, selectedFeature?.id, selectedFeature?.serviceId, viewId]);
3814
+ // Use ref to avoid circular dependency when reading locationSelections
3815
+ const locationSelectionsRef = React.useRef(locationSelections);
3816
+ React.useEffect(() => {
3817
+ locationSelectionsRef.current = locationSelections;
3818
+ }, [locationSelections]);
3819
+ // Makes position requests for all user-selected sources for the current map pin position.
3820
+ // Map pin change on its own implies a new position selection.
3499
3821
  useEffect(() => {
3500
- // Use first source. Multiple sources will be implemented later.
3501
- // https://gitlab.com/opengeoweb/geoweb-assets/-/issues/4254
3502
- const source = sourceSelections[0];
3503
- if (source?.collectionId && !selectedFeature?.id && selectedMapPin?.lon && selectedMapPin.lat) {
3504
- dispatch(soundingsActions.updateSelectedLocation({
3505
- sourceId: source.collectionId,
3506
- location: [selectedMapPin.lon, selectedMapPin.lat]
3507
- }));
3822
+ if (presetJustLoadedRef.current) {
3823
+ return;
3824
+ }
3825
+ if (!selectedFeature?.id && !selectedMapPin?.id && selectedMapPin?.lon && selectedMapPin.lat) {
3826
+ const newLocation = [selectedMapPin.lon, selectedMapPin.lat];
3827
+ // Update location for all selected sources
3828
+ sourceSelections.forEach(source => {
3829
+ if (source?.collectionId) {
3830
+ const currentLocation = locationSelectionsRef.current[source.collectionId];
3831
+ const isSameLocation = Array.isArray(currentLocation) && currentLocation[0] === newLocation[0] && currentLocation[1] === newLocation[1];
3832
+ if (!isSameLocation) {
3833
+ dispatch(soundingsActions.updateSelectedLocation({
3834
+ sourceId: source.collectionId,
3835
+ location: newLocation,
3836
+ panelId: viewId,
3837
+ origin: SoundingsActionOrigin.soundingsComponent
3838
+ }));
3839
+ }
3840
+ }
3841
+ });
3842
+ }
3843
+ }, [selectedMapPin, selectedFeature?.id, sourceSelections, dispatch, viewId]);
3844
+ useEffect(() => {
3845
+ if (!locationData || locationData.length === 0) {
3846
+ return;
3847
+ }
3848
+ if (selectedMapIds.length === 0) {
3849
+ return;
3850
+ }
3851
+ let changedSourceId;
3852
+ for (const source of sourceSelections) {
3853
+ if (source?.collectionId) {
3854
+ const currentLocation = locationSelections[source.collectionId];
3855
+ const previousLocation = previousLocationsRef.current[source.collectionId];
3856
+ if (currentLocation !== previousLocation) {
3857
+ changedSourceId = source.collectionId;
3858
+ if (!presetJustLoadedRef.current) {
3859
+ break;
3860
+ }
3861
+ }
3862
+ }
3508
3863
  }
3509
- }, [selectedMapPin, selectedFeature?.id, sourceSelections, dispatch]);
3864
+ if (!changedSourceId) {
3865
+ return;
3866
+ }
3867
+ const selectedLocation = locationSelections[changedSourceId];
3868
+ if (!selectedLocation) {
3869
+ return;
3870
+ }
3871
+ previousLocationsRef.current = {
3872
+ ...previousLocationsRef.current,
3873
+ [changedSourceId]: selectedLocation
3874
+ };
3875
+ const serviceData = locationData.find(service => service.id === changedSourceId);
3876
+ if (!serviceData) {
3877
+ return;
3878
+ }
3879
+ if (typeof selectedLocation === 'string') {
3880
+ const foundFeature = serviceData.features.find(feature => feature.id === selectedLocation);
3881
+ if (foundFeature?.geometry.type === 'Point') {
3882
+ const [lon, lat] = foundFeature.geometry.coordinates;
3883
+ const mapPinNeedsUpdate = selectedMapPin?.id !== selectedLocation || selectedMapPin?.lon !== lon || selectedMapPin?.lat !== lat;
3884
+ const featureNeedsUpdate = selectedFeature?.id !== selectedLocation;
3885
+ if (mapPinNeedsUpdate) {
3886
+ selectedMapIds.forEach(mapId => {
3887
+ dispatch(mapActions.setMapPinLocation({
3888
+ mapId,
3889
+ mapPinLocation: {
3890
+ lon,
3891
+ lat,
3892
+ id: selectedLocation
3893
+ }
3894
+ }));
3895
+ });
3896
+ }
3897
+ if (featureNeedsUpdate) {
3898
+ dispatch(genericActions.addSharedData({
3899
+ panelId: viewId,
3900
+ data: {
3901
+ selectedFeatureId: selectedLocation
3902
+ }
3903
+ }));
3904
+ }
3905
+ }
3906
+ } else if (Array.isArray(selectedLocation)) {
3907
+ const [lon, lat] = selectedLocation;
3908
+ const needsUpdate = selectedMapPin?.lon !== lon || selectedMapPin?.lat !== lat;
3909
+ if (needsUpdate) {
3910
+ selectedMapIds.forEach(mapId => {
3911
+ dispatch(mapActions.setMapPinLocation({
3912
+ mapId,
3913
+ mapPinLocation: {
3914
+ lon,
3915
+ lat
3916
+ }
3917
+ }));
3918
+ });
3919
+ }
3920
+ }
3921
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3922
+ }, [locationData, locationSelections, sourceSelections, selectedMapIds, dispatch, viewId]);
3510
3923
  const {
3511
3924
  error,
3512
3925
  data,
3513
3926
  isPending
3514
3927
  } = useMultipleSources(sourceSelections, locationSelections, selectedTime);
3928
+ const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store));
3929
+ const changeOrder = newOrder => {
3930
+ const orderedIds = newOrder.map(collection => collection.id);
3931
+ dispatch(soundingsActions.changeSoundingPanelOrder({
3932
+ newOrderOfIds: orderedIds
3933
+ }));
3934
+ };
3515
3935
  return jsxs(Fragment, {
3516
3936
  children: [!!error && jsx(AlertBanner, {
3517
3937
  shouldClose: true,
@@ -3566,10 +3986,12 @@ const SoundingsConnect = ({
3566
3986
  },
3567
3987
  children: jsx(SharedMouseYProvider, {
3568
3988
  children: jsx(SortableAccordions, {
3569
- chosenDiagramType: chosenDiagramType,
3989
+ chosenDiagramType: diagramType,
3570
3990
  data: data,
3991
+ onOrderChange: changeOrder,
3571
3992
  setChosenDiagramType: setChosenDiagramType,
3572
3993
  size: size,
3994
+ soundingPanelsInOrder: soundingPanelsInOrder,
3573
3995
  viewRef: viewRef
3574
3996
  })
3575
3997
  })
@@ -3579,7 +4001,9 @@ const SoundingsConnect = ({
3579
4001
  flex: '0 0 auto',
3580
4002
  height: `${TIMESLIDER_HEIGHT}px`
3581
4003
  },
3582
- children: jsx(SoundingsTimeSliderConnect, {})
4004
+ children: jsx(SoundingsTimeSliderConnect, {
4005
+ viewId: viewId
4006
+ })
3583
4007
  })]
3584
4008
  })
3585
4009
  })]
@@ -3655,6 +4079,7 @@ const SoundingsSourceMenu = ({
3655
4079
  instanceId,
3656
4080
  url
3657
4081
  } = sourceSelections[collection.id] || {};
4082
+ const sourceUrl = url || collection.url;
3658
4083
  const hasInstances = collection.instances.length > 0;
3659
4084
  const selectedInstanceId = instanceId || collection.instances[0]?.id || undefined;
3660
4085
  return jsxs(ListItem, {
@@ -3669,7 +4094,7 @@ const SoundingsSourceMenu = ({
3669
4094
  onChange: event => {
3670
4095
  const verticalRange = selectedInstanceId !== undefined ? extractVerticalRange(collection, selectedInstanceId) : '';
3671
4096
  updateCollection(collection.id, {
3672
- url,
4097
+ url: sourceUrl,
3673
4098
  isChecked: event.target.checked,
3674
4099
  instanceId: selectedInstanceId,
3675
4100
  verticalRange
@@ -3685,7 +4110,7 @@ const SoundingsSourceMenu = ({
3685
4110
  const instanceId = event.target.value;
3686
4111
  const verticalRange = extractVerticalRange(collection, instanceId);
3687
4112
  updateCollection(collection.id, {
3688
- url,
4113
+ url: sourceUrl,
3689
4114
  instanceId,
3690
4115
  verticalRange
3691
4116
  });
@@ -3748,30 +4173,39 @@ const SoundingsSourceDialogConnect = ({
3748
4173
  dialogOrder,
3749
4174
  setDialogOrder
3750
4175
  } = useSetupDialog(uiTypes.DialogTypes.SoundingsSources);
4176
+ const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
3751
4177
  const updateCollection = React.useCallback((sourceId, value) => {
3752
4178
  dispatch(soundingsActions.updateSelectedSource({
3753
4179
  sourceId,
3754
- value
4180
+ value,
4181
+ panelId,
4182
+ origin: SoundingsActionOrigin.soundingsComponent
3755
4183
  }));
3756
- }, [dispatch]);
4184
+ }, [dispatch, panelId]);
3757
4185
  const sourceSelections = useSelector(getSourceSelections);
3758
4186
  // Set mappings for each collection
3759
4187
  React.useEffect(() => {
3760
4188
  soundingsCollections?.forEach(collection => {
4189
+ const currentSelection = sourceSelections[collection.id];
4190
+ if (currentSelection?.url && currentSelection?.parameterMappings) {
4191
+ return;
4192
+ }
4193
+ const newValue = {
4194
+ url: collection.url,
4195
+ isChecked: currentSelection?.isChecked || false,
4196
+ parameterMappings: collection.parameterMappings,
4197
+ ...(collection.name && collection.name !== '' ? {
4198
+ name: collection.name
4199
+ } : {})
4200
+ };
3761
4201
  dispatch(soundingsActions.updateSelectedSource({
3762
4202
  sourceId: collection.id,
3763
- value: {
3764
- url: collection.url,
3765
- isChecked: sourceSelections[collection.id]?.isChecked || false,
3766
- parameterMappings: collection.parameterMappings,
3767
- ...(collection.name && collection.name !== '' ? {
3768
- name: collection.name
3769
- } : {})
3770
- }
4203
+ value: newValue,
4204
+ origin: SoundingsActionOrigin.soundingsManager
3771
4205
  }));
3772
4206
  });
3773
4207
  // eslint-disable-next-line react-hooks/exhaustive-deps
3774
- }, [dispatch, soundingsCollections]);
4208
+ }, [dispatch, soundingsCollections, panelId]);
3775
4209
  const {
3776
4210
  data,
3777
4211
  isPending,
@@ -3941,8 +4375,6 @@ const LocationsMenuConnect = ({
3941
4375
  const locationSelections = useSelector(getLocationSelections);
3942
4376
  const selectedSourceIds = useSelector(getSelectedSourcesIds);
3943
4377
  const sourcesAmount = useSelector(getSelectedSourcesAmount);
3944
- const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsLocations));
3945
- const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, panelId));
3946
4378
  const {
3947
4379
  data,
3948
4380
  isPending,
@@ -3951,22 +4383,6 @@ const LocationsMenuConnect = ({
3951
4383
  const services = useMemo(() => {
3952
4384
  return data?.filter(service => selectedSourceIds.includes(service.id)) || [];
3953
4385
  }, [data, selectedSourceIds]);
3954
- const handleFeatureSelect = (panelId, featureId) => {
3955
- dispatch(genericActions.addSharedData({
3956
- panelId,
3957
- data: {
3958
- selectedFeatureId: featureId
3959
- }
3960
- }));
3961
- };
3962
- const setMapPinLocation = mapPinLocation => {
3963
- selectedMapIds.forEach(mapId => {
3964
- dispatch(mapActions.setMapPinLocation({
3965
- mapId,
3966
- mapPinLocation
3967
- }));
3968
- });
3969
- };
3970
4386
  return jsx(LocationsMenu, {
3971
4387
  error: error,
3972
4388
  isLoading: isPending,
@@ -3982,50 +4398,118 @@ const LocationsMenuConnect = ({
3982
4398
  sourceId,
3983
4399
  location: locationId
3984
4400
  }));
3985
- // Use first source. Multiple sources will be implemented later.
3986
- // https://gitlab.com/opengeoweb/geoweb-assets/-/issues/4254
3987
- const isFirstSource = sourceId === selectedSourceIds[0];
3988
- if (!isFirstSource) {
3989
- return;
3990
- }
3991
- if (locationId) {
3992
- handleFeatureSelect(panelId, locationId);
3993
- }
3994
- const foundFeature = services[0].features.find(feature => feature.id === locationId);
3995
- // check type for typescript
3996
- if (foundFeature?.geometry.type === 'Point') {
3997
- const [lon, lat] = foundFeature.geometry.coordinates;
3998
- setMapPinLocation({
3999
- lon,
4000
- lat,
4001
- id: locationId
4002
- });
4003
- }
4004
4401
  }
4005
4402
  });
4006
4403
  };
4007
4404
 
4405
+ var SoundingManagerDummyPreset = {
4406
+ order: [
4407
+ "soundings",
4408
+ "windbarbs"
4409
+ ],
4410
+ entities: {
4411
+ soundings: {
4412
+ id: "soundings",
4413
+ name: "Soundings",
4414
+ isDisplayed: true
4415
+ },
4416
+ windbarbs: {
4417
+ id: "windbarbs",
4418
+ name: "Wind Barbs",
4419
+ isDisplayed: true
4420
+ }
4421
+ }
4422
+ };
4423
+ var preset = {
4424
+ SoundingManagerDummyPreset: SoundingManagerDummyPreset
4425
+ };
4426
+
4427
+ const SoundingManagerListItem = ({
4428
+ name,
4429
+ isDisplayed
4430
+ }) => {
4431
+ return jsx(Card, {
4432
+ className: "sortable-item",
4433
+ sx: {
4434
+ margin: '4px 0 2px',
4435
+ backgroundColor: isDisplayed ? 'white' : 'rgb(232, 232, 232)'
4436
+ },
4437
+ children: jsxs(Grid, {
4438
+ alignItems: "center",
4439
+ container: true,
4440
+ direction: "row",
4441
+ justifyContent: "flex-start",
4442
+ sx: {
4443
+ height: '32px'
4444
+ },
4445
+ children: [jsx(IconButton, {
4446
+ "aria-label": "drag-handle",
4447
+ className: "drag-handle",
4448
+ size: "small",
4449
+ sx: {
4450
+ width: '24px',
4451
+ height: '24px',
4452
+ paddingRight: '8px',
4453
+ paddingLeft: '8px'
4454
+ },
4455
+ children: jsx(DragHandle, {
4456
+ fontSize: "small"
4457
+ })
4458
+ }), jsx(Box, {
4459
+ sx: {
4460
+ display: 'flex',
4461
+ alignItems: 'center'
4462
+ },
4463
+ children: isDisplayed ? jsx(Visibility, {}) : jsx(VisibilityOff, {})
4464
+ }), jsxs(Box, {
4465
+ sx: {
4466
+ paddingRight: '8px',
4467
+ paddingLeft: '8px'
4468
+ },
4469
+ children: [" ", name, " "]
4470
+ })]
4471
+ })
4472
+ });
4473
+ };
4474
+
4008
4475
  const SoundingsManager = ({
4009
4476
  isOpen,
4010
4477
  onClose,
4011
4478
  onMouseDown,
4012
- order
4479
+ order,
4480
+ soundingPanelsInOrder,
4481
+ onOrderChange
4013
4482
  }) => {
4483
+ const sortableList = React.useMemo(() => soundingPanelsInOrder.map(collection => ({
4484
+ ...collection
4485
+ })), [soundingPanelsInOrder]);
4014
4486
  return jsx(ToolContainerDraggable, {
4015
4487
  isOpen: isOpen,
4016
4488
  minWidth: 300,
4017
4489
  onClose: onClose,
4018
4490
  onMouseDown: onMouseDown,
4019
4491
  order: order,
4020
- startPosition: {
4021
- top: 40,
4022
- left: 50
4023
- },
4024
- startSize: {
4025
- width: 320
4026
- },
4027
- title: "Sounding Manager",
4028
- children: "Soundings Manager"
4492
+ title: "Soundings Manager",
4493
+ children: jsx(ReactSortable, {
4494
+ animation: 200,
4495
+ direction: "vertical",
4496
+ draggable: ".sortable-item",
4497
+ handle: ".drag-handle",
4498
+ list: sortableList,
4499
+ setList: onOrderChange,
4500
+ style: {
4501
+ padding: '5px'
4502
+ },
4503
+ tag: "div",
4504
+ children: sortableList.map(({
4505
+ id,
4506
+ name,
4507
+ isDisplayed
4508
+ }) => jsx(SoundingManagerListItem, {
4509
+ isDisplayed: isDisplayed,
4510
+ name: name
4511
+ }, id))
4512
+ })
4029
4513
  });
4030
4514
  };
4031
4515
 
@@ -4036,18 +4520,32 @@ const SoundingsManagerConnect = () => {
4036
4520
  dialogOrder,
4037
4521
  setDialogOrder
4038
4522
  } = useSetupDialog(uiTypes.DialogTypes.SoundingsManager);
4523
+ const dispatch = useDispatch();
4524
+ React.useEffect(() => {
4525
+ const data = preset.SoundingManagerDummyPreset;
4526
+ dispatch(soundingsActions.addSoundingPanelsFromPreset(data));
4527
+ }, [dispatch]);
4528
+ const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store));
4529
+ const changeOrder = newOrder => {
4530
+ const orderedIds = newOrder.map(collection => collection.id);
4531
+ dispatch(soundingsActions.changeSoundingPanelOrder({
4532
+ newOrderOfIds: orderedIds
4533
+ }));
4534
+ };
4039
4535
  return jsx(SoundingsManager, {
4040
4536
  isOpen: isDialogOpen,
4041
4537
  onClose: onCloseDialog,
4042
4538
  onMouseDown: setDialogOrder,
4043
- order: dialogOrder
4539
+ onOrderChange: changeOrder,
4540
+ order: dialogOrder,
4541
+ soundingPanelsInOrder: soundingPanelsInOrder
4044
4542
  });
4045
4543
  };
4046
4544
 
4047
4545
  const soundingsLookUp = payload => {
4048
4546
  if (payload.componentType === 'Soundings') {
4049
4547
  return jsx(SoundingsConnect, {
4050
- diagramType: payload.diagramType ? payload.diagramType : 'SKEWT',
4548
+ soundingsPreset: payload.initialProps,
4051
4549
  viewId: payload.id
4052
4550
  });
4053
4551
  }
@@ -4082,4 +4580,4 @@ const createMockStore = mockState => configureStore({
4082
4580
  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(storeMiddlewares)
4083
4581
  });
4084
4582
 
4085
- export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsConnect, SoundingsManagerConnect, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingDiagrams, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateInTestsAndStories, useSoundingsTranslation };
4583
+ export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsActionOrigin, SoundingsConnect, SoundingsManagerConnect, SoundingsSourceDialogConnect, types as SoundingsTypes, createMockStore, initSoundingsI18n, soundingDiagrams, soundingsActions, soundingsLookUp, soundingsReducer, soundingsReducersMap, selectors as soundingsSelectors, soundingsTranslations, translateInTestsAndStories, useSoundingsTranslation };