Draw meteo values by the city on map
This commit is contained in:
@@ -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<string | undefined>,
|
||||||
|
getCities: Accessor<Set<string>>,
|
||||||
|
getField: Accessor<string>,
|
||||||
|
getStart: Accessor<Date>,
|
||||||
|
getEnd: Accessor<Date>,
|
||||||
|
}> = (props) => {
|
||||||
|
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||||
|
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>, string, Date, Date] => [
|
||||||
|
props.getCities(),
|
||||||
|
props.getField(),
|
||||||
|
props.getStart(),
|
||||||
|
props.getEnd(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [meteoValues] = createResource(citiesFieldDate, async ([cities, field, start, end]: [Set<string>, 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 <canvas ref={setCanvas} width={1000} height={570} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export const Result: Component<{
|
|||||||
const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm");
|
const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm");
|
||||||
const queryEnd = () => moment(props.getEnd()).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;
|
if (city === undefined) return undefined;
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
const response = await fetch(`${apiHost}/api/query/city/${city}/${queryStart()}-${queryEnd()}/hour/${field}/list`);
|
const response = await fetch(`${apiHost}/api/query/city/${city}/${queryStart()}-${queryEnd()}/hour/${field}/list`);
|
||||||
@@ -20,17 +20,27 @@ export const Result: Component<{
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMeteo = async (city: string | undefined) => {
|
const fetchMeteo = async ([city, getStart, getEnd]: [string | undefined, Date, Date]) => {
|
||||||
if (city === undefined) return undefined;
|
if (city === undefined) return undefined;
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
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();
|
const json = await response.json();
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cityFieldSignal = (): [string | undefined, string] => [props.getCity(), props.getField()];
|
const cityFieldDate = (): [string | undefined, string, Date, Date] => [
|
||||||
const [listResource] = createResource(cityFieldSignal, fetchList);
|
props.getCity(),
|
||||||
const [meteoResource] = createResource(props.getCity, fetchMeteo);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+31
-65
@@ -1,58 +1,37 @@
|
|||||||
import { Component, createEffect, createSignal, onMount } from "solid-js";
|
import { Component, batch, createSignal } from "solid-js";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import { cityCoords } from "./cityCoords";
|
|
||||||
import { SelectCity } from "../components/SelectCity";
|
import { SelectCity } from "../components/SelectCity";
|
||||||
|
|
||||||
import mapUrl from "../assets/map_1000x570.webp";
|
|
||||||
import "../css/station.css"
|
|
||||||
import { SelectField } from "../components/SelectField";
|
import { SelectField } from "../components/SelectField";
|
||||||
import { coordToCity } from "./coordToCity";
|
|
||||||
import { Result } from "./Result";
|
import { Result } from "./Result";
|
||||||
|
import { MapResult } from "./MapResult";
|
||||||
|
import "../css/station.css"
|
||||||
|
|
||||||
export const Station: Component<{}> = () => {
|
export const Station: Component<{}> = () => {
|
||||||
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
|
||||||
const [getImg, setImg] = createSignal(new Image());
|
|
||||||
const [getShowCities, setShowCities] = createSignal(false);
|
const [getShowCities, setShowCities] = createSignal(false);
|
||||||
const [getCities, setCities] = createSignal<Set<string>>(new Set(["Ainaži", "Rīga", "Rēzekne", "Liepāja", "Daugavpils", "Ventspils", "Madona"]));
|
const [getCities, setCities] = createSignal<Set<string>>(new Set(["Ainaži", "Rīga", "Rēzekne", "Liepāja", "Daugavpils", "Ventspils", "Madona"]));
|
||||||
const [getDate, setDate] = createSignal(moment());
|
|
||||||
const [getField, setField] = createSignal("tempMax");
|
const [getField, setField] = createSignal("tempMax");
|
||||||
const [getCity, setCity] = createSignal<string | undefined>("Rīga");
|
const [getCity, setCity] = createSignal<string | undefined>("Rīga");
|
||||||
const [getStart, setStart] = createSignal(moment().subtract(1, "days").toDate());
|
const [getStart, setStart] = createSignal(moment().subtract(1, "days").toDate());
|
||||||
const [getEnd, setEnd] = createSignal(moment().toDate());
|
const [getEnd, setEnd] = createSignal(moment().toDate());
|
||||||
|
|
||||||
let ctx: CanvasRenderingContext2D | undefined;
|
|
||||||
|
|
||||||
onMount(() => {
|
function handleDateChange(value: string) {
|
||||||
const canvas = getCanvas()!;
|
const today = moment();
|
||||||
canvas.addEventListener("click", event => {
|
const inputDate = moment(value, 'YYYY-MM-DD');
|
||||||
const rect = canvas.getBoundingClientRect();
|
if (today.isSame(inputDate, "date")) {
|
||||||
const scaleX = canvas.width / rect.width;
|
batch(() => {
|
||||||
const scaleY = canvas.height / rect.height;
|
setStart(today.subtract(1, "days").toDate());
|
||||||
|
setEnd(today.toDate());
|
||||||
const x = Math.round((event.clientX - rect.left) * scaleX);
|
});
|
||||||
const y = Math.round((event.clientY - rect.top) * scaleY);
|
} else {
|
||||||
|
batch(() => {
|
||||||
const selectedCityCoords = Object.entries(cityCoords).filter(([city]) => getCities().has(city));
|
setStart(inputDate.set({ hour: 0, minute: 0, second: 0 }).toDate());
|
||||||
const optionalCity = coordToCity(x, y, selectedCityCoords);
|
setEnd(inputDate.set({ hour: 23, minute: 59, second: 59 }).toDate());
|
||||||
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(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="stationWrapper">
|
<div class="stationWrapper">
|
||||||
@@ -64,13 +43,23 @@ export const Station: Component<{}> = () => {
|
|||||||
value="Select stations"
|
value="Select stations"
|
||||||
onClick={() => setShowCities(!getShowCities())}
|
onClick={() => setShowCities(!getShowCities())}
|
||||||
/>
|
/>
|
||||||
<input type="date" value={getDate().format("YYYY-MM-DD")} />
|
<input
|
||||||
|
type="date"
|
||||||
|
value={moment(getEnd()).format("YYYY-MM-DD")}
|
||||||
|
onChange={e => handleDateChange(e.target.value)}
|
||||||
|
/>
|
||||||
<SelectField getField={getField} setField={setField} />
|
<SelectField getField={getField} setField={setField} />
|
||||||
</div>
|
</div>
|
||||||
<div class={"cities " + (getShowCities() ? "visible" : "hidden")}>
|
<div class={"cities " + (getShowCities() ? "visible" : "hidden")}>
|
||||||
<SelectCity getCities={getCities} setCities={setCities} />
|
<SelectCity getCities={getCities} setCities={setCities} />
|
||||||
</div>
|
</div>
|
||||||
<canvas ref={setCanvas} width={1000} height={570} />
|
<MapResult
|
||||||
|
setCity={setCity}
|
||||||
|
getCities={getCities}
|
||||||
|
getField={getField}
|
||||||
|
getStart={getStart}
|
||||||
|
getEnd={getEnd}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<Result
|
<Result
|
||||||
@@ -83,27 +72,4 @@ export const Station: Component<{}> = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function drawOnMap(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
imgArr: [HTMLImageElement],
|
|
||||||
cities: Set<string>,
|
|
||||||
): 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user