@opengeoweb/soundings 15.3.0 → 16.0.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,14 +1,14 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { Menu, ListSubheader, Typography, MenuItem, Box, Stack, Card, IconButton, Tooltip, LinearProgress, List, ListItem, ListItemIcon, Checkbox, ListItemText, Grid, ListItemButton } from '@mui/material';
3
- import { range, minBy, isEqual } from 'lodash';
3
+ import { range, isEqual } from 'lodash';
4
4
  import React, { useRef, useEffect, useMemo, useState, Fragment as Fragment$1 } from 'react';
5
5
  import { useDispatch, useSelector, shallowEqual } from 'react-redux';
6
6
  import { safelyInitI18nForTestsAndStories, sharedTranslations, SHARED_NAMESPACE, CustomToggleButton, dateUtils, SwitchButton, CustomSlider, AlertBanner, ToolContainerDraggable, StyledSelect, SearchField, CustomAccordion } from '@opengeoweb/shared';
7
- import { selectorMemoizationOptions, uiSelectors, uiTypes, uiActions, genericSelectors, mapSelectors, genericActions, syncGroupsActions, mapActions, useSetupDialog, storeReducerMap, storeMiddlewares } from '@opengeoweb/store';
8
- import { ParseISOTimeRangeDuration, parseISO8601IntervalToDateInterval, webmapUtils } from '@opengeoweb/webmap';
7
+ import { selectorMemoizationOptions, uiSelectors, uiTypes, uiActions, genericActions, genericSelectors, useSyncgroupTimeState, mapSelectors, syncGroupsActions, mapActions, useSetupDialog, storeReducerMap, storeMiddlewares } from '@opengeoweb/store';
8
+ import { ArrowDropDownDown, DragHandle, Layers, Sources, Close, Visibility, VisibilityOff } from '@opengeoweb/theme';
9
+ import { ParseISOTimeRangeDuration, parseISO8601IntervalToDateInterval, handleDateUtilsISOString, webmapUtils } from '@opengeoweb/webmap';
9
10
  import { MapFeatureClass } from '@opengeoweb/webmap-react';
10
11
  import { ReactSortable } from 'react-sortablejs';
11
- import { ArrowDropDownDown, DragHandle, Layers, Sources, Visibility, VisibilityOff } from '@opengeoweb/theme';
12
12
  import * as d3 from 'd3';
13
13
  import i18n from 'i18next';
14
14
  export { default as i18n } from 'i18next';
@@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next';
16
16
  import { timesliderTranslations, TIME_SLIDER_NAMESPACE, BackwardForwardStepButton } from '@opengeoweb/time-slider';
17
17
  import { useResizable } from 'react-resizable-layout';
18
18
  import { useQueries } from '@tanstack/react-query';
19
- import { getHeaders, handleResponse } from '@opengeoweb/api';
19
+ import { withJsonFormat, getHeaders, handleResponse } from '@opengeoweb/api';
20
20
  import { createSlice, createAction, createSelector, combineReducers, configureStore } from '@reduxjs/toolkit';
21
21
  import { List as List$1 } from 'react-window';
22
22
 
@@ -32,6 +32,7 @@ var en = {
32
32
  "diagram-BIJVOET": "Bijvoet",
33
33
  "diagram-EMAGRAM": "Emagram",
34
34
  "coming-soon": "Coming soon",
35
+ "soundings-time-mismatch-title": "Selected time in the sounding view does not match map view time.",
35
36
  "soundings-toggle-location-markers-label": "Markers",
36
37
  "soundings-toggle-location-markers-tooltip": "Show/hide location markers"
37
38
  };
@@ -47,8 +48,9 @@ var no = {
47
48
  "diagram-BIJVOET": "Bijvoet",
48
49
  "diagram-EMAGRAM": "Emagram",
49
50
  "coming-soon": "Kommer snart",
50
- "soundings-toggle-location-markers-label": "ikke oversatt",
51
- "soundings-toggle-location-markers-tooltip": "ikke oversatt"
51
+ "soundings-time-mismatch-title": "Selected time in the sounding view does not match map view time.",
52
+ "soundings-toggle-location-markers-label": "Lokasjoner",
53
+ "soundings-toggle-location-markers-tooltip": "Vis/skjul lokasjoner"
52
54
  };
