@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 +230 -92
- package/package.json +9 -7
- package/src/lib/api/edr-api/api.d.ts +3 -2
- package/src/lib/api/edr-api/hooks.d.ts +1 -0
- package/src/lib/api/edr-api/types.d.ts +2 -64
- package/src/lib/api/edr-api/utils.d.ts +9 -1
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSlider.stories.d.ts +1 -0
- package/src/lib/components/SoundingsTimeSlider/SoundingsTimeSliderConnect.d.ts +1 -5
- package/src/lib/store/types.d.ts +1 -1
- package/src/lib/api/edr-api/crss-types.d.ts +0 -26
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
|
|
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
|
|
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
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
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
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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
|
-
}
|
|
1874
|
-
const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '',
|
|
1958
|
+
}) => {
|
|
1959
|
+
const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', datetime || '', parameterMappings || defaultParameterMappings);
|
|
1875
1960
|
return queryParams;
|
|
1876
|
-
}), [
|
|
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
|
|
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: '
|
|
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: "
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
17
|
+
"@opengeoweb/shared": "12.6.0",
|
|
18
18
|
"@mui/material": "^7.0.1",
|
|
19
|
-
"@opengeoweb/store": "12.
|
|
19
|
+
"@opengeoweb/store": "12.6.0",
|
|
20
20
|
"react-redux": "^9.2.0",
|
|
21
|
-
"@opengeoweb/theme": "12.
|
|
21
|
+
"@opengeoweb/theme": "12.6.0",
|
|
22
22
|
"@reduxjs/toolkit": "^2.6.1",
|
|
23
|
-
"@opengeoweb/timeslider": "12.
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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[];
|
|
@@ -1,6 +1,2 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
3
|
-
timesteps: string[];
|
|
4
|
-
}
|
|
5
|
-
export declare const SoundingsTimeSliderConnect: ({ timesteps, }: SoundingsTimeSliderConnectProps) => React.ReactElement;
|
|
6
|
-
export {};
|
|
2
|
+
export declare const SoundingsTimeSliderConnect: () => React.ReactElement;
|
package/src/lib/store/types.d.ts
CHANGED
|
@@ -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 {};
|