From 65dfcf052a76b38296c1e2424dc3605502442a60 Mon Sep 17 00:00:00 2001 From: Guntis Smaukstelis Date: Fri, 12 Jan 2024 17:13:39 +0200 Subject: [PATCH] Query and display city field chart and all city weather data --- src/main/scala/db/Main.scala | 19 +++--- src/main/scala/db/PostgresService.scala | 29 +++++++++ src/main/scala/server/Server.scala | 4 ++ web/src/cities/chart/CityChart.tsx | 14 ++-- web/src/cities/chart/CityLargeChart.tsx | 14 ++-- web/src/station/Result.tsx | 85 ++++++++++++++++++++++++- web/src/station/Station.tsx | 11 +++- 7 files changed, 149 insertions(+), 27 deletions(-) diff --git a/src/main/scala/db/Main.scala b/src/main/scala/db/Main.scala index 9166234..680982e 100644 --- a/src/main/scala/db/Main.scala +++ b/src/main/scala/db/Main.scala @@ -45,22 +45,25 @@ object Main { // } yield re val formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmm") - val from = LocalDateTime.parse("20231210_0000", formatter) - val to = LocalDateTime.parse("20231214_2359", formatter) - val cities = NonEmptyList.of("Ainaži", "Rīga", "Kolka", "Vičaki") + val from = LocalDateTime.parse("20240110_1000", formatter) + val to = LocalDateTime.parse("20240111_1000", formatter) +// val cities = NonEmptyList.of("Ainaži", "Rīga", "Kolka", "Vičaki") // val cities = NonEmptyList.of("Rīga") - val query = UserQuery(cities, "tempMax", AggregateKey.List, ChronoUnit.DAYS, from, to) +// val query = UserQuery(cities, "tempMax", AggregateKey.List, ChronoUnit.DAYS, from, to) val re = for { postgresService <- PostgresService.of(xa) - re <- postgresService.query(query) + re <- postgresService.queryCityAllFields("Rīga", from, to) } yield re - val jssson = re.map(result => ResponseWrapper(result, query)) - .map(responseWrapper => responseWrapper.asJson.pretty) +// val jssson = re.map(result => ResponseWrapper(result, query)) +// .map(responseWrapper => responseWrapper.asJson.pretty) - println(jssson.unsafeRunSync()) + println(re.unsafeRunSync()) + + +// println(jssson.unsafeRunSync()) // val result = createWeatherTable(xa).unsafeRunSync() // val result = insertInWeatherTable(xa).unsafeRunSync() diff --git a/src/main/scala/db/PostgresService.scala b/src/main/scala/db/PostgresService.scala index 7224165..42fce3d 100644 --- a/src/main/scala/db/PostgresService.scala +++ b/src/main/scala/db/PostgresService.scala @@ -71,6 +71,35 @@ class PostgresService(transactor: Transactor[IO], log: Logger[IO]) { }.map(_.toMap) } + def queryCityAllFields(city: String, from: LocalDateTime, to: LocalDateTime): IO[Map[String, Option[Double]]] = { + val query = + fr"SELECT MAX(tempMax), MIN(tempMin), AVG(tempAvg), SUM(precipitation), AVG(windAvg), MAX(windMax), MIN(visibilityMin), AVG(visibilityAvg), AVG(snowAvg), AVG(atmPressure), AVG(dewPoint), AVG(humidity), SUM(sunDuration)" ++ + fr" FROM weather" ++ + fr" WHERE city = $city" ++ + fr" AND dateTime BETWEEN $from AND $to" + + query.query[(Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double], Option[Double])] + .unique + .map { case (tempMax, tempMin, tempAvg, precipitation, windAvg, windMax, visibilityMin, visibilityAvg, snowAvg, atmPressure, dewPoint, humidity, sunDuration) => + Map( + "tempMax" -> tempMax, + "tempMin" -> tempMin, + "tempAvg" -> tempAvg, + "precipitation" -> precipitation, + "windAvg" -> windAvg, + "windMax" -> windMax, + "visibilityMin" -> visibilityMin, + "visibilityAvg" -> visibilityAvg, + "snowAvg" -> snowAvg, + "atmPressure" -> atmPressure, + "dewPoint" -> dewPoint, + "humidity" -> humidity, + "sunDuration" -> sunDuration + ) + } + .transact(transactor) + } + def query(userQuery: UserQuery): IO[Map[String, Option[AggregateValue]]] = { if (userQuery.field == "phenomena") { // this handles strings // TODO query list and distinct values from phenomena diff --git a/src/main/scala/server/Server.scala b/src/main/scala/server/Server.scala index 9f9f250..b3f9d9f 100644 --- a/src/main/scala/server/Server.scala +++ b/src/main/scala/server/Server.scala @@ -58,6 +58,10 @@ class Server(postgresService: PostgresService, fetch: FetchService, log: Logger[ .map(result => ResponseWrapper(result, userQuery)) .flatMap(responseWrapper => Ok(responseWrapper.asJson.pretty)) + // http://0.0.0.0:8080/api/query/city/Kolka/20230414_2200-20230501_1230/allFields + case GET -> Root / "query" / "city" / (city: String) / DateTimeRange(from, to) / "allFields" => + postgresService.queryCityAllFields(city, from, to).flatMap(result => Ok(result.asJson)) + // http://0.0.0.0:8080/api/query/country/20230414_2200-20230501_1230/tempMax,tempMin,tempAvg,precipitation case GET -> Root / "query" / "country" / DateTimeRange(from, to) / AggFieldList(fieldList) => postgresService.queryCountry(from, to, fieldList) diff --git a/web/src/cities/chart/CityChart.tsx b/web/src/cities/chart/CityChart.tsx index 9689c71..8cbb735 100644 --- a/web/src/cities/chart/CityChart.tsx +++ b/web/src/cities/chart/CityChart.tsx @@ -6,15 +6,15 @@ import { CityLargeChart } from "./CityLargeChart"; import { createCustomChart } from "./CustomChart"; export const CityChart: Component<{ - city: string; - data: [string, number | null][]; - query: DataQuery; + city: () => string; + data: () => [string, number | null][]; + query: () => DataQuery; }> = (props) => { const [getCanvas, setCanvas] = createSignal(); const [getIsLarge, setIsLarge] = createSignal(false); let chart: Chart | undefined; - const data: [string, number | null][] = props.data.map(([dateStr, value]) => [ + const data: [string, number | null][] = props.data().map(([dateStr, value]) => [ formatDateString(dateStr), value, ]); @@ -30,9 +30,9 @@ export const CityChart: Component<{ false, timestamps, values, - props.city, - props.query.field, - props.query.granularity, + props.city(), + props.query().field, + props.query().granularity, ); }); diff --git a/web/src/cities/chart/CityLargeChart.tsx b/web/src/cities/chart/CityLargeChart.tsx index 8759d87..3f0f3e0 100644 --- a/web/src/cities/chart/CityLargeChart.tsx +++ b/web/src/cities/chart/CityLargeChart.tsx @@ -6,14 +6,14 @@ import "../../css/overlay.css" import { createCustomChart } from "./CustomChart"; export const CityLargeChart: Component<{ - city: string; - data: [string, number | null][]; - query: DataQuery; + city: () => string; + data: () => [string, number | null][]; + query: () => DataQuery; close: () => void }> = (props) => { const [getCanvas, setCanvas] = createSignal(); let chart: Chart; - const data: [string, number | null][] = props.data.map(([dateStr, value]) => [ + const data: [string, number | null][] = props.data().map(([dateStr, value]) => [ formatDateString(dateStr), value, ]); @@ -30,9 +30,9 @@ export const CityLargeChart: Component<{ true, timestamps, values, - props.city, - props.query.field, - props.query.granularity, + props.city(), + props.query().field, + props.query().granularity, ); }); diff --git a/web/src/station/Result.tsx b/web/src/station/Result.tsx index 1c6a350..9aafabb 100644 --- a/web/src/station/Result.tsx +++ b/web/src/station/Result.tsx @@ -1,9 +1,88 @@ -import { Accessor, Component } from "solid-js"; +import { Accessor, Component, createResource } from "solid-js"; +import { apiHost } from "../consts"; +import moment from "moment"; +import { CityChart } from "../cities/chart/CityChart"; + +export const Result: Component<{ + getCity: Accessor, + getField: Accessor, + getStart: Accessor, + getEnd: Accessor, +}> = (props) => { + const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm"); + const queryEnd = () => moment(props.getEnd()).format("YYYYMMDD_HHmm"); + + const fetchList = async (city: string | undefined) => { + if (city === undefined) return undefined; + await new Promise(resolve => setTimeout(resolve, 500)); + const response = await fetch(`${apiHost}/api/query/city/${props.getCity()}/${queryStart()}-${queryEnd()}/hour/${props.getField()}/list`); + const json = await response.json(); + return json; + } + + const fetchMeteo = async (city: string | undefined) => { + if (city === undefined) return undefined; + await new Promise(resolve => setTimeout(resolve, 500)); + const response = await fetch(`${apiHost}/api/query/city/${props.getCity()}/${queryStart()}-${queryEnd()}/allFields`); + const json = await response.json(); + return json; + } + + const [listResource] = createResource(props.getCity, fetchList); + const [meteoResource] = createResource(props.getCity, fetchMeteo); -export const Result: Component<{ getCity: Accessor}> = ({ getCity }) => { return (
- { getCity() } + { + props.getCity() === undefined &&
Select city!
+ } + + { /* City weather field as chart */} + { listResource.loading && ( +
+ + Loading chart +
+ )} + { listResource.error && ( +
Error while querying: ${listResource.error}
+ )} + { listResource() && listResource() instanceof Error && +
{ listResource().message }
+ } + { listResource() && props.getCity() && +
+

{ props.getCity() }

+ listResource().query.cities[0]} + data={() => listResource().result[listResource().query.cities[0]]} + query={() => listResource().query} + /> +
+ } + + { /* City all weather fields with double values */} + { meteoResource.loading && ( +
+ + Loading meteo data +
+ )} + { meteoResource.error && ( +
Error while querying: ${meteoResource.error}
+ )} + { meteoResource() && meteoResource() instanceof Error && +
{ meteoResource().message }
+ } + { meteoResource() && props.getCity() && +
    + { Object.entries(meteoResource()) + .sort((a, b) => a[0] > b[0] ? 1 : -1) + .map(([key, value]: any) => +
  • {key}: {value}
  • + )} +
+ }
); } \ No newline at end of file diff --git a/web/src/station/Station.tsx b/web/src/station/Station.tsx index d3a959c..228b1fc 100644 --- a/web/src/station/Station.tsx +++ b/web/src/station/Station.tsx @@ -17,7 +17,9 @@ export const Station: Component<{}> = () => { const [getCities, setCities] = createSignal>(new Set(["Ainaži", "Rīga", "Rēzekne", "Liepāja", "Daugavpils", "Ventspils", "Madona"])); const [getDate, setDate] = createSignal(moment()); const [getField, setField] = createSignal("tempMax"); - const [getCity, setCity] = createSignal(undefined); + const [getCity, setCity] = createSignal("Rīga"); + const [getStart, setStart] = createSignal(moment().subtract(1, "days").toDate()); + const [getEnd, setEnd] = createSignal(moment().toDate()); let ctx: CanvasRenderingContext2D | undefined; @@ -71,7 +73,12 @@ export const Station: Component<{}> = () => {
- +