@opengeoweb/soundings 15.0.0 → 15.2.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,8 +1,9 @@
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';
@@ -16,9 +17,8 @@ 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
- import { createSlice, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
21
- import { FixedSizeList } from 'react-window';
20
+ import { createSlice, createAction, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
21
+ import { List as List$1 } from 'react-window';
22
22
 
23
23
  var en = {
24
24
  sources: "Sources",
@@ -30,7 +30,10 @@ 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
+ "coming-soon": "Coming soon",
35
+ "soundings-toggle-location-markers-label": "Markers",
36
+ "soundings-toggle-location-markers-tooltip": "Show/hide location markers"
34
37
  };
35
38
  var no = {
36
39
  sources: "Kilder",
@@ -42,7 +45,10 @@ var no = {
42
45
  options: "Valg",
43
46
  "diagram-SKEWT": "Skew-T",
44
47
  "diagram-BIJVOET": "Bijvoet",
45
- "diagram-EMAGRAM": "Emagram"
48
+ "diagram-EMAGRAM": "Emagram",
49
+ "coming-soon": "Kommer snart",
50
+ "soundings-toggle-location-markers-label": "ikke oversatt",
51
+ "soundings-toggle-location-markers-tooltip": "ikke oversatt"
46
52
  };
47
53
  var nl = {
48
54
  sources: "Bronnen",
@@ -54,7 +60,10 @@ var nl = {
54
60
  options: "Opties",
55
61
  "diagram-SKEWT": "Skew-T",
56
62
  "diagram-BIJVOET": "Bijvoet",
57
- "diagram-EMAGRAM": "Emagram"
63
+ "diagram-EMAGRAM": "Emagram",
64
+ "coming-soon": "Binnenkort beschikbaar",
65
+ "soundings-toggle-location-markers-label": "Kaart locaties",
66
+ "soundings-toggle-location-markers-tooltip": "Toon/Verberg locatie punten"
58
67
  };
59
68
  var fi = {
60
69
  sources: "Lähteet",
@@ -66,7 +75,10 @@ var fi = {
66
75
  options: "Valinnat",
67
76
  "diagram-SKEWT": "Skew-T",
68
77
  "diagram-BIJVOET": "Bijvoet",
69
- "diagram-EMAGRAM": "Emagram"
78
+ "diagram-EMAGRAM": "Emagram",
79
+ "coming-soon": "Tulossa pian",
80
+ "soundings-toggle-location-markers-label": "Merkit",
81
+ "soundings-toggle-location-markers-tooltip": "Näytä/piilota kohdemerkit"
70
82
  };
71
83
  var soundingsTranslations = {
72
84
  en: en,
@@ -917,7 +929,7 @@ const drawIsothermsBijvoet = ({
917
929
  const pathString = lineGenerator(points);
918
930
  const strID = `${temp}-isotherm-BIJVOET`;
919
931
  svg.append('path').attr('id', strID).attr('d', pathString).style('fill', 'none').style('stroke-width', temp % 10 === 0 ? 2 : 1).style('stroke', temp === 0 ? '#D27D2D' : 'orange').style('stroke-opacity', 0.5).attr('clip-path', `url(#clip-${d3GraphId}-BIJVOETd3)`);
920
- if (temp % 10 === 0 && points[0] && points[0][1] === height) {
932
+ if (temp % 10 === 0 && points[0]?.[1] === height) {
921
933
  svg.append('text').text(temp).attr('text-anchor', 'middle').attr('x', `${points[0][0]}px`).attr('y', `${height + 12}px`).attr('fill', temp === 0 ? '#D27D2D' : 'orange').attr('font-size', '10px');
922
934
  }
923
935
  }
@@ -2039,7 +2051,8 @@ const SoundingsAccordion = React.forwardRef(({
2039
2051
  children,
2040
2052
  setSoundingsIsClosed,
2041
2053
  setPreviousHeight,
2042
- size
2054
+ size,
2055
+ isDisplayed
2043
2056
  }, aspectRatioRef) => {
2044
2057
  const containerRef = React.useRef(null);
2045
2058
  const widthRef = React.useRef(null);
@@ -2156,6 +2169,7 @@ const SoundingsAccordion = React.forwardRef(({
2156
2169
  };
2157
2170
  }, [autoSizing, minWidth, setContainerWidth]);
2158
2171
  const cardStyleWhenAuto = {
2172
+ display: isDisplayed ? 'block' : 'none',
2159
2173
  width: 'auto',
2160
2174
  flex: '1 1 100%',
2161
2175
  minWidth: 0,
@@ -2163,12 +2177,14 @@ const SoundingsAccordion = React.forwardRef(({
2163
2177
  boxSizing: 'border-box'
2164
2178
  };
2165
2179
  const cardStyleWhenLocked = {
2180
+ display: isDisplayed ? 'block' : 'none',
2166
2181
  width: containerWidth,
2167
2182
  flex: '0 1 auto',
2168
2183
  minWidth,
2169
2184
  maxWidth: maxWidth || '100%',
2170
2185
  boxSizing: 'border-box'
2171
2186
  };
2187
+ //
2172
2188
  return jsx(Card, {
2173
2189
  className: "sortable-item",
2174
2190
  "data-autosizing": autoSizing ? 'true' : 'false',
@@ -2283,7 +2299,7 @@ const SortableAccordions = ({
2283
2299
  }) => {
2284
2300
  const [soundingsIsClosed, setSoundingsIsClosed] = React.useState(false);
2285
2301
  const [previousHeight, setPreviousHeight] = React.useState(size);
2286
- const createPanel = React.useCallback(id => {
2302
+ const createPanel = React.useCallback((id, isDisplayed) => {
2287
2303
  const normalizedId = id.toLowerCase().replace(/\s+/g, '');
2288
2304
  switch (normalizedId) {
2289
2305
  case 'soundings':
@@ -2293,6 +2309,7 @@ const SortableAccordions = ({
2293
2309
  diagramType: chosenDiagramType,
2294
2310
  setDiagramType: setChosenDiagramType
2295
2311
  }),
2312
+ isDisplayed: isDisplayed,
2296
2313
  name: "Soundings",
2297
2314
  ref: viewRef,
2298
2315
  setPreviousHeight: setPreviousHeight,
@@ -2316,6 +2333,7 @@ const SortableAccordions = ({
2316
2333
  });
2317
2334
  case 'windbarbs':
2318
2335
  return jsx(SoundingsAccordion, {
2336
+ isDisplayed: isDisplayed,
2319
2337
  maxWidth: 200,
2320
2338
  name: "Wind Barbs",
2321
2339
  children: jsx(WindBarbs, {
@@ -2370,16 +2388,18 @@ const SortableAccordions = ({
2370
2388
  width: '100%'
2371
2389
  },
2372
2390
  tag: "div",
2373
- children: soundingPanelsInOrder.filter(entry => entry.isDisplayed).flatMap(({
2374
- id
2391
+ children: soundingPanelsInOrder.flatMap(({
2392
+ id,
2393
+ isDisplayed
2375
2394
  }) => {
2376
- const panel = createPanel(id);
2395
+ const panel = createPanel(id, isDisplayed);
2377
2396
  if (!panel) {
2378
2397
  return [];
2379
2398
  }
2380
2399
  return panel ? jsx(React.Fragment, {
2381
2400
  children: React.cloneElement(panel, {
2382
2401
  accordionId: id,
2402
+ isDisplayed,
2383
2403
  handleButtonComponent: jsx(IconButton, {
2384
2404
  "aria-label": "drag-handle",
2385
2405
  className: "drag-handle",
@@ -2850,10 +2870,28 @@ const useMultipleSources = (sources, locations, datetime) => {
2850
2870
  parameterMappings,
2851
2871
  verticalRange
2852
2872
  }, index) => {
2853
- // If acceptance time logic is needed, it could come here.
2854
- const dateToUse = datetime &&
2855
- // Only use selected time if exact match if found
2856
- timesteps[index].includes(new Date(datetime).toISOString()) ? datetime : '';
2873
+ // Find the best matching timestep for this source
2874
+ let dateToUse = '';
2875
+ if (datetime && timesteps[index] && timesteps[index].length > 0) {
2876
+ const requestedTime = new Date(datetime).getTime();
2877
+ const availableTimesteps = timesteps[index];
2878
+ // Check for exact match first
2879
+ if (availableTimesteps.includes(new Date(datetime).toISOString())) {
2880
+ dateToUse = datetime;
2881
+ } else {
2882
+ // Find nearest timestep (prefer earlier timesteps if equidistant)
2883
+ let nearestTimestep = availableTimesteps[0];
2884
+ let minDiff = Math.abs(new Date(availableTimesteps[0]).getTime() - requestedTime);
2885
+ for (const timestep of availableTimesteps) {
2886
+ const diff = Math.abs(new Date(timestep).getTime() - requestedTime);
2887
+ if (diff < minDiff) {
2888
+ minDiff = diff;
2889
+ nearestTimestep = timestep;
2890
+ }
2891
+ }
2892
+ dateToUse = nearestTimestep;
2893
+ }
2894
+ }
2857
2895
  const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dateToUse || '', parameterMappings || defaultParameterMappings, verticalRange || '');
2858
2896
  return queryParams;
2859
2897
  });
@@ -3035,6 +3073,48 @@ const useGetCollectionLocations = (collections, enabled) => {
3035
3073
  });
3036
3074
  };
3037
3075
 
3076
+ /* *
3077
+ * Licensed under the Apache License, Version 2.0 (the "License");
3078
+ * you may not use this file except in compliance with the License.
3079
+ * You may obtain a copy of the License at
3080
+ *
3081
+ * http://www.apache.org/licenses/LICENSE-2.0
3082
+ *
3083
+ * Unless required by applicable law or agreed to in writing, software
3084
+ * distributed under the License is distributed on an "AS IS" BASIS,
3085
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3086
+ * See the License for the specific language governing permissions and
3087
+ * limitations under the License.
3088
+ *
3089
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
3090
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
3091
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
3092
+ * */
3093
+ const emptyPanelIds = [];
3094
+ const toStringArray = value => {
3095
+ if (!Array.isArray(value)) {
3096
+ return emptyPanelIds;
3097
+ }
3098
+ return value.filter(item => typeof item === 'string');
3099
+ };
3100
+ const getRegisteredPanelIds = state => {
3101
+ if (!state) {
3102
+ return emptyPanelIds;
3103
+ }
3104
+ const registeredPanelIds = toStringArray(state.registeredPanelIds);
3105
+ if (registeredPanelIds.length > 0) {
3106
+ return registeredPanelIds;
3107
+ }
3108
+ return Object.keys(state.byId);
3109
+ };
3110
+ const isRegisteredPanelId = (state, panelId) => {
3111
+ if (!panelId) {
3112
+ return false;
3113
+ }
3114
+ return getRegisteredPanelIds(state).includes(panelId);
3115
+ };
3116
+ const getPanelIdFromAction = action => action.payload?.panelId;
3117
+
3038
3118
  /* *
3039
3119
  * Licensed under the Apache License, Version 2.0 (the "License");
3040
3120
  * you may not use this file except in compliance with the License.
@@ -3056,15 +3136,48 @@ const initialSoundingsState = {
3056
3136
  selectedSources: {},
3057
3137
  selectedLocations: {},
3058
3138
  selectedTime: new Date().toISOString(),
3139
+ diagramType: 'SKEWT',
3059
3140
  soundingPanels: {
3060
- order: [],
3061
- entities: {}
3062
- }
3141
+ order: ['soundings', 'windbarbs'],
3142
+ entities: {
3143
+ soundings: {
3144
+ id: 'soundings',
3145
+ name: 'Soundings',
3146
+ isDisplayed: true
3147
+ },
3148
+ windbarbs: {
3149
+ id: 'windbarbs',
3150
+ name: 'Wind Barbs',
3151
+ isDisplayed: true
3152
+ }
3153
+ }
3154
+ },
3155
+ shouldHideLocationMarkers: false
3156
+ };
3157
+ const initialState = {
3158
+ byId: {},
3159
+ registeredPanelIds: [],
3160
+ soundingsCollections: []
3063
3161
  };
3064
3162
  const slice = createSlice({
3065
3163
  initialState: initialSoundingsState,
3066
3164
  name: 'soundings',
3067
3165
  reducers: {
3166
+ registerSoundingsPanel: (draft, _payloadAction) => {
3167
+ },
3168
+ registerSoundingsPreset: (draft, payloadAction) => {
3169
+ const {
3170
+ preset
3171
+ } = payloadAction.payload;
3172
+ if (preset) {
3173
+ draft.selectedSources = preset.selectedSources ?? {};
3174
+ draft.selectedLocations = preset.selectedLocations ?? {};
3175
+ draft.selectedTime = preset.selectedTime ?? new Date().toISOString();
3176
+ draft.diagramType = preset.diagramType ?? 'SKEWT';
3177
+ draft.soundingPanels = preset.soundingPanels ?? initialSoundingsState.soundingPanels;
3178
+ draft.shouldHideLocationMarkers = preset.shouldHideLocationMarkers ?? false;
3179
+ }
3180
+ },
3068
3181
  updateSelectedSource: (draft, payloadAction) => {
3069
3182
  const {
3070
3183
  sourceId,
@@ -3088,11 +3201,20 @@ const slice = createSlice({
3088
3201
  } = payloadAction.payload;
3089
3202
  draft.selectedTime = selectedTime;
3090
3203
  },
3204
+ updateDiagramType: (draft, payloadAction) => {
3205
+ draft.diagramType = payloadAction.payload.diagramType;
3206
+ },
3207
+ toggleLocationMarkers: (draft, payloadAction) => {
3208
+ const {
3209
+ shouldHideLocationMarkers
3210
+ } = payloadAction.payload;
3211
+ draft.shouldHideLocationMarkers = shouldHideLocationMarkers;
3212
+ },
3091
3213
  addSoundingPanelsFromPreset: (draft, payloadAction) => {
3092
3214
  const {
3093
3215
  order,
3094
3216
  entities
3095
- } = payloadAction.payload;
3217
+ } = payloadAction.payload.panels;
3096
3218
  draft.soundingPanels.order = order;
3097
3219
  draft.soundingPanels.entities = entities;
3098
3220
  },
@@ -3101,13 +3223,86 @@ const slice = createSlice({
3101
3223
  newOrderOfIds
3102
3224
  } = payloadAction.payload;
3103
3225
  draft.soundingPanels.order = newOrderOfIds;
3226
+ },
3227
+ toggleSoundingPanelVisibility: (draft, payloadAction) => {
3228
+ const {
3229
+ id
3230
+ } = payloadAction.payload;
3231
+ const panel = draft.soundingPanels.entities[id];
3232
+ if (panel) {
3233
+ panel.isDisplayed = !panel.isDisplayed;
3234
+ }
3104
3235
  }
3105
3236
  }
3106
3237
  });
3107
- const {
3108
- reducer: soundingsReducer,
3109
- actions: soundingsActions
3110
- } = slice;
3238
+ const setSoundingsCollections = createAction('soundings/setCollections');
3239
+ const soundingsActions = {
3240
+ ...slice.actions,
3241
+ setSoundingsCollections
3242
+ };
3243
+ const shouldHandlePanelAction = (state, action) => {
3244
+ return isRegisteredPanelId(state, getPanelIdFromAction(action));
3245
+ };
3246
+ const normalizeSoundingsState = state => {
3247
+ if ('byId' in state) {
3248
+ return state;
3249
+ }
3250
+ return {
3251
+ ...initialState,
3252
+ byId: {
3253
+ ...initialState.byId,
3254
+ default: state
3255
+ },
3256
+ registeredPanelIds: ['default']
3257
+ };
3258
+ };
3259
+ const soundingsReducer = (state, action) => {
3260
+ const normalizedState = normalizeSoundingsState(state ?? initialState);
3261
+ if (action.type === setSoundingsCollections.type) {
3262
+ const collections = action.payload;
3263
+ if (collections === normalizedState.soundingsCollections) {
3264
+ return normalizedState;
3265
+ }
3266
+ return {
3267
+ ...normalizedState,
3268
+ soundingsCollections: collections
3269
+ };
3270
+ }
3271
+ if (action.type === slice.actions.registerSoundingsPanel.type) {
3272
+ const {
3273
+ panelId
3274
+ } = action.payload;
3275
+ if (!panelId) {
3276
+ return normalizedState;
3277
+ }
3278
+ if (normalizedState.registeredPanelIds?.includes(panelId)) {
3279
+ return normalizedState;
3280
+ }
3281
+ return {
3282
+ ...normalizedState,
3283
+ registeredPanelIds: [...(normalizedState.registeredPanelIds ?? []), panelId]
3284
+ };
3285
+ }
3286
+ if (!shouldHandlePanelAction(normalizedState, action)) {
3287
+ return normalizedState;
3288
+ }
3289
+ const panelId = getPanelIdFromAction(action);
3290
+ if (!panelId) {
3291
+ return normalizedState;
3292
+ }
3293
+ const currentPanelState = normalizedState.byId[panelId];
3294
+ const nextPanelState = slice.reducer(currentPanelState ?? initialSoundingsState, action);
3295
+ if (nextPanelState === currentPanelState) {
3296
+ return normalizedState;
3297
+ }
3298
+ return {
3299
+ ...normalizedState,
3300
+ byId: {
3301
+ ...normalizedState.byId,
3302
+ [panelId]: nextPanelState
3303
+ }
3304
+ };
3305
+ };
3111
3306
 
3112
3307
  /* *
3113
3308
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -3126,12 +3321,23 @@ const {
3126
3321
  * Copyright 2025 - Finnish Meteorological Institute (FMI)
3127
3322
  * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
3128
3323
  * */
3129
- const getSoundingsStore = store => store?.soundings || initialSoundingsState;
3324
+ const emptySoundingsCollections = [];
3325
+ const getSoundingsStore = (store, panelId) => {
3326
+ const soundingsState = store?.soundings;
3327
+ if (!panelId || !isRegisteredPanelId(soundingsState, panelId)) {
3328
+ return initialSoundingsState;
3329
+ }
3330
+ const panelStore = soundingsState?.byId[panelId];
3331
+ return panelStore ?? initialSoundingsState;
3332
+ };
3333
+ const getSoundingsCollections = store => store?.soundings?.soundingsCollections || emptySoundingsCollections;
3130
3334
  const getSourceSelections = createSelector(getSoundingsStore, store => {
3131
3335
  return store.selectedSources;
3132
3336
  }, selectorMemoizationOptions);
3133
3337
  const getLocationSelections = createSelector(getSoundingsStore, store => store.selectedLocations, selectorMemoizationOptions);
3134
- const getSelectedTime = createSelector(getSoundingsStore, store => store.selectedTime, selectorMemoizationOptions);
3338
+ const getSelectedTime = createSelector(getSoundingsStore, store => store.selectedTime ?? new Date().toISOString(), selectorMemoizationOptions);
3339
+ const getDiagramType = createSelector(getSoundingsStore, store => store.diagramType ?? 'SKEWT', selectorMemoizationOptions);
3340
+ const getShouldHideLocationMarkers = createSelector(getSoundingsStore, store => store.shouldHideLocationMarkers ?? false, selectorMemoizationOptions);
3135
3341
  const getSelectedSources = createSelector(getSoundingsStore, store => {
3136
3342
  const sources = store.selectedSources;
3137
3343
  const activeSources = Object.keys(sources).reduce((list, key) => {
@@ -3139,11 +3345,11 @@ const getSelectedSources = createSelector(getSoundingsStore, store => {
3139
3345
  if (value.isChecked === true) {
3140
3346
  return list.concat({
3141
3347
  collectionId: key,
3142
- url: value.url,
3348
+ url: value.url ?? '',
3143
3349
  instanceId: value.instanceId,
3144
3350
  verticalRange: value.verticalRange,
3145
3351
  parameterMappings: value.parameterMappings,
3146
- name: value.name || ''
3352
+ name: value.name ?? ''
3147
3353
  });
3148
3354
  }
3149
3355
  return list;
@@ -3156,17 +3362,138 @@ const getSelectedSourcesAmount = createSelector(getSelectedSources, activeSource
3156
3362
  const getSelectedSourcesIds = createSelector(getSelectedSources, activeSources => {
3157
3363
  return activeSources.map(source => source.collectionId);
3158
3364
  }, selectorMemoizationOptions);
3365
+ const getSoundingsPreset = createSelector(getSoundingsStore, soundingsStore => {
3366
+ const {
3367
+ selectedTime,
3368
+ selectedSources,
3369
+ ...rest
3370
+ } = soundingsStore;
3371
+ const filteredSources = Object.entries(selectedSources).reduce((acc, [key, value]) => {
3372
+ const {
3373
+ instanceId,
3374
+ ...sourceWithoutInstance
3375
+ } = value;
3376
+ return {
3377
+ ...acc,
3378
+ [key]: sourceWithoutInstance
3379
+ };
3380
+ }, {});
3381
+ return {
3382
+ ...rest,
3383
+ selectedSources: filteredSources
3384
+ };
3385
+ }, selectorMemoizationOptions);
3386
+ const EMPTY_SOUDINGS_PANELS = {
3387
+ order: [],
3388
+ entities: {}
3389
+ };
3159
3390
  const getSoundingPanelsInOrder = createSelector(getSoundingsStore, store => {
3160
3391
  const {
3161
3392
  order = [],
3162
3393
  entities = {}
3163
- } = store.soundingPanels || {
3164
- order: [],
3165
- entities: {}
3166
- };
3394
+ } = store.soundingPanels || EMPTY_SOUDINGS_PANELS;
3167
3395
  return order.map(id => entities[id]);
3168
3396
  }, selectorMemoizationOptions);
3169
3397
 
3398
+ var selectors = /*#__PURE__*/Object.freeze({
3399
+ __proto__: null,
3400
+ getDiagramType: getDiagramType,
3401
+ getLocationSelections: getLocationSelections,
3402
+ getSelectedSources: getSelectedSources,
3403
+ getSelectedSourcesAmount: getSelectedSourcesAmount,
3404
+ getSelectedSourcesIds: getSelectedSourcesIds,
3405
+ getSelectedTime: getSelectedTime,
3406
+ getShouldHideLocationMarkers: getShouldHideLocationMarkers,
3407
+ getSoundingPanelsInOrder: getSoundingPanelsInOrder,
3408
+ getSoundingsCollections: getSoundingsCollections,
3409
+ getSoundingsPreset: getSoundingsPreset,
3410
+ getSourceSelections: getSourceSelections
3411
+ });
3412
+
3413
+ /* *
3414
+ * Licensed under the Apache License, Version 2.0 (the "License");
3415
+ * you may not use this file except in compliance with the License.
3416
+ * You may obtain a copy of the License at
3417
+ *
3418
+ * http://www.apache.org/licenses/LICENSE-2.0
3419
+ *
3420
+ * Unless required by applicable law or agreed to in writing, software
3421
+ * distributed under the License is distributed on an "AS IS" BASIS,
3422
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3423
+ * See the License for the specific language governing permissions and
3424
+ * limitations under the License.
3425
+ *
3426
+ * Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
3427
+ * Copyright 2025 - Finnish Meteorological Institute (FMI)
3428
+ * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
3429
+ * */
3430
+ var SoundingsActionOrigin;
3431
+ (function (SoundingsActionOrigin) {
3432
+ SoundingsActionOrigin["soundingsManager"] = "soundingsManager";
3433
+ SoundingsActionOrigin["soundingsComponent"] = "soundingsComponent";
3434
+ })(SoundingsActionOrigin || (SoundingsActionOrigin = {}));
3435
+
3436
+ var types = /*#__PURE__*/Object.freeze({
3437
+ __proto__: null,
3438
+ get SoundingsActionOrigin () { return SoundingsActionOrigin; }
3439
+ });
3440
+
3441
+ const SoundingsMarkersSwitch = ({
3442
+ shouldHideMarkers,
3443
+ toggleMarkers
3444
+ }) => {
3445
+ const {
3446
+ t
3447
+ } = useSoundingsTranslation();
3448
+ return jsxs(Box, {
3449
+ sx: {
3450
+ display: 'flex',
3451
+ flexWrap: 'wrap',
3452
+ alignItems: 'flex-start',
3453
+ 'label:first-of-type': {
3454
+ marginLeft: '5px',
3455
+ marginRight: '-5px'
3456
+ },
3457
+ 'label:last-of-type': {
3458
+ marginLeft: '-5px'
3459
+ },
3460
+ '.switch-button label': {
3461
+ display: 'none'
3462
+ },
3463
+ '.markers-label': {
3464
+ display: 'none'
3465
+ },
3466
+ '@media (min-width: 720px)': {
3467
+ minWidth: '146px',
3468
+ '.markers-label, .switch-button label': {
3469
+ display: 'initial'
3470
+ }
3471
+ }
3472
+ },
3473
+ children: [jsx(Typography, {
3474
+ className: "markers-label",
3475
+ sx: {
3476
+ marginTop: '4px'
3477
+ },
3478
+ variant: "body2",
3479
+ children: t('soundings-toggle-location-markers-label')
3480
+ }), jsx(Box, {
3481
+ sx: {
3482
+ marginTop: '-5px'
3483
+ },
3484
+ children: jsx(SwitchButton, {
3485
+ activeLabel: "ON",
3486
+ checked: !shouldHideMarkers,
3487
+ inActiveLabel: "OFF",
3488
+ onChange: event => {
3489
+ toggleMarkers(!event.target.checked);
3490
+ },
3491
+ tooltip: t('soundings-toggle-location-markers-tooltip')
3492
+ })
3493
+ })]
3494
+ });
3495
+ };
3496
+
3170
3497
  var MenuOptions;
3171
3498
  (function (MenuOptions) {
3172
3499
  MenuOptions["soundingsManager"] = "Sd Manager";
@@ -3180,7 +3507,9 @@ const buttonStyle = {
3180
3507
  const SoundingsMenuBar = ({
3181
3508
  handleClick,
3182
3509
  isOpen,
3183
- selectedSourcesAmount = 0
3510
+ selectedSourcesAmount = 0,
3511
+ shouldHideMarkers,
3512
+ toggleMarkers
3184
3513
  }) => {
3185
3514
  const {
3186
3515
  t
@@ -3219,6 +3548,9 @@ const SoundingsMenuBar = ({
3219
3548
  sx: buttonStyle,
3220
3549
  variant: "tool",
3221
3550
  children: t('locations')
3551
+ }), jsx(SoundingsMarkersSwitch, {
3552
+ shouldHideMarkers: shouldHideMarkers,
3553
+ toggleMarkers: toggleMarkers
3222
3554
  })]
3223
3555
  });
3224
3556
  };
@@ -3228,13 +3560,26 @@ const MenuBarConnect = ({
3228
3560
  source
3229
3561
  }) => {
3230
3562
  const dispatch = useDispatch();
3563
+ React.useEffect(() => {
3564
+ dispatch(soundingsActions.registerSoundingsPanel({
3565
+ panelId: viewId
3566
+ }));
3567
+ }, [dispatch, viewId]);
3231
3568
  const currentActiveMapIdSources = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
3232
3569
  const currentActiveMapIdLocations = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsLocations));
3233
3570
  const currentActiveMapIdsoundingsManager = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsManager));
3234
3571
  const isSourcesOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsSources));
3235
3572
  const isLocationsOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsLocations));
3236
3573
  const isSoundingsManagerOpenInStore = useSelector(store => uiSelectors.getisDialogOpen(store, uiTypes.DialogTypes.SoundingsManager));
3237
- const sourcesAmount = useSelector(getSelectedSourcesAmount);
3574
+ const sourcesAmount = useSelector(state => getSelectedSourcesAmount(state, viewId));
3575
+ const shouldHideLocationMarkers = useSelector(state => getShouldHideLocationMarkers(state, viewId));
3576
+ const toggleLocationMarkers = React.useCallback(shouldHide => {
3577
+ dispatch(soundingsActions.toggleLocationMarkers({
3578
+ shouldHideLocationMarkers: shouldHide,
3579
+ panelId: viewId,
3580
+ origin: SoundingsActionOrigin.soundingsComponent
3581
+ }));
3582
+ }, [dispatch, viewId]);
3238
3583
  const dispatchActiveMapId = React.useCallback((dialogType, isOpen, activeMapId) => void dispatch(uiActions.setActiveMapIdForDialog({
3239
3584
  type: dialogType,
3240
3585
  mapId: viewId,
@@ -3257,12 +3602,14 @@ const MenuBarConnect = ({
3257
3602
  const isOpen = {
3258
3603
  [MenuOptions.sources]: currentActiveMapIdSources === viewId && isSourcesOpenInStore,
3259
3604
  [MenuOptions.locations]: currentActiveMapIdLocations === viewId && isLocationsOpenInStore,
3260
- [MenuOptions.soundingsManager]: isSoundingsManagerOpenInStore
3605
+ [MenuOptions.soundingsManager]: currentActiveMapIdsoundingsManager === viewId && isSoundingsManagerOpenInStore
3261
3606
  };
3262
3607
  return jsx(SoundingsMenuBar, {
3263
3608
  handleClick: handleClick,
3264
3609
  isOpen: isOpen,
3265
- selectedSourcesAmount: sourcesAmount
3610
+ selectedSourcesAmount: sourcesAmount,
3611
+ shouldHideMarkers: shouldHideLocationMarkers,
3612
+ toggleMarkers: toggleLocationMarkers
3266
3613
  });
3267
3614
  };
3268
3615
 
@@ -3402,24 +3749,33 @@ const roundToClosestTimestep = (timeToRound, availableSteps) => {
3402
3749
  }
3403
3750
  return timeToRound;
3404
3751
  };
3405
- const SoundingsTimeSliderConnect = () => {
3406
- const selectedTime = useSelector(getSelectedTime);
3407
- const sourceSelections = useSelector(getSelectedSources);
3752
+ const SoundingsTimeSliderConnect = ({
3753
+ viewId
3754
+ }) => {
3755
+ const selectedTime = useSelector(state => getSelectedTime(state, viewId));
3756
+ const sourceSelections = useSelector(state => getSelectedSources(state, viewId));
3408
3757
  const dispatch = useDispatch();
3409
- const updateSelectedTime = React.useCallback(selectedTime => {
3758
+ const updateSelectedTime = React.useCallback((selectedTime, isInitialCorrection = false) => {
3410
3759
  dispatch(soundingsActions.updateSelectedTime({
3411
- selectedTime
3760
+ selectedTime,
3761
+ panelId: viewId,
3762
+ origin: isInitialCorrection ? SoundingsActionOrigin.soundingsManager : SoundingsActionOrigin.soundingsComponent
3412
3763
  }));
3413
- }, [dispatch]);
3764
+ }, [dispatch, viewId]);
3414
3765
  const {
3415
3766
  data: timesteps
3416
3767
  } = useTimesteps(sourceSelections);
3417
3768
  const mergedTimesteps = [...new Set(timesteps.flat())].sort();
3769
+ const hasInitializedTime = React.useRef(false);
3418
3770
  // Ensure that the selected time is always one of the available timesteps
3419
3771
  const validSelectedTime = roundToClosestTimestep(selectedTime, mergedTimesteps);
3420
3772
  useEffect(() => {
3421
3773
  if (selectedTime !== validSelectedTime) {
3422
- updateSelectedTime(validSelectedTime);
3774
+ const isInitial = !hasInitializedTime.current;
3775
+ updateSelectedTime(validSelectedTime, isInitial);
3776
+ if (isInitial) {
3777
+ hasInitializedTime.current = true;
3778
+ }
3423
3779
  }
3424
3780
  }, [selectedTime, updateSelectedTime, validSelectedTime]);
3425
3781
  return jsx(SoundingsTimeSlider, {
@@ -3458,33 +3814,86 @@ const useGetSize = viewRef => {
3458
3814
  };
3459
3815
  const SoundingsConnect = ({
3460
3816
  viewId,
3461
- diagramType = 'SKEWT'
3817
+ soundingsPreset
3462
3818
  }) => {
3463
3819
  const viewRef = React.useRef(null);
3464
3820
  const size = useGetSize(viewRef);
3465
- const [chosenDiagramType, setChosenDiagramType] = React.useState(diagramType);
3466
3821
  const geojsonLayerId = React.useRef(webmapUtils.generateLayerId()).current;
3467
3822
  const dispatch = useDispatch();
3468
- const selectedTime = useSelector(getSelectedTime);
3469
- const sourceSelections = useSelector(getSelectedSources);
3470
- const locationSelections = useSelector(getLocationSelections);
3471
- const selectedFeature = useSelector(state => genericSelectors.getSelectedFeature(state.syncGroups, viewId));
3472
- const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, viewId));
3823
+ const soundingsPresetRef = React.useRef();
3824
+ const presetJustLoadedRef = React.useRef(false);
3825
+ const initialLocationsRef = React.useRef({});
3826
+ const previousLocationsRef = React.useRef({});
3827
+ React.useLayoutEffect(() => {
3828
+ dispatch(soundingsActions.registerSoundingsPanel({
3829
+ panelId: viewId
3830
+ }));
3831
+ if (soundingsPreset && !isEqual(soundingsPreset, soundingsPresetRef.current)) {
3832
+ soundingsPresetRef.current = soundingsPreset;
3833
+ presetJustLoadedRef.current = true;
3834
+ initialLocationsRef.current = soundingsPreset.selectedLocations || {};
3835
+ previousLocationsRef.current = {};
3836
+ dispatch(soundingsActions.registerSoundingsPreset({
3837
+ panelId: viewId,
3838
+ preset: soundingsPreset
3839
+ }));
3840
+ setTimeout(() => {
3841
+ presetJustLoadedRef.current = false;
3842
+ initialLocationsRef.current = {};
3843
+ }, 500);
3844
+ } else if (!soundingsPreset) {
3845
+ soundingsPresetRef.current = undefined;
3846
+ initialLocationsRef.current = {};
3847
+ previousLocationsRef.current = {};
3848
+ }
3849
+ }, [dispatch, soundingsPreset, viewId]);
3850
+ const selectedTime = useSelector(state => getSelectedTime(state, viewId));
3851
+ const sourceSelections = useSelector(state => getSelectedSources(state, viewId), shallowEqual);
3852
+ const locationSelections = useSelector(state => getLocationSelections(state, viewId), shallowEqual);
3853
+ const diagramType = useSelector(state => getDiagramType(state, viewId));
3854
+ const shouldHideLocationMarkers = useSelector(state => getShouldHideLocationMarkers(state, viewId));
3855
+ const setChosenDiagramType = React.useCallback(diagramType => {
3856
+ dispatch(soundingsActions.updateDiagramType({
3857
+ diagramType,
3858
+ panelId: viewId,
3859
+ origin: SoundingsActionOrigin.soundingsComponent
3860
+ }));
3861
+ }, [dispatch, viewId]);
3862
+ const selectedFeature = useSelector(state => genericSelectors.getSelectedFeature(state.syncGroups, viewId), shallowEqual);
3863
+ const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, viewId), shallowEqual);
3864
+ const firstMapId = selectedMapIds[0];
3473
3865
  const selectedMapPin = useSelector(state =>
3474
3866
  // mapPins are synced so we can arbitrarily choose the first linked map
3475
- mapSelectors.getPinLocation(state, selectedMapIds[0]));
3476
- const collections = useMemo(() => sourceSelections.map(({
3477
- collectionId,
3478
- url,
3479
- name
3480
- }) => ({
3481
- id: collectionId,
3482
- url,
3483
- name: name || collectionId
3484
- })), [sourceSelections]);
3867
+ mapSelectors.getPinLocation(state, firstMapId), shallowEqual);
3868
+ const collectionsRef = React.useRef();
3869
+ const collections = useMemo(() => {
3870
+ const newCollections = sourceSelections.map(({
3871
+ collectionId,
3872
+ url,
3873
+ name
3874
+ }) => ({
3875
+ id: collectionId,
3876
+ url,
3877
+ name: name || collectionId
3878
+ }));
3879
+ if (isEqual(newCollections, collectionsRef.current)) {
3880
+ return collectionsRef.current;
3881
+ }
3882
+ collectionsRef.current = newCollections;
3883
+ return newCollections;
3884
+ }, [sourceSelections]);
3485
3885
  const {
3486
- data: locationData
3886
+ data: rawLocationData
3487
3887
  } = useGetCollectionLocations(collections, true);
3888
+ // Stabilize locationData to prevent infinite loops caused by new array references
3889
+ const locationDataRef = React.useRef();
3890
+ const locationData = useMemo(() => {
3891
+ if (isEqual(rawLocationData, locationDataRef.current)) {
3892
+ return locationDataRef.current;
3893
+ }
3894
+ locationDataRef.current = rawLocationData;
3895
+ return rawLocationData;
3896
+ }, [rawLocationData]);
3488
3897
  /* Make sure the map pin is visible when linking new maps */
3489
3898
  useEffect(() => {
3490
3899
  const toggleMapPinVisibility = displayMapPin => {
@@ -3502,25 +3911,26 @@ const SoundingsConnect = ({
3502
3911
  };
3503
3912
  }, [dispatch, selectedMapIds]);
3504
3913
  // construct and send geojson to map for drawing locations
3505
- useEffect(() => {
3506
- const geoJsonForMap = {
3507
- type: 'FeatureCollection',
3508
- features: (locationData || []).flatMap(({
3509
- id: serviceId,
3510
- features
3511
- }) => features.map(feature => ({
3512
- type: 'Feature',
3513
- geometry: feature.geometry,
3914
+ const geoJsonForMap = useMemo(() => ({
3915
+ type: 'FeatureCollection',
3916
+ features: (locationData || []).flatMap(({
3917
+ id: serviceId,
3918
+ features
3919
+ }) => features.map(feature => ({
3920
+ type: 'Feature',
3921
+ geometry: feature.geometry,
3922
+ id: feature.id,
3923
+ properties: {
3924
+ name: feature.properties.name,
3925
+ mapFeatureClass: MapFeatureClass.ObservationStation,
3926
+ highlightSelectedFeature: selectedFeature?.id === feature.id,
3514
3927
  id: feature.id,
3515
- properties: {
3516
- name: feature.properties.name,
3517
- mapFeatureClass: MapFeatureClass.ObservationStation,
3518
- highlightSelectedFeature: selectedFeature?.id === feature.id,
3519
- id: feature.id,
3520
- serviceId
3521
- }
3522
- })))
3523
- };
3928
+ serviceId,
3929
+ hidden: shouldHideLocationMarkers
3930
+ }
3931
+ })))
3932
+ }), [locationData, selectedFeature?.id, shouldHideLocationMarkers]);
3933
+ useEffect(() => {
3524
3934
  dispatch(genericActions.addSharedData({
3525
3935
  panelId: viewId,
3526
3936
  data: {
@@ -3530,7 +3940,7 @@ const SoundingsConnect = ({
3530
3940
  }]
3531
3941
  }
3532
3942
  }));
3533
- }, [dispatch, geojsonLayerId, locationData, selectedFeature?.id, viewId]);
3943
+ }, [dispatch, geojsonLayerId, geoJsonForMap, viewId]);
3534
3944
  useEffect(() => () => {
3535
3945
  if (viewId) {
3536
3946
  dispatch(syncGroupsActions.deleteSharedData({
@@ -3541,35 +3951,143 @@ const SoundingsConnect = ({
3541
3951
  // Map location selection has changed. Update soundings location
3542
3952
  // since the location menu may be out of sync
3543
3953
  useEffect(() => {
3954
+ if (presetJustLoadedRef.current) {
3955
+ return;
3956
+ }
3544
3957
  const serviceId = selectedFeature?.serviceId;
3545
- if (serviceId) {
3546
- dispatch(soundingsActions.updateSelectedLocation({
3547
- sourceId: serviceId,
3548
- location: selectedFeature?.id
3549
- }));
3958
+ const featureId = selectedFeature?.id;
3959
+ if (serviceId && featureId) {
3960
+ const currentLocation = locationSelectionsRef.current[serviceId];
3961
+ const initialLocation = initialLocationsRef.current[serviceId];
3962
+ if (currentLocation !== featureId && initialLocation !== featureId) {
3963
+ dispatch(soundingsActions.updateSelectedLocation({
3964
+ sourceId: serviceId,
3965
+ location: featureId,
3966
+ panelId: viewId,
3967
+ origin: SoundingsActionOrigin.soundingsComponent
3968
+ }));
3969
+ }
3550
3970
  }
3551
- }, [dispatch, selectedFeature?.id, selectedFeature?.serviceId]);
3552
- // Map pin change on its own implies position selection
3971
+ }, [dispatch, selectedFeature?.id, selectedFeature?.serviceId, viewId]);
3972
+ // Use ref to avoid circular dependency when reading locationSelections
3973
+ const locationSelectionsRef = React.useRef(locationSelections);
3974
+ React.useEffect(() => {
3975
+ locationSelectionsRef.current = locationSelections;
3976
+ }, [locationSelections]);
3977
+ // Makes position requests for all user-selected sources for the current map pin position.
3978
+ // Map pin change on its own implies a new position selection.
3553
3979
  useEffect(() => {
3554
- // Use first source. Multiple sources will be implemented later.
3555
- // https://gitlab.com/opengeoweb/geoweb-assets/-/issues/4254
3556
- const source = sourceSelections[0];
3557
- if (source?.collectionId && !selectedFeature?.id && selectedMapPin?.lon && selectedMapPin.lat) {
3558
- dispatch(soundingsActions.updateSelectedLocation({
3559
- sourceId: source.collectionId,
3560
- location: [selectedMapPin.lon, selectedMapPin.lat]
3561
- }));
3980
+ if (presetJustLoadedRef.current) {
3981
+ return;
3982
+ }
3983
+ if (!selectedFeature?.id && !selectedMapPin?.id && selectedMapPin?.lon && selectedMapPin.lat) {
3984
+ const newLocation = [selectedMapPin.lon, selectedMapPin.lat];
3985
+ // Update location for all selected sources
3986
+ sourceSelections.forEach(source => {
3987
+ if (source?.collectionId) {
3988
+ const currentLocation = locationSelectionsRef.current[source.collectionId];
3989
+ const isSameLocation = Array.isArray(currentLocation) && currentLocation[0] === newLocation[0] && currentLocation[1] === newLocation[1];
3990
+ if (!isSameLocation) {
3991
+ dispatch(soundingsActions.updateSelectedLocation({
3992
+ sourceId: source.collectionId,
3993
+ location: newLocation,
3994
+ panelId: viewId,
3995
+ origin: SoundingsActionOrigin.soundingsComponent
3996
+ }));
3997
+ }
3998
+ }
3999
+ });
3562
4000
  }
3563
- }, [selectedMapPin, selectedFeature?.id, sourceSelections, dispatch]);
4001
+ }, [selectedMapPin, selectedFeature?.id, sourceSelections, dispatch, viewId]);
4002
+ useEffect(() => {
4003
+ if (!locationData || locationData.length === 0) {
4004
+ return;
4005
+ }
4006
+ if (selectedMapIds.length === 0) {
4007
+ return;
4008
+ }
4009
+ let changedSourceId;
4010
+ for (const source of sourceSelections) {
4011
+ if (source?.collectionId) {
4012
+ const currentLocation = locationSelections[source.collectionId];
4013
+ const previousLocation = previousLocationsRef.current[source.collectionId];
4014
+ if (currentLocation !== previousLocation) {
4015
+ changedSourceId = source.collectionId;
4016
+ if (!presetJustLoadedRef.current) {
4017
+ break;
4018
+ }
4019
+ }
4020
+ }
4021
+ }
4022
+ if (!changedSourceId) {
4023
+ return;
4024
+ }
4025
+ const selectedLocation = locationSelections[changedSourceId];
4026
+ if (!selectedLocation) {
4027
+ return;
4028
+ }
4029
+ previousLocationsRef.current = {
4030
+ ...previousLocationsRef.current,
4031
+ [changedSourceId]: selectedLocation
4032
+ };
4033
+ const serviceData = locationData.find(service => service.id === changedSourceId);
4034
+ if (!serviceData) {
4035
+ return;
4036
+ }
4037
+ if (typeof selectedLocation === 'string') {
4038
+ const foundFeature = serviceData.features.find(feature => feature.id === selectedLocation);
4039
+ if (foundFeature?.geometry.type === 'Point') {
4040
+ const [lon, lat] = foundFeature.geometry.coordinates;
4041
+ const mapPinNeedsUpdate = selectedMapPin?.id !== selectedLocation || selectedMapPin?.lon !== lon || selectedMapPin?.lat !== lat;
4042
+ const featureNeedsUpdate = selectedFeature?.id !== selectedLocation;
4043
+ if (mapPinNeedsUpdate) {
4044
+ selectedMapIds.forEach(mapId => {
4045
+ dispatch(mapActions.setMapPinLocation({
4046
+ mapId,
4047
+ mapPinLocation: {
4048
+ lon,
4049
+ lat,
4050
+ id: selectedLocation
4051
+ }
4052
+ }));
4053
+ });
4054
+ }
4055
+ if (featureNeedsUpdate) {
4056
+ dispatch(genericActions.addSharedData({
4057
+ panelId: viewId,
4058
+ data: {
4059
+ selectedFeatureId: selectedLocation
4060
+ }
4061
+ }));
4062
+ }
4063
+ }
4064
+ } else if (Array.isArray(selectedLocation)) {
4065
+ const [lon, lat] = selectedLocation;
4066
+ const needsUpdate = selectedMapPin?.lon !== lon || selectedMapPin?.lat !== lat;
4067
+ if (needsUpdate) {
4068
+ selectedMapIds.forEach(mapId => {
4069
+ dispatch(mapActions.setMapPinLocation({
4070
+ mapId,
4071
+ mapPinLocation: {
4072
+ lon,
4073
+ lat
4074
+ }
4075
+ }));
4076
+ });
4077
+ }
4078
+ }
4079
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4080
+ }, [locationData, locationSelections, sourceSelections, selectedMapIds, dispatch, viewId]);
3564
4081
  const {
3565
4082
  error,
3566
4083
  data,
3567
4084
  isPending
3568
4085
  } = useMultipleSources(sourceSelections, locationSelections, selectedTime);
3569
- const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store));
4086
+ const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store, viewId));
3570
4087
  const changeOrder = newOrder => {
3571
4088
  const orderedIds = newOrder.map(collection => collection.id);
3572
4089
  dispatch(soundingsActions.changeSoundingPanelOrder({
4090
+ panelId: viewId,
3573
4091
  newOrderOfIds: orderedIds
3574
4092
  }));
3575
4093
  };
@@ -3627,7 +4145,7 @@ const SoundingsConnect = ({
3627
4145
  },
3628
4146
  children: jsx(SharedMouseYProvider, {
3629
4147
  children: jsx(SortableAccordions, {
3630
- chosenDiagramType: chosenDiagramType,
4148
+ chosenDiagramType: diagramType,
3631
4149
  data: data,
3632
4150
  onOrderChange: changeOrder,
3633
4151
  setChosenDiagramType: setChosenDiagramType,
@@ -3642,7 +4160,9 @@ const SoundingsConnect = ({
3642
4160
  flex: '0 0 auto',
3643
4161
  height: `${TIMESLIDER_HEIGHT}px`
3644
4162
  },
3645
- children: jsx(SoundingsTimeSliderConnect, {})
4163
+ children: jsx(SoundingsTimeSliderConnect, {
4164
+ viewId: viewId
4165
+ })
3646
4166
  })]
3647
4167
  })
3648
4168
  })]
@@ -3718,6 +4238,7 @@ const SoundingsSourceMenu = ({
3718
4238
  instanceId,
3719
4239
  url
3720
4240
  } = sourceSelections[collection.id] || {};
4241
+ const sourceUrl = url || collection.url;
3721
4242
  const hasInstances = collection.instances.length > 0;
3722
4243
  const selectedInstanceId = instanceId || collection.instances[0]?.id || undefined;
3723
4244
  return jsxs(ListItem, {
@@ -3732,7 +4253,7 @@ const SoundingsSourceMenu = ({
3732
4253
  onChange: event => {
3733
4254
  const verticalRange = selectedInstanceId !== undefined ? extractVerticalRange(collection, selectedInstanceId) : '';
3734
4255
  updateCollection(collection.id, {
3735
- url,
4256
+ url: sourceUrl,
3736
4257
  isChecked: event.target.checked,
3737
4258
  instanceId: selectedInstanceId,
3738
4259
  verticalRange
@@ -3748,7 +4269,7 @@ const SoundingsSourceMenu = ({
3748
4269
  const instanceId = event.target.value;
3749
4270
  const verticalRange = extractVerticalRange(collection, instanceId);
3750
4271
  updateCollection(collection.id, {
3751
- url,
4272
+ url: sourceUrl,
3752
4273
  instanceId,
3753
4274
  verticalRange
3754
4275
  });
@@ -3811,35 +4332,52 @@ const SoundingsSourceDialogConnect = ({
3811
4332
  dialogOrder,
3812
4333
  setDialogOrder
3813
4334
  } = useSetupDialog(uiTypes.DialogTypes.SoundingsSources);
4335
+ const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsSources));
3814
4336
  const updateCollection = React.useCallback((sourceId, value) => {
3815
4337
  dispatch(soundingsActions.updateSelectedSource({
3816
4338
  sourceId,
3817
- value
4339
+ value,
4340
+ panelId,
4341
+ origin: SoundingsActionOrigin.soundingsComponent
3818
4342
  }));
3819
- }, [dispatch]);
3820
- const sourceSelections = useSelector(getSourceSelections);
4343
+ }, [dispatch, panelId]);
4344
+ const soundingsCollectionsFromStore = useSelector(state => getSoundingsCollections(state));
4345
+ const collections = soundingsCollections ?? soundingsCollectionsFromStore;
4346
+ const sourceSelections = useSelector(state => getSourceSelections(state, panelId));
3821
4347
  // Set mappings for each collection
3822
4348
  React.useEffect(() => {
3823
- soundingsCollections?.forEach(collection => {
4349
+ if (soundingsCollections && soundingsCollections.length > 0) {
4350
+ dispatch(soundingsActions.setSoundingsCollections(soundingsCollections));
4351
+ }
4352
+ }, [dispatch, soundingsCollections]);
4353
+ React.useEffect(() => {
4354
+ collections?.forEach(collection => {
4355
+ const currentSelection = sourceSelections[collection.id];
4356
+ if (currentSelection?.url && currentSelection?.parameterMappings) {
4357
+ return;
4358
+ }
4359
+ const newValue = {
4360
+ url: collection.url,
4361
+ isChecked: currentSelection?.isChecked || false,
4362
+ parameterMappings: collection.parameterMappings,
4363
+ ...(collection.name && collection.name !== '' ? {
4364
+ name: collection.name
4365
+ } : {})
4366
+ };
3824
4367
  dispatch(soundingsActions.updateSelectedSource({
3825
4368
  sourceId: collection.id,
3826
- value: {
3827
- url: collection.url,
3828
- isChecked: sourceSelections[collection.id]?.isChecked || false,
3829
- parameterMappings: collection.parameterMappings,
3830
- ...(collection.name && collection.name !== '' ? {
3831
- name: collection.name
3832
- } : {})
3833
- }
4369
+ value: newValue,
4370
+ panelId,
4371
+ origin: SoundingsActionOrigin.soundingsManager
3834
4372
  }));
3835
4373
  });
3836
4374
  // eslint-disable-next-line react-hooks/exhaustive-deps
3837
- }, [dispatch, soundingsCollections]);
4375
+ }, [dispatch, collections, panelId]);
3838
4376
  const {
3839
4377
  data,
3840
4378
  isPending,
3841
4379
  error
3842
- } = useGetCollectionsInstances(soundingsCollections, isDialogOpen);
4380
+ } = useGetCollectionsInstances(collections, isDialogOpen);
3843
4381
  return jsx(SoundingsSourceMenu, {
3844
4382
  error: error,
3845
4383
  handleClose: onCloseDialog,
@@ -3853,6 +4391,44 @@ const SoundingsSourceDialogConnect = ({
3853
4391
  });
3854
4392
  };
3855
4393
 
4394
+ const RowComponent = ({
4395
+ index,
4396
+ style,
4397
+ service,
4398
+ isChecked,
4399
+ updateSelectedLocation
4400
+ }) => {
4401
+ const location = service.features[index];
4402
+ return jsx("div", {
4403
+ style: style,
4404
+ children: jsx(ListItem, {
4405
+ disableGutters: true,
4406
+ disablePadding: true,
4407
+ children: jsxs(ListItemButton, {
4408
+ dense: true,
4409
+ onClick: () => {
4410
+ updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
4411
+ },
4412
+ children: [jsx(ListItemIcon, {
4413
+ children: jsx(Checkbox, {
4414
+ checked: isChecked(service.id, location.id),
4415
+ disableRipple: true,
4416
+ edge: "start",
4417
+ slotProps: {
4418
+ input: {
4419
+ 'aria-labelledby': location.id
4420
+ }
4421
+ },
4422
+ tabIndex: -1
4423
+ })
4424
+ }), jsx(ListItemText, {
4425
+ id: location.id,
4426
+ primary: location.properties.name
4427
+ })]
4428
+ })
4429
+ })
4430
+ }, `${service.id}-${location.id}`);
4431
+ };
3856
4432
  const LocationsMenu = ({
3857
4433
  services,
3858
4434
  selectedSourcesAmount,
@@ -3944,45 +4520,18 @@ const LocationsMenu = ({
3944
4520
  }
3945
4521
  },
3946
4522
  title: service.name,
3947
- children: jsx(FixedSizeList, {
3948
- height: 320,
3949
- itemCount: service.features.length,
3950
- itemSize: 40,
3951
- width: "100%",
3952
- children: ({
3953
- index,
3954
- style
3955
- }) => {
3956
- const location = service.features[index];
3957
- return jsx("div", {
3958
- style: style,
3959
- children: jsx(ListItem, {
3960
- disableGutters: true,
3961
- disablePadding: true,
3962
- children: jsxs(ListItemButton, {
3963
- dense: true,
3964
- onClick: () => {
3965
- updateSelectedLocation(service.id, isChecked(service.id, location.id) ? undefined : location.id);
3966
- },
3967
- children: [jsx(ListItemIcon, {
3968
- children: jsx(Checkbox, {
3969
- checked: isChecked(service.id, location.id),
3970
- disableRipple: true,
3971
- edge: "start",
3972
- slotProps: {
3973
- input: {
3974
- 'aria-labelledby': location.id
3975
- }
3976
- },
3977
- tabIndex: -1
3978
- })
3979
- }), jsx(ListItemText, {
3980
- id: location.id,
3981
- primary: location.properties.name
3982
- })]
3983
- })
3984
- })
3985
- }, `${service.id}-${location.id}`);
4523
+ children: jsx(List$1, {
4524
+ rowComponent: RowComponent,
4525
+ rowCount: service.features.length,
4526
+ rowHeight: 40,
4527
+ rowProps: {
4528
+ service,
4529
+ isChecked,
4530
+ updateSelectedLocation
4531
+ },
4532
+ style: {
4533
+ height: 320,
4534
+ width: '100%'
3986
4535
  }
3987
4536
  })
3988
4537
  })
@@ -4001,35 +4550,25 @@ const LocationsMenuConnect = ({
4001
4550
  dialogOrder,
4002
4551
  setDialogOrder
4003
4552
  } = useSetupDialog(uiTypes.DialogTypes.SoundingsLocations);
4004
- const locationSelections = useSelector(getLocationSelections);
4005
- const selectedSourceIds = useSelector(getSelectedSourcesIds);
4006
- const sourcesAmount = useSelector(getSelectedSourcesAmount);
4007
4553
  const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsLocations));
4008
- const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, panelId));
4554
+ const locationSelections = useSelector(state => getLocationSelections(state, panelId));
4555
+ const selectedSourceIds = useSelector(state => getSelectedSourcesIds(state, panelId));
4556
+ const sourcesAmount = useSelector(state => getSelectedSourcesAmount(state, panelId));
4557
+ const soundingsCollectionsFromStore = useSelector(state => getSoundingsCollections(state));
4558
+ const collections = soundingsCollections ?? soundingsCollectionsFromStore;
4559
+ React.useEffect(() => {
4560
+ if (soundingsCollections && soundingsCollections.length > 0) {
4561
+ dispatch(soundingsActions.setSoundingsCollections(soundingsCollections));
4562
+ }
4563
+ }, [dispatch, soundingsCollections]);
4009
4564
  const {
4010
4565
  data,
4011
4566
  isPending,
4012
4567
  error
4013
- } = useGetCollectionLocations(soundingsCollections, isDialogOpen);
4568
+ } = useGetCollectionLocations(collections, isDialogOpen);
4014
4569
  const services = useMemo(() => {
4015
4570
  return data?.filter(service => selectedSourceIds.includes(service.id)) || [];
4016
4571
  }, [data, selectedSourceIds]);
4017
- const handleFeatureSelect = (panelId, featureId) => {
4018
- dispatch(genericActions.addSharedData({
4019
- panelId,
4020
- data: {
4021
- selectedFeatureId: featureId
4022
- }
4023
- }));
4024
- };
4025
- const setMapPinLocation = mapPinLocation => {
4026
- selectedMapIds.forEach(mapId => {
4027
- dispatch(mapActions.setMapPinLocation({
4028
- mapId,
4029
- mapPinLocation
4030
- }));
4031
- });
4032
- };
4033
4572
  return jsx(LocationsMenu, {
4034
4573
  error: error,
4035
4574
  isLoading: isPending,
@@ -4043,27 +4582,9 @@ const LocationsMenuConnect = ({
4043
4582
  updateSelectedLocation: (sourceId, locationId) => {
4044
4583
  dispatch(soundingsActions.updateSelectedLocation({
4045
4584
  sourceId,
4046
- location: locationId
4585
+ location: locationId,
4586
+ panelId
4047
4587
  }));
4048
- // Use first source. Multiple sources will be implemented later.
4049
- // https://gitlab.com/opengeoweb/geoweb-assets/-/issues/4254
4050
- const isFirstSource = sourceId === selectedSourceIds[0];
4051
- if (!isFirstSource) {
4052
- return;
4053
- }
4054
- if (locationId) {
4055
- handleFeatureSelect(panelId, locationId);
4056
- }
4057
- const foundFeature = services[0].features.find(feature => feature.id === locationId);
4058
- // check type for typescript
4059
- if (foundFeature?.geometry.type === 'Point') {
4060
- const [lon, lat] = foundFeature.geometry.coordinates;
4061
- setMapPinLocation({
4062
- lon,
4063
- lat,
4064
- id: locationId
4065
- });
4066
- }
4067
4588
  }
4068
4589
  });
4069
4590
  };
@@ -4091,8 +4612,10 @@ var preset = {
4091
4612
  };
4092
4613
 
4093
4614
  const SoundingManagerListItem = ({
4615
+ id,
4094
4616
  name,
4095
- isDisplayed
4617
+ isDisplayed,
4618
+ onToggleVisibility
4096
4619
  }) => {
4097
4620
  return jsx(Card, {
4098
4621
  className: "sortable-item",
@@ -4126,7 +4649,16 @@ const SoundingManagerListItem = ({
4126
4649
  display: 'flex',
4127
4650
  alignItems: 'center'
4128
4651
  },
4129
- children: isDisplayed ? jsx(Visibility, {}) : jsx(VisibilityOff, {})
4652
+ children: jsx(IconButton, {
4653
+ "aria-label": "toggle-visibility",
4654
+ onClick: () => onToggleVisibility(id),
4655
+ size: "small",
4656
+ sx: {
4657
+ width: '24px',
4658
+ height: '24px'
4659
+ },
4660
+ children: isDisplayed ? jsx(Visibility, {}) : jsx(VisibilityOff, {})
4661
+ })
4130
4662
  }), jsxs(Box, {
4131
4663
  sx: {
4132
4664
  paddingRight: '8px',
@@ -4144,7 +4676,8 @@ const SoundingsManager = ({
4144
4676
  onMouseDown,
4145
4677
  order,
4146
4678
  soundingPanelsInOrder,
4147
- onOrderChange
4679
+ onOrderChange,
4680
+ onToggleVisibility
4148
4681
  }) => {
4149
4682
  const sortableList = React.useMemo(() => soundingPanelsInOrder.map(collection => ({
4150
4683
  ...collection
@@ -4172,8 +4705,10 @@ const SoundingsManager = ({
4172
4705
  name,
4173
4706
  isDisplayed
4174
4707
  }) => jsx(SoundingManagerListItem, {
4708
+ id: id,
4175
4709
  isDisplayed: isDisplayed,
4176
- name: name
4710
+ name: name,
4711
+ onToggleVisibility: onToggleVisibility
4177
4712
  }, id))
4178
4713
  })
4179
4714
  });
@@ -4187,22 +4722,33 @@ const SoundingsManagerConnect = () => {
4187
4722
  setDialogOrder
4188
4723
  } = useSetupDialog(uiTypes.DialogTypes.SoundingsManager);
4189
4724
  const dispatch = useDispatch();
4725
+ const panelId = useSelector(store => uiSelectors.getDialogMapId(store, uiTypes.DialogTypes.SoundingsManager));
4190
4726
  React.useEffect(() => {
4191
4727
  const data = preset.SoundingManagerDummyPreset;
4192
- dispatch(soundingsActions.addSoundingPanelsFromPreset(data));
4193
- }, [dispatch]);
4194
- const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store));
4728
+ dispatch(soundingsActions.addSoundingPanelsFromPreset({
4729
+ panelId,
4730
+ panels: data
4731
+ }));
4732
+ }, [dispatch, panelId]);
4733
+ const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store, panelId));
4195
4734
  const changeOrder = newOrder => {
4196
4735
  const orderedIds = newOrder.map(collection => collection.id);
4197
4736
  dispatch(soundingsActions.changeSoundingPanelOrder({
4737
+ panelId,
4198
4738
  newOrderOfIds: orderedIds
4199
4739
  }));
4200
4740
  };
4741
+ const toggleVisibility = id => {
4742
+ dispatch(soundingsActions.toggleSoundingPanelVisibility({
4743
+ id
4744
+ }));
4745
+ };
4201
4746
  return jsx(SoundingsManager, {
4202
4747
  isOpen: isDialogOpen,
4203
4748
  onClose: onCloseDialog,
4204
4749
  onMouseDown: setDialogOrder,
4205
4750
  onOrderChange: changeOrder,
4751
+ onToggleVisibility: toggleVisibility,
4206
4752
  order: dialogOrder,
4207
4753
  soundingPanelsInOrder: soundingPanelsInOrder
4208
4754
  });
@@ -4211,7 +4757,7 @@ const SoundingsManagerConnect = () => {
4211
4757
  const soundingsLookUp = payload => {
4212
4758
  if (payload.componentType === 'Soundings') {
4213
4759
  return jsx(SoundingsConnect, {
4214
- diagramType: payload.diagramType ? payload.diagramType : 'SKEWT',
4760
+ soundingsPreset: payload.initialProps,
4215
4761
  viewId: payload.id
4216
4762
  });
4217
4763
  }
@@ -4246,4 +4792,4 @@ const createMockStore = mockState => configureStore({
4246
4792
  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(storeMiddlewares)
4247
4793
  });
4248
4794
 
4249
- export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsConnect, SoundingsManagerConnect, SoundingsSourceDialogConnect, createMockStore, initSoundingsI18n, soundingDiagrams, soundingsLookUp, soundingsReducersMap, soundingsTranslations, translateInTestsAndStories, useSoundingsTranslation };
4795
+ export { LocationsMenuConnect, SOUNDINGS_NAMESPACE, SoundingsActionOrigin, SoundingsConnect, SoundingsManagerConnect, SoundingsSourceDialogConnect, types as SoundingsTypes, createMockStore, initSoundingsI18n, soundingDiagrams, soundingsActions, soundingsLookUp, soundingsReducer, soundingsReducersMap, selectors as soundingsSelectors, soundingsTranslations, translateInTestsAndStories, useSoundingsTranslation };