@opengeoweb/soundings 12.4.2 → 12.5.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/README.md +49 -1
- package/index.esm.js +524 -172
- package/package.json +6 -6
- package/src/lib/api/edr-api/api.d.ts +4 -4
- package/src/lib/api/edr-api/hooks.d.ts +19 -1
- package/src/lib/api/edr-api/types.d.ts +1 -0
- package/src/lib/api/edr-api/utils.d.ts +1 -1
- package/src/lib/api/edr-api/utils.spec.d.ts +1 -0
- package/src/lib/components/MenuBar/LocationsMenu.d.ts +4 -1
- package/src/lib/components/MenuBar/LocationsMenuConnect.spec.d.ts +1 -0
- package/src/lib/components/Soundings/Soundings.d.ts +2 -1
- package/src/lib/components/Soundings/Soundings.stories.d.ts +2 -0
- package/src/lib/components/Soundings/constants.d.ts +16 -0
- package/src/lib/components/Soundings/drawUtils.d.ts +14 -7
- package/src/lib/components/Soundings/types.d.ts +1 -0
- package/src/lib/components/SoundingsTooltip/utils.d.ts +3 -2
- package/src/lib/components/StaticLines/bijvoetUtils.d.ts +6 -0
- package/src/lib/components/StaticLines/drawDryAdiabats.d.ts +10 -2
- package/src/lib/components/StaticLines/drawIsobars.d.ts +6 -3
- package/src/lib/components/StaticLines/drawIsohumes.d.ts +11 -2
- package/src/lib/components/StaticLines/drawIsotherms.d.ts +11 -2
- package/src/lib/components/StaticLines/drawMoistAdiabats.d.ts +11 -2
- package/src/lib/components/StaticLines/utils.d.ts +6 -3
- package/src/lib/components/WindBarbs/utils.d.ts +3 -4
- package/src/lib/store/selectors.d.ts +41 -0
- package/src/lib/store/types.d.ts +4 -0
package/index.esm.js
CHANGED
|
@@ -54,6 +54,119 @@ var soundingsTranslations = {
|
|
|
54
54
|
fi: fi
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
/* *
|
|
58
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
59
|
+
* you may not use this file except in compliance with the License.
|
|
60
|
+
* You may obtain a copy of the License at
|
|
61
|
+
*
|
|
62
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
63
|
+
*
|
|
64
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
65
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
66
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
67
|
+
* See the License for the specific language governing permissions and
|
|
68
|
+
* limitations under the License.
|
|
69
|
+
*
|
|
70
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
71
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
72
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
73
|
+
* */
|
|
74
|
+
// Define pressure and temperature values for static lines
|
|
75
|
+
const maxPressure = 1050;
|
|
76
|
+
const minPressure = 150;
|
|
77
|
+
const staticPressureArray = [maxPressure];
|
|
78
|
+
const dp = -5;
|
|
79
|
+
const minTemperature = -40;
|
|
80
|
+
const maxTemperature = 40.6;
|
|
81
|
+
let pressure = maxPressure;
|
|
82
|
+
while (pressure > minPressure) {
|
|
83
|
+
pressure += dp;
|
|
84
|
+
staticPressureArray.push(pressure);
|
|
85
|
+
}
|
|
86
|
+
// Bijvoet constants
|
|
87
|
+
const T_KAPPA = 0.2857;
|
|
88
|
+
const T_KTHETA = 332.0;
|
|
89
|
+
const KELVIN = 273.156;
|
|
90
|
+
const WV_CA = 17.56881;
|
|
91
|
+
const WV_CB = 241.8945;
|
|
92
|
+
const WV_CE = 610.7;
|
|
93
|
+
const EPSILON = 0.622;
|
|
94
|
+
const R_AIR = 287.04;
|
|
95
|
+
const HEATCP = 1005;
|
|
96
|
+
const REFERENCE_PRESSURE = 1000 * 100; // 1000hPa = 100000Pa
|
|
97
|
+
|
|
98
|
+
/* *
|
|
99
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
100
|
+
* you may not use this file except in compliance with the License.
|
|
101
|
+
* You may obtain a copy of the License at
|
|
102
|
+
*
|
|
103
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
104
|
+
*
|
|
105
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
106
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
107
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
108
|
+
* See the License for the specific language governing permissions and
|
|
109
|
+
* limitations under the License.
|
|
110
|
+
*
|
|
111
|
+
* Copyright 2025 - Koninklijk Nederlands Meteorologisch Instituut (KNMI)
|
|
112
|
+
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
113
|
+
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
114
|
+
* */
|
|
115
|
+
const tempToKelvin = value => value + KELVIN;
|
|
116
|
+
// Calculate pressure in Pa instead of hPa
|
|
117
|
+
const pressToPascal = value => value * 100;
|
|
118
|
+
const bijvoetInterpolationTheta = (minRange, maxRange) => {
|
|
119
|
+
return t => {
|
|
120
|
+
const min = minTemperature + KELVIN;
|
|
121
|
+
const max = maxTemperature + KELVIN;
|
|
122
|
+
const xCoord = (Math.sign(t - T_KTHETA) * Math.abs(t - T_KTHETA) ** T_KAPPA - Math.sign(min - T_KTHETA) * Math.abs(min - T_KTHETA) ** T_KAPPA) / (Math.sign(max - T_KTHETA) * Math.abs(max - T_KTHETA) ** T_KAPPA - Math.sign(min - T_KTHETA) * Math.abs(min - T_KTHETA) ** T_KAPPA);
|
|
123
|
+
return minRange * (1 - xCoord) + maxRange * xCoord;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const bijvoetInterpolationPres = (minRange, maxRange) => {
|
|
127
|
+
return t => {
|
|
128
|
+
const yCoord = (pressToPascal(t) ** T_KAPPA - pressToPascal(minPressure) ** T_KAPPA) / (pressToPascal(maxPressure) ** T_KAPPA - pressToPascal(minPressure) ** T_KAPPA);
|
|
129
|
+
return minRange * (1 - yCoord) + maxRange * yCoord;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
// Returns pressure in hPa
|
|
133
|
+
const pressureFromPosition = y => {
|
|
134
|
+
return (y * (pressToPascal(maxPressure) ** T_KAPPA - pressToPascal(minPressure) ** T_KAPPA) + pressToPascal(minPressure) ** T_KAPPA) ** (1 / T_KAPPA) / 100;
|
|
135
|
+
};
|
|
136
|
+
const calculateESat = temperature => {
|
|
137
|
+
const tempKelvin = temperature - KELVIN;
|
|
138
|
+
return WV_CE * Math.exp(WV_CA * tempKelvin / (tempKelvin + WV_CB));
|
|
139
|
+
};
|
|
140
|
+
const latHeat = temperature => {
|
|
141
|
+
return R_AIR / EPSILON * WV_CA * WV_CB * (temperature / (temperature - KELVIN + WV_CB)) ** 2;
|
|
142
|
+
};
|
|
143
|
+
const dTdp = (pressure, temperature) => {
|
|
144
|
+
const eSat = calculateESat(temperature);
|
|
145
|
+
const Rs = EPSILON * eSat / (pressure - eSat);
|
|
146
|
+
const Lv = latHeat(temperature);
|
|
147
|
+
const dTdp = 2.0 * temperature / (7.0 * pressure);
|
|
148
|
+
const dTdp1 = dTdp * (1.0 + Rs * Lv / (R_AIR * temperature));
|
|
149
|
+
const dTdp2 = dTdp1 / (1.0 + Rs / (7.0 * EPSILON) + (Lv / temperature) ** 2 * Rs * EPSILON / (R_AIR * HEATCP));
|
|
150
|
+
return dTdp2;
|
|
151
|
+
};
|
|
152
|
+
const calculateThetaFromPressTemp = (pressure, temperature, basePressure) => {
|
|
153
|
+
const base = basePressure || REFERENCE_PRESSURE;
|
|
154
|
+
const dpPascal = pressure > base ? pressToPascal(-10) : pressToPascal(10); // hPa steps to Pa steps
|
|
155
|
+
const numberOfSteps = Math.round((base - pressure) / dpPascal) < 1 ? 1 : Math.round((base - pressure) / dpPascal);
|
|
156
|
+
const pstep = (base - pressure) / numberOfSteps;
|
|
157
|
+
let tempN = temperature;
|
|
158
|
+
let pressureN = pressure;
|
|
159
|
+
for (let n = 0; n < numberOfSteps; n += 1) {
|
|
160
|
+
const k1 = pstep * dTdp(pressureN, tempN);
|
|
161
|
+
const k2 = pstep * dTdp(pressureN + 0.5 * pstep, tempN + 0.5 * k1);
|
|
162
|
+
const k3 = pstep * dTdp(pressureN + 0.5 * pstep, tempN + 0.5 * k2);
|
|
163
|
+
const k4 = pstep * dTdp(pressureN + pstep, tempN + k3);
|
|
164
|
+
tempN += k1 / 6.0 + k2 / 3.0 + k3 / 3.0 + k4 / 6.0;
|
|
165
|
+
pressureN += pstep;
|
|
166
|
+
}
|
|
167
|
+
return tempN;
|
|
168
|
+
};
|
|
169
|
+
|
|
57
170
|
/* *
|
|
58
171
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
59
172
|
* you may not use this file except in compliance with the License.
|
|
@@ -73,22 +186,25 @@ var soundingsTranslations = {
|
|
|
73
186
|
* */
|
|
74
187
|
const isobarValues = [200, 300, 400, 500, 700, 850, 925, 1000];
|
|
75
188
|
const drawIsobars = ({
|
|
189
|
+
diagramType,
|
|
76
190
|
minTemperature,
|
|
77
191
|
maxTemperature,
|
|
78
192
|
xScale,
|
|
79
193
|
yScale,
|
|
80
194
|
svg
|
|
81
195
|
}) => {
|
|
82
|
-
//
|
|
196
|
+
// For Bijvoet, the x-axis is NOT temperature but theta_es
|
|
197
|
+
const xmin = diagramType === 'BIJVOET' ? tempToKelvin(minTemperature) : minTemperature;
|
|
198
|
+
const xmax = diagramType === 'BIJVOET' ? tempToKelvin(maxTemperature) : maxTemperature;
|
|
83
199
|
for (const pressureVal of isobarValues) {
|
|
84
200
|
const Px = [];
|
|
85
|
-
const tempBounds = [
|
|
201
|
+
const tempBounds = [xmin, xmax];
|
|
86
202
|
for (const bound of tempBounds) {
|
|
87
203
|
Px.push([xScale(bound), yScale(pressureVal)]);
|
|
88
204
|
}
|
|
89
205
|
const lineGenerator = d3.line();
|
|
90
206
|
const isoPathString = lineGenerator(Px);
|
|
91
|
-
const strID = `${pressureVal}
|
|
207
|
+
const strID = `${pressureVal}-isobar`;
|
|
92
208
|
svg.append('path').attr('id', strID).attr('type', 'isobar').attr('d', isoPathString).style('fill', 'none').style('stroke-width', 1.5).style('stroke', '#B5B51F').style('stroke-opacity', 0.5);
|
|
93
209
|
}
|
|
94
210
|
};
|
|
@@ -116,37 +232,56 @@ const initializeSvg = ({
|
|
|
116
232
|
svgRef,
|
|
117
233
|
width,
|
|
118
234
|
height,
|
|
119
|
-
margin
|
|
235
|
+
margin,
|
|
236
|
+
diagramType
|
|
120
237
|
}) => {
|
|
121
|
-
const
|
|
122
|
-
svg.
|
|
123
|
-
svg.append('
|
|
238
|
+
const diagram = diagramType === 'BIJVOET' ? 'bijvoet' : 'skewT';
|
|
239
|
+
const svg = d3.select(svgRef.current).attr('width', width + (margin.left + margin.right)).attr('height', height + (margin.top + margin.bottom)).attr('id', diagram).attr('data-testid', diagram).append('g').attr('transform', `translate(${margin.left + margin.right},${margin.top})`);
|
|
240
|
+
svg.append('clipPath').attr('id', `clip-${diagram}d3`).append('rect').attr('width', width).attr('height', height).attr('x', 0).attr('y', 0);
|
|
241
|
+
svg.append('rect').attr('height', height).attr('width', width).attr('fill', '#ffffff00').attr('opacity', 1).attr('id', `${diagram}d3`).attr('clip-path', `url(#clip-${diagram}d3)`);
|
|
124
242
|
return svg;
|
|
125
243
|
};
|
|
244
|
+
const createScales = (diagramType, width, height, minPressure, maxPressure, minTemperature, maxTemperature) => {
|
|
245
|
+
if (diagramType === 'BIJVOET') {
|
|
246
|
+
// Note, for Bijvoet, the x-axis is NOT temperature but theta_es
|
|
247
|
+
// Both axis aren't linear nor logarithmic, hence we need an interpolation function to determine (x,y) based on (theta_es,p)
|
|
248
|
+
// These scales work the same as for SkewT -> you pass in a pressure (or theta value) and you receive the location in the svg
|
|
249
|
+
const xScale = d3.scaleLinear().domain([0, 1]).range([0, width]).interpolate(bijvoetInterpolationTheta);
|
|
250
|
+
const yScale = d3.scaleLinear().domain([1, 0]).range([height, 0]).interpolate(bijvoetInterpolationPres);
|
|
251
|
+
return [xScale, yScale];
|
|
252
|
+
}
|
|
253
|
+
const xScale = d3.scaleLinear().domain([minTemperature, maxTemperature]).range([0, width]);
|
|
254
|
+
const yScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]);
|
|
255
|
+
return [xScale, yScale];
|
|
256
|
+
};
|
|
126
257
|
const drawAxes = ({
|
|
127
258
|
svg,
|
|
128
259
|
xScale,
|
|
129
260
|
yScale,
|
|
130
|
-
height
|
|
261
|
+
height,
|
|
262
|
+
diagramType
|
|
131
263
|
}) => {
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
264
|
+
// For Bijvoet, we don't show an x-axis as we're interested in the isotherms only, not theta
|
|
265
|
+
if (diagramType !== 'BIJVOET') {
|
|
266
|
+
// Create x-axis with intervals
|
|
267
|
+
const xTicks = xScale.ticks(10);
|
|
268
|
+
const xMiddleValue = xTicks[Math.floor(xTicks.length / 2)];
|
|
269
|
+
svg.append('g').attr('type', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickValues(xTicks).tickSizeInner(0).tickSizeOuter(0));
|
|
270
|
+
// Reformat in the case of the middle tick label
|
|
271
|
+
// to add the unit part
|
|
272
|
+
svg.selectAll('g[type="x-axis"] .tick').filter(function () {
|
|
273
|
+
return parseInt(d3.select(this).select('text').text(), 10) === xMiddleValue;
|
|
274
|
+
}).each(function () {
|
|
275
|
+
const unitPart = '°C';
|
|
276
|
+
d3.select(this).select('text')
|
|
277
|
+
// tspan necessary to use a different font-size
|
|
278
|
+
.append('tspan').text(unitPart).attr('font-size', '11px').attr('dy', '0.04em');
|
|
279
|
+
});
|
|
280
|
+
}
|
|
146
281
|
// Create y-axis with intervals
|
|
147
282
|
// Make sure all zeroes are printed (1000 vs 1k)
|
|
148
283
|
const formatTicks = d3.format('.0f');
|
|
149
|
-
svg.append('g').call(d3.axisLeft(yScale).tickValues(isobarValues).tickFormat(formatTicks).tickSizeInner(0).tickSizeOuter(0)).call(g => g.select('.domain').remove()).append('text').attr('fill', 'currentColor').attr('x', '0').attr('y', yScale(400)).attr('font-size', '11px').attr('dy', '-0.9em').attr('text-anchor', 'left').text('hPa');
|
|
284
|
+
svg.append('g').attr('type', 'y-axis').call(d3.axisLeft(yScale).tickValues(isobarValues).tickFormat(formatTicks).tickSizeInner(0).tickSizeOuter(0)).call(g => g.select('.domain').remove()).append('text').attr('fill', 'currentColor').attr('x', '0').attr('y', yScale(400)).attr('font-size', '11px').attr('dy', '-0.9em').attr('text-anchor', 'left').text('hPa');
|
|
150
285
|
};
|
|
151
286
|
const addLegends = ({
|
|
152
287
|
svg,
|
|
@@ -157,6 +292,15 @@ const addLegends = ({
|
|
|
157
292
|
// Add a legend for the dewpoint line
|
|
158
293
|
svg.append('text').attr('x', width - 137).attr('y', 40).attr('fill', SOUNDING_COLORS[0]).text('Dewpoint (°C)');
|
|
159
294
|
};
|
|
295
|
+
const mapToPlot = (diagramType, filteredPressureValues, filteredTemperatureValues, xScale, yScale, height) => {
|
|
296
|
+
if (diagramType === 'BIJVOET') {
|
|
297
|
+
const thetaValues = filteredTemperatureValues.map((tempValue, index) => calculateThetaFromPressTemp(pressToPascal(filteredPressureValues[index]), tempToKelvin(tempValue)));
|
|
298
|
+
return d3.zip(filteredPressureValues, thetaValues).map(([pressure, theta]) => [xScale(theta), yScale(pressure)]);
|
|
299
|
+
}
|
|
300
|
+
const Px = d3.zip(filteredPressureValues, filteredTemperatureValues).map(([pressure, temperature]) => [xScale(temperature), yScale(pressure)]);
|
|
301
|
+
// Skew/shear transformation for skew-t
|
|
302
|
+
return Px.map(([x, y]) => [x + height - y, y]);
|
|
303
|
+
};
|
|
160
304
|
const plotSounding = ({
|
|
161
305
|
temperatureValues,
|
|
162
306
|
pressureValues,
|
|
@@ -165,16 +309,16 @@ const plotSounding = ({
|
|
|
165
309
|
xScale,
|
|
166
310
|
yScale,
|
|
167
311
|
height,
|
|
168
|
-
svg
|
|
312
|
+
svg,
|
|
313
|
+
diagramType
|
|
169
314
|
}) => {
|
|
315
|
+
const diagram = diagramType === 'BIJVOET' ? 'bijvoet' : 'skewT';
|
|
170
316
|
const [filteredPressureValues, filteredTemperatureValues] = filterDomainRangeForParameter(pressureValues, temperatureValues);
|
|
171
317
|
// Map temp and pressure to pixels
|
|
172
|
-
const Px =
|
|
173
|
-
// Skew/shear transformation for skew-t
|
|
174
|
-
const skewedPx = Px.map(([x, y]) => [x + height - y, y]);
|
|
318
|
+
const Px = mapToPlot(diagramType, filteredPressureValues, filteredTemperatureValues, xScale, yScale, height);
|
|
175
319
|
const lineGenerator = d3.line();
|
|
176
|
-
const pathString = lineGenerator(
|
|
177
|
-
svg.append('path').attr('d', pathString).attr('x', 0).attr('y', 0).style('fill', 'none').style('stroke-width', 2).style('stroke', color).style('stroke-opacity', 1).style('stroke-dasharray', lineStyle).attr('clip-path',
|
|
320
|
+
const pathString = lineGenerator(Px);
|
|
321
|
+
svg.append('path').attr('d', pathString).attr('id', 'dataline').attr('x', 0).attr('y', 0).style('fill', 'none').style('stroke-width', 2).style('stroke', color).style('stroke-opacity', 1).style('stroke-dasharray', lineStyle).attr('clip-path', `url(#clip-${diagram}d3)`);
|
|
178
322
|
};
|
|
179
323
|
// Use this function to filter the pressure and parameter array so no NaN or null values are included
|
|
180
324
|
const filterDomainRangeForParameter = (pressureDomain, parameterDomain) => {
|
|
@@ -248,6 +392,13 @@ const getParamHtml = (paramValue, colorIndex, unit, isLastElement, noDistance =
|
|
|
248
392
|
}
|
|
249
393
|
return `<p style="${noDistance ? 'margin: 0;' : 'margin-top: 0;'} color: ${SOUNDING_COLORS[colorIndex]}">${!isNaN(paramValue) && paramValue !== null ? `${paramValue.toFixed(1)}${unit}` : '-'}${noDistance ? '' : '<br/>'}</p>`;
|
|
250
394
|
};
|
|
395
|
+
const extractPressureFromY = (diagramType, y, yScale) => {
|
|
396
|
+
if (diagramType === 'BIJVOET') {
|
|
397
|
+
const frac = yScale.invert(y);
|
|
398
|
+
return pressureFromPosition(frac);
|
|
399
|
+
}
|
|
400
|
+
return yScale.invert(y);
|
|
401
|
+
};
|
|
251
402
|
const drawSoundingsTooltip = ({
|
|
252
403
|
event,
|
|
253
404
|
yScale,
|
|
@@ -258,14 +409,15 @@ const drawSoundingsTooltip = ({
|
|
|
258
409
|
windRefd3,
|
|
259
410
|
margin,
|
|
260
411
|
width,
|
|
261
|
-
tooltipData
|
|
412
|
+
tooltipData,
|
|
413
|
+
diagramType
|
|
262
414
|
}) => {
|
|
263
415
|
if (tooltipData.length === 0) {
|
|
264
416
|
return;
|
|
265
417
|
}
|
|
266
418
|
const [, y] = d3.pointer(event);
|
|
267
419
|
const pressureY = event.clientY - svgRectangle.top - 10;
|
|
268
|
-
const pressure =
|
|
420
|
+
const pressure = extractPressureFromY(diagramType, y, yScale);
|
|
269
421
|
const interpolatedData = tooltipData.map(dataSet => {
|
|
270
422
|
return {
|
|
271
423
|
hoveredPressure: dataSet.interpolatePressure(pressure),
|
|
@@ -658,12 +810,10 @@ const extendXAxis = (path, horizontalBarbMargin) => {
|
|
|
658
810
|
};
|
|
659
811
|
const drawWindBarbs = ({
|
|
660
812
|
svgRef,
|
|
813
|
+
yScale,
|
|
661
814
|
data,
|
|
662
815
|
width,
|
|
663
|
-
height,
|
|
664
816
|
margin,
|
|
665
|
-
minPressure,
|
|
666
|
-
maxPressure,
|
|
667
817
|
barbColor = 'black'
|
|
668
818
|
}) => {
|
|
669
819
|
const barbsSize = 25;
|
|
@@ -690,7 +840,6 @@ const drawWindBarbs = ({
|
|
|
690
840
|
d3.select(this).attr('d', newD);
|
|
691
841
|
});
|
|
692
842
|
svg.attr('width', currentWidth + margin.left + horizontalBarbMargin);
|
|
693
|
-
const pressureScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]).clamp(true);
|
|
694
843
|
const barbGroup = svg.append('g').attr('class', 'wind-group').attr('transform',
|
|
695
844
|
// This translation should be revisited when implementing
|
|
696
845
|
// the plotting of several datasets at once
|
|
@@ -718,7 +867,7 @@ const drawWindBarbs = ({
|
|
|
718
867
|
const cx = bbox.x + bbox.width / 2;
|
|
719
868
|
const cy = bbox.y + bbox.height / 2;
|
|
720
869
|
const rotation = 180 + data.windDirection;
|
|
721
|
-
const y =
|
|
870
|
+
const y = yScale(data.pressure);
|
|
722
871
|
// Place items in the correct vertical position
|
|
723
872
|
// Correcting for barbSize
|
|
724
873
|
group.attr('transform', `translate(0, ${y - barbsSize * scalingFactor / 8})`);
|
|
@@ -777,7 +926,7 @@ const getWindSymbol = value => {
|
|
|
777
926
|
const CP$1 = 1.03e3;
|
|
778
927
|
const Po = 1000.0;
|
|
779
928
|
const dryAdiabaticLapserate = (theta, pressure, temperature, dp) => {
|
|
780
|
-
const Tt = (theta +
|
|
929
|
+
const Tt = (theta + KELVIN) * (Po / pressure) ** (-287 / CP$1) - KELVIN;
|
|
781
930
|
const dt = Tt - temperature;
|
|
782
931
|
return [dp, dt];
|
|
783
932
|
};
|
|
@@ -808,6 +957,31 @@ const drawDryAdiabats = ({
|
|
|
808
957
|
svg.append('path').attr('d', daPathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'green').style('opacity', 0.3).attr('clip-path', 'url(#clip-skewTd3)');
|
|
809
958
|
}
|
|
810
959
|
};
|
|
960
|
+
const tempPresDryAiabat = (pressure, pres0, temp0) => temp0 * (pressure / pres0) ** (2.0 / 7.0);
|
|
961
|
+
const drawDryAdiabatsBijvoet = ({
|
|
962
|
+
staticPressureArray,
|
|
963
|
+
xScale,
|
|
964
|
+
yScale,
|
|
965
|
+
svg
|
|
966
|
+
}) => {
|
|
967
|
+
for (let Tbase = -20; Tbase < 40; Tbase += 5) {
|
|
968
|
+
const Px = [];
|
|
969
|
+
const tempKelvin = tempToKelvin(Tbase);
|
|
970
|
+
const tempDry = calculateThetaFromPressTemp(REFERENCE_PRESSURE, tempKelvin, 1000);
|
|
971
|
+
for (const pressure of staticPressureArray) {
|
|
972
|
+
const pres = pressToPascal(pressure); // Pressure in Pa
|
|
973
|
+
const tempPresDry = tempPresDryAiabat(pres, 1000, tempDry);
|
|
974
|
+
const theta = calculateThetaFromPressTemp(pres, tempPresDry);
|
|
975
|
+
const Ppx = yScale(pressure);
|
|
976
|
+
const Tpx = xScale(theta);
|
|
977
|
+
Px.push([Tpx, Ppx]);
|
|
978
|
+
}
|
|
979
|
+
const lineGenerator = d3.line();
|
|
980
|
+
const daPathString = lineGenerator(Px);
|
|
981
|
+
const strID = `${Tbase}-dryadiabat`;
|
|
982
|
+
svg.append('path').attr('id', strID).attr('d', daPathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'green').style('opacity', 0.3).attr('clip-path', 'url(#clip-bijvoetd3)');
|
|
983
|
+
}
|
|
984
|
+
};
|
|
811
985
|
|
|
812
986
|
/* *
|
|
813
987
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -853,6 +1027,35 @@ const drawIsohumes = ({
|
|
|
853
1027
|
svg.append('text').text(isohumeVal).attr('text-anchor', 'middle').attr('x', `${Px[0][0] + 20}px`).attr('y', `${height - 10}px`).attr('fill', '#5267A9').attr('font-size', '10px');
|
|
854
1028
|
}
|
|
855
1029
|
};
|
|
1030
|
+
const isohumeValuesBijvoet = [1, 2, 4, 7, 11, 16, 22, 29];
|
|
1031
|
+
const mixr2Tdew = (mixr, pressure) => {
|
|
1032
|
+
const mixingRation = mixr < 1e-6 ? 1e-6 : mixr;
|
|
1033
|
+
const eVap = pressure * mixingRation / (EPSILON + mixingRation);
|
|
1034
|
+
return WV_CB / (WV_CA / Math.log(eVap / WV_CE) - 1.0) + KELVIN;
|
|
1035
|
+
};
|
|
1036
|
+
const drawIsohumesBijvoet = ({
|
|
1037
|
+
staticPressureArray,
|
|
1038
|
+
height,
|
|
1039
|
+
xScale,
|
|
1040
|
+
yScale,
|
|
1041
|
+
svg
|
|
1042
|
+
}) => {
|
|
1043
|
+
for (const isohumeVal of isohumeValuesBijvoet) {
|
|
1044
|
+
const Px = [];
|
|
1045
|
+
const mixr = isohumeVal * 1e-3;
|
|
1046
|
+
for (const pressureVal of staticPressureArray) {
|
|
1047
|
+
const pres = pressToPascal(pressureVal); // Pressure in Pa
|
|
1048
|
+
const Tdew = mixr2Tdew(mixr, pres);
|
|
1049
|
+
const theta = calculateThetaFromPressTemp(pres, Tdew, REFERENCE_PRESSURE);
|
|
1050
|
+
Px.push([xScale(theta), yScale(pressureVal)]);
|
|
1051
|
+
}
|
|
1052
|
+
const lineGenerator = d3.line();
|
|
1053
|
+
const pathStringQ = lineGenerator(Px);
|
|
1054
|
+
const strID = `${isohumeVal}isohume`;
|
|
1055
|
+
svg.append('path').attr('id', strID).attr('d', pathStringQ).style('fill', 'none').style('stroke-width', 1).style('stroke-dasharray', '15, 8').style('stroke', '#5267A9').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-bijvoetd3)');
|
|
1056
|
+
svg.append('text').text(isohumeVal).attr('text-anchor', 'middle').attr('x', `${Px[0][0] + 20}px`).attr('y', `${height - 10}px`).attr('fill', '#5267A9').attr('font-size', '10px');
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
856
1059
|
|
|
857
1060
|
/* *
|
|
858
1061
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -892,6 +1095,32 @@ const drawIsotherms = ({
|
|
|
892
1095
|
svg.append('path').attr('d', pathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'orange').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
|
|
893
1096
|
}
|
|
894
1097
|
};
|
|
1098
|
+
const drawIsothermsBijvoet = ({
|
|
1099
|
+
staticPressureArray,
|
|
1100
|
+
xScale,
|
|
1101
|
+
yScale,
|
|
1102
|
+
svg,
|
|
1103
|
+
height
|
|
1104
|
+
}) => {
|
|
1105
|
+
// lines of constant temperature (10 degree interval from -80...+40C)
|
|
1106
|
+
for (let temp = -150; temp <= 40; temp += 2) {
|
|
1107
|
+
const points = [];
|
|
1108
|
+
// start locatie van elke isotherm
|
|
1109
|
+
const Tiso = tempToKelvin(temp);
|
|
1110
|
+
for (const pressure of staticPressureArray) {
|
|
1111
|
+
const theta = calculateThetaFromPressTemp(pressToPascal(pressure), Tiso, REFERENCE_PRESSURE);
|
|
1112
|
+
points.push([xScale(theta), yScale(pressure)]);
|
|
1113
|
+
}
|
|
1114
|
+
const lineGenerator = d3.line();
|
|
1115
|
+
const pathString = lineGenerator(points);
|
|
1116
|
+
const strID = `${temp}-isotherm-bijvoet`;
|
|
1117
|
+
svg.append('path').attr('id', strID).attr('d', pathString).style('fill', 'none').style('stroke-width', temp % 10 === 0 ? 2 : 1).style('stroke', temp === 0 ? '#D27D2D' : 'orange').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-bijvoetd3)');
|
|
1118
|
+
// only show value every 10 degrees and if isotherm reaches bottom of plot
|
|
1119
|
+
if (temp % 10 === 0 && points[0] && points[0][1] === height) {
|
|
1120
|
+
svg.append('text').text(temp).attr('text-anchor', 'middle').attr('x', `${points[0][0]}px`).attr('y', `${height + 12}px`).attr('fill', temp === 0 ? '#D27D2D' : 'orange').attr('font-size', '10px');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
895
1124
|
|
|
896
1125
|
/* *
|
|
897
1126
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -911,7 +1140,6 @@ const drawIsotherms = ({
|
|
|
911
1140
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
912
1141
|
* */
|
|
913
1142
|
const CP = 1.03e3;
|
|
914
|
-
const KELVIN = 273.15;
|
|
915
1143
|
const L_CONST = 2.5e6;
|
|
916
1144
|
const RD = 287.0;
|
|
917
1145
|
const RV = 461.0;
|
|
@@ -987,6 +1215,26 @@ const drawMoistAdiabats = ({
|
|
|
987
1215
|
svg.append('path').attr('d', maPathStringBottom).style('fill', 'none').style('stroke-width', 1).style('stroke', 'purple').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-skewTd3)');
|
|
988
1216
|
}
|
|
989
1217
|
};
|
|
1218
|
+
const drawMoistAdiabatsBijvoet = ({
|
|
1219
|
+
minPressure,
|
|
1220
|
+
maxPressure,
|
|
1221
|
+
xScale,
|
|
1222
|
+
yScale,
|
|
1223
|
+
svg
|
|
1224
|
+
}) => {
|
|
1225
|
+
for (let Tbase = -20; Tbase < 40; Tbase += 5) {
|
|
1226
|
+
const theta = Tbase + KELVIN;
|
|
1227
|
+
const Px = [];
|
|
1228
|
+
const pressureBounds = [minPressure, maxPressure];
|
|
1229
|
+
for (const bound of pressureBounds) {
|
|
1230
|
+
Px.push([xScale(theta), yScale(bound)]);
|
|
1231
|
+
}
|
|
1232
|
+
const lineGenerator = d3.line();
|
|
1233
|
+
const isoPathString = lineGenerator(Px);
|
|
1234
|
+
const strID = `${Tbase}-moistadiabat`;
|
|
1235
|
+
svg.append('path').attr('id', strID).attr('d', isoPathString).style('fill', 'none').style('stroke-width', 1).style('stroke', 'purple').style('stroke-opacity', 0.5).attr('clip-path', 'url(#clip-bijvoetd3)');
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
990
1238
|
|
|
991
1239
|
/* *
|
|
992
1240
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1006,6 +1254,7 @@ const drawMoistAdiabats = ({
|
|
|
1006
1254
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1007
1255
|
* */
|
|
1008
1256
|
const drawStaticLines = ({
|
|
1257
|
+
diagramType,
|
|
1009
1258
|
dp,
|
|
1010
1259
|
staticPressureArray,
|
|
1011
1260
|
minTemperature,
|
|
@@ -1019,47 +1268,78 @@ const drawStaticLines = ({
|
|
|
1019
1268
|
svg
|
|
1020
1269
|
}) => {
|
|
1021
1270
|
drawIsobars({
|
|
1271
|
+
diagramType,
|
|
1022
1272
|
minTemperature,
|
|
1023
1273
|
maxTemperature,
|
|
1024
1274
|
xScale,
|
|
1025
1275
|
yScale,
|
|
1026
1276
|
svg
|
|
1027
1277
|
});
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1278
|
+
if (diagramType === 'BIJVOET') {
|
|
1279
|
+
drawMoistAdiabatsBijvoet({
|
|
1280
|
+
minPressure,
|
|
1281
|
+
maxPressure,
|
|
1282
|
+
xScale,
|
|
1283
|
+
yScale,
|
|
1284
|
+
svg
|
|
1285
|
+
});
|
|
1286
|
+
drawIsothermsBijvoet({
|
|
1287
|
+
staticPressureArray,
|
|
1288
|
+
xScale,
|
|
1289
|
+
yScale,
|
|
1290
|
+
svg,
|
|
1291
|
+
height
|
|
1292
|
+
});
|
|
1293
|
+
drawDryAdiabatsBijvoet({
|
|
1294
|
+
staticPressureArray,
|
|
1295
|
+
xScale,
|
|
1296
|
+
yScale,
|
|
1297
|
+
svg
|
|
1298
|
+
});
|
|
1299
|
+
drawIsohumesBijvoet({
|
|
1300
|
+
staticPressureArray,
|
|
1301
|
+
height,
|
|
1302
|
+
xScale,
|
|
1303
|
+
yScale,
|
|
1304
|
+
svg
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
drawIsotherms({
|
|
1308
|
+
staticPressureArray,
|
|
1309
|
+
height,
|
|
1310
|
+
xScale,
|
|
1311
|
+
yScale,
|
|
1312
|
+
svg
|
|
1313
|
+
});
|
|
1314
|
+
drawMoistAdiabats({
|
|
1315
|
+
staticPressureArray,
|
|
1316
|
+
minTemperature,
|
|
1317
|
+
maxTemperature,
|
|
1318
|
+
minPressure,
|
|
1319
|
+
maxPressure,
|
|
1320
|
+
width,
|
|
1321
|
+
height,
|
|
1322
|
+
xScale,
|
|
1323
|
+
yScale,
|
|
1324
|
+
svg,
|
|
1325
|
+
dp
|
|
1326
|
+
});
|
|
1327
|
+
drawDryAdiabats({
|
|
1328
|
+
staticPressureArray,
|
|
1329
|
+
height,
|
|
1330
|
+
xScale,
|
|
1331
|
+
yScale,
|
|
1332
|
+
svg,
|
|
1333
|
+
dp
|
|
1334
|
+
});
|
|
1335
|
+
drawIsohumes({
|
|
1336
|
+
staticPressureArray,
|
|
1337
|
+
height,
|
|
1338
|
+
xScale,
|
|
1339
|
+
yScale,
|
|
1340
|
+
svg
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1063
1343
|
};
|
|
1064
1344
|
|
|
1065
1345
|
const shadow = '0 1px 5px 0 rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.14)';
|
|
@@ -1090,22 +1370,11 @@ const soundingsStyle = {
|
|
|
1090
1370
|
fontWeight: 'bold'
|
|
1091
1371
|
}
|
|
1092
1372
|
};
|
|
1093
|
-
// Define pressure and temperature values for static lines
|
|
1094
|
-
const maxPressure = 1050;
|
|
1095
|
-
const minPressure = 150;
|
|
1096
|
-
const staticPressureArray = [maxPressure];
|
|
1097
|
-
const dp = -5;
|
|
1098
|
-
const minTemperature = -40;
|
|
1099
|
-
const maxTemperature = 40.6;
|
|
1100
|
-
let pressure = maxPressure;
|
|
1101
|
-
while (pressure > minPressure) {
|
|
1102
|
-
pressure += dp;
|
|
1103
|
-
staticPressureArray.push(pressure);
|
|
1104
|
-
}
|
|
1105
1373
|
const Soundings = ({
|
|
1106
1374
|
data,
|
|
1107
1375
|
svgDimensions,
|
|
1108
|
-
showTooltip = true
|
|
1376
|
+
showTooltip = true,
|
|
1377
|
+
diagramType = 'SKEWT'
|
|
1109
1378
|
}) => {
|
|
1110
1379
|
const svgRef = useRef(null);
|
|
1111
1380
|
const canvasRef = useRef(null);
|
|
@@ -1139,21 +1408,23 @@ const Soundings = ({
|
|
|
1139
1408
|
svgRef,
|
|
1140
1409
|
width,
|
|
1141
1410
|
height,
|
|
1142
|
-
margin
|
|
1411
|
+
margin,
|
|
1412
|
+
diagramType
|
|
1143
1413
|
});
|
|
1144
1414
|
const svgRectangle = svgRef.current.getBoundingClientRect();
|
|
1145
1415
|
// Set scales for temperature and pressure
|
|
1146
|
-
const xScale =
|
|
1147
|
-
const yScale = d3.scaleLog().domain([maxPressure, minPressure]).range([height, 0]);
|
|
1416
|
+
const [xScale, yScale] = createScales(diagramType, width, height, minPressure, maxPressure, minTemperature, maxTemperature);
|
|
1148
1417
|
// Add axis'
|
|
1149
1418
|
drawAxes({
|
|
1150
1419
|
svg,
|
|
1151
1420
|
xScale,
|
|
1152
1421
|
yScale,
|
|
1153
|
-
height
|
|
1422
|
+
height,
|
|
1423
|
+
diagramType
|
|
1154
1424
|
});
|
|
1155
1425
|
// Draw static lines
|
|
1156
1426
|
drawStaticLines({
|
|
1427
|
+
diagramType,
|
|
1157
1428
|
dp,
|
|
1158
1429
|
staticPressureArray,
|
|
1159
1430
|
minTemperature,
|
|
@@ -1184,7 +1455,8 @@ const Soundings = ({
|
|
|
1184
1455
|
xScale,
|
|
1185
1456
|
yScale,
|
|
1186
1457
|
height,
|
|
1187
|
-
svg
|
|
1458
|
+
svg,
|
|
1459
|
+
diagramType
|
|
1188
1460
|
});
|
|
1189
1461
|
const dewpointValues = features.map(feature => feature.properties.dewpoint);
|
|
1190
1462
|
// Dewpoint
|
|
@@ -1196,7 +1468,8 @@ const Soundings = ({
|
|
|
1196
1468
|
xScale,
|
|
1197
1469
|
yScale,
|
|
1198
1470
|
height,
|
|
1199
|
-
svg
|
|
1471
|
+
svg,
|
|
1472
|
+
diagramType
|
|
1200
1473
|
});
|
|
1201
1474
|
const windData = features.map(feature => ({
|
|
1202
1475
|
windDirection: feature.properties.windDirection,
|
|
@@ -1205,12 +1478,10 @@ const Soundings = ({
|
|
|
1205
1478
|
}));
|
|
1206
1479
|
drawWindBarbs({
|
|
1207
1480
|
svgRef,
|
|
1481
|
+
yScale,
|
|
1208
1482
|
data: windData,
|
|
1209
1483
|
width,
|
|
1210
|
-
height,
|
|
1211
1484
|
margin,
|
|
1212
|
-
minPressure,
|
|
1213
|
-
maxPressure,
|
|
1214
1485
|
barbColor: color
|
|
1215
1486
|
});
|
|
1216
1487
|
});
|
|
@@ -1232,7 +1503,8 @@ const Soundings = ({
|
|
|
1232
1503
|
tempRefd3,
|
|
1233
1504
|
dewpointRefd3,
|
|
1234
1505
|
windRefd3,
|
|
1235
|
-
tooltipData
|
|
1506
|
+
tooltipData,
|
|
1507
|
+
diagramType
|
|
1236
1508
|
});
|
|
1237
1509
|
});
|
|
1238
1510
|
}
|
|
@@ -1342,15 +1614,14 @@ const Soundings = ({
|
|
|
1342
1614
|
* Copyright 2025 - Finnish Meteorological Institute (FMI)
|
|
1343
1615
|
* Copyright 2025 - The Norwegian Meteorological Institute (MET Norway)
|
|
1344
1616
|
* */
|
|
1345
|
-
const
|
|
1346
|
-
const getCollectionLocations = collectionId => fetch(`${BASE_URL}/collections/${collectionId}/locations`, {
|
|
1617
|
+
const getCollectionLocations = (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/locations`, {
|
|
1347
1618
|
headers: getHeaders()
|
|
1348
1619
|
}).then(handleResponse);
|
|
1349
|
-
const getCollectionInstance = collectionId => fetch(`${
|
|
1620
|
+
const getCollectionInstance = (collectionId, baseUrl) => fetch(`${baseUrl}/collections/${collectionId}/instances`, {
|
|
1350
1621
|
headers: getHeaders()
|
|
1351
1622
|
}).then(handleResponse);
|
|
1352
|
-
const getEdrCollectionForLocation = (collectionId, instanceId, locationId, parameters, datetime) => {
|
|
1353
|
-
const url = new URL(`${
|
|
1623
|
+
const getEdrCollectionForLocation = (collectionId, baseUrl, instanceId, locationId, parameters, datetime) => {
|
|
1624
|
+
const url = new URL(`${baseUrl}/collections/${collectionId}${instanceId ? `/instances/${instanceId}` : ''}/locations/${locationId}`);
|
|
1354
1625
|
url.searchParams.append('parameter-name', parameters.join(','));
|
|
1355
1626
|
url.searchParams.append('datetime', datetime);
|
|
1356
1627
|
url.searchParams.append('f', 'CoverageJSON');
|
|
@@ -1358,8 +1629,8 @@ const getEdrCollectionForLocation = (collectionId, instanceId, locationId, param
|
|
|
1358
1629
|
headers: getHeaders()
|
|
1359
1630
|
}).then(handleResponse);
|
|
1360
1631
|
};
|
|
1361
|
-
const getEdrCollectionForPosition = (collectionId, instanceId, coordinates, parameters, datetime) => {
|
|
1362
|
-
const url = new URL(`${
|
|
1632
|
+
const getEdrCollectionForPosition = (collectionId, baseUrl, instanceId, coordinates, parameters, datetime) => {
|
|
1633
|
+
const url = new URL(`${baseUrl}/collections/${collectionId}${instanceId ? `/instances/${instanceId}` : ''}/position`);
|
|
1363
1634
|
url.searchParams.append('coords', `POINT(${coordinates[0]} ${coordinates[1]})`);
|
|
1364
1635
|
url.searchParams.append('parameter-name', parameters.join(','));
|
|
1365
1636
|
url.searchParams.append('datetime', datetime);
|
|
@@ -1390,7 +1661,6 @@ const epsilon = 0.622; // Ratio of molecular weights of water vapour and dry air
|
|
|
1390
1661
|
const e0 = 611.2; // Reference vapor pressure [Pa]
|
|
1391
1662
|
const aConstant = 17.67;
|
|
1392
1663
|
const bConstant = 243.5; // Constants for water, b in Celsius
|
|
1393
|
-
const tKelvinOffset = 273.15; // To convert Celsius to Kelvin
|
|
1394
1664
|
const gConstant = 461.5; // Gas constant ratio between water vapour and air [J/(K*kg)]
|
|
1395
1665
|
const hConstant = 2.5e6; // Latent heat for water vaporization [J/kg]
|
|
1396
1666
|
// Formula to use when dewpoint is not available
|
|
@@ -1412,13 +1682,13 @@ function dewpointFromRelativeHumidity(tCelsius,
|
|
|
1412
1682
|
// air temperature [°C]
|
|
1413
1683
|
relHumidity) {
|
|
1414
1684
|
// Convert to Kelvin
|
|
1415
|
-
const tKelvin = tCelsius +
|
|
1685
|
+
const tKelvin = tCelsius + KELVIN;
|
|
1416
1686
|
// Calculate dewpoint
|
|
1417
1687
|
const dewpoint = tKelvin / (1 - tKelvin * Math.log(relHumidity * 0.01) * gConstant / hConstant);
|
|
1418
1688
|
return dewpoint;
|
|
1419
1689
|
}
|
|
1420
1690
|
// Parsing and conversion function
|
|
1421
|
-
function parseCoverageCollection(response) {
|
|
1691
|
+
function parseCoverageCollection(response, parameterMappings) {
|
|
1422
1692
|
const features = [];
|
|
1423
1693
|
const {
|
|
1424
1694
|
t: tAxis,
|
|
@@ -1438,34 +1708,38 @@ function parseCoverageCollection(response) {
|
|
|
1438
1708
|
const yCoord = coverage.domain.axes.y.values[0];
|
|
1439
1709
|
const zValues = coverage.domain.axes.z.values;
|
|
1440
1710
|
zValues.forEach((_, zIndex) => {
|
|
1441
|
-
const temp = parseFloat(ranges.temperature?.values[zIndex]);
|
|
1442
|
-
const
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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);
|
|
1452
1726
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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);
|
|
1457
1735
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
pressure * 100);
|
|
1466
|
-
} else if (!Number.isNaN(relativeHumidity)) {
|
|
1467
|
-
dewpoint = dewpointFromRelativeHumidity(temp, relativeHumidity);
|
|
1468
|
-
}
|
|
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;
|
|
1469
1743
|
}
|
|
1470
1744
|
features.push({
|
|
1471
1745
|
type: 'Feature',
|
|
@@ -1514,7 +1788,8 @@ const sortSoundingFeatures = data => {
|
|
|
1514
1788
|
return featureB.properties.pressure - featureA.properties.pressure;
|
|
1515
1789
|
});
|
|
1516
1790
|
};
|
|
1517
|
-
const constructQueryParams = (collectionId, instanceId, location, datetime,
|
|
1791
|
+
const constructQueryParams = (collectionId, baseUrl, instanceId, location, datetime, parameterMappings) => {
|
|
1792
|
+
const parameters = getParameterMappings(parameterMappings);
|
|
1518
1793
|
if (typeof location === 'string') {
|
|
1519
1794
|
// LOCATION call
|
|
1520
1795
|
return {
|
|
@@ -1525,8 +1800,8 @@ const constructQueryParams = (collectionId, instanceId, location, datetime, para
|
|
|
1525
1800
|
return [];
|
|
1526
1801
|
}
|
|
1527
1802
|
try {
|
|
1528
|
-
const rawData = await getEdrCollectionForLocation(collectionId, instanceId, location, parameters, datetime);
|
|
1529
|
-
const parsedData = parseCoverageCollection(rawData);
|
|
1803
|
+
const rawData = await getEdrCollectionForLocation(collectionId, baseUrl, instanceId, location, parameters, datetime);
|
|
1804
|
+
const parsedData = parseCoverageCollection(rawData, parameterMappings);
|
|
1530
1805
|
return sortSoundingFeatures(parsedData);
|
|
1531
1806
|
} catch (err) {
|
|
1532
1807
|
throw new Error('Error fetching EDR data collection by location');
|
|
@@ -1543,8 +1818,8 @@ const constructQueryParams = (collectionId, instanceId, location, datetime, para
|
|
|
1543
1818
|
return [];
|
|
1544
1819
|
}
|
|
1545
1820
|
try {
|
|
1546
|
-
const rawData = await getEdrCollectionForPosition(collectionId, instanceId, location, parameters, datetime);
|
|
1547
|
-
const parsedData = parseCoverageCollection(rawData);
|
|
1821
|
+
const rawData = await getEdrCollectionForPosition(collectionId, baseUrl, instanceId, location, parameters, datetime);
|
|
1822
|
+
const parsedData = parseCoverageCollection(rawData, parameterMappings);
|
|
1548
1823
|
return sortSoundingFeatures(parsedData);
|
|
1549
1824
|
} catch (err) {
|
|
1550
1825
|
throw new Error('Error fetching EDR data collection by position');
|
|
@@ -1552,35 +1827,51 @@ const constructQueryParams = (collectionId, instanceId, location, datetime, para
|
|
|
1552
1827
|
}
|
|
1553
1828
|
};
|
|
1554
1829
|
};
|
|
1555
|
-
const useFakeTime = (collectionId, instanceId) => {
|
|
1830
|
+
const useFakeTime = (collectionId, baseUrl, instanceId) => {
|
|
1556
1831
|
return useQuery({
|
|
1557
1832
|
queryKey: ['temporary-instance-call', collectionId, instanceId],
|
|
1558
1833
|
queryFn: async () => {
|
|
1559
|
-
return fetch(
|
|
1834
|
+
return fetch(`${baseUrl}/collections/${collectionId}/instances/${instanceId}`, {
|
|
1560
1835
|
headers: getHeaders()
|
|
1561
1836
|
}).then(handleResponse).then(data => collectionId === undefined || data === undefined ? '' : data.extent.temporal.interval[0][0]);
|
|
1562
1837
|
},
|
|
1563
1838
|
enabled: !!collectionId && !!instanceId
|
|
1564
1839
|
});
|
|
1565
1840
|
};
|
|
1566
|
-
const
|
|
1841
|
+
const defaultParameterMappings = {
|
|
1842
|
+
temperature: 'temperature',
|
|
1843
|
+
windDirection: 'winddirection',
|
|
1844
|
+
windSpeed: 'windspeedms',
|
|
1845
|
+
windU: 'windums',
|
|
1846
|
+
windV: 'windvms',
|
|
1847
|
+
pressure: 'pressure',
|
|
1848
|
+
dewpoint: 'dewpoint',
|
|
1849
|
+
humidity: 'humidity',
|
|
1850
|
+
specificHumidity: 'specifichumidity',
|
|
1851
|
+
relativeHumidity: 'relativehumidity'
|
|
1852
|
+
};
|
|
1853
|
+
const getParameterMappings = parameterMappings => {
|
|
1854
|
+
return Object.keys(parameterMappings).map(key => parameterMappings[key]);
|
|
1855
|
+
};
|
|
1567
1856
|
const useMultipleSources = (sources, locations, datetime) => {
|
|
1568
1857
|
// TODO: Remove once we pass in the datetime coming from the timeslider!
|
|
1569
1858
|
const {
|
|
1570
1859
|
data: date0
|
|
1571
|
-
} = useFakeTime(sources[0]?.collectionId, sources[0]?.instanceId);
|
|
1860
|
+
} = useFakeTime(sources[0]?.collectionId, sources[0]?.url, sources[0]?.instanceId);
|
|
1572
1861
|
const {
|
|
1573
1862
|
data: date1
|
|
1574
|
-
} = useFakeTime(sources[1]?.collectionId, sources[1]?.instanceId);
|
|
1863
|
+
} = useFakeTime(sources[1]?.collectionId, sources[1]?.url, sources[1]?.instanceId);
|
|
1575
1864
|
const {
|
|
1576
1865
|
data: date2
|
|
1577
|
-
} = useFakeTime(sources[2]?.collectionId, sources[2]?.instanceId);
|
|
1866
|
+
} = useFakeTime(sources[2]?.collectionId, sources[2]?.url, sources[2]?.instanceId);
|
|
1578
1867
|
const dates = useMemo(() => [date0, date1, date2], [date0, date1, date2]);
|
|
1579
1868
|
const queries = useMemo(() => sources.map(({
|
|
1580
1869
|
collectionId,
|
|
1581
|
-
|
|
1870
|
+
url,
|
|
1871
|
+
instanceId,
|
|
1872
|
+
parameterMappings
|
|
1582
1873
|
}, index) => {
|
|
1583
|
-
const queryParams = constructQueryParams(collectionId, instanceId, locations[collectionId] || '', dates[index] || '',
|
|
1874
|
+
const queryParams = constructQueryParams(collectionId, url, instanceId, locations[collectionId] || '', dates[index] || '', parameterMappings || defaultParameterMappings);
|
|
1584
1875
|
return queryParams;
|
|
1585
1876
|
}), [dates, datetime, locations, sources]);
|
|
1586
1877
|
return useQueries({
|
|
@@ -1600,12 +1891,20 @@ const useMultipleSources = (sources, locations, datetime) => {
|
|
|
1600
1891
|
});
|
|
1601
1892
|
};
|
|
1602
1893
|
const useGetCollectionsInstances = (collections, isDialogOpen) => {
|
|
1603
|
-
const collectionIds = useMemo(() => collections?.map(collection =>
|
|
1604
|
-
|
|
1894
|
+
const collectionIds = useMemo(() => collections?.map(collection => {
|
|
1895
|
+
return {
|
|
1896
|
+
id: collection.id,
|
|
1897
|
+
url: collection.url
|
|
1898
|
+
};
|
|
1899
|
+
}) || [], [collections]);
|
|
1900
|
+
const queries = useMemo(() => collectionIds.map(({
|
|
1901
|
+
id,
|
|
1902
|
+
url
|
|
1903
|
+
}) => {
|
|
1605
1904
|
return {
|
|
1606
1905
|
queryKey: ['edr-soundings-instances', id],
|
|
1607
1906
|
queryFn: async () => {
|
|
1608
|
-
const data = await getCollectionInstance(id);
|
|
1907
|
+
const data = await getCollectionInstance(id, url);
|
|
1609
1908
|
return data;
|
|
1610
1909
|
},
|
|
1611
1910
|
enabled: isDialogOpen
|
|
@@ -1638,6 +1937,33 @@ const useGetCollectionsInstances = (collections, isDialogOpen) => {
|
|
|
1638
1937
|
data: dataWithCollectionInfo
|
|
1639
1938
|
};
|
|
1640
1939
|
};
|
|
1940
|
+
const useGetCollectionLocations = (collections, isDialogOpen) => {
|
|
1941
|
+
const edrQueries = useMemo(() => collections?.map(collection => ({
|
|
1942
|
+
queryKey: ['edr-locations', collection.id],
|
|
1943
|
+
queryFn: async () => {
|
|
1944
|
+
const locations = await getCollectionLocations(collection.id, collection.url);
|
|
1945
|
+
return {
|
|
1946
|
+
...locations,
|
|
1947
|
+
id: collection.id,
|
|
1948
|
+
name: collection.name
|
|
1949
|
+
};
|
|
1950
|
+
},
|
|
1951
|
+
enabled: isDialogOpen
|
|
1952
|
+
})), [collections, isDialogOpen]);
|
|
1953
|
+
return useQueries({
|
|
1954
|
+
queries: edrQueries,
|
|
1955
|
+
combine: results => {
|
|
1956
|
+
const data = results.map(result => result.data).filter(result => !!result);
|
|
1957
|
+
const isPending = results.some(result => result.isPending);
|
|
1958
|
+
const error = results.find(result => result?.error)?.error;
|
|
1959
|
+
return {
|
|
1960
|
+
data,
|
|
1961
|
+
isPending,
|
|
1962
|
+
error
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
};
|
|
1641
1967
|
|
|
1642
1968
|
/* *
|
|
1643
1969
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -1832,7 +2158,9 @@ const getSelectedSources = createSelector(getSoundingsStore, store => {
|
|
|
1832
2158
|
if (value.isChecked === true) {
|
|
1833
2159
|
return list.concat({
|
|
1834
2160
|
collectionId: key,
|
|
1835
|
-
|
|
2161
|
+
url: value.url,
|
|
2162
|
+
instanceId: value.instanceId,
|
|
2163
|
+
parameterMappings: value.parameterMappings
|
|
1836
2164
|
});
|
|
1837
2165
|
}
|
|
1838
2166
|
return list;
|
|
@@ -1842,6 +2170,9 @@ const getSelectedSources = createSelector(getSoundingsStore, store => {
|
|
|
1842
2170
|
const getSelectedSourcesAmount = createSelector(getSelectedSources, activeSources => {
|
|
1843
2171
|
return activeSources.length;
|
|
1844
2172
|
}, selectorMemoizationOptions);
|
|
2173
|
+
const getSelectedSourcesIds = createSelector(getSelectedSources, activeSources => {
|
|
2174
|
+
return activeSources.map(source => source.collectionId);
|
|
2175
|
+
}, selectorMemoizationOptions);
|
|
1845
2176
|
|
|
1846
2177
|
const SoundingsTimeSliderConnect = ({
|
|
1847
2178
|
timesteps
|
|
@@ -2139,6 +2470,20 @@ const SoundingsSourceDialogConnect = ({
|
|
|
2139
2470
|
}));
|
|
2140
2471
|
}, [dispatch]);
|
|
2141
2472
|
const sourceSelections = useSelector(getSourceSelections);
|
|
2473
|
+
// Set mappings for each collection
|
|
2474
|
+
React.useEffect(() => {
|
|
2475
|
+
soundingsCollections?.forEach(collection => {
|
|
2476
|
+
dispatch(soundingsActions.updateSelectedSource({
|
|
2477
|
+
sourceId: collection.id,
|
|
2478
|
+
value: {
|
|
2479
|
+
url: collection.url,
|
|
2480
|
+
isChecked: sourceSelections[collection.id]?.isChecked || false,
|
|
2481
|
+
parameterMappings: collection.parameterMappings
|
|
2482
|
+
}
|
|
2483
|
+
}));
|
|
2484
|
+
});
|
|
2485
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2486
|
+
}, [dispatch, soundingsCollections]);
|
|
2142
2487
|
const {
|
|
2143
2488
|
data,
|
|
2144
2489
|
isPending,
|
|
@@ -2159,12 +2504,15 @@ const SoundingsSourceDialogConnect = ({
|
|
|
2159
2504
|
|
|
2160
2505
|
const LocationsMenu = ({
|
|
2161
2506
|
services,
|
|
2507
|
+
selectedSourcesAmount,
|
|
2162
2508
|
selectedLocations,
|
|
2163
2509
|
isOpen,
|
|
2164
2510
|
onClose,
|
|
2165
2511
|
updateSelectedLocation,
|
|
2166
2512
|
onMouseDown = () => {},
|
|
2167
|
-
order = 0
|
|
2513
|
+
order = 0,
|
|
2514
|
+
isLoading,
|
|
2515
|
+
error
|
|
2168
2516
|
}) => {
|
|
2169
2517
|
const [searchFilter, setSearchFilter] = useState('');
|
|
2170
2518
|
const {
|
|
@@ -2182,7 +2530,7 @@ const LocationsMenu = ({
|
|
|
2182
2530
|
});
|
|
2183
2531
|
}, [services, searchFilter]);
|
|
2184
2532
|
const isChecked = (sourceId, locationId) => selectedLocations[sourceId] === locationId;
|
|
2185
|
-
return
|
|
2533
|
+
return jsxs(ToolContainerDraggable, {
|
|
2186
2534
|
startSize: {
|
|
2187
2535
|
width: 320,
|
|
2188
2536
|
height: 500
|
|
@@ -2198,7 +2546,15 @@ const LocationsMenu = ({
|
|
|
2198
2546
|
bounds: "parent",
|
|
2199
2547
|
onMouseDown: onMouseDown,
|
|
2200
2548
|
order: order,
|
|
2201
|
-
children:
|
|
2549
|
+
children: [isLoading && jsx(LinearProgress, {
|
|
2550
|
+
sx: {
|
|
2551
|
+
width: '100%',
|
|
2552
|
+
position: 'absolute'
|
|
2553
|
+
}
|
|
2554
|
+
}), error && jsx(AlertBanner, {
|
|
2555
|
+
title: error.message,
|
|
2556
|
+
shouldClose: true
|
|
2557
|
+
}), selectedSourcesAmount === 0 ? jsx(Typography, {
|
|
2202
2558
|
sx: {
|
|
2203
2559
|
margin: 1
|
|
2204
2560
|
},
|
|
@@ -2280,7 +2636,7 @@ const LocationsMenu = ({
|
|
|
2280
2636
|
})
|
|
2281
2637
|
})
|
|
2282
2638
|
}, service.id))]
|
|
2283
|
-
})
|
|
2639
|
+
})]
|
|
2284
2640
|
});
|
|
2285
2641
|
};
|
|
2286
2642
|
|
|
@@ -2295,34 +2651,30 @@ const LocationsMenuConnect = ({
|
|
|
2295
2651
|
setDialogOrder
|
|
2296
2652
|
} = useSetupDialog(uiTypes.DialogTypes.SoundingsLocations);
|
|
2297
2653
|
const locationSelections = useSelector(getLocationSelections);
|
|
2298
|
-
const
|
|
2299
|
-
const
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
};
|
|
2309
|
-
}
|
|
2310
|
-
})), [soundingsCollections]);
|
|
2311
|
-
const services = useQueries({
|
|
2312
|
-
queries: edrQueries,
|
|
2313
|
-
combine: results => results.map(result => result.data).filter(result => !!result)
|
|
2314
|
-
});
|
|
2654
|
+
const selectedSourceIds = useSelector(getSelectedSourcesIds);
|
|
2655
|
+
const sourcesAmount = useSelector(getSelectedSourcesAmount);
|
|
2656
|
+
const {
|
|
2657
|
+
data,
|
|
2658
|
+
isPending,
|
|
2659
|
+
error
|
|
2660
|
+
} = useGetCollectionLocations(soundingsCollections, isDialogOpen);
|
|
2661
|
+
const services = useMemo(() => {
|
|
2662
|
+
return data?.filter(service => selectedSourceIds.includes(service.id)) || [];
|
|
2663
|
+
}, [data, selectedSourceIds]);
|
|
2315
2664
|
return jsx(LocationsMenu, {
|
|
2316
2665
|
isOpen: isDialogOpen,
|
|
2317
2666
|
onClose: onCloseDialog,
|
|
2318
|
-
services: services
|
|
2667
|
+
services: services,
|
|
2668
|
+
selectedSourcesAmount: sourcesAmount,
|
|
2319
2669
|
selectedLocations: locationSelections,
|
|
2320
2670
|
updateSelectedLocation: (sourceId, locationId) => void dispatch(soundingsActions.updateSelectedLocation({
|
|
2321
2671
|
sourceId,
|
|
2322
2672
|
locationId
|
|
2323
2673
|
})),
|
|
2324
2674
|
onMouseDown: setDialogOrder,
|
|
2325
|
-
order: dialogOrder
|
|
2675
|
+
order: dialogOrder,
|
|
2676
|
+
isLoading: isPending,
|
|
2677
|
+
error: error
|
|
2326
2678
|
});
|
|
2327
2679
|
};
|
|
2328
2680
|
|