@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 +276 -70
- package/package.json +8 -8
- package/src/lib/api/edr-api/hooks.d.ts +1 -1
- package/src/lib/api/edr-api/utils.d.ts +1 -0
- package/src/lib/components/Soundings/SoundingsConnect.stories.d.ts +1 -0
- package/src/lib/components/Soundings/timeMismatchUtils.d.ts +4 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSliderConnect.d.ts +3 -1
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,
|
|
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,
|
|
8
|
-
import {
|
|
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-
|
|
51
|
-
"soundings-toggle-location-markers-
|
|
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
|
-
//
|
|
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
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
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
|
-
|
|
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
|
|
3938
|
-
const precedingIndex = Math.max(currentIndex - 1, 0);
|
|
4089
|
+
const precedingIndex = Math.max(currentTimeIndex - 1, 0);
|
|
3939
4090
|
updateSelectedTime(timesteps[precedingIndex]);
|
|
3940
|
-
}, [
|
|
4091
|
+
}, [currentTimeIndex, updateSelectedTime, timesteps]);
|
|
3941
4092
|
const onStepForward = React.useCallback(() => {
|
|
3942
|
-
const
|
|
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
|
-
}, [
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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: [
|
|
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": "
|
|
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": "
|
|
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": "
|
|
17
|
+
"@opengeoweb/shared": "16.0.0",
|
|
18
18
|
"@mui/material": "^7.0.1",
|
|
19
|
-
"@opengeoweb/store": "
|
|
19
|
+
"@opengeoweb/store": "16.0.0",
|
|
20
20
|
"react-redux": "^9.2.0",
|
|
21
|
-
"@opengeoweb/theme": "
|
|
21
|
+
"@opengeoweb/theme": "16.0.0",
|
|
22
22
|
"@reduxjs/toolkit": "^2.6.1",
|
|
23
|
-
"@opengeoweb/time-slider": "
|
|
23
|
+
"@opengeoweb/time-slider": "16.0.0",
|
|
24
24
|
"react-window": "^2.2.6",
|
|
25
|
-
"@opengeoweb/webmap": "
|
|
25
|
+
"@opengeoweb/webmap": "16.0.0",
|
|
26
26
|
"lodash": "^4.17.21",
|
|
27
27
|
"@types/geojson": "^7946.0.14",
|
|
28
|
-
"@opengeoweb/webmap-react": "
|
|
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: {
|
|
@@ -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 {};
|