Download structure for all files in one go. For better UX

This commit is contained in:
Guntis Smaukstelis
2025-02-14 20:26:57 +02:00
parent 8bd5be413d
commit 0b221bcfee
6 changed files with 68 additions and 35 deletions
+7
View File
@@ -54,6 +54,13 @@ class DataService(log: Logger[IO]) {
} }
} }
def getAllFileStructure(): IO[List[Grib]] = {
for {
fileList <- getFileList()
allStructure <- fileList.traverse(getGribStucture)
} yield allStructure.flatten
}
def getBinaryChunk(offset: Int, length: Int, fileName: String): IO[Array[Byte]] = { def getBinaryChunk(offset: Int, length: Int, fileName: String): IO[Array[Byte]] = {
IO.blocking { IO.blocking {
val source = Source.fromFile(s"$GRIB_FOLDER/$fileName", "ISO-8859-1") val source = Source.fromFile(s"$GRIB_FOLDER/$fileName", "ISO-8859-1")
+8 -5
View File
@@ -51,17 +51,20 @@ class Server(postgresService: PostgresService, dataService: DataService, fetch:
private case class ResponseWrapper(result: Map[String, Option[Aggregate.AggregateValue]], query: UserQuery) private case class ResponseWrapper(result: Map[String, Option[Aggregate.AggregateValue]], query: UserQuery)
private val apiRoutes = HttpRoutes.of[IO] { private val apiRoutes = HttpRoutes.of[IO] {
// http://0.0.0.0:8080/api/show/grib-name/harmonie_2025-02-01T1500Z_2025-02-01T180000Z.grib
case GET -> Root / "show" / "grib" / fileName =>
// val fileName = "data/HARMONIE_DINI_SF_2025-01-24T030000Z_2025-01-26T010000Z.grib"
dataService.getGribStucture(fileName).flatMap(response => Ok(response.asJson.pretty))
case GET -> Root / "show" / "gribName" => Ok("{\"fileName\":\"TODO replace this fake name\"}") case GET -> Root / "show" / "gribName" => Ok("{\"fileName\":\"TODO replace this fake name\"}")
// http://0.0.0.0:8080/api/show/grib-all-structure
case GET -> Root / "show" / "grib-all-structure" =>
dataService.getAllFileStructure().flatMap(gribList => Ok(gribList.asJson))
// http://0.0.0.0:8080/api/show/grib-list // http://0.0.0.0:8080/api/show/grib-list
case GET -> Root / "show" / "grib-list" => case GET -> Root / "show" / "grib-list" =>
dataService.getFileList().flatMap(fileList => Ok(fileList.asJson)) dataService.getFileList().flatMap(fileList => Ok(fileList.asJson))
// http://0.0.0.0:8080/api/show/grib-name/harmonie_2025-02-01T1500Z_2025-02-01T180000Z.grib
case GET -> Root / "show" / "grib" / fileName =>
dataService.getGribStucture(fileName).flatMap(response => Ok(response.asJson.pretty))
case GET -> Root / "grib" / "binary-chunk" / ValidateInt(binaryOffset) / ValidateInt(binaryLength) / fileName => case GET -> Root / "grib" / "binary-chunk" / ValidateInt(binaryOffset) / ValidateInt(binaryLength) / fileName =>
dataService.getBinaryChunk(binaryOffset, binaryLength, fileName).flatMap(buffer => Ok(buffer)) dataService.getBinaryChunk(binaryOffset, binaryLength, fileName).flatMap(buffer => Ok(buffer))
+9 -24
View File
@@ -1,49 +1,35 @@
import { Accessor, Component, createEffect, createMemo, createSignal, Setter } from 'solid-js' import { Accessor, Component, createEffect, createSignal, Setter } from 'solid-js'
import styles from './harmonie.module.css' import styles from './harmonie.module.css'
import { apiHost } from '../consts' 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, getFakeWindDirection } from './draw/windDirection' import { fetchWindData } from './draw/windDirection'
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 }
export const GribFile: Component<{ export const GribFile: Component<{
name: string, name: string,
isActive: Accessor<boolean>,
getCanvas: Accessor<HTMLCanvasElement | undefined>, getCanvas: Accessor<HTMLCanvasElement | undefined>,
setIsLoading: Setter<boolean>, setIsLoading: Setter<boolean>,
getGribList: Accessor<GribMessage[]>
getIsCrop: Accessor<boolean>, getIsCrop: Accessor<boolean>,
onClick: (name: string) => void, onClick: (name: string) => void,
}> = ({ }> = ({
name, name,
isActive,
getCanvas, getCanvas,
setIsLoading, setIsLoading,
getGribList,
getIsCrop, getIsCrop,
onClick, onClick,
}) => { }) => {
const [getGribList, setGribList] = createSignal<GribMessage[]>([])
function onFileClick() {
onClick(name)
if (getGribList().length === 0) {
fetch(`${apiHost}/api/show/grib/${name}`)
.then(re => re.json())
.then((gribList: GribMessage[]) => {
const windSpeed = gribList.find(m => m.meteo.discipline===0 && m.meteo.category===2 && m.meteo.product===1)
if (windSpeed) gribList.push(getFakeWindDirection(windSpeed))
setGribList(gribList)
})
}
}
let cachedMessages: GribMessage[] = [] let cachedMessages: GribMessage[] = []
let cachedBuffers: Uint8Array[] = [] let cachedBuffers: Uint8Array[] = []
let cachedBitmasks: Uint8Array[] = [] let cachedBitmasks: Uint8Array[] = []
const [getIsActive, setIsActive] = createSignal(false)
createEffect(async () => { createEffect(async () => {
setIsLoading(true) setIsLoading(true)
const cropBounds = getIsCrop() ? CROP_BOUNDS : undefined const cropBounds = getIsCrop() ? CROP_BOUNDS : undefined
@@ -89,13 +75,12 @@ export const GribFile: Component<{
} }
return <li return <li
class={isActive() ? styles.active : ''} class={getIsActive() ? styles.active : ''}
onClick={onFileClick} onClick={() => onClick(name)}
> >
<div class={styles.name}>{ trimName(name) }</div> <div class={styles.name} onClick={() => setIsActive(!getIsActive())}>{ trimName(name) }</div>
<ul class={styles.meteoParams}> <ul class={styles.meteoParams}>
{ getGribList() { getGribList()
.sort((a, b) => a.title > b.title ? 1 : -1)
.map((grib, i) => .map((grib, i) =>
<li onClick={() => onParamClick(i)}>{ grib.title.replace('meteorology, ', '') }</li> <li onClick={() => onParamClick(i)}>{ grib.title.replace('meteorology, ', '') }</li>
)} )}
+33 -5
View File
@@ -4,22 +4,50 @@ import { apiHost } from '../consts'
import styles from './harmonie.module.css' import styles from './harmonie.module.css'
import { GribFile } from './GribFile' import { GribFile } from './GribFile'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { GribMessage } from './interfaces'
import { fetchJson } from '../helpers/fetch'
import { getFakeWindDirection, isWindSpeed } from './draw/windDirection'
export const Harmonie: Component<{}> = () => { export const Harmonie: Component<{}> = () => {
const [getFileList, setFileList] = createSignal<string[]>([]) const [getFileList, setFileList] = createSignal<string[]>([])
const [getActiveGrib, setActiveGrib] = createSignal('')
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>() const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>()
const [getIsLoading, setIsLoading] = createSignal(true) const [getIsLoading, setIsLoading] = createSignal(true)
const [getIsCrop, setIsCrop] = createSignal(true) const [getIsCrop, setIsCrop] = createSignal(true)
const [getGribList, setGribList] = createSignal<GribMessage[]>([])
fetch(`${apiHost}/api/show/grib-list`) fetchJson(`${apiHost}/api/show/grib-list`)
.then(re => re.json())
.then(fileList => { .then(fileList => {
fileList.sort((a: string, b: string) => a < b ? 1 : -1) fileList.sort((a: string, b: string) => a < b ? 1 : -1)
setFileList(fileList) setFileList(fileList)
}) })
.finally(() => setIsLoading(false)) .finally(() => setIsLoading(false))
function onGribMessageClick(fileName: string) {
if(!getGribList().length) {
setIsLoading(true)
fetchJson(`${apiHost}/api/show/grib-all-structure`)
.then((gribList: GribMessage[]) => {
setGribList([
...gribList,
...gribList.filter(isWindSpeed).map(getFakeWindDirection),
].sort((a, b) => a.title > b.title ? 1 : -1))
})
.finally(() => setIsLoading(false))
}
}
function getCurrentGribList(fileName: string): GribMessage[] {
const [referenceTime, forecastTime] = fileName.replace('harmonie_', '')
.replace('.grib', '')
.split('_')
if (!referenceTime || ! forecastTime) return []
return getGribList().filter(g =>
g.time.referenceTime === referenceTime
&& g.time.forecastTime === forecastTime
)
}
return <div class={styles.container}> return <div class={styles.container}>
<div class={styles.column}> <div class={styles.column}>
<label> <label>
@@ -30,11 +58,11 @@ export const Harmonie: Component<{}> = () => {
{getFileList().map(fileName => {getFileList().map(fileName =>
<GribFile <GribFile
name={fileName} name={fileName}
isActive={() => getActiveGrib() === fileName}
getCanvas={getCanvas} getCanvas={getCanvas}
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
getGribList={() => getCurrentGribList(fileName)}
getIsCrop={getIsCrop} getIsCrop={getIsCrop}
onClick={() => setActiveGrib(fileName)} onClick={() => onGribMessageClick(fileName)}
/> />
)} )}
</ul> </ul>
+4
View File
@@ -174,6 +174,10 @@ function toInt(bytes: Uint8Array): number {
return bytes.reduce((acc, curr) => acc * 256 + curr) return bytes.reduce((acc, curr) => acc * 256 + curr)
} }
export function isWindSpeed(grib: GribMessage): boolean {
return grib.meteo.discipline===0 && grib.meteo.category===2 && grib.meteo.product===1
}
export function getFakeWindDirection(windSpeed: GribMessage): GribMessage { export function getFakeWindDirection(windSpeed: GribMessage): GribMessage {
const modifiedWindSpeed = structuredClone(windSpeed) const modifiedWindSpeed = structuredClone(windSpeed)
modifiedWindSpeed.meteo = {...modifiedWindSpeed.meteo, product: 192} modifiedWindSpeed.meteo = {...modifiedWindSpeed.meteo, product: 192}
+6
View File
@@ -26,6 +26,7 @@ export type GribMessage = {
title: string, title: string,
meteo: MeteoParam, meteo: MeteoParam,
grid: MeteoGrid, grid: MeteoGrid,
time: GribTime,
bitsPerDataPoint: number, bitsPerDataPoint: number,
subType: string, subType: string,
conversion: MeteoConversion, conversion: MeteoConversion,
@@ -37,3 +38,8 @@ export type GribSection = {
size: number, size: number,
id: number, id: number,
} }
export type GribTime = {
referenceTime: string,
forecastTime: string,
}