From 28d0ab2267a1e0a2fd90802f4f7b4c2441bc2c09 Mon Sep 17 00:00:00 2001 From: Guntis Smaukstelis Date: Fri, 14 Feb 2025 22:35:02 +0200 Subject: [PATCH] Calculate and draw hour precipitation rate --- web/src/harmonie/GribFile.tsx | 20 +++--- web/src/harmonie/Harmonie.tsx | 5 +- web/src/harmonie/draw/drawGrib.ts | 14 ++++- web/src/harmonie/draw/precipitation.ts | 86 +++++++++++++++++++++++++- web/src/harmonie/draw/windDirection.ts | 11 ++-- 5 files changed, 117 insertions(+), 19 deletions(-) diff --git a/web/src/harmonie/GribFile.tsx b/web/src/harmonie/GribFile.tsx index ca4829c..6fe3fd8 100644 --- a/web/src/harmonie/GribFile.tsx +++ b/web/src/harmonie/GribFile.tsx @@ -5,7 +5,8 @@ import { apiHost } from '../consts' import { GribMessage } from './interfaces' import { fetchBuffer } from '../helpers/fetch' 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 } @@ -13,14 +14,16 @@ export const GribFile: Component<{ name: string, getCanvas: Accessor, setIsLoading: Setter, - getGribList: Accessor + getFileGribList: Accessor // specific reference and forecast time (in one file) + getAllGribLists: Accessor getIsCrop: Accessor, onClick: (name: string) => void, }> = ({ name, getCanvas, setIsLoading, - getGribList, + getFileGribList, + getAllGribLists, getIsCrop, onClick, }) => { @@ -42,7 +45,7 @@ export const GribFile: Component<{ function onParamClick(paramId: number) { setIsLoading(true); - const grib = getGribList()[paramId] + const grib = getFileGribList()[paramId] const bitmaskSection = grib.sections.find(section => section.id === 6) const binarySection = grib.sections.find(section => section.id === 7) if (!bitmaskSection || !binarySection) return; @@ -55,13 +58,14 @@ export const GribFile: Component<{ const binaryOffset = binarySection.offset + 5 const binaryLength = binarySection.size - 5 - const fetchPromise: Promise<[GribMessage[], ArrayBuffer[], ArrayBuffer[]]> = grib.meteo.discipline === 0 && grib.meteo.category === 2 && grib.meteo.product === 192 - ? fetchWindData(grib, getGribList(), name) - : Promise.all([ + let fetchPromise: Promise<[GribMessage[], ArrayBuffer[], ArrayBuffer[]]> = Promise.all([ Promise.resolve([grib]), fetchBuffer(`${apiHost}/api/grib/binary-chunk/${binaryOffset}/${binaryLength}/${name}`).then(b=>[b]), bitmaskPromise, ]) + + if(isCalculatedWindDirection(grib)) fetchPromise = fetchWindData(grib, getFileGribList(), name) + if(isCalculatedHourPrecipitation(grib)) fetchPromise = fetchHourPrecipitationData(grib, getAllGribLists()) fetchPromise.then(([messages, binaryBuffers, bitmasks]) => { cachedMessages = messages @@ -80,7 +84,7 @@ export const GribFile: Component<{ >
setIsActive(!getIsActive())}>{ trimName(name) }
    - { getGribList() + { getFileGribList() .map((grib, i) =>
  • onParamClick(i)}>{ grib.title.replace('meteorology, ', '') }
  • )} diff --git a/web/src/harmonie/Harmonie.tsx b/web/src/harmonie/Harmonie.tsx index 1de2e6b..64e0318 100644 --- a/web/src/harmonie/Harmonie.tsx +++ b/web/src/harmonie/Harmonie.tsx @@ -7,6 +7,7 @@ import { LoadingSpinner } from '../components/LoadingSpinner' import { GribMessage } from './interfaces' import { fetchJson } from '../helpers/fetch' import { getFakeWindDirection, isWindSpeed } from './draw/windDirection' +import { getFakeHourPrecipitation, isPrecipitation } from './draw/precipitation' export const Harmonie: Component<{}> = () => { const [getFileList, setFileList] = createSignal([]) @@ -30,6 +31,7 @@ export const Harmonie: Component<{}> = () => { setGribList([ ...gribList, ...gribList.filter(isWindSpeed).map(getFakeWindDirection), + ...gribList.filter(isPrecipitation).map(getFakeHourPrecipitation), ].sort((a, b) => a.title > b.title ? 1 : -1)) }) .finally(() => setIsLoading(false)) @@ -60,7 +62,8 @@ export const Harmonie: Component<{}> = () => { name={fileName} getCanvas={getCanvas} setIsLoading={setIsLoading} - getGribList={() => getCurrentGribList(fileName)} + getFileGribList={() => getCurrentGribList(fileName)} + getAllGribLists={getGribList} getIsCrop={getIsCrop} onClick={() => onGribMessageClick(fileName)} /> diff --git a/web/src/harmonie/draw/drawGrib.ts b/web/src/harmonie/draw/drawGrib.ts index bdf4cc6..bba07fc 100644 --- a/web/src/harmonie/draw/drawGrib.ts +++ b/web/src/harmonie/draw/drawGrib.ts @@ -3,9 +3,9 @@ import { GribMessage, MeteoParam } from '../interfaces' import { applyBitmask } from './bitmask' import { extractFromBounds } from './bounds' import { categoricalRainColors } from './categoricalRain' -import { precipitationColors } from './precipitation' +import { hourPrecipitationColors, isCalculatedHourPrecipitation, precipitationColors } from './precipitation' 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 } @@ -41,7 +41,7 @@ export function drawGrib( let imgData = ctx.createImageData(cols, rows) 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) } @@ -60,6 +60,7 @@ export function drawGrib( const CATEGORICAL_RAIN = [0, 1, 192] const TOTAL_PRECIPITATION = [0, 1, 52] +const HOUR_PRECIPITATION = [0, 1, 236] const RAIN_PRECIPITATION = [0, 1, 65] const TEMPERATURE = [0, 0, 0] const WIND_DIRECTION = [0, 2, 192] @@ -98,6 +99,13 @@ function fillImageData( else if (isMeteoEqual(meteo, TOTAL_PRECIPITATION)) { 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)) { color = precipitationColors(encodedValue, conversion) } diff --git a/web/src/harmonie/draw/precipitation.ts b/web/src/harmonie/draw/precipitation.ts index 72bf523..874b6e7 100644 --- a/web/src/harmonie/draw/precipitation.ts +++ b/web/src/harmonie/draw/precipitation.ts @@ -1,6 +1,9 @@ +import moment from 'moment' import { valueToColorInterpolated, valueToColorThreshold } from '../../helpers/interpolateColors' -import { MeteoConversion } from '../interfaces' +import { GribMessage, MeteoConversion } from '../interfaces' import { PRECIPITATION } from './constants' +import { fetchBuffer } from '../../helpers/fetch' +import { apiHost } from '../../consts' export function precipitationColors( @@ -8,10 +11,89 @@ export function precipitationColors( { reference, binaryScale, decimalScale}: MeteoConversion, isInterpolated = true, ): [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) return isInterpolated ? valueToColorInterpolated(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 | 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 +} \ No newline at end of file diff --git a/web/src/harmonie/draw/windDirection.ts b/web/src/harmonie/draw/windDirection.ts index 708e915..f1da65a 100644 --- a/web/src/harmonie/draw/windDirection.ts +++ b/web/src/harmonie/draw/windDirection.ts @@ -149,7 +149,7 @@ export function fetchWindData( const section7u = windU.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 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/${vBinaryOffset}/${vBinaryLength}/${fileName}`), ]).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 buffers = [bufferU, bufferU, bufferV] return [messages, buffers, []] @@ -175,7 +172,11 @@ function toInt(bytes: Uint8Array): number { } 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 {