Query and display weather data for all country
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import moment from "moment";
|
||||
import { createSignal } from "solid-js";
|
||||
import { QueryResult } from "./result/QueryResult";
|
||||
|
||||
import { SelectCity } from "./SelectCity";
|
||||
import { SelectField } from "./SelectField";
|
||||
import { SelectGranularity } from "./SelectGranularity";
|
||||
import { SelectKey } from "./SelectKey";
|
||||
import { SelectTimeRange } from "../components/SelectTimeRange";
|
||||
|
||||
const nowRounded = new Date(new Date().setMinutes(30));
|
||||
const dayAgo = moment(nowRounded).subtract(1, "days").subtract(30, "minutes").toDate();
|
||||
|
||||
export function Cities() {
|
||||
const [getCities, setCities] = createSignal<Set<string>>(new Set([]));
|
||||
const [getStart, setStart] = createSignal(dayAgo);
|
||||
const [getEnd, setEnd] = createSignal(nowRounded);
|
||||
const [getField, setField] = createSignal("tempMax");
|
||||
const [getKey, setKey] = createSignal("max");
|
||||
const [getGranularity, setGranularity] = createSignal("hour");
|
||||
|
||||
return (
|
||||
<div class="aggregator">
|
||||
<h2>Cities</h2>
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
<SelectCity getCities={getCities} setCities={setCities} />
|
||||
</div>
|
||||
<div class="column">
|
||||
<div>
|
||||
<SelectTimeRange
|
||||
getStart={getStart}
|
||||
setStart={setStart}
|
||||
getEnd={getEnd}
|
||||
setEnd={setEnd}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SelectField getField={getField} setField={setField} />
|
||||
<SelectKey getKey={getKey} setKey={setKey} />
|
||||
<SelectGranularity getGranularity={getGranularity} setGranularity={setGranularity} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<QueryResult
|
||||
getCities={getCities}
|
||||
getStart={getStart}
|
||||
getEnd={getEnd}
|
||||
getField={getField}
|
||||
getKey={getKey}
|
||||
getGranularity={getGranularity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Accessor, Component, createEffect, Setter } from "solid-js";
|
||||
import { cityList } from "../consts";
|
||||
|
||||
export const SelectCity: Component<{
|
||||
getCities: Accessor<Set<string>>,
|
||||
setCities: Setter<Set<string>>,
|
||||
}> = ({ getCities, setCities }) => {
|
||||
function handleSelect(e: MouseEvent) {
|
||||
if (!e.target) return;
|
||||
const target = e.target as HTMLInputElement;
|
||||
const { checked: selected, value: city } = target;
|
||||
const selectedCities = new Set([...getCities()]);
|
||||
if (selected) selectedCities.add(city);
|
||||
else selectedCities.delete(city);
|
||||
|
||||
setCities(selectedCities);
|
||||
}
|
||||
|
||||
function selectAll(e: MouseEvent) {
|
||||
if (!e.target) return;
|
||||
const target = e.target as HTMLInputElement;
|
||||
const selectedCities = target.checked
|
||||
? new Set([...cityList])
|
||||
: new Set([]);
|
||||
|
||||
setCities(selectedCities);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
Select all <input
|
||||
type="checkbox"
|
||||
onClick={selectAll}
|
||||
/>
|
||||
<ul>{cityList.map(city =>
|
||||
<label>
|
||||
<li>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="city"
|
||||
value={city}
|
||||
checked={getCities().has(city)}
|
||||
onClick={handleSelect}
|
||||
/>
|
||||
{city}
|
||||
</li>
|
||||
</label>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Accessor, Component, Setter } from "solid-js";
|
||||
|
||||
import { weatherField } from "../consts";
|
||||
|
||||
export const SelectField: Component<{
|
||||
getField: Accessor<string>,
|
||||
setField: Setter<string>,
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<h4>Select field</h4>
|
||||
<select onChange={(e) => props.setField(e.target.value)}>
|
||||
{ weatherField.map(field =>
|
||||
<option
|
||||
value={field}
|
||||
selected={field === props.getField() ? true : false}
|
||||
>{field}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Accessor, Component, Setter } from "solid-js";
|
||||
|
||||
import { aggregateGranularity } from "../consts";
|
||||
|
||||
export const SelectGranularity: Component<{
|
||||
getGranularity: Accessor<string>,
|
||||
setGranularity: Setter<string>,
|
||||
}> = ({ getGranularity, setGranularity }) => {
|
||||
return (
|
||||
<div>
|
||||
<h4>Select granularity</h4>
|
||||
<select onChange={(e) => setGranularity(e.target.value)}>
|
||||
{ aggregateGranularity.map(granularity =>
|
||||
<option
|
||||
value={granularity}
|
||||
selected={granularity === getGranularity() ? true : false}
|
||||
>{granularity}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Accessor, Component, Setter } from "solid-js";
|
||||
|
||||
import { aggregateKey } from "../consts";
|
||||
|
||||
export const SelectKey: Component<{
|
||||
getKey: Accessor<string>,
|
||||
setKey: Setter<string>,
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<h4>Select key</h4>
|
||||
<ul>
|
||||
{ aggregateKey.map(key =>
|
||||
<label><li><input
|
||||
type="checkbox"
|
||||
name="aggregateKey"
|
||||
value={key}
|
||||
checked={key === props.getKey() ? true : false}
|
||||
onClick={() => props.setKey(key)}
|
||||
/>{key}</li></label>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Accessor, Component, Setter } from "solid-js";
|
||||
import { resultOrder, ResultOrderKeys } from "../consts";
|
||||
|
||||
export const SelectOrder: Component<{
|
||||
getter: Accessor<ResultOrderKeys>,
|
||||
setter: Setter<ResultOrderKeys>,
|
||||
}> = ({ getter, setter }) => {
|
||||
return (
|
||||
<div>
|
||||
Order by:
|
||||
<select onChange={e => setter(e.target.value as ResultOrderKeys)}>
|
||||
{ Object.keys(resultOrder).map(orderKey =>
|
||||
<option
|
||||
value={orderKey}
|
||||
selected={orderKey === getter()}
|
||||
>
|
||||
{orderKey}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { onMount, onCleanup, createSignal, Component } from "solid-js";
|
||||
import { Chart } from "chart.js";
|
||||
|
||||
import { DataQuery, formatDateString } from "../helpers";
|
||||
import { CityLargeChart } from "./CityLargeChart";
|
||||
import { createCustomChart } from "./CustomChart";
|
||||
|
||||
export const CityChart: Component<{
|
||||
city: string;
|
||||
data: [string, number | null][];
|
||||
query: DataQuery;
|
||||
}> = (props) => {
|
||||
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||
const [getIsLarge, setIsLarge] = createSignal(false);
|
||||
|
||||
let chart: Chart | undefined;
|
||||
const data: [string, number | null][] = props.data.map(([dateStr, value]) => [
|
||||
formatDateString(dateStr),
|
||||
value,
|
||||
]);
|
||||
|
||||
// Split the data into two arrays for the x and y axis
|
||||
const timestamps = data.map(item => item[0]);
|
||||
const values = data.map(item => item[1]);
|
||||
|
||||
onMount(() => {
|
||||
const ctx = getCanvas()!.getContext("2d")!;
|
||||
chart = createCustomChart(
|
||||
ctx,
|
||||
false,
|
||||
timestamps,
|
||||
values,
|
||||
props.city,
|
||||
props.query.field,
|
||||
props.query.granularity,
|
||||
);
|
||||
});
|
||||
|
||||
function handleCanvasClick() {
|
||||
setIsLarge(!getIsLarge());
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
chart?.destroy();
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<canvas ref={setCanvas} onClick={handleCanvasClick} />
|
||||
<div style={{ display: getIsLarge() ? "block" : "none" }}>
|
||||
<CityLargeChart city={props.city} data={props.data} query={props.query} close={() => setIsLarge(false)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { onMount, createSignal, Component } from "solid-js";
|
||||
import { Chart } from "chart.js";
|
||||
|
||||
import { DataQuery, formatDateString } from "../helpers";
|
||||
import "../../css/overlay.css"
|
||||
import { createCustomChart } from "./CustomChart";
|
||||
|
||||
export const CityLargeChart: Component<{
|
||||
city: string;
|
||||
data: [string, number | null][];
|
||||
query: DataQuery;
|
||||
close: () => void
|
||||
}> = (props) => {
|
||||
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||
let chart: Chart;
|
||||
const data: [string, number | null][] = props.data.map(([dateStr, value]) => [
|
||||
formatDateString(dateStr),
|
||||
value,
|
||||
]);
|
||||
|
||||
// Split the data into two arrays for the x and y axis
|
||||
const timestamps = data.map(item => item[0]);
|
||||
const values = data.map(item => item[1]);
|
||||
|
||||
onMount(() => {
|
||||
const ctx = getCanvas()!.getContext('2d')!;
|
||||
|
||||
chart = createCustomChart(
|
||||
ctx,
|
||||
true,
|
||||
timestamps,
|
||||
values,
|
||||
props.city,
|
||||
props.query.field,
|
||||
props.query.granularity,
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="overlay" onClick={props.close}>
|
||||
<div class="overlay-content">
|
||||
<canvas ref={setCanvas} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Component } from "solid-js";
|
||||
import { CityChart } from "./CityChart";
|
||||
import { DataQuery } from "../helpers";
|
||||
|
||||
export const CityResult: Component<{ city: string, query: DataQuery, result: any }> = ({ city, query, result }) => {
|
||||
return (
|
||||
<div class="result-item">
|
||||
<h4>{ city }</h4>
|
||||
{
|
||||
isDateNumber(result)
|
||||
? <CityChart city={city} data={result} query={query}/>
|
||||
: result
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isDateNumber(value: unknown): value is [string, number | null][] {
|
||||
if (!Array.isArray(value)) return false;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
let item = value[i];
|
||||
if (
|
||||
!Array.isArray(item)
|
||||
|| item.length !== 2
|
||||
|| typeof item[0] !== "string"
|
||||
|| (typeof item[1] !== "number" && item[1] !== null)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
BarElement,
|
||||
BarController,
|
||||
CategoryScale,
|
||||
Chart,
|
||||
LinearScale,
|
||||
LineController,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
} from "chart.js";
|
||||
|
||||
// Register chart elements which will be used
|
||||
Chart.register(
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
CategoryScale,
|
||||
);
|
||||
|
||||
const barChartData = ["precipitation", "sunDuration", "snowAvg"];
|
||||
|
||||
export function createCustomChart(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
showBig: boolean,
|
||||
timestamps: string[],
|
||||
values: Array<number | null>,
|
||||
city: string,
|
||||
field: string,
|
||||
granularity: string,
|
||||
): Chart {
|
||||
const dataConfig = {
|
||||
labels: timestamps,
|
||||
datasets: [{
|
||||
label: field,
|
||||
data: values,
|
||||
fill: false,
|
||||
borderColor: "rgb(75, 192, 192)",
|
||||
backgroundColor: "rgb(75, 192, 192)",
|
||||
tension: 0.1,
|
||||
}]
|
||||
};
|
||||
|
||||
const optionsConfig = {
|
||||
plugins: {
|
||||
title: {
|
||||
display: showBig,
|
||||
text: city,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: showBig,
|
||||
text: granularity,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: showBig,
|
||||
text: field,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!barChartData.includes(field)) {
|
||||
dataConfig.datasets[0].backgroundColor = "#FFFFFF";
|
||||
}
|
||||
|
||||
return new Chart(ctx, {
|
||||
type: barChartData.includes(field) ? "bar" : "line",
|
||||
data: dataConfig,
|
||||
options: {
|
||||
...optionsConfig,
|
||||
animation: false,
|
||||
spanGaps: false,
|
||||
},
|
||||
plugins: [ canvas_bg_plugin ],
|
||||
});
|
||||
}
|
||||
|
||||
const canvas_bg_plugin = {
|
||||
id: "canvas_bg_plugin",
|
||||
beforeDraw: (chart: any, args: any, options: any) => {
|
||||
const { ctx } = chart;
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import moment from "moment";
|
||||
|
||||
export function formatDateString(str: string): string {
|
||||
const date = moment(new Date(str));
|
||||
switch (str.length) {
|
||||
// year: 2023
|
||||
case 4: return date.format("yyyy");
|
||||
// year-month: 2023-06
|
||||
case 7: return date.format("yyyy-MM");
|
||||
// year-month-day: 2023-06-23
|
||||
case 10: return date.format("yyyy-MM-DD");
|
||||
// year-month-day hour:minute 2023-06-23T23:59
|
||||
default: return date.format("HH:mm");
|
||||
}
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
query: DataQuery;
|
||||
result: {[key: string]: any};
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
cities: string[];
|
||||
field: string;
|
||||
granularity: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export function isQueryResult(variable: any): variable is QueryResult {
|
||||
return variable
|
||||
&& variable.query
|
||||
&& isDataQuery(variable.query)
|
||||
&& variable.result
|
||||
&& (
|
||||
typeof variable.result === "object"
|
||||
&& !Array.isArray(variable.result)
|
||||
&& variable.result !== null
|
||||
);
|
||||
}
|
||||
|
||||
export function isDataQuery(variable: any): variable is DataQuery {
|
||||
return variable &&
|
||||
Array.isArray(variable.cities) &&
|
||||
typeof variable.field === 'string' &&
|
||||
typeof variable.granularity === 'string' &&
|
||||
typeof variable.key === 'string';
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Component, createEffect, createSignal, onMount } from "solid-js";
|
||||
|
||||
import { ResultKeyVal } from "../../consts";
|
||||
import { cityCoords } from "./cityCoords";
|
||||
import { drawRotatedImage, getAngleFromString } from "./windAngles";
|
||||
import { WindInputs, WindSignals } from "./WindInputs";
|
||||
|
||||
import mapUrl from "../../assets/map_1920x1080.webp";
|
||||
import arrowUrl from "../../assets/arrow.webp";
|
||||
|
||||
export const MapView: Component<{ data: () => ResultKeyVal[] }> = ({ data }) => {
|
||||
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||
const [getImg, setImg] = createSignal(new Image());
|
||||
let lastCoords: [string, {x: number, y: number }, number][] = [];
|
||||
|
||||
const arrowImg = new Image();
|
||||
arrowImg.src = arrowUrl;
|
||||
|
||||
const windSignals: WindSignals = {
|
||||
direction: createSignal(""),
|
||||
speed: createSignal(""),
|
||||
gusts: createSignal(""),
|
||||
roundValues: createSignal(false),
|
||||
}
|
||||
|
||||
function getWindData(): [string, string, string, boolean] {
|
||||
return [
|
||||
windSignals.direction[0](),
|
||||
windSignals.speed[0](),
|
||||
windSignals.gusts[0](),
|
||||
windSignals.roundValues[0](),
|
||||
];
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const ctx = getCanvas()!.getContext("2d")!;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImg(img);
|
||||
drawOnMap(
|
||||
ctx,
|
||||
[img, arrowImg],
|
||||
lastCoords,
|
||||
getWindData(),
|
||||
);
|
||||
};
|
||||
img.src = mapUrl;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const ctx = getCanvas()!.getContext("2d")!;
|
||||
const coordsAndData: [string, {x: number, y: number }, number][] = data()
|
||||
.map(([city, value]) => [
|
||||
city,
|
||||
cityCoords[city as keyof typeof cityCoords],
|
||||
typeof value === "number" ? value : -99,
|
||||
]);
|
||||
lastCoords = coordsAndData;
|
||||
drawOnMap(
|
||||
ctx,
|
||||
[getImg(),arrowImg],
|
||||
coordsAndData,
|
||||
getWindData(),
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<WindInputs signals={windSignals} />
|
||||
<canvas ref={setCanvas} width="1920px" height="1080px" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function drawOnMap(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
imgArr: [HTMLImageElement, HTMLImageElement],
|
||||
coords: [string, {x: number, y: number }, number][],
|
||||
windData: [string, string, string, boolean],
|
||||
): void {
|
||||
const size = 80;
|
||||
|
||||
const [windDirection, windSpeed, windGusts, roundValues] = windData;
|
||||
const [bgImg, arrowImg] = imgArr;
|
||||
|
||||
// bg
|
||||
ctx.drawImage(bgImg, 0, 0);
|
||||
|
||||
// city boxes
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
coords.forEach(([, {x, y}, value]) => {
|
||||
ctx.rect(x, y, size, size);
|
||||
});
|
||||
ctx.fill();
|
||||
|
||||
// weather values
|
||||
ctx.font = "bold 30px Rubik";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = "#000000";
|
||||
coords.forEach(([, {x, y}, value]) => {
|
||||
ctx.rect(x, y, size, size);
|
||||
const numericValue = roundValues ? Math.round(value) : value;
|
||||
const stringValue = numericValue.toString().replace(".", ",");
|
||||
ctx.fillText(stringValue, x + size/2, y + size/2);
|
||||
});
|
||||
|
||||
// wind values
|
||||
ctx.font = "bold 45px Rubik";
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
const boxMiddleX = 1675;
|
||||
|
||||
const directionWidth = ctx.measureText(windDirection).width;
|
||||
ctx.fillText(windDirection, boxMiddleX - directionWidth / 2 + 30, 370);
|
||||
const angleInDegrees = getAngleFromString(windDirection);
|
||||
drawRotatedImage(ctx, arrowImg, boxMiddleX - directionWidth / 2 - 30, 370, angleInDegrees);
|
||||
|
||||
const speedWidth = ctx.measureText(windSpeed).width;
|
||||
ctx.fillText(windSpeed, boxMiddleX - speedWidth / 2 - 30, 575);
|
||||
ctx.font = "bold 25px Rubik";
|
||||
ctx.fillText("M/S", boxMiddleX + speedWidth / 2 - 22, 590);
|
||||
|
||||
ctx.font = "bold 45px Rubik";
|
||||
const gustsWidth = ctx.measureText(windGusts).width;
|
||||
ctx.fillText(windGusts, boxMiddleX - gustsWidth / 2 - 30, 790);
|
||||
ctx.font = "bold 25px Rubik";
|
||||
ctx.fillText("M/S", boxMiddleX + gustsWidth / 2 - 22, 805);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, Signal } from "solid-js";
|
||||
|
||||
export interface WindSignals {
|
||||
direction: Signal<string>;
|
||||
speed: Signal<string>;
|
||||
gusts: Signal<string>;
|
||||
roundValues: Signal<boolean>;
|
||||
}
|
||||
|
||||
export const WindInputs: Component<{signals: WindSignals}> = (
|
||||
{ signals: {
|
||||
direction: [getDirection, setDirection],
|
||||
speed: [getSpeed, setSpeed],
|
||||
gusts: [getGusts, setGusts],
|
||||
roundValues: [getRoundValues, setRoundValues],
|
||||
}}
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={getRoundValues()}
|
||||
onChange={e => setRoundValues(e.target.checked)}
|
||||
/>
|
||||
Round values
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
onInput={e => setDirection(e.target.value)}
|
||||
/>
|
||||
Wind direction
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
onInput={e => setSpeed(e.target.value)}
|
||||
/>
|
||||
Wind speed
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
onInput={e => setGusts(e.target.value)}
|
||||
/>
|
||||
Gusts
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export const cityCoords = {
|
||||
"Ainaži": { x: 685, y: 146 },
|
||||
"Alūksne": { x: 1176, y: 294 },
|
||||
"Bauska": { x: 721, y: 608 },
|
||||
"Dagda": { x: 1280, y: 680 },
|
||||
"Daugavgrīva": { x: 640, y: 410 },
|
||||
"Daugavpils": { x: 1140, y: 739 },
|
||||
"Dobele": { x: 550, y: 580 },
|
||||
"Gulbene": { x: 1100, y: 370 },
|
||||
"Jelgava": { x: 620, y: 570 },
|
||||
"Kalnciems": { x: 600, y: 500 },
|
||||
"Kolka": { x: 380, y: 200 },
|
||||
"Kuldīga": { x: 300, y: 470 },
|
||||
"Lielpēči": { x: 780, y: 490 },
|
||||
"Liepāja": { x: 124, y: 638 },
|
||||
"Madona": { x: 1004, y: 432 },
|
||||
"Mērsrags": { x: 480, y: 300 },
|
||||
"Pāvilosta": { x: 160, y: 450 },
|
||||
"Piedruja": { x: 1290, y: 770 },
|
||||
"Priekuļi": { x: 900, y: 320 },
|
||||
"Rēzekne": { x: 1229, y: 544 },
|
||||
"Rīga": { x: 689, y: 430 },
|
||||
"Rucava": { x: 140, y: 710 },
|
||||
"Rūjiena": { x: 870, y: 140 },
|
||||
"Saldus": { x: 383, y: 561 },
|
||||
"Sigulda": { x: 810, y: 380 },
|
||||
"Sīļi": { x: 1050, y: 640 },
|
||||
"Skrīveri": { x: 880, y: 530 },
|
||||
"Skulte": { x: 710, y: 320 },
|
||||
"Stende": { x: 402, y: 400 },
|
||||
"Ventspils": { x: 210, y: 329 },
|
||||
"Vičaki": { x: 270, y: 260},
|
||||
"Zīlāni": { x: 983, y: 581 },
|
||||
"Zosēni": { x: 970, y: 370 },
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
const windAngles: {[key: string]: number} = {
|
||||
"R": 0,
|
||||
"ZR": 45,
|
||||
"Z": 90,
|
||||
"ZA": 135,
|
||||
"A": 180,
|
||||
"DA": 225,
|
||||
"D": 270,
|
||||
"DR": 315,
|
||||
}
|
||||
|
||||
export function getAngleFromString(str: string): number {
|
||||
return windAngles[str] ?? 0;
|
||||
}
|
||||
|
||||
export function drawRotatedImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
img: HTMLImageElement,
|
||||
x: number,
|
||||
y: number,
|
||||
angleInDegrees: number): void {
|
||||
const angleInRadians = angleInDegrees * (Math.PI / 180);
|
||||
ctx.translate(x + img.width / 2, y + img.height / 2);
|
||||
ctx.rotate(angleInRadians);
|
||||
ctx.drawImage(img, -img.width / 2, -img.height / 2, img.width, img.height);
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import moment from "moment";
|
||||
import { Accessor, Component, createResource, createSignal } from "solid-js";
|
||||
|
||||
import "../../css/Result.css"
|
||||
import { apiHost } from "../../consts";
|
||||
import { isQueryResult } from "../helpers";
|
||||
import { Result } from "./Result";
|
||||
|
||||
export const QueryResult: Component<{
|
||||
getCities: Accessor<Set<string>>,
|
||||
getStart: Accessor<Date>,
|
||||
getEnd: Accessor<Date>,
|
||||
getField: Accessor<string>,
|
||||
getKey: Accessor<string>,
|
||||
getGranularity: Accessor<string>,
|
||||
}> = (props) => {
|
||||
const cities = () => [...props.getCities()].join(",");
|
||||
const queryStart = () => moment(props.getStart()).format("YYYYMMDD_HHmm");
|
||||
const queryEnd = () => moment(props.getEnd()).format("YYYYMMDD_HHmm");
|
||||
const [getTimestamp, setTimestamp] = createSignal<number>(0);
|
||||
|
||||
const fetchQuery = async (timestamp: number) => {
|
||||
if (cities() === "") return new Error("ERROR: Select cities!");
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
const response = await fetch(`${apiHost}/api/query/city/${cities()}/${queryStart()}-${queryEnd()}/${props.getGranularity()}/${props.getField()}/${props.getKey()}`);
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
const [queryResource] = createResource(getTimestamp, fetchQuery);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="button"
|
||||
class="primary"
|
||||
value="Query Data"
|
||||
onClick={() => setTimestamp(Date.now())}
|
||||
/>
|
||||
{ queryResource.loading && (
|
||||
<div>
|
||||
<span class="spinner"></span>
|
||||
<span style={{ "padding-left": "16px" }}>Loading query</span>
|
||||
</div>
|
||||
)}
|
||||
{ queryResource.error && (
|
||||
<div>Error while querying: ${queryResource.error}</div>
|
||||
)}
|
||||
{ queryResource() && queryResource() instanceof Error &&
|
||||
<div>{ queryResource().message }</div>
|
||||
}
|
||||
{ queryResource() && isQueryResult( queryResource() ) &&
|
||||
<Result result={queryResource} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Component, createSignal, Resource } from "solid-js";
|
||||
|
||||
import "../../css/Result.css"
|
||||
import { ResultKeyVal, resultOrder, ResultOrderKeys } from "../../consts";
|
||||
import { SelectOrder } from "../SelectOrder";
|
||||
import { CityResult } from "../chart/CityResult";
|
||||
import { QueryResult } from "../helpers";
|
||||
import { MapView } from "../map/MapView";
|
||||
|
||||
export const Result: Component<{
|
||||
result: Resource<QueryResult>
|
||||
}> = ({ result: resultResource }) => {
|
||||
const [getOrderKey, setOrderKey] = createSignal<ResultOrderKeys>("A -> Z");
|
||||
type ResultView = "text" | "map";
|
||||
const [getResultView, setResultView] = createSignal<ResultView>("text");
|
||||
|
||||
const cityData = () =>
|
||||
Object.entries(resultResource()!.result)
|
||||
.map(keyVal => [...keyVal] as ResultKeyVal)
|
||||
.sort((a, b) => resultOrder[getOrderKey()](a, b));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="resultTitle">
|
||||
<h3>Result:</h3>
|
||||
<span>
|
||||
<input
|
||||
type="button"
|
||||
class="secondary"
|
||||
value="text"
|
||||
onClick={() => setResultView("text")}
|
||||
/>
|
||||
|
|
||||
<input
|
||||
type="button"
|
||||
class="secondary"
|
||||
value="map"
|
||||
onClick={() => setResultView("map")}
|
||||
/>
|
||||
</span>
|
||||
<span style={{ visibility: getResultView() === "text" ? "visible" : "hidden" }}>
|
||||
<SelectOrder getter={getOrderKey} setter={setOrderKey} />
|
||||
</span>
|
||||
</div>
|
||||
<div class={getResultView() === "text" ? "result-container" : ""}>
|
||||
{ getResultView() === "text"
|
||||
? cityData().map(([city, data]) =>
|
||||
<CityResult
|
||||
city={city}
|
||||
query={resultResource()!.query}
|
||||
result={data}
|
||||
/>)
|
||||
: <MapView data={cityData} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user