53
55
  var nl = {
54
56
  sources: "Bronnen",
@@ -62,6 +64,7 @@ var nl = {
62
64
  "diagram-BIJVOET": "Bijvoet",
63
65
  "diagram-EMAGRAM": "Emagram",
64
66
  "coming-soon": "Binnenkort beschikbaar",
67
+ "soundings-time-mismatch-title": "Selected time in the sounding view does not match map view time.",
65
68
  "soundings-toggle-location-markers-label": "Kaart locaties",
66
69
  "soundings-toggle-location-markers-tooltip": "Toon/Verberg locatie punten"
67
70
  };
@@ -77,6 +80,7 @@ var fi = {
77
80
  "diagram-BIJVOET": "Bijvoet",
78
81
  "diagram-EMAGRAM": "Emagram",
79
82
  "coming-soon": "Tulossa pian",
83
+ "soundings-time-mismatch-title": "Selected time in the sounding view does not match map view time.",
80
84
  "soundings-toggle-location-markers-label": "Merkit",
81
85
  "soundings-toggle-location-markers-tooltip": "Näytä/piilota kohdemerkit"
82
86
  };
@@ -2744,6 +2748,40 @@ const SoundingsLegend = ({
2744
2748
  });
2745
2749
  };
2746
2750
 
2751
+ /* *
2752
+ * Licensed under the Apache License, Version 2.0 (the "License");
2753
+ * you may not use this file except in compliance with the License.
2754
+ * You may obtain a copy of the License at
2755
+ *
2756
+ * http://www.apache.org/licenses/LICENSE-2.0
2757
+ *
2758
+ * Unless required by applicable law or agreed to in writing, software
2759
+ * distributed under the License is distributed on an "AS IS" BASIS,
2760
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2761
+ * See the License for the specific language governing permissions and
2762
+ * limitations under the License.
2763
+ *
2764
+ * Copyright 2026 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
2765
+ * Copyright 2026 - Finnish Meteorological Institute (FMI)
2766
+ * Copyright 2026 - The Norwegian Meteorological Institute (MET Norway)
2767
+ * */
2768
+ const getTimeMismatchInfo = (isTimeSynced, warningTimeInUse, sourceTimesteps) => {
2769
+ if (!isTimeSynced || !warningTimeInUse) {
2770
+ return undefined;
2771
+ }
2772
+ const requestedTimestamp = new Date(warningTimeInUse).getTime();
2773
+ const allSourceTimesteps = sourceTimesteps.flat().map(timestep => new Date(timestep).getTime()).filter(time => Number.isFinite(time)).sort((aTime, bTime) => aTime - bTime);
2774
+ if (allSourceTimesteps.length === 0) {
2775
+ return undefined;
2776
+ }
2777
+ const firstAvailableTime = allSourceTimesteps[0];
2778
+ const lastAvailableTime = allSourceTimesteps[allSourceTimesteps.length - 1];
2779
+ const isOutsideOverallSoundingsRange = requestedTimestamp < firstAvailableTime || requestedTimestamp > lastAvailableTime;
2780
+ return isOutsideOverallSoundingsRange ? {
2781
+ mismatch: true
2782
+ } : undefined;
2783
+ };
2784
+
2747
2785
  /* *
2748
2786
  * Licensed under the Apache License, Version 2.0 (the "License");
2749
2787
  * you may not use this file except in compliance with the License.
@@ -2761,13 +2799,13 @@ const SoundingsLegend = ({
2761
2799
  * Copyright 2025 - Finnish Meteorological Institute (FMI)
2762
2800
  * Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
2763
2801
  * */
