@opengeoweb/soundings 12.5.0 → 12.6.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
@@ -4,8 +4,10 @@ import { Card, Box, Stack, LinearProgress, List, ListItem, ListItemIcon, Checkbo
4
4
  import { safelyInitI18nForTestsAndStories, sharedTranslations, SHARED_NAMESPACE, dateUtils, CustomSlider, CustomToggleButton, AlertBanner, ToolContainerDraggable, StyledSelect, SearchField, CustomAccordion } from '@opengeoweb/shared';
5
5
  import React, { useRef, useEffect, useMemo, useState, Fragment as Fragment$1 } from 'react';
6
6
  import * as d3 from 'd3';
7
- import { useQueries, useQuery } from '@tanstack/react-query';
7
+ import { useQueries } from '@tanstack/react-query';
8
8
  import { getHeaders, handleResponse } from '@opengeoweb/api';
9
+ import { ParseISOTimeRangeDuration, parseISO8601IntervalToDateInterval } from '@opengeoweb/webmap';
10
+ import { range } from 'lodash';
9
11
  import { timesliderTranslations, TIMESLIDER_NAMESPACE, BackwardForwardStepButton } from '@opengeoweb/timeslider';
10
12
  import i18n from 'i18next';
11
13
  export { default as i18n } from 'i18next';
@@ -1617,7 +1619,7 @@ const Soundings = ({
1617
1619
  const getCollectionLocations = (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/locations`, {
1618
1620
  headers: getHeaders()
1619
1621
  }).then(handleResponse);
1620
- const getCollectionInstance = (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/instances`, {
1622
+ const getCollectionInstances = (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/instances`, {
1621
1623
  headers: getHeaders()
1622
1624
  }).then(handleResponse);
1623
1625
  const getEdrCollectionForLocation = (collectionId, baseUrl, instanceId, locationId, parameters, datetime) => {
@@ -1690,6 +1692,32 @@ relHumidity) {
1690
1692
  // Parsing and conversion function
1691
1693
  function parseCoverageCollection(response, parameterMappings) {
1692
1694
  const features = [];
1695
+ const pressureArray = [];
1696
+ const tempArray = [];
1697
+ const dewpointArray = [];
1698
+ const windDirArray = [];
1699
+ const windSpeedArray = [];
1700
+ const windUArray = [];
1701
+ const windVArray = [];
1702
+ const specificHumidityArray = [];
1703
+ const relativeHumidityArray = [];
1704
+ const emtpyDataArray = {
1705
+ time: '',
1706
+ xCoord: NaN,
1707
+ yCoord: NaN,
1708
+ data: {
1709
+ pressureArray,
1710
+ tempArray,
1711
+ dewpointArray,
1712
+ windDirArray,
1713
+ windSpeedArray,
1714
+ windUArray,
1715
+ windVArray,
1716
+ specificHumidityArray,
1717
+ relativeHumidityArray
1718
+ }
1719
+ };
1720
+ const data = {};
1693
1721
  const {
1694
1722
  t: tAxis,
1695
1723
  x: xAxis,
@@ -1706,60 +1734,139 @@ function parseCoverageCollection(response, parameterMappings) {
1706
1734
  const time = coverage.domain.axes.t?.values[0];
1707
1735
  const xCoord = coverage.domain.axes.x.values[0];
1708
1736
  const yCoord = coverage.domain.axes.y.values[0];
1737
+ const coverageIdentifier = `${time}-${xCoord}-${yCoord}`;
1738
+ if (data[coverageIdentifier] === undefined) {
1739
+ data[coverageIdentifier] = {
1740
+ ...emtpyDataArray
1741
+ };
1742
+ }
1743
+ data[coverageIdentifier].time = time;
1744
+ data[coverageIdentifier].xCoord = xCoord;
1745
+ data[coverageIdentifier].yCoord = yCoord;
1709
1746
  const zValues = coverage.domain.axes.z.values;
1710
1747
  zValues.forEach((_, zIndex) => {
1711
- const temp = parseFloat(ranges[parameterMappings.temperature]?.values[zIndex]);
1712
- const pressureFromRange = parseFloat(ranges[parameterMappings.pressure]?.values[zIndex]);
1713
- const pressure = !Number.isNaN(pressureFromRange) ? pressureFromRange : parseFloat(zValues[zIndex]);
1714
- // Extract dewpoint - use fallback calculations if no dewpoint in parameter mappings
1715
- let dewpoint = NaN;
1716
- if (parameterMappings.dewpoint) {
1717
- dewpoint = parseFloat(ranges[parameterMappings.dewpoint]?.values[zIndex]);
1718
- } else if (parameterMappings.specificHumidity) {
1719
- const specificHumidity = parseFloat(ranges[parameterMappings.specificHumidity]?.values[zIndex]);
1720
- dewpoint = dewpointFromSpecificHumidity(specificHumidity / 1000,
1721
- // from g/kg to kg/kg
1722
- pressToPascal(pressure));
1723
- } else if (parameterMappings.relativeHumidity) {
1724
- const relativeHumidity = parseFloat(ranges[parameterMappings.relativeHumidity]?.values[zIndex]);
1725
- dewpoint = dewpointFromRelativeHumidity(temp, relativeHumidity);
1748
+ if (data[coverageIdentifier].data.pressureArray.length === zIndex || ranges[parameterMappings.pressure]?.values[zIndex]) {
1749
+ const pressureFromRange = parseFloat(ranges[parameterMappings.pressure]?.values[zIndex]);
1750
+ const pressure = !Number.isNaN(pressureFromRange) ? pressureFromRange : parseFloat(zValues[zIndex]);
1751
+ data[coverageIdentifier].data.pressureArray[zIndex] = pressure;
1726
1752
  }
1727
- // Extract wind speed and direction - use fallback calculations if no speed and direction in parameter mappings
1728
- let windSpeed = NaN;
1729
- if (parameterMappings.windSpeed) {
1730
- windSpeed = parseFloat(ranges[parameterMappings.windSpeed]?.values[zIndex]);
1731
- } else if (parameterMappings.windU && parameterMappings.windV) {
1732
- const windU = parseFloat(ranges[parameterMappings.windU]?.values[zIndex]);
1733
- const windV = parseFloat(ranges[parameterMappings.windV]?.values[zIndex]);
1734
- windSpeed = Math.sqrt(windU ** 2 + windV ** 2);
1753
+ if (ranges[parameterMappings.temperature]) {
1754
+ data[coverageIdentifier].data.tempArray.push(parseFloat(ranges[parameterMappings.temperature]?.values[zIndex]));
1735
1755
  }
1736
- let windDirection = NaN;
1737
- if (parameterMappings.windDirection) {
1738
- windDirection = parseFloat(ranges[parameterMappings.windDirection]?.values[zIndex]);
1739
- } else if (parameterMappings.windU && parameterMappings.windV) {
1740
- const windU = parseFloat(ranges[parameterMappings.windU]?.values[zIndex]);
1741
- const windV = parseFloat(ranges[parameterMappings.windV]?.values[zIndex]);
1742
- windDirection = (Math.atan2(-windU, -windV) * (180 / Math.PI) + 360) % 360;
1756
+ if (parameterMappings.dewpoint && ranges[parameterMappings.dewpoint]) {
1757
+ data[coverageIdentifier].data.dewpointArray.push(parseFloat(ranges[parameterMappings.dewpoint]?.values[zIndex]));
1758
+ } else if (parameterMappings.specificHumidity && ranges[parameterMappings.specificHumidity]) {
1759
+ data[coverageIdentifier].data.specificHumidityArray.push(parseFloat(ranges[parameterMappings.specificHumidity]?.values[zIndex]));
1760
+ } else if (parameterMappings.relativeHumidity && ranges[parameterMappings.relativeHumidity]) {
1761
+ data[coverageIdentifier].data.relativeHumidityArray.push(parseFloat(ranges[parameterMappings.relativeHumidity]?.values[zIndex]));
1762
+ }
1763
+ if (parameterMappings.windSpeed && ranges[parameterMappings.windSpeed]) {
1764
+ data[coverageIdentifier].data.windSpeedArray.push(parseFloat(ranges[parameterMappings.windSpeed]?.values[zIndex]));
1765
+ }
1766
+ if (parameterMappings.windDirection && ranges[parameterMappings.windDirection]) {
1767
+ data[coverageIdentifier].data.windDirArray.push(parseFloat(ranges[parameterMappings.windDirection]?.values[zIndex]));
1768
+ }
1769
+ if (parameterMappings.windU && ranges[parameterMappings.windU]) {
1770
+ data[coverageIdentifier].data.windUArray.push(parseFloat(ranges[parameterMappings.windU]?.values[zIndex]));
1771
+ }
1772
+ if (parameterMappings.windV && ranges[parameterMappings.windV]) {
1773
+ data[coverageIdentifier].data.windVArray.push(parseFloat(ranges[parameterMappings.windV]?.values[zIndex]));
1774
+ }
1775
+ });
1776
+ });
1777
+ // For now we only accept one data set per call so we only take the first one
1778
+ const firstCollectionIdentifier = Object.keys(data)[0];
1779
+ const collectionData = data[firstCollectionIdentifier].data;
1780
+ collectionData.pressureArray.forEach((pressure, index) => {
1781
+ // Extract dewpoint - use fallback calculations if no dewpoint in parameter mappings
1782
+ let dewpoint = NaN;
1783
+ if (parameterMappings.dewpoint) {
1784
+ dewpoint = collectionData.dewpointArray[index];
1785
+ } else if (parameterMappings.specificHumidity) {
1786
+ const specificHumidity = collectionData.specificHumidityArray[index];
1787
+ dewpoint = dewpointFromSpecificHumidity(specificHumidity / 1000,
1788
+ // from g/kg to kg/kg
1789
+ pressToPascal(pressure));
1790
+ } else if (parameterMappings.relativeHumidity) {
1791
+ const relativeHumidity = collectionData.relativeHumidityArray[index];
1792
+ dewpoint = dewpointFromRelativeHumidity(tempArray[index], relativeHumidity);
1793
+ }
1794
+ // Extract wind speed and direction - use fallback calculations if no speed and direction in parameter mappings
1795
+ let windSpeed = NaN;
1796
+ if (parameterMappings.windSpeed) {
1797
+ windSpeed = collectionData.windSpeedArray[index];
1798
+ } else if (parameterMappings.windU && parameterMappings.windV) {
1799
+ const windU = collectionData.windUArray[index];
1800
+ const windV = collectionData.windVArray[index];
1801
+ windSpeed = Math.sqrt(windU ** 2 + windV ** 2);
1802
+ }
1803
+ let windDirection = NaN;
1804
+ if (parameterMappings.windDirection) {
1805
+ windDirection = collectionData.windDirArray[index];
1806
+ } else if (parameterMappings.windU && parameterMappings.windV) {
1807
+ const windU = collectionData.windUArray[index];
1808
+ const windV = collectionData.windVArray[index];
1809
+ windDirection = (Math.atan2(-windU, -windV) * (180 / Math.PI) + 360) % 360;
1810
+ }
1811
+ features.push({
1812
+ type: 'Feature',
1813
+ geometry: {
1814
+ type: 'Point',
1815
+ coordinates: [data[firstCollectionIdentifier].xCoord, data[firstCollectionIdentifier].yCoord]
1816
+ },
1817
+ properties: {
1818
+ time: data[firstCollectionIdentifier].time,
1819
+ temp: tempArray[index],
1820
+ dewpoint,
1821
+ pressure: pressureArray[index],
1822
+ windSpeed,
1823
+ windDirection
1743
1824
  }
1744
- features.push({
1745
- type: 'Feature',
1746
- geometry: {
1747
- type: 'Point',
1748
- coordinates: [xCoord, yCoord]
1749
- },
1750
- properties: {
1751
- time,
1752
- temp,
1753
- dewpoint,
1754
- pressure,
1755
- windSpeed,
1756
- windDirection
1757
- }
1758
- });
1759
1825
  });
1760
1826
  });
1761
1827
  return features;
1762
1828
  }
1829
+ /**
1830
+ * Parse duration with repeats
1831
+ * @param dateString e.g. 'R67/2025-04-22T09:00:00Z/PT60M'
1832
+ * @returns Array of ISO datetime strings, e.g., ['2025-04-22T09:00:00.000Z', ...]
1833
+ */
1834
+ const parseDurationWithRepeats = dateString => {
1835
+ const parts = dateString.split('/');
1836
+ const numOfRepeats = Number(parts[0].substring(1));
1837
+ const start = new Date(parts[1]).getTime();
1838
+ const interval = parseISO8601IntervalToDateInterval(parts[2]).getTime();
1839
+ const iterations = range(numOfRepeats);
1840
+ const dateStrings = iterations.map(i => start + i * interval).map(millis => new Date(millis).toISOString());
1841
+ return dateStrings;
1842
+ };
1843
+ const getTimestepsFromInstance = instance => {
1844
+ const timeExtentValues = instance?.extent?.temporal?.values;
1845
+ if (!timeExtentValues) {
1846
+ return [];
1847
+ }
1848
+ try {
1849
+ // parse the time extent, if needed
1850
+ if (timeExtentValues.length === 1) {
1851
+ const timeExtent = timeExtentValues[0];
1852
+ // duration with repeats
1853
+ if (timeExtent.match(/^R\d+\/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\/\S+$/)) {
1854
+ return parseDurationWithRepeats(timeExtent);
1855
+ }
1856
+ // format without repeats
1857
+ const parseObject = new ParseISOTimeRangeDuration(timeExtent);
1858
+ const timeValues = range(parseObject.getTimeSteps()).map(timestep => parseObject.getDateAtTimeStep(timestep).toISOString());
1859
+ return timeValues;
1860
+ }
1861
+ // If more than one value, assume time extent is given as individual timesteps.
1862
+ // Pass through toISOString to ensure consistent timestamp format.
1863
+ const isoFormattedValues = timeExtentValues.map(value => new Date(value).toISOString());
1864
+ return isoFormattedValues;
1865
+ } catch (error) {
1866
+ console.warn(error);
1867
+ }
1868
+ return [];
1869
+ };
1763
1870
 
1764
1871
  /* *
1765
1872
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -1827,17 +1934,6 @@ const constructQueryParams = (collectionId, baseUrl, instanceId, location, datet
1827
1934
  }
1828
1935
  };
1829
1936
  };
1830
- const useFakeTime = (collectionId, baseUrl, instanceId) => {
1831
- return useQuery({
1832
- queryKey: ['temporary-instance-call', collectionId, instanceId],
1833
- queryFn: async () => {
1834
- return fetch(`${baseUrl}/collections/${collectionId}/instances/${instanceId}`, {
1835
- headers: getHeaders()
1836
- }).then(handleResponse).then(data => collectionId === undefined || data === undefined ? '' : data.extent.temporal.interval[0][0]);
1837
- },
1838
- enabled: !!collectionId && !!instanceId
1839
- });
1840
- };
1841
1937
  const defaultParameterMappings = {
1842
1938
  temperature: 'temperature',
1843
1939
  windDirection: 'winddirection',
@@ -1854,26 +1950,15 @@ const getParameterMappings = parameterMappings => {
1854
1950
  return Object.keys(parameterMappings).map(key => parameterMappings[key]);
1855
1951
  };
1856
1952
  const useMultipleSources = (sources, locations, datetime) => {
1857
- // TODO: Remove once we pass in the datetime coming from the timeslider!
1858
- const {
1859
- data: date0
1860
- } = useFakeTime(sources[0]?.collectionId, sources[0]?.url, sources[0]?.instanceId);
1861
- const {
1862
- data: date1
1863
- } = useFakeTime(sources[1]?.collectionId, sources[1]?.url, sources[1]?.instanceId);
1864
- const {
1865
- data: date2
1866
- } = useFakeTime(sources[2]?.collectionId, sources[2]?.url, sources[2]?.instanceId);
1867
- const dates = useMemo(() => [date0, date1, date2], [date0, date1, date2]);
1868
1953
  const queries = useMemo(() => sources.map(({
1869
1954
  collectionId,
1870
1955
  url,
1871
1956
  instanceId,
1872
1957
  parameterMappings
1873
- }, index) => {
1874
- const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dates[index] || '', parameterMappings || defaultParameterMappings);
1958
+ }) => {
1959
+ const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', datetime || '', parameterMappings || defaultParameterMappings);
1875
1960
  return queryParams;
1876
- }), [dates, datetime, locations, sources]);
1961
+ }), [datetime, locations, sources]);
1877
1962
  return useQueries({
1878
1963
  queries,
1879
1964
  combine: queryResults => {
@@ -1890,6 +1975,31 @@ const useMultipleSources = (sources, locations, datetime) => {
1890
1975
  }
1891
1976
  });
1892
1977
  };
1978
+ const useTimesteps = sourceSelections => {
1979
+ const queries = React.useMemo(() => sourceSelections.map(({
1980
+ collectionId,
1981
+ instanceId,
1982
+ url
1983
+ }) => ({
1984
+ queryKey: ['edr-soundings-instances', collectionId],
1985
+ queryFn: async () => {
1986
+ const data = await getCollectionInstances(collectionId, url);
1987
+ return data;
1988
+ },
1989
+ select: data => {
1990
+ const instance = data.instances.find(instance => instance.id === instanceId);
1991
+ return getTimestepsFromInstance(instance);
1992
+ }
1993
+ })), [sourceSelections]);
1994
+ const results = useQueries({
1995
+ queries,
1996
+ combine: queryResults => {
1997
+ const timestepArrays = queryResults.map(result => result.data).filter(steps => !!steps);
1998
+ return timestepArrays;
1999
+ }
2000
+ });
2001
+ return results;
2002
+ };
1893
2003
  const useGetCollectionsInstances = (collections, isDialogOpen) => {
1894
2004
  const collectionIds = useMemo(() => collections?.map(collection => {
1895
2005
  return {
@@ -1904,7 +2014,7 @@ const useGetCollectionsInstances = (collections, isDialogOpen) => {
1904
2014
  return {
1905
2015
  queryKey: ['edr-soundings-instances', id],
1906
2016
  queryFn: async () => {
1907
- const data = await getCollectionInstance(id, url);
2017
+ const data = await getCollectionInstances(id, url);
1908
2018
  return data;
1909
2019
  },
1910
2020
  enabled: isDialogOpen
@@ -2002,6 +2112,16 @@ const translateKeyOutsideComponents = (key, params = undefined) => i18n.t(key, {
2002
2112
  });
2003
2113
  const useSoundingsTranslation = () => useTranslation(ns);
2004
2114
 
2115
+ const MAX_LABEL_COUNT = 15;
2116
+ const shouldHideLabel = (nItems, index) => {
2117
+ const nthLabelsToShow = Math.ceil(nItems / MAX_LABEL_COUNT);
2118
+ return nItems > MAX_LABEL_COUNT &&
2119
+ // hide labels if too many items
2120
+ index !== nItems - 1 && (
2121
+ // not last item (always shown)
2122
+ // only show every nth label, skip label within last n/2 items so end doesn't get cluttered
2123
+ index % nthLabelsToShow !== 0 || nItems - 1 - index < nthLabelsToShow / 2);
2124
+ };
2005
2125
  const SoundingsTimeSlider = ({
2006
2126
  timesteps,
2007
2127
  selectedTime,
@@ -2010,9 +2130,9 @@ const SoundingsTimeSlider = ({
2010
2130
  const {
2011
2131
  i18n
2012
2132
  } = useSoundingsTranslation();
2013
- const marks = timesteps.filter(date => dateUtils.isValidIsoDateString(date)).map(date => ({
2133
+ const marks = timesteps.filter(date => dateUtils.isValidIsoDateString(date)).map((date, index, arr) => ({
2014
2134
  value: dateUtils.unix(dateUtils.getUtcFromString(date)),
2015
- label: dateUtils.dateToString(dateUtils.getUtcFromString(date), 'iii d. HH:mm', true, i18n.language)
2135
+ label: shouldHideLabel(arr.length, index) ? '' : dateUtils.dateToString(dateUtils.getUtcFromString(date), 'iii d. HH:mm', true, i18n.language)
2016
2136
  }));
2017
2137
  const onStepBackward = React.useCallback(() => {
2018
2138
  const currentIndex = timesteps.indexOf(selectedTime);
@@ -2027,12 +2147,15 @@ const SoundingsTimeSlider = ({
2027
2147
  return jsxs("div", {
2028
2148
  style: {
2029
2149
  display: 'flex',
2030
- marginTop: '17px'
2150
+ marginTop: '40px',
2151
+ marginRight: '4rem'
2031
2152
  },
2032
2153
  children: [jsxs("div", {
2033
2154
  style: {
2034
2155
  minWidth: '60px',
2035
- marginTop: '2px'
2156
+ marginTop: '2px',
2157
+ display: 'flex',
2158
+ gap: '4px'
2036
2159
  },
2037
2160
  children: [jsx(BackwardForwardStepButton, {
2038
2161
  isForwardStep: false,
@@ -2051,7 +2174,7 @@ const SoundingsTimeSlider = ({
2051
2174
  disabled: true
2052
2175
  }) : jsx(CustomSlider, {
2053
2176
  orientation: "horizontal",
2054
- valueLabelDisplay: "auto",
2177
+ valueLabelDisplay: "on",
2055
2178
  valueLabelFormat: value => dateUtils.dateToString(dateUtils.fromUnix(value), "yyyy iii dd MMM HH:mm 'UTC'", true, i18n.language),
2056
2179
  marks: marks,
2057
2180
  step: null,
@@ -2061,6 +2184,21 @@ const SoundingsTimeSlider = ({
2061
2184
  onChange: (_, value) => {
2062
2185
  updateSelectedTime(dateUtils.fromUnix(value).toISOString());
2063
2186
  },
2187
+ // Workaround for MUI bug where labels are just draggable area and clicking on one
2188
+ // does not necessarily select the described timestep
2189
+ slotProps: {
2190
+ markLabel: {
2191
+ onPointerDown: event => {
2192
+ event.preventDefault();
2193
+ const markIndex = event.currentTarget.getAttribute('data-index');
2194
+ const mark = marks[Number(markIndex)];
2195
+ const {
2196
+ value
2197
+ } = mark;
2198
+ updateSelectedTime(dateUtils.fromUnix(value).toISOString());
2199
+ }
2200
+ }
2201
+ },
2064
2202
  extraStyles: {
2065
2203
  margin: '0px 20px 0px 20px',
2066
2204
  '.MuiSlider-markLabel': {
@@ -2174,18 +2312,21 @@ const getSelectedSourcesIds = createSelector(getSelectedSources, activeSources =
2174
2312
  return activeSources.map(source => source.collectionId);
2175
2313
  }, selectorMemoizationOptions);
2176
2314
 
2177
- const SoundingsTimeSliderConnect = ({
2178
- timesteps
2179
- }) => {
2315
+ const SoundingsTimeSliderConnect = () => {
2180
2316
  const selectedTime = useSelector(getSelectedTime);
2317
+ const sourceSelections = useSelector(getSelectedSources);
2181
2318
  const dispatch = useDispatch();
2182
2319
  const updateSelectedTime = React.useCallback(selectedTime => {
2183
2320
  dispatch(soundingsActions.updateSelectedTime({
2184
2321
  selectedTime
2185
2322
  }));
2186
2323
  }, [dispatch]);
2324
+ const timesteps = useTimesteps(sourceSelections);
2325
+ const mergedTimesteps = Array.from(new Set(timesteps.flat())).sort();
2326
+ // TODO: next line should work in Storybook but does not: https://gitlab.com/opengeoweb/geoweb-assets/-/issues/4025
2327
+ // const mergedTimesteps = [...new Set(timestepArrays.flat())].sort();
2187
2328
  return jsx(SoundingsTimeSlider, {
2188
- timesteps: timesteps,
2329
+ timesteps: mergedTimesteps,
2189
2330
  selectedTime: selectedTime,
2190
2331
  updateSelectedTime: updateSelectedTime
2191
2332
  });
@@ -2278,15 +2419,14 @@ const MenuBarConnect = ({
2278
2419
  const SoundingsConnect = ({
2279
2420
  viewId
2280
2421
  }) => {
2281
- // TODO: use in follow-up: https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
2282
- // const selectedTime = useSelector(getSelectedTime);
2422
+ const selectedTime = useSelector(getSelectedTime);
2283
2423
  const sourceSelections = useSelector(getSelectedSources);
2284
2424
  const locationSelections = useSelector(getLocationSelections);
2285
2425
  const {
2286
2426
  error,
2287
2427
  data,
2288
2428
  isPending
2289
- } = useMultipleSources(sourceSelections, locationSelections);
2429
+ } = useMultipleSources(sourceSelections, locationSelections, selectedTime);
2290
2430
  return jsxs(Fragment, {
2291
2431
  children: [error && jsx(AlertBanner, {
2292
2432
  title: error.message,
@@ -2320,12 +2460,7 @@ const SoundingsConnect = ({
2320
2460
  top: 0
2321
2461
  }
2322
2462
  }
2323
- }), jsx(SoundingsTimeSliderConnect
2324
- // TODO: use real timesteps https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
2325
- , {
2326
- // TODO: use real timesteps https://gitlab.com/opengeoweb/opengeoweb/-/issues/5991
2327
- timesteps: []
2328
- })]
2463
+ }), jsx(SoundingsTimeSliderConnect, {})]
2329
2464
  })]
2330
2465
  });
2331
2466
  };
@@ -2379,7 +2514,8 @@ const SoundingsSourceMenu = ({
2379
2514
  children: soundingsCollections && soundingsCollections.length > 0 ? soundingsCollections.map(collection => {
2380
2515
  const {
2381
2516
  isChecked,
2382
- instanceId
2517
+ instanceId,
2518
+ url
2383
2519
  } = sourceSelections[collection.id] || {};
2384
2520
  const hasInstances = collection.instances.length > 0;
2385
2521
  const selectedInstanceId = instanceId || collection.instances[0]?.id || undefined;
@@ -2392,6 +2528,7 @@ const SoundingsSourceMenu = ({
2392
2528
  children: jsx(Checkbox, {
2393
2529
  checked: !!isChecked,
2394
2530
  onChange: event => updateCollection(collection.id, {
2531
+ url,
2395
2532
  isChecked: event.target.checked,
2396
2533
  instanceId: selectedInstanceId
2397
2534
  }),
@@ -2405,6 +2542,7 @@ const SoundingsSourceMenu = ({
2405
2542
  skipLocalStyling: false,
2406
2543
  value: selectedInstanceId || t('noInstancesAvailable'),
2407
2544
  onChange: event => updateCollection(collection.id, {
2545
+ url,
2408
2546
  instanceId: event.target.value
2409
2547
  }),
2410
2548
  renderValue: value => jsx(Box, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeoweb/soundings",
3
- "version": "12.5.0",
3
+ "version": "12.6.0",
4
4
  "description": "GeoWeb soundings library for the opengeoweb project",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -8,20 +8,22 @@
8
8
  "url": "git@gitlab.com:opengeoweb/opengeoweb.git"
9
9
  },
10
10
  "dependencies": {
11
- "@opengeoweb/api": "12.5.0",
11
+ "@opengeoweb/api": "12.6.0",
12
12
  "@tanstack/react-query": "^5.69.2",
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": "12.5.0",
17
+ "@opengeoweb/shared": "12.6.0",
18
18
  "@mui/material": "^7.0.1",
19
- "@opengeoweb/store": "12.5.0",
19
+ "@opengeoweb/store": "12.6.0",
20
20
  "react-redux": "^9.2.0",
21
- "@opengeoweb/theme": "12.5.0",
21
+ "@opengeoweb/theme": "12.6.0",
22
22
  "@reduxjs/toolkit": "^2.6.1",
23
- "@opengeoweb/timeslider": "12.5.0",
24
- "react-window": "^1.8.10"
23
+ "@opengeoweb/timeslider": "12.6.0",
24
+ "react-window": "^1.8.10",
25
+ "@opengeoweb/webmap": "12.6.0",
26
+ "lodash": "^4.17.21"
25
27
  },
26
28
  "module": "./index.esm.js",
27
29
  "type": "module",
@@ -1,5 +1,6 @@
1
- import { LocationsResponseFromBE, SoundingsInstances, CoverageCollection } from './types';
1
+ import { CoverageCollection } from '@opengeoweb/shared';
2
+ import { LocationsResponseFromBE, SoundingsInstances } from './types';
2
3
  export declare const getCollectionLocations: (collectionId: string, baseUrl: string) => Promise<LocationsResponseFromBE>;
3
- export declare const getCollectionInstance: (collectionId: string, baseUrl: string) => Promise<SoundingsInstances>;
4
+ export declare const getCollectionInstances: (collectionId: string, baseUrl: string) => Promise<SoundingsInstances>;
4
5
  export declare const getEdrCollectionForLocation: (collectionId: string, baseUrl: string, instanceId: string | undefined, locationId: string, parameters: string[], datetime: string) => Promise<CoverageCollection>;
5
6
  export declare const getEdrCollectionForPosition: (collectionId: string, baseUrl: string, instanceId: string | undefined, coordinates: [number, number], parameters: string[], datetime: string) => Promise<CoverageCollection>;
@@ -22,6 +22,7 @@ export declare const useMultipleSources: (sources: SourceType[], locations: Reco
22
22
  isPending: boolean;
23
23
  error: Error | null | undefined;
24
24
  };
25
+ export declare const useTimesteps: (sourceSelections: SourceType[]) => string[][];
25
26
  export declare const useGetCollectionsInstances: (collections?: SoundingsCollection[], isDialogOpen?: boolean) => {
26
27
  data: SoundingsCollectionsWithInstances[] | undefined;
27
28
  isPending: boolean;
@@ -1,5 +1,5 @@
1
1
  import { Geometry } from 'geojson';
2
- import { Referencing } from './crss-types';
2
+ import { EDRInstance } from '@opengeoweb/shared';
3
3
  export interface FeaturesFromBE {
4
4
  id: string;
5
5
  type: string;
@@ -14,65 +14,6 @@ export interface LocationsResponseFromBE {
14
14
  features: FeaturesFromBE[];
15
15
  type: string;
16
16
  }
17
- interface AxisValues {
18
- values: number[] | string[];
19
- }
20
- interface SingleValueAxis {
21
- values: [number];
22
- }
23
- interface SingleTimeValueAxis {
24
- values: [string];
25
- }
26
- interface VerticalProfileAxes {
27
- x: SingleValueAxis;
28
- y: SingleValueAxis;
29
- z: AxisValues;
30
- t?: SingleTimeValueAxis;
31
- }
32
- export interface VerticalProfileDomain {
33
- type: 'Domain';
34
- domainType: 'VerticalProfile';
35
- axes: VerticalProfileAxes;
36
- referencing?: Referencing[];
37
- }
38
- interface NdArray {
39
- type: 'NdArray';
40
- dataType: 'float' | 'integer' | 'string';
41
- axisNames?: string[];
42
- shape?: number[];
43
- values: number[] | string[];
44
- }
45
- type Ranges = Record<string, NdArray>;
46
- type Description = Record<string, string>;
47
- type Label = Record<string, string>;
48
- interface Parameter {
49
- type: 'Parameter';
50
- id?: string;
51
- label?: Label;
52
- description?: Description;
53
- unit?: {
54
- label?: Label;
55
- symbol?: Record<string, string>;
56
- };
57
- observedProperty: {
58
- id?: string;
59
- label: Label;
60
- description?: Description;
61
- };
62
- }
63
- export interface Coverage {
64
- type: 'Coverage';
65
- domain: VerticalProfileDomain;
66
- ranges: Ranges;
67
- }
68
- export interface CoverageCollection {
69
- type: 'CoverageCollection';
70
- domainType: VerticalProfileDomain;
71
- parameters: Record<string, Parameter>;
72
- ranges: Record<string, NdArray>;
73
- coverages: Coverage[];
74
- referencing: Referencing[];
75
- }
76
17
  export interface SoundingFeatureProperties {
77
18
  time: string;
78
19
  temp: number;
@@ -86,11 +27,8 @@ export interface SoundingFeature {
86
27
  geometry: Geometry;
87
28
  properties: SoundingFeatureProperties;
88
29
  }
89
- export interface SoundingsInstance {
90
- id: string;
91
- }
30
+ export type SoundingsInstance = EDRInstance;
92
31
  export interface SoundingsInstances {
93
32
  instances: SoundingsInstance[];
94
33
  }
95
34
  export type SoundingDiagramType = 'SKEWT' | 'BIJVOET';
96
- export {};
@@ -1,2 +1,10 @@
1
- import { CoverageCollection, SoundingFeature } from './types';
1
+ import { CoverageCollection } from '@opengeoweb/shared';
2
+ import { SoundingFeature, SoundingsInstance } from './types';
2
3
  export declare function parseCoverageCollection(response: CoverageCollection, parameterMappings: Record<string, string>): SoundingFeature[];
4
+ /**
5
+ * Parse duration with repeats
6
+ * @param dateString e.g. 'R67/2025-04-22T09:00:00Z/PT60M'
7
+ * @returns Array of ISO datetime strings, e.g., ['2025-04-22T09:00:00.000Z', ...]
8
+ */
9
+ export declare const parseDurationWithRepeats: (dateString: string) => string[];
10
+ export declare const getTimestepsFromInstance: (instance?: SoundingsInstance) => string[];
@@ -6,3 +6,4 @@ type Story = StoryObj<typeof SoundingsTimeSlider>;
6
6
  export declare const ComponentLight: Story;
7
7
  export declare const ComponentDark: Story;
8
8
  export declare const NoTimesteps: Story;
9
+ export declare const ManyTimesteps: Story;
@@ -1,6 +1,2 @@
1
1
  import React from 'react';
2
- interface SoundingsTimeSliderConnectProps {
3
- timesteps: string[];
4
- }
5
- export declare const SoundingsTimeSliderConnect: ({ timesteps, }: SoundingsTimeSliderConnectProps) => React.ReactElement;
6
- export {};
2
+ export declare const SoundingsTimeSliderConnect: () => React.ReactElement;
@@ -9,7 +9,7 @@ export interface UpdateSourcePayload {
9
9
  value: SourceSelection;
10
10
  }
11
11
  export interface SourceSelection {
12
- url?: string;
12
+ url: string;
13
13
  instanceId?: string;
14
14
  isChecked?: boolean;
15
15
  parameterMappings?: Record<string, string>;
@@ -1,26 +0,0 @@
1
- export interface Referencing {
2
- coordinates: string[];
3
- system: CRSSystem;
4
- }
5
- interface GeographicCRS {
6
- type: 'GeographicCRS';
7
- id: string;
8
- }
9
- interface ProjectedCRS {
10
- type: 'ProjectedCRS';
11
- id: string;
12
- }
13
- interface VerticalCRS {
14
- type: 'VerticalCRS';
15
- id: string;
16
- }
17
- interface EngineeringCRS {
18
- type: 'EngineeringCRS';
19
- id?: string;
20
- }
21
- interface TemporalRS {
22
- type: 'TemporalRS';
23
- calendar: string;
24
- }
25
- type CRSSystem = GeographicCRS | ProjectedCRS | VerticalCRS | EngineeringCRS | TemporalRS;
26
- export {};