Download structure for all files in one go. For better UX
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user