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]] = {
|
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")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user