diff --git a/web/src/station/MapResult.tsx b/web/src/station/MapResult.tsx new file mode 100644 index 0000000..95aee7f --- /dev/null +++ b/web/src/station/MapResult.tsx @@ -0,0 +1,118 @@ +import { Accessor, Component, Setter, createEffect, createResource, createSignal, onMount } from "solid-js"; + +import { coordToCity } from "./coordToCity"; +import { cityCoords } from "./cityCoords"; +import mapUrl from "../assets/map_1000x570.webp"; +import moment from "moment"; +import { apiHost } from "../consts"; + +export const MapResult: Component<{ + setCity: Setter, + getCities: Accessor>, + getField: Accessor, + getStart: Accessor, + getEnd: Accessor, +}> = (props) => { + const [getCanvas, setCanvas] = createSignal(); + const [getImg, setImg] = createSignal(new Image()); + + let ctx: CanvasRenderingContext2D | undefined; + + onMount(() => { + const canvas = getCanvas()!; + canvas.addEventListener("click", event => { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const x = Math.round((event.clientX - rect.left) * scaleX); + const y = Math.round((event.clientY - rect.top) * scaleY); + + const selectedCityCoords = Object.entries(cityCoords).filter(([city]) => props.getCities().has(city)); + const optionalCity = coordToCity(x, y, selectedCityCoords); + props.setCity(optionalCity); + }); + + ctx = canvas.getContext("2d")!; + + const img = new Image(); + img.onload = () => setImg(img); + img.src = mapUrl; + }); + + const citiesFieldDate = (): [Set, string, Date, Date] => [ + props.getCities(), + props.getField(), + props.getStart(), + props.getEnd(), + ]; + + const [meteoValues] = createResource(citiesFieldDate, async ([cities, field, start, end]: [Set, string, Date, Date]) => { + if (cities.size === 0) return; + const queryStart = moment(start).format("YYYYMMDD_HHmm"); + const queryEnd = moment(end).format("YYYYMMDD_HHmm"); + let aggregate: string = "avg"; + if (["tempMax", "windMax"].includes(field)) aggregate = "max"; + if (["tempMin", "visibilityMin"].includes(field)) aggregate = "min"; + if (["precipitation", "sunDuration"].includes(field)) aggregate = "sum"; + const response = await fetch(`${apiHost}/api/query/city/${[...cities].join(",")}/${queryStart}-${queryEnd}/hour/${field}/${aggregate}`); + const json = await response.json(); + return json.result; + }); + + createEffect(() => { + if (!ctx || !getImg() || !meteoValues()) return; + const values = meteoValues(); + if (!isValidResult(values)) return; + + const cityValues: [string, number | undefined][] = [...props.getCities()].map(city => [city, values[city]]); + + drawOnMap( + ctx!, + [getImg()], + cityValues, + ); + }); + + return ; +} + +function drawOnMap( + ctx: CanvasRenderingContext2D, + imgArr: [HTMLImageElement], + cityValues: [string, number | undefined][], +): void { + ctx.drawImage(imgArr[0], 0, 0); + ctx.fillStyle = "red"; + ctx.font = "18px serif"; + cityValues.forEach(([city, optionalValue]) => { + const coord = cityCoords[city]; + if (!coord) return; + + const value = optionalValue === undefined ? "" : optionalValue + ""; + + ctx.beginPath(); + // ctx.arc(coord.x, coord.y, 4, 0, 2 * Math.PI); + const cityTextSize = ctx.measureText(city); + ctx.fillText(city, coord.x - cityTextSize.width/2, coord.y - 4); + const valueTextSize = ctx.measureText(value); + ctx.fillText(value, coord.x - valueTextSize.width/2, coord.y + 14); + ctx.fill(); + // const boxWidth = 60; + // const boxHeight = 30; + // ctx.strokeRect(coord.x-boxWidth/2, coord.y-boxHeight/2, boxWidth, boxHeight); + }); +} + +function isValidResult(obj: any): obj is { [key: string]: number } { + if (typeof obj !== "object" || obj === null) { + return false; + } + + for (const key in obj) { + if (typeof key !== "string") return false; + if (typeof obj[key] !== "number") return false; + } + + return true; +} \ No newline at end of file diff --git a/web/src/station/Result.tsx b/web/src/station/Result.tsx index a77ff83..b231d09 100644 --- a/web/src/station/Result.tsx +++ b/web/src/station/Result.tsx @@ -12,7 +12,7 @@ export const Result: Component<{ const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm"); const queryEnd = () => moment(props.getEnd()).format("YYYYMMDD_HHmm"); - const fetchList = async ([city, field]: [string | undefined, string]) => { + const fetchList = async ([city, field, getStart, getEnd]: [string | undefined, string, Date, Date]) => { if (city === undefined) return undefined; await new Promise(resolve => setTimeout(resolve, 500)); const response = await fetch(`${apiHost}/api/query/city/${city}/${queryStart()}-${queryEnd()}/hour/${field}/list`); @@ -20,17 +20,27 @@ export const Result: Component<{ return json; } - const fetchMeteo = async (city: string | undefined) => { + const fetchMeteo = async ([city, getStart, getEnd]: [string | undefined, Date, Date]) => { 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 response = await fetch(`${apiHost}/api/query/city/${city}/${queryStart()}-${queryEnd()}/allFields`); const json = await response.json(); return json; } - const cityFieldSignal = (): [string | undefined, string] => [props.getCity(), props.getField()]; - const [listResource] = createResource(cityFieldSignal, fetchList); - const [meteoResource] = createResource(props.getCity, fetchMeteo); + const cityFieldDate = (): [string | undefined, string, Date, Date] => [ + props.getCity(), + props.getField(), + props.getStart(), + props.getEnd(), + ]; + const cityDate = (): [string | undefined, Date, Date] => [ + props.getCity(), + props.getStart(), + props.getEnd(), + ] + const [listResource] = createResource(cityFieldDate, fetchList); + const [meteoResource] = createResource(cityDate, fetchMeteo); return (
diff --git a/web/src/station/Station.tsx b/web/src/station/Station.tsx index 228b1fc..437c008 100644 --- a/web/src/station/Station.tsx +++ b/web/src/station/Station.tsx @@ -1,58 +1,37 @@ -import { Component, createEffect, createSignal, onMount } from "solid-js"; +import { Component, batch, createSignal } from "solid-js"; import moment from "moment"; -import { cityCoords } from "./cityCoords"; import { SelectCity } from "../components/SelectCity"; - -import mapUrl from "../assets/map_1000x570.webp"; -import "../css/station.css" import { SelectField } from "../components/SelectField"; -import { coordToCity } from "./coordToCity"; import { Result } from "./Result"; +import { MapResult } from "./MapResult"; +import "../css/station.css" export const Station: Component<{}> = () => { - const [getCanvas, setCanvas] = createSignal(); - const [getImg, setImg] = createSignal(new Image()); const [getShowCities, setShowCities] = createSignal(false); 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("Rīga"); const [getStart, setStart] = createSignal(moment().subtract(1, "days").toDate()); const [getEnd, setEnd] = createSignal(moment().toDate()); - let ctx: CanvasRenderingContext2D | undefined; + - onMount(() => { - const canvas = getCanvas()!; - canvas.addEventListener("click", event => { - const rect = canvas.getBoundingClientRect(); - const scaleX = canvas.width / rect.width; - const scaleY = canvas.height / rect.height; - - const x = Math.round((event.clientX - rect.left) * scaleX); - const y = Math.round((event.clientY - rect.top) * scaleY); - - const selectedCityCoords = Object.entries(cityCoords).filter(([city]) => getCities().has(city)); - const optionalCity = coordToCity(x, y, selectedCityCoords); - setCity(optionalCity); - }); - - ctx = canvas.getContext("2d")!; - - const img = new Image(); - img.onload = () => setImg(img); - img.src = mapUrl; - }); - - createEffect(() => { - if (!ctx || !getImg()) return; - drawOnMap( - ctx!, - [getImg()], - getCities(), - ); - }); + function handleDateChange(value: string) { + const today = moment(); + const inputDate = moment(value, 'YYYY-MM-DD'); + if (today.isSame(inputDate, "date")) { + batch(() => { + setStart(today.subtract(1, "days").toDate()); + setEnd(today.toDate()); + }); + } else { + batch(() => { + setStart(inputDate.set({ hour: 0, minute: 0, second: 0 }).toDate()); + setEnd(inputDate.set({ hour: 23, minute: 59, second: 59 }).toDate()); + }); + } + } return (
@@ -64,13 +43,23 @@ export const Station: Component<{}> = () => { value="Select stations" onClick={() => setShowCities(!getShowCities())} /> - + handleDateChange(e.target.value)} + />
- +
= () => {
); -} - -function drawOnMap( - ctx: CanvasRenderingContext2D, - imgArr: [HTMLImageElement], - cities: Set, -): void { - ctx.drawImage(imgArr[0], 0, 0); - ctx.fillStyle = "red"; - ctx.font = "18px serif"; - cities.forEach(city => { - const coord = cityCoords[city]; - if (!coord) return; - - ctx.beginPath(); - ctx.arc(coord.x, coord.y, 4, 0, 2 * Math.PI); - const cityTextSize = ctx.measureText(city); - ctx.fillText(city, coord.x - cityTextSize.width/2, coord.y - 6); - ctx.fill(); - // const boxWidth = 60; - // const boxHeight = 30; - // ctx.strokeRect(coord.x-boxWidth/2, coord.y-boxHeight/2, boxWidth, boxHeight); - }); } \ No newline at end of file