2764
- const getCollectionLocations = async (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/locations`, {
2802
+ const getCollectionLocations = async (collectionId, baseUrl) => fetch(withJsonFormat(`${baseUrl}/collections/${collectionId}/locations`), {
2765
2803
  headers: getHeaders()
2766
2804
  }).then(handleResponse);
2767
- const getCollectionInstances = async (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/instances`, {
2805
+ const getCollectionInstances = async (collectionId, baseUrl) => fetch(withJsonFormat(`${baseUrl}/collections/${collectionId}/instances`), {
2768
2806
  headers: getHeaders()
2769
2807
  }).then(handleResponse);
2770
- const getCollectionDetails = async (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}`, {
2808
+ const getCollectionDetails = async (collectionId, baseUrl) => fetch(withJsonFormat(`${baseUrl}/collections/${collectionId}`), {
2771
2809
  headers: getHeaders()
2772
2810
  }).then(handleResponse);
2773
2811
  const getEdrCollectionForLocation = async (collectionId, baseUrl, instanceId, locationId, parameters, datetime, verticalRange) => {
@@ -2847,6 +2885,25 @@ const getWindSpeedInKts = (windSpeed, windSpeedUnit) => {
2847
2885
  // Formula to convert from m/s to kts
2848
2886
  return windSpeed * ktsConstant;
2849
2887
  };
2888
+ const getNearestTimestep = (requestedDatetime, availableTimesteps) => {
2889
+ if (!requestedDatetime || availableTimesteps.length === 0) {
2890
+ return '';
2891
+ }
2892
+ const requestedTime = new Date(requestedDatetime).getTime();
2893
+ let nearestTimestep = availableTimesteps[0];
2894
+ let nearestTimestepMs = new Date(nearestTimestep).getTime();
2895
+ let minDiff = Math.abs(nearestTimestepMs - requestedTime);
2896
+ for (const timestep of availableTimesteps) {
2897
+ const timestepMs = new Date(timestep).getTime();
2898
+ const diff = Math.abs(timestepMs - requestedTime);
2899
+ if (diff < minDiff || diff === minDiff && timestepMs < nearestTimestepMs) {
2900
+ minDiff = diff;
2901
+ nearestTimestep = timestep;
2902
+ nearestTimestepMs = timestepMs;
2903
+ }
2904
+ }
2905
+ return nearestTimestep;
2906
+ };
2850
2907
  // Parsing and conversion function
2851
2908
  function parseCoverageCollection(response, parameterMappings, windSpeedUnit) {
2852
2909
  const features = [];
@@ -3122,7 +3179,7 @@ const defaultParameterMappings = {
3122
3179
  const getParameterMappings = parameterMappings => {
3123
3180
  return Object.keys(parameterMappings).map(key => parameterMappings[key]);
3124
3181
  };
3125
- const useMultipleSources = (sources, locations, datetime) => {
3182
+ const useMultipleSources = (sources, locations, datetime, timeSelectionMode = 'nearestWithinRange') => {
3126
3183
  const {
3127
3184
  data: timesteps,
3128
3185
  error: timestepsError,
@@ -3137,32 +3194,31 @@ const useMultipleSources = (sources, locations, datetime) => {
3137
3194
  windSpeedUnit,
3138
3195
  verticalRange
3139
3196
  }, index) => {
3140
- // Find the best matching timestep for this source
3197
+ // Per source: choose either exact match or nearest-in-range timestep.
3141
3198
  let dateToUse = '';
3142
3199
  if (datetime && timesteps[index] && timesteps[index].length > 0) {
3143
3200
  const requestedTime = new Date(datetime).getTime();
3144
- const availableTimesteps = timesteps[index];
3145
- // Check for exact match first
3146
- if (availableTimesteps.includes(new Date(datetime).toISOString())) {
3147
- dateToUse = datetime;
3148
- } else {
3149
- // Find nearest timestep (prefer earlier timesteps if equidistant)
3150
- let nearestTimestep = availableTimesteps[0];
3151
- let minDiff = Math.abs(new Date(availableTimesteps[0]).getTime() - requestedTime);
3152
- for (const timestep of availableTimesteps) {
3153
- const diff = Math.abs(new Date(timestep).getTime() - requestedTime);
3154
- if (diff < minDiff) {
3155
- minDiff = diff;
3156
- nearestTimestep = timestep;
3201
+ if (Number.isFinite(requestedTime)) {
3202
+ const exactMatch = timesteps[index].find(timestep => new Date(timestep).getTime() === requestedTime);
3203
+ if (timeSelectionMode === 'exact') {
3204
+ dateToUse = exactMatch || '';
3205
+ } else {
3206
+ const availableTimes = timesteps[index].map(timestep => new Date(timestep).getTime()).filter(time => Number.isFinite(time)).sort((aTime, bTime) => aTime - bTime);
3207
+ if (availableTimes.length > 0) {
3208
+ const firstAvailableTime = availableTimes[0];
3209
+ const lastAvailableTime = availableTimes[availableTimes.length - 1];
3210
+ const isWithinSourceRange = requestedTime >= firstAvailableTime && requestedTime <= lastAvailableTime;
3211
+ if (isWithinSourceRange) {
3212
+ dateToUse = getNearestTimestep(datetime, timesteps[index]);
3213
+ }
3157
3214
  }
3158
3215
  }
3159
- dateToUse = nearestTimestep;
3160
3216
  }
3161
3217
  }
3162
3218
  const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dateToUse || '', parameterMappings || defaultParameterMappings, windSpeedUnit || 'ms', verticalRange || '');
3163
3219
  return queryParams;
3164
3220
  });
3165
- }, [datetime, locations, sources, timesteps]);
3221
+ }, [datetime, locations, sources, timeSelectionMode, timesteps]);
3166
3222
  return useQueries({
3167
3223
  queries,
3168
3224
  combine: queryResults => {
@@ -3202,7 +3258,8 @@ const useTimesteps = sourceSelections => {
3202
3258
  return getTimestepsFromInstanceOrCollection(instance);
3203
3259
  }
3204
3260
  return getTimestepsFromInstanceOrCollection(data);
3205
- }
3261
+ },
3262
+ refetchInterval: 5 * 60 * 1000
3206
3263
  })), [sourceSelections]);
3207
3264
  const results = useQueries({
3208
3265
  queries,
@@ -3446,7 +3503,23 @@ const slice = createSlice({
3446
3503
  draft.selectedLocations = preset.selectedLocations ?? {};
3447
3504
  draft.selectedTime = preset.selectedTime ?? new Date().toISOString();
3448
3505
  draft.diagramType = preset.diagramType ?? 'SKEWT';
3449
- draft.soundingPanels = preset.soundingPanels ?? initialSoundingsState.soundingPanels;
3506
+ // Ensure we show all available panels, even if not stored in preset
3507
+ if (preset.soundingPanels) {
3508
+ draft.soundingPanels = {
3509
+ ...preset.soundingPanels
3510
+ };
3511
+ initialSoundingsState.soundingPanels.order.forEach(panel => {
3512
+ if (!preset.soundingPanels.order.find(presetPanel => panel === presetPanel)) {
3513
+ draft.soundingPanels.order = [...draft.soundingPanels.order, panel];
3514
+ draft.soundingPanels.entities = {
3515
+ ...draft.soundingPanels.entities,
3516
+ [panel]: initialSoundingsState.soundingPanels.entities[panel]
3517
+ };
3518
+ }
3519
+ });
3520
+ } else {
3521
+ draft.soundingPanels = initialSoundingsState.soundingPanels;
3522
+ }
3450
3523
  draft.shouldHideLocationMarkers = preset.shouldHideLocationMarkers ?? false;
3451
3524
  }
3452
3525
  },
@@ -3530,6 +3603,46 @@ const normalizeSoundingsState = state => {
3530
3603
  };
3531
3604
  const soundingsReducer = (state, action) => {
3532
3605
  const normalizedState = normalizeSoundingsState(state ?? initialState);
3606
+ if (action.type === 'GENERIC_SYNC_SETTIME') {
3607
+ const {
3608
+ source,
3609
+ targets: targetsFromAction
3610
+ } = action.payload;
3611
+ const targets = [...targetsFromAction];
3612
+ if (source?.payload) {
3613
+ targets.unshift({
3614
+ targetId: source.payload.sourceId,
3615
+ value: source.payload.value
3616
+ });
3617
+ }
3618
+ let hasChanges = false;
3619
+ const nextById = {
3620
+ ...normalizedState.byId
3621
+ };
3622
+ targets.forEach(({
3623
+ targetId,
3624
+ value
3625
+ }) => {
3626
+ if (!targetId || !value?.currentTime || !nextById[targetId]) {
3627
+ return;
3628
+ }
3629
+ if (nextById[targetId].selectedTime === value.currentTime) {
3630
+ return;
3631
+ }
3632
+ hasChanges = true;
3633
+ nextById[targetId] = {
3634
+ ...nextById[targetId],
3635
+ selectedTime: value.currentTime
3636
+ };
3637
+ });
3638
+ if (hasChanges) {
3639
+ return {
3640
+ ...normalizedState,
3641
+ byId: nextById
3642
+ };
3643
+ }
3644
+ return normalizedState;
3645
+ }
3533
3646
  if (action.type === setSoundingsCollections.type) {
3534
3647
  const collections = action.payload;
3535
3648
  if (collections === normalizedState.soundingsCollections) {
@@ -3933,16 +4046,53 @@ const SoundingsTimeSlider = ({
3933
4046
  value: dateUtils.unix(dateUtils.getUtcFromString(date)),
3934
4047
  label: shouldHideLabel(arr.length, index, maxLabelCount) ? '' : dateUtils.dateToString(dateUtils.getUtcFromString(date), 'iii d. HH:mm', true, i18n.language)
3935
4048
  }));
4049
+ const currentTimeIndex = React.useMemo(() => {
4050
+ if (timesteps.length === 0) {
4051
+ return -1;
4052
+ }
4053
+ const selectedUtc = dateUtils.getUtcFromString(selectedTime);
4054
+ if (!selectedUtc) {
4055
+ return 0;
4056
+ }
4057
+ const selectedUnix = dateUtils.unix(selectedUtc);
4058
+ const indexByExactTimestamp = timesteps.findIndex(time => {
4059
+ const utcTime = dateUtils.getUtcFromString(time);
4060
+ if (!utcTime) {
4061
+ return false;
4062
+ }
4063
+ return dateUtils.unix(utcTime) === selectedUnix;
4064
+ });
4065
+ if (indexByExactTimestamp !== -1) {
4066
+ return indexByExactTimestamp;
4067
+ }
4068
+ const indexByExactString = timesteps.findIndex(time => time === selectedTime);
4069
+ if (indexByExactString !== -1) {
4070
+ return indexByExactString;
4071
+ }
4072
+ // If selected time is not a sounding timestep, anchor on the nearest timestep
4073
+ // so forward/backward navigation continues from the expected point in time.
4074
+ return timesteps.reduce((nearestIndex, time, index) => {
4075
+ const utcTime = dateUtils.getUtcFromString(time);
4076
+ if (!utcTime) {
4077
+ return nearestIndex;
4078
+ }
4079
+ const currentDiff = Math.abs(dateUtils.unix(utcTime) - selectedUnix);
4080
+ const nearestUtc = dateUtils.getUtcFromString(timesteps[nearestIndex]);
4081
+ if (!nearestUtc) {
4082
+ return index;
4083
+ }
4084
+ const nearestDiff = Math.abs(dateUtils.unix(nearestUtc) - selectedUnix);
4085
+ return currentDiff < nearestDiff ? index : nearestIndex;
4086
+ }, 0);
4087
+ }, [selectedTime, timesteps]);
3936
4088
  const onStepBackward = React.useCallback(() => {
3937
- const currentIndex = timesteps.indexOf(selectedTime);
3938
- const precedingIndex = Math.max(currentIndex - 1, 0);
4089
+ const precedingIndex = Math.max(currentTimeIndex - 1, 0);
3939
4090
  updateSelectedTime(timesteps[precedingIndex]);
3940
- }, [selectedTime, updateSelectedTime, timesteps]);
4091
+ }, [currentTimeIndex, updateSelectedTime, timesteps]);
3941
4092
  const onStepForward = React.useCallback(() => {
3942
- const currentIndex = timesteps.indexOf(selectedTime);
3943
- const followingIndex = Math.min(currentIndex + 1, timesteps.length - 1);
4093
+ const followingIndex = Math.min(currentTimeIndex + 1, timesteps.length - 1);
3944
4094
  updateSelectedTime(timesteps[followingIndex]);
3945
- }, [selectedTime, updateSelectedTime, timesteps]);
4095
+ }, [currentTimeIndex, updateSelectedTime, timesteps]);
3946
4096
  return jsxs("div", {
3947
4097
  ref: sliderRef,
3948
4098
  style: {
@@ -4012,47 +4162,50 @@ const SoundingsTimeSlider = ({
4012
4162
  });
4013
4163
  };
4014
4164
 
4015
- const roundToClosestTimestep = (timeToRound, availableSteps) => {
4016
- if (availableSteps.length > 0 && !availableSteps.includes(timeToRound)) {
4017
- const selectionValue = dateUtils.unix(new Date(timeToRound));
4018
- const closestTimestep = minBy(availableSteps, timeValue =>
4019
- // distance to selected time
4020
- Math.abs(selectionValue - dateUtils.unix(new Date(timeValue))));
4021
- return closestTimestep || availableSteps[0];
4022
- }
4023
- return timeToRound;
4024
- };
4025
4165
  const SoundingsTimeSliderConnect = ({
4026
- viewId
4166
+ viewId,
4167
+ timeInUse,
4168
+ syncEnabled = false
4027
4169
  }) => {
4028
- const selectedTime = useSelector(state => getSelectedTime(state, viewId));
4170
+ const selectedTimeInStore = useSelector(state => getSelectedTime(state, viewId));
4171
+ const selectedTime = timeInUse || selectedTimeInStore;
4029
4172
  const sourceSelections = useSelector(state => getSelectedSources(state, viewId));
4030
4173
  const dispatch = useDispatch();
4031
- const updateSelectedTime = React.useCallback((selectedTime, isInitialCorrection = false) => {
4174
+ const updateSelectedTime = React.useCallback(selectedTime => {
4175
+ if (syncEnabled) {
4176
+ dispatch(genericActions.setTime({
4177
+ sourceId: viewId,
4178
+ origin: 'SoundingsTimeSliderConnect',
4179
+ value: {
4180
+ currentTime: handleDateUtilsISOString(selectedTime)
4181
+ }
4182
+ }));
4183
+ return;
4184
+ }
4032
4185
  dispatch(soundingsActions.updateSelectedTime({
4033
4186
  selectedTime,
4034
4187
  panelId: viewId,
4035
- origin: isInitialCorrection ? SoundingsActionOrigin.soundingsManager : SoundingsActionOrigin.soundingsComponent
4188
+ origin: SoundingsActionOrigin.soundingsComponent
4036
4189
  }));
4037
- }, [dispatch, viewId]);
4190
+ }, [dispatch, syncEnabled, viewId]);
4038
4191
  const {
4039
4192
  data: timesteps
4040
4193
  } = useTimesteps(sourceSelections);
4041
4194
  const mergedTimesteps = [...new Set(timesteps.flat())].sort();
4042
- const hasInitializedTime = React.useRef(false);
4043
- // Ensure that the selected time is always one of the available timesteps
4044
- const validSelectedTime = roundToClosestTimestep(selectedTime, mergedTimesteps);
4045
- useEffect(() => {
4046
- if (selectedTime !== validSelectedTime) {
4047
- const isInitial = !hasInitializedTime.current;
4048
- updateSelectedTime(validSelectedTime, isInitial);
4049
- if (isInitial) {
4050
- hasInitializedTime.current = true;
4051
- }
4195
+ const validSelectedTime = mergedTimesteps.length > 0 && !mergedTimesteps.includes(selectedTimeInStore) ? getNearestTimestep(selectedTimeInStore, mergedTimesteps) : selectedTimeInStore;
4196
+ React.useEffect(() => {
4197
+ // Local mode: keep selectedTime aligned with available timesteps.
4198
+ if (syncEnabled || selectedTimeInStore === validSelectedTime) {
4199
+ return;
4052
4200
  }
4053
- }, [selectedTime, updateSelectedTime, validSelectedTime]);
4201
+ dispatch(soundingsActions.updateSelectedTime({
4202
+ selectedTime: validSelectedTime,
4203
+ panelId: viewId,
4204
+ origin: SoundingsActionOrigin.soundingsComponent
4205
+ }));
4206
+ }, [dispatch, selectedTimeInStore, syncEnabled, validSelectedTime, viewId]);
4054
4207
  return jsx(SoundingsTimeSlider, {
4055
- selectedTime: validSelectedTime,
4208
+ selectedTime: selectedTime,
4056
4209
  timesteps: mergedTimesteps,
4057
4210
  updateSelectedTime: updateSelectedTime
4058
4211
  });
@@ -4089,6 +4242,9 @@ const SoundingsConnect = ({
4089
4242
  viewId,
4090
4243
  soundingsPreset
4091
4244
  }) => {
4245
+ const {
4246
+ t
4247
+ } = useSoundingsTranslation();
4092
4248
  const viewRef = React.useRef(null);
4093
4249
  const size = useGetSize(viewRef);
4094
4250
  const geojsonLayerId = React.useRef(webmapUtils.generateLayerId()).current;
@@ -4125,6 +4281,25 @@ const SoundingsConnect = ({
4125
4281
  const locationSelections = useSelector(state => getLocationSelections(state, viewId), shallowEqual);
4126
4282
  const diagramType = useSelector(state => getDiagramType(state, viewId));
4127
4283
  const shouldHideLocationMarkers = useSelector(state => getShouldHideLocationMarkers(state, viewId));
4284
+ const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, viewId), shallowEqual);
4285
+ const firstLinkedMapId = selectedMapIds[0];
4286
+ const {
4287
+ data: sourceTimesteps
4288
+ } = useTimesteps(sourceSelections);
4289
+ const syncgroupTimeState = useSyncgroupTimeState(viewId, 0);
4290
+ const syncgroupSelectedTime = useMemo(() => {
4291
+ if (syncgroupTimeState?.current === undefined || !Number.isFinite(syncgroupTimeState.current)) {
4292
+ return undefined;
4293
+ }
4294
+ return dateUtils.fromUnix(syncgroupTimeState.current).toISOString();
4295
+ }, [syncgroupTimeState]);
4296
+ const isTimeSynced = !!syncgroupSelectedTime;
4297
+ const selectedTimeInUse = syncgroupSelectedTime || selectedTime;
4298
+ const timeMismatchInfo = useMemo(() => {
4299
+ return getTimeMismatchInfo(isTimeSynced, selectedTimeInUse, sourceTimesteps);
4300
+ }, [isTimeSynced, selectedTimeInUse, sourceTimesteps]);
4301
+ const mismatchKey = timeMismatchInfo ? selectedTimeInUse || '' : '';
4302
+ const [isTimeMismatchDismissed, setIsTimeMismatchDismissed] = React.useState(false);
4128
4303
  const setChosenDiagramType = React.useCallback(diagramType => {
4129
4304
  dispatch(soundingsActions.updateDiagramType({
4130
4305
  diagramType,
@@ -4133,11 +4308,9 @@ const SoundingsConnect = ({
4133
4308
  }));
4134
4309
  }, [dispatch, viewId]);
4135
4310
  const selectedFeature = useSelector(state => genericSelectors.getSelectedFeature(state.syncGroups, viewId), shallowEqual);
4136
- const selectedMapIds = useSelector(state => genericSelectors.getLinkedMaps(state, viewId), shallowEqual);
4137
- const firstMapId = selectedMapIds[0];
4138
4311
  const selectedMapPin = useSelector(state =>
4139
4312
  // mapPins are synced so we can arbitrarily choose the first linked map
4140
- mapSelectors.getPinLocation(state, firstMapId), shallowEqual);
4313
+ mapSelectors.getPinLocation(state, firstLinkedMapId), shallowEqual);
4141
4314
  const collectionsRef = React.useRef();
4142
4315
  const collections = useMemo(() => {
4143
4316
  const newCollections = sourceSelections.map(({
@@ -4355,7 +4528,10 @@ const SoundingsConnect = ({
4355
4528
  error,
4356
4529
  data,
4357
4530
  isPending
4358
- } = useMultipleSources(sourceSelections, locationSelections, selectedTime);
4531
+ } = useMultipleSources(sourceSelections, locationSelections, selectedTimeInUse, 'exact');
4532
+ useEffect(() => {
4533
+ setIsTimeMismatchDismissed(false);
4534
+ }, [mismatchKey]);
4359
4535
  const soundingPanelsInOrder = useSelector(store => getSoundingPanelsInOrder(store, viewId));
4360
4536
  const changeOrder = newOrder => {
4361
4537
  const orderedIds = newOrder.map(collection => collection.id);
@@ -4373,14 +4549,18 @@ const SoundingsConnect = ({
4373
4549
  color: "secondary",
4374
4550
  sx: {
4375
4551
  position: 'absolute',
4376
- width: '100%'
4552
+ top: 0,
4553
+ left: 0,
4554
+ width: '100%',
4555
+ zIndex: 20
4377
4556
  }
4378
4557
  }), jsxs(Stack, {
4379
4558
  sx: {
4380
4559
  padding: '12px',
4381
4560
  width: '100%',
4382
4561
  height: '100%',
4383
- backgroundColor: 'geowebColors.background.surfaceApp'
4562
+ backgroundColor: 'geowebColors.background.surfaceApp',
4563
+ position: 'relative'
4384
4564
  },
4385
4565
  children: [jsx(Box, {
4386
4566
  paddingLeft: 5,
@@ -4390,7 +4570,8 @@ const SoundingsConnect = ({
4390
4570
  }), jsx(Card, {
4391
4571
  sx: {
4392
4572
  width: '100%',
4393
- height: '100%'
4573
+ height: '100%',
4574
+ position: 'relative'
4394
4575
  },
4395
4576
  children: jsxs(Stack, {
4396
4577
  spacing: 1,
@@ -4399,7 +4580,30 @@ const SoundingsConnect = ({
4399
4580
  height: '100%',
4400
4581
  display: 'flex'
4401
4582
  },
4402
- children: [jsx(Box, {
4583
+ children: [!!timeMismatchInfo && !isTimeMismatchDismissed && jsxs(Box, {
4584
+ sx: {
4585
+ flex: '0 0 auto',
4586
+ position: 'relative'
4587
+ },
4588
+ children: [jsx(AlertBanner, {
4589
+ severity: "warning",
4590
+ title: t('soundings-time-mismatch-title')
4591
+ }), jsx(IconButton, {
4592
+ "aria-label": "Close warning",
4593
+ onClick: () => setIsTimeMismatchDismissed(true),
4594
+ size: "small",
4595
+ sx: {
4596
+ position: 'absolute',
4597
+ right: 8,
4598
+ top: 10,
4599
+ padding: 0.25,
4600
+ zIndex: 1
4601
+ },
4602
+ children: jsx(Close, {
4603
+ fontSize: "small"
4604
+ })
4605
+ })]
4606
+ }), jsx(Box, {
4403
4607
  sx: {
4404
4608
  display: 'flex',
4405
4609
  justifyContent: 'center',
@@ -4435,6 +4639,8 @@ const SoundingsConnect = ({
4435
4639
  height: `${TIMESLIDER_HEIGHT}px`
4436
4640
  },
4437
4641
  children: jsx(SoundingsTimeSliderConnect, {
4642
+ syncEnabled: isTimeSynced,
4643
+ timeInUse: selectedTimeInUse,
4438
4644
  viewId: viewId
4439
4645
  })
4440
4646
  })]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeoweb/soundings",
3
- "version": "15.3.0",
3
+ "version": "16.0.0",
4
4
  "description": "GeoWeb soundings library for the opengeoweb project",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,24 +8,24 @@
8
8
  "url": "git@gitlab.com:opengeoweb/opengeoweb.git"
9
9
  },
10
10
  "dependencies": {
11
- "@opengeoweb/api": "15.3.0",
11
+ "@opengeoweb/api": "16.0.0",
12
12
  "@tanstack/react-query": "^5.85.5",
13
13
  "react": "^18.3.1",
14
14
  "d3": "^7.9.0",
15
15
  "react-i18next": "^15.1.1",
16
16
  "i18next": "^25.0.1",
17
- "@opengeoweb/shared": "15.3.0",
17
+ "@opengeoweb/shared": "16.0.0",
18
18
  "@mui/material": "^7.0.1",
19
- "@opengeoweb/store": "15.3.0",
19
+ "@opengeoweb/store": "16.0.0",
20
20
  "react-redux": "^9.2.0",
21
- "@opengeoweb/theme": "15.3.0",
21
+ "@opengeoweb/theme": "16.0.0",
22
22
  "@reduxjs/toolkit": "^2.6.1",
23
- "@opengeoweb/time-slider": "15.3.0",
23
+ "@opengeoweb/time-slider": "16.0.0",
24
24
  "react-window": "^2.2.6",
25
- "@opengeoweb/webmap": "15.3.0",
25
+ "@opengeoweb/webmap": "16.0.0",
26
26
  "lodash": "^4.17.21",
27
27
  "@types/geojson": "^7946.0.14",
28
- "@opengeoweb/webmap-react": "15.3.0",
28
+ "@opengeoweb/webmap-react": "16.0.0",
29
29
  "react-resizable-layout": "^0.7.3",
30
30
  "react-sortablejs": "^6.1.4"
31
31
  },
@@ -17,7 +17,7 @@ export declare const defaultParameterMappings: {
17
17
  relativeHumidity: string;
18
18
  };
19
19
  export declare const getParameterMappings: (parameterMappings: Record<string, string>) => string[];
20
- export declare const useMultipleSources: (sources: SourceType[], locations: Record<string, string | undefined | Coordinates>, datetime?: string) => {
20
+ export declare const useMultipleSources: (sources: SourceType[], locations: Record<string, string | undefined | Coordinates>, datetime?: string, timeSelectionMode?: "exact" | "nearestWithinRange") => {
21
21
  data: SoundingFeature[][];
22
22
  isPending: boolean;
23
23
  error: Error | null | undefined;
@@ -1,6 +1,7 @@
1
1
  import type { Coverage, CoverageCollection, VerticalProfileDomain } from '@opengeoweb/shared';
2
2
  import type { CollectionDetails, SoundingFeature, SoundingsInstance } from './types';
3
3
  export declare const getWindSpeedInKts: (windSpeed: number, windSpeedUnit: string) => number;
4
+ export declare const getNearestTimestep: (requestedDatetime: string, availableTimesteps: string[]) => string;
4
5
  export declare function parseCoverageCollection(response: CoverageCollection<VerticalProfileDomain> | Coverage<VerticalProfileDomain>, parameterMappings: Record<string, string>, windSpeedUnit: string): SoundingFeature[];
5
6
  /**
6
7
  * Parse duration with repeats
@@ -8,6 +8,7 @@ export default _default;
8
8
  type Story = StoryObj<typeof SoundingsConnect>;
9
9
  export declare const SoundingsConnectStory: Story;
10
10
  export declare const SoundingsConnectStoryDark: Story;
11
+ export declare const SoundingsConnectWarningStory: Story;
11
12
  export declare const SoundingsClosedWindBarbsOpenStory: Story;
12
13
  export declare const SoundingsClosedWindBarbsOpenStoryDark: Story;
13
14
  export declare const SoundingsConnectLoading: {
@@ -0,0 +1,4 @@
1
+ export interface TimeMismatchInfo {
2
+ mismatch: true;
3
+ }
4
+ export declare const getTimeMismatchInfo: (isTimeSynced: boolean, warningTimeInUse: string | undefined, sourceTimesteps: string[][]) => TimeMismatchInfo | undefined;
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
2
  interface SoundingsTimeSliderConnectProps {
3
3
  viewId: string;
4
+ timeInUse?: string;
5
+ syncEnabled?: boolean;
4
6
  }
5
- export declare const SoundingsTimeSliderConnect: ({ viewId, }: SoundingsTimeSliderConnectProps) => React.ReactElement;
7
+ export declare const SoundingsTimeSliderConnect: ({ viewId, timeInUse, syncEnabled, }: SoundingsTimeSliderConnectProps) => React.ReactElement;
6
8
  export {};