Calculate and draw hour precipitation rate

This commit is contained in:
Guntis Smaukstelis
2025-02-14 22:35:02 +02:00
parent 0b221bcfee
commit 28d0ab2267
5 changed files with 117 additions and 19 deletions
+12 -8
View File
@@ -5,7 +5,8 @@ import { apiHost } from '../consts'
import { GribMessage } from './interfaces' import { GribMessage } from './interfaces'
import { fetchBuffer } from '../helpers/fetch' import { fetchBuffer } from '../helpers/fetch'
import { drawGrib } from './draw/drawGrib' import { drawGrib } from './draw/drawGrib'
import { fetchWindData } from './draw/windDirection' import { fetchWindData, isCalculatedWindDirection } from './draw/windDirection'
import { fetchHourPrecipitationData, isCalculatedHourPrecipitation } from './draw/precipitation'
const CROP_BOUNDS = { x: 1906-1-400, y: 950, width: 400, height: 300 } const CROP_BOUNDS = { x: 1906-1-400, y: 950, width: 400, height: 300 }
@@ -13,14 +14,16 @@ export const GribFile: Component<{
name: string, name: string,
getCanvas: Accessor<HTMLCanvasElement | undefined>, getCanvas: Accessor<HTMLCanvasElement | undefined>,
setIsLoading: Setter<boolean>, setIsLoading: Setter<boolean>,
getGribList: Accessor<GribMessage[]> getFileGribList: Accessor<GribMessage[]> // specific reference and forecast time (in one file)
getAllGribLists: Accessor<GribMessage[]>
getIsCrop: Accessor<boolean>, getIsCrop: Accessor<boolean>,
onClick: (name: string) => void, onClick: (name: string) => void,
}> = ({ }> = ({
name, name,
getCanvas, getCanvas,
setIsLoading, setIsLoading,
getGribList, getFileGribList,
getAllGribLists,
getIsCrop, getIsCrop,
onClick, onClick,
}) => { }) => {
@@ -42,7 +45,7 @@ export const GribFile: Component<{
function onParamClick(paramId: number) { function onParamClick(paramId: number) {
setIsLoading(true); setIsLoading(true);
const grib = getGribList()[paramId] const grib = getFileGribList()[paramId]
const bitmaskSection = grib.sections.find(section => section.id === 6) const bitmaskSection = grib.sections.find(section => section.id === 6)
const binarySection = grib.sections.find(section => section.id === 7) const binarySection = grib.sections.find(section => section.id === 7)
if (!bitmaskSection || !binarySection) return; if (!bitmaskSection || !binarySection) return;
@@ -55,14 +58,15 @@ export const GribFile: Component<{
const binaryOffset = binarySection.offset + 5 const binaryOffset = binarySection.offset + 5
const binaryLength = binarySection.size - 5 const binaryLength = binarySection.size - 5
const fetchPromise: Promise<[GribMessage[], ArrayBuffer[], ArrayBuffer[]]> = grib.meteo.discipline === 0 && grib.meteo.category === 2 && grib.meteo.product === 192 let fetchPromise: Promise<[GribMessage[], ArrayBuffer[], ArrayBuffer[]]> = Promise.all([
? fetchWindData(grib, getGribList(), name)
: Promise.all([
Promise.resolve([grib]), Promise.resolve([grib]),
fetchBuffer(`${apiHost}/api/grib/binary-chunk/${binaryOffset}/${binaryLength}/${name}`).then(b=>[b]), fetchBuffer(`${apiHost}/api/grib/binary-chunk/${binaryOffset}/${binaryLength}/${name}`).then(b=>[b]),
bitmaskPromise, bitmaskPromise,
]) ])
if(isCalculatedWindDirection(grib)) fetchPromise = fetchWindData(grib, getFileGribList(), name)
if(isCalculatedHourPrecipitation(grib)) fetchPromise = fetchHourPrecipitationData(grib, getAllGribLists())
fetchPromise.then(([messages, binaryBuffers, bitmasks]) => { fetchPromise.then(([messages, binaryBuffers, bitmasks]) => {
cachedMessages = messages cachedMessages = messages
cachedBuffers = binaryBuffers.map(b => new Uint8Array(b)) cachedBuffers = binaryBuffers.map(b => new Uint8Array(b))
@@ -80,7 +84,7 @@ export const GribFile: Component<{
> >
<div class={styles.name} onClick={() => setIsActive(!getIsActive())}>{ trimName(name) }</div> <div class={styles.name} onClick={() => setIsActive(!getIsActive())}>{ trimName(name) }</div>
<ul class={styles.meteoParams}> <ul class={styles.meteoParams}>
{ getGribList() { getFileGribList()
.map((grib, i) => .map((grib, i) =>
<li onClick={() => onParamClick(i)}>{ grib.title.replace('meteorology, ', '') }</li> <li onClick={() => onParamClick(i)}>{ grib.title.replace('meteorology, ', '') }</li>
)} )}
+4 -1
View File
@@ -7,6 +7,7 @@ import { LoadingSpinner } from '../components/LoadingSpinner'
import { GribMessage } from './interfaces' import { GribMessage } from './interfaces'
import { fetchJson } from '../helpers/fetch' import { fetchJson } from '../helpers/fetch'
import { getFakeWindDirection, isWindSpeed } from './draw/windDirection' import { getFakeWindDirection, isWindSpeed } from './draw/windDirection'
import { getFakeHourPrecipitation, isPrecipitation } from './draw/precipitation'
export const Harmonie: Component<{}> = () => { export const Harmonie: Component<{}> = () => {
const [getFileList, setFileList] = createSignal<string[]>([]) const [getFileList, setFileList] = createSignal<string[]>([])
@@ -30,6 +31,7 @@ export const Harmonie: Component<{}> = () => {
setGribList([ setGribList([
...gribList, ...gribList,
...gribList.filter(isWindSpeed).map(getFakeWindDirection), ...gribList.filter(isWindSpeed).map(getFakeWindDirection),
...gribList.filter(isPrecipitation).map(getFakeHourPrecipitation),
].sort((a, b) => a.title > b.title ? 1 : -1)) ].sort((a, b) => a.title > b.title ? 1 : -1))
}) })
.finally(() => setIsLoading(false)) .finally(() => setIsLoading(false))
@@ -60,7 +62,8 @@ export const Harmonie: Component<{}> = () => {
name={fileName} name={fileName}
getCanvas={getCanvas} getCanvas={getCanvas}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
getGribList={() => getCurrentGribList(fileName)} getFileGribList={() => getCurrentGribList(fileName)}
getAllGribLists={getGribList}
getIsCrop={getIsCrop} getIsCrop={getIsCrop}
onClick={() => onGribMessageClick(fileName)} onClick={() => onGribMessageClick(fileName)}
/> />
+11 -3
View File
@@ -3,9 +3,9 @@ import { GribMessage, MeteoParam } from '../interfaces'
import { applyBitmask } from './bitmask' import { applyBitmask } from './bitmask'
import { extractFromBounds } from './bounds' import { extractFromBounds } from './bounds'
import { categoricalRainColors } from './categoricalRain' import { categoricalRainColors } from './categoricalRain'
import { precipitationColors } from './precipitation' import { hourPrecipitationColors, isCalculatedHourPrecipitation, precipitationColors } from './precipitation'
import { temperatureColors } from './temperature' import { temperatureColors } from './temperature'
import { windDirectionArrows, windDirectionColors, windSpeedColors } from './windDirection' import { isCalculatedWindDirection, windDirectionArrows, windDirectionColors, windSpeedColors } from './windDirection'
export type CropBounds = { x: number, y: number, width: number, height: number } export type CropBounds = { x: number, y: number, width: number, height: number }
@@ -41,7 +41,7 @@ export function drawGrib(
let imgData = ctx.createImageData(cols, rows) let imgData = ctx.createImageData(cols, rows)
fillImageData(imgData, messages, modifiedBuffers) fillImageData(imgData, messages, modifiedBuffers)
if (grib.meteo.discipline === 0 && grib.meteo.category === 2 && grib.meteo.product === 192) { if (isCalculatedWindDirection(grib)) {
imgData = windDirectionArrows(imgData, messages, modifiedBuffers) imgData = windDirectionArrows(imgData, messages, modifiedBuffers)
} }
@@ -60,6 +60,7 @@ export function drawGrib(
const CATEGORICAL_RAIN = [0, 1, 192] const CATEGORICAL_RAIN = [0, 1, 192]
const TOTAL_PRECIPITATION = [0, 1, 52] const TOTAL_PRECIPITATION = [0, 1, 52]
const HOUR_PRECIPITATION = [0, 1, 236]
const RAIN_PRECIPITATION = [0, 1, 65] const RAIN_PRECIPITATION = [0, 1, 65]
const TEMPERATURE = [0, 0, 0] const TEMPERATURE = [0, 0, 0]
const WIND_DIRECTION = [0, 2, 192] const WIND_DIRECTION = [0, 2, 192]
@@ -98,6 +99,13 @@ function fillImageData(
else if (isMeteoEqual(meteo, TOTAL_PRECIPITATION)) { else if (isMeteoEqual(meteo, TOTAL_PRECIPITATION)) {
color = precipitationColors(encodedValue, conversion) color = precipitationColors(encodedValue, conversion)
} }
else if (isMeteoEqual(meteo, HOUR_PRECIPITATION)) {
const [, nowPrec, prevPrec] = buffers
const encodedValNow = toInt(nowPrec.slice(bufferI, bufferI+bitsPerDataPoint/8))
const encodedValPrev = toInt(prevPrec.slice(bufferI, bufferI+bitsPerDataPoint/8))
const [, metaNow, metaPrev] = messages
color = hourPrecipitationColors(encodedValNow, metaNow.conversion, encodedValPrev, metaPrev.conversion)
}
else if (isMeteoEqual(meteo, RAIN_PRECIPITATION)) { else if (isMeteoEqual(meteo, RAIN_PRECIPITATION)) {
color = precipitationColors(encodedValue, conversion) color = precipitationColors(encodedValue, conversion)
} }
+84 -2
View File
@@ -1,6 +1,9 @@
import moment from 'moment'
import { valueToColorInterpolated, valueToColorThreshold } from '../../helpers/interpolateColors' import { valueToColorInterpolated, valueToColorThreshold } from '../../helpers/interpolateColors'
import { MeteoConversion } from '../interfaces' import { GribMessage, MeteoConversion } from '../interfaces'
import { PRECIPITATION } from './constants' import { PRECIPITATION } from './constants'
import { fetchBuffer } from '../../helpers/fetch'
import { apiHost } from '../../consts'
export function precipitationColors( export function precipitationColors(
@@ -8,10 +11,89 @@ export function precipitationColors(
{ reference, binaryScale, decimalScale}: MeteoConversion, { reference, binaryScale, decimalScale}: MeteoConversion,
isInterpolated = true, isInterpolated = true,
): [number, number, number, number] { ): [number, number, number, number] {
// const rainMM = (reference + encodedValue * Math.pow(2, binaryScale)) * Math.pow(10, -decimalScale) / 1000
const rainMM = (reference + encodedValue * Math.pow(2, binaryScale)) * Math.pow(10, -decimalScale) const rainMM = (reference + encodedValue * Math.pow(2, binaryScale)) * Math.pow(10, -decimalScale)
return isInterpolated return isInterpolated
? valueToColorInterpolated(rainMM, PRECIPITATION) ? valueToColorInterpolated(rainMM, PRECIPITATION)
: valueToColorThreshold(rainMM, PRECIPITATION) : valueToColorThreshold(rainMM, PRECIPITATION)
} }
export function hourPrecipitationColors(
encodedValNow: number,
convNow: MeteoConversion,
encodedValPrev: number,
convPrev: MeteoConversion,
isInterpolated = true,
): [number, number, number, number] {
const nowVal = (convNow.reference + encodedValNow * Math.pow(2, convNow.binaryScale)) * Math.pow(10, -convNow.decimalScale)
const prevVal = (convPrev.reference + encodedValPrev * Math.pow(2, convPrev.binaryScale)) * Math.pow(10, -convPrev.decimalScale)
const rainMM = nowVal - prevVal
return isInterpolated
? valueToColorInterpolated(rainMM, PRECIPITATION)
: valueToColorThreshold(rainMM, PRECIPITATION)
}
export function fetchHourPrecipitationData(
customMessage: GribMessage,
gribArr: GribMessage[],
): Promise<[GribMessage[], ArrayBuffer[], ArrayBuffer[]]> {
const totalPrecipitation = gribArr.find(g => isPrecipitation(g)
&& g.time.referenceTime === customMessage.time.referenceTime
&& g.time.forecastTime === customMessage.time.forecastTime
)
if (!totalPrecipitation) throw new Error('Not found total precipitation')
const { forecastTime } = totalPrecipitation.time
const prevForecastTime = moment(forecastTime.replace(/(\d{2})(\d{2})Z/, '$1:$2:00Z'))
.subtract(1, 'hours')
.utc()
.format("YYYY-MM-DDTHHmm")+'Z'
const prevTotalPrecipitation = gribArr.find(g => isPrecipitation(g)
&& g.time.referenceTime === customMessage.time.referenceTime
&& g.time.forecastTime === prevForecastTime
)
const section7now = totalPrecipitation.sections.find(section => section.id === 7)
const section7prev = prevTotalPrecipitation?.sections.find(section => section.id === 7)
if (!section7now) throw new Error('Didnt found binary section for total precipitation')
const nowBinaryOffset = section7now.offset + 5
const nowBinaryLength = section7now.size - 5
const nowFileName = `harmonie_${totalPrecipitation.time.referenceTime}_${totalPrecipitation.time.forecastTime}.grib`
// for oldest message there is no more -1h message
let prevPromise: Promise<ArrayBuffer> | undefined
if (prevTotalPrecipitation && section7prev) {
const prevBinaryOffset = section7prev.offset + 5
const prevBinaryLength = section7prev.size - 5
const prevFileName = `harmonie_${prevTotalPrecipitation.time.referenceTime}_${prevTotalPrecipitation.time.forecastTime}.grib`
prevPromise = fetchBuffer(`${apiHost}/api/grib/binary-chunk/${prevBinaryOffset}/${prevBinaryLength}/${prevFileName}`)
}
if (!prevPromise) prevPromise = new Promise(resolve => resolve(new Uint8Array(nowBinaryLength).buffer))
return Promise.all([
fetchBuffer(`${apiHost}/api/grib/binary-chunk/${nowBinaryOffset}/${nowBinaryLength}/${nowFileName}`),
prevPromise,
]).then(([bufferNow, bufferPrev]) => {
const messages = [customMessage, totalPrecipitation, prevTotalPrecipitation ?? totalPrecipitation]
const buffers = [bufferNow, bufferNow, bufferPrev]
return [messages, buffers, []]
})
}
export function isPrecipitation(grib: GribMessage): boolean {
return grib.meteo.discipline === 0 && grib.meteo.category === 1 && grib.meteo.product === 52
}
export function isCalculatedHourPrecipitation(grib: GribMessage): boolean {
return grib.meteo.discipline === 0 && grib.meteo.category === 1 && grib.meteo.product === 236
}
export function getFakeHourPrecipitation(totalPrecipitation: GribMessage): GribMessage {
const modifiedPrecipitation = structuredClone(totalPrecipitation)
modifiedPrecipitation.meteo = {...modifiedPrecipitation.meteo, product: 236}
modifiedPrecipitation.title = 'meteorology, moisture, hour precipitation rate'
return modifiedPrecipitation
}
+6 -5
View File
@@ -149,7 +149,7 @@ export function fetchWindData(
const section7u = windU.sections.find(section => section.id === 7) const section7u = windU.sections.find(section => section.id === 7)
const section7v = windV.sections.find(section => section.id === 7) const section7v = windV.sections.find(section => section.id === 7)
if (!section7u || !section7v) throw new Error('Didnt found binaru section for wind u/v') if (!section7u || !section7v) throw new Error('Didnt found binary section for wind u/v')
const uBinaryOffset = section7u.offset + 5 const uBinaryOffset = section7u.offset + 5
const uBinaryLength = section7u.size - 5 const uBinaryLength = section7u.size - 5
@@ -161,9 +161,6 @@ export function fetchWindData(
fetchBuffer(`${apiHost}/api/grib/binary-chunk/${uBinaryOffset}/${uBinaryLength}/${fileName}`), fetchBuffer(`${apiHost}/api/grib/binary-chunk/${uBinaryOffset}/${uBinaryLength}/${fileName}`),
fetchBuffer(`${apiHost}/api/grib/binary-chunk/${vBinaryOffset}/${vBinaryLength}/${fileName}`), fetchBuffer(`${apiHost}/api/grib/binary-chunk/${vBinaryOffset}/${vBinaryLength}/${fileName}`),
]).then(([bufferU, bufferV]) => { ]).then(([bufferU, bufferV]) => {
const buffer = new Uint8Array(bufferU.byteLength + bufferV.byteLength)
buffer.set(new Uint8Array(bufferU))
buffer.set(new Uint8Array(bufferV), bufferU.byteLength)
const messages = [customMessage, windU, windV] const messages = [customMessage, windU, windV]
const buffers = [bufferU, bufferU, bufferV] const buffers = [bufferU, bufferU, bufferV]
return [messages, buffers, []] return [messages, buffers, []]
@@ -175,7 +172,11 @@ function toInt(bytes: Uint8Array): number {
} }
export function isWindSpeed(grib: GribMessage): boolean { export function isWindSpeed(grib: GribMessage): boolean {
return grib.meteo.discipline===0 && grib.meteo.category===2 && grib.meteo.product===1 return grib.meteo.discipline === 0 && grib.meteo.category === 2 && grib.meteo.product === 1
}
export function isCalculatedWindDirection(grib: GribMessage): boolean {
return grib.meteo.discipline === 0 && grib.meteo.category === 2 && grib.meteo.product === 192
} }
export function getFakeWindDirection(windSpeed: GribMessage): GribMessage { export function getFakeWindDirection(windSpeed: GribMessage): GribMessage {