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]] = {
IO.blocking {
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 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\"}")
// 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
case GET -> Root / "show" / "grib-list" =>
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 =>
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 { apiHost } from '../consts'
import { GribMessage } from './interfaces'
import { fetchBuffer } from '../helpers/fetch'
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 }
export const GribFile: Component<{
name: string,
isActive: Accessor<boolean>,
getCanvas: Accessor<HTMLCanvasElement | undefined>,
setIsLoading: Setter<boolean>,
getGribList: Accessor<GribMessage[]>
getIsCrop: Accessor<boolean>,
onClick: (name: string) => void,
}> = ({
name,
isActive,
getCanvas,
setIsLoading,
getGribList,
getIsCrop,
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 cachedBuffers: Uint8Array[] = []
let cachedBitmasks: Uint8Array[] = []
const [getIsActive, setIsActive] = createSignal(false)
createEffect(async () => {
setIsLoading(true)
const cropBounds = getIsCrop() ? CROP_BOUNDS : undefined
@@ -89,13 +75,12 @@ export const GribFile: Component<{
}
return <li
class={isActive() ? styles.active : ''}
onClick={onFileClick}
class={getIsActive() ? styles.active : ''}
onClick={() => onClick(name)}
>
<div class={styles.name}>{ trimName(name) }</div>
<div class={styles.name} onClick={() => setIsActive(!getIsActive())}>{ trimName(name) }</div>
<ul class={styles.meteoParams}>
{ getGribList()
.sort((a, b) => a.title > b.title ? 1 : -1)
.map((grib, i) =>
<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 { GribFile } from './GribFile'
import { LoadingSpinner } from '../components/LoadingSpinner'
import { GribMessage } from './interfaces'
import { fetchJson } from '../helpers/fetch'
import { getFakeWindDirection, isWindSpeed } from './draw/windDirection'
export const Harmonie: Component<{}> = () => {
const [getFileList, setFileList] = createSignal<string[]>([])
const [getActiveGrib, setActiveGrib] = createSignal('')
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>()
const [getIsLoading, setIsLoading] = createSignal(true)
const [getIsCrop, setIsCrop] = createSignal(true)
const [getGribList, setGribList] = createSignal<GribMessage[]>([])
fetch(`${apiHost}/api/show/grib-list`)
.then(re => re.json())
fetchJson(`${apiHost}/api/show/grib-list`)
.then(fileList => {
fileList.sort((a: string, b: string) => a < b ? 1 : -1)
setFileList(fileList)
})
.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}>
<div class={styles.column}>
<label>
@@ -30,11 +58,11 @@ export const Harmonie: Component<{}> = () => {
{getFileList().map(fileName =>
<GribFile
name={fileName}
isActive={() => getActiveGrib() === fileName}
getCanvas={getCanvas}
setIsLoading={setIsLoading}
getGribList={() => getCurrentGribList(fileName)}
getIsCrop={getIsCrop}
onClick={() => setActiveGrib(fileName)}
onClick={() => onGribMessageClick(fileName)}
/>
)}
</ul>
+4
View File
@@ -174,6 +174,10 @@ function toInt(bytes: Uint8Array): number {
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 {
const modifiedWindSpeed = structuredClone(windSpeed)
modifiedWindSpeed.meteo = {...modifiedWindSpeed.meteo, product: 192}
+6
View File
@@ -26,6 +26,7 @@ export type GribMessage = {
title: string,
meteo: MeteoParam,
grid: MeteoGrid,
time: GribTime,
bitsPerDataPoint: number,
subType: string,
conversion: MeteoConversion,
@@ -37,3 +38,8 @@ export type GribSection = {
size: number,
id: number,
}
export type GribTime = {
referenceTime: string,
forecastTime: string,
}