Compare commits
No commits in common. "front-2-fix-gen" and "main" have entirely different histories.
front-2-fi
...
main
@ -1,10 +1,11 @@
|
|||||||
|
from PyWeather.weather.stations.davis import VantagePro
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
|
||||||
import mariadb
|
import mariadb
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
|
import gc
|
||||||
|
import time
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
from PyWeather.weather.stations.davis import VantagePro
|
|
||||||
|
|
||||||
logging.basicConfig(filename="Stations.log",
|
logging.basicConfig(filename="Stations.log",
|
||||||
format='%(asctime)s %(message)s',
|
format='%(asctime)s %(message)s',
|
||||||
@ -12,10 +13,37 @@ logging.basicConfig(filename="Stations.log",
|
|||||||
logger = logging.getLogger('davis_api')
|
logger = logging.getLogger('davis_api')
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
console_handler = logging.StreamHandler()
|
|
||||||
console_handler.setLevel(logging.DEBUG)
|
def write_data(device, station, send=True):
|
||||||
console_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
|
try:
|
||||||
logger.addHandler(console_handler)
|
#device.parse()
|
||||||
|
data = device.fields
|
||||||
|
print(data)
|
||||||
|
if len(data) < 1:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print(data)
|
||||||
|
fields = ['BarTrend', 'CRC', 'DateStamp', 'DewPoint', 'HeatIndex', 'ETDay', 'HeatIndex',
|
||||||
|
'HumIn', 'HumOut', 'Pressure', 'RainDay', 'RainMonth', 'RainRate', 'RainStorm',
|
||||||
|
'RainYear', 'SunRise', 'SunSet', 'TempIn', 'TempOut', 'WindDir', 'WindSpeed',
|
||||||
|
'WindSpeed10Min']
|
||||||
|
|
||||||
|
if send:
|
||||||
|
placeholders = ', '.join(['%s'] * len(fields))
|
||||||
|
field_names = ', '.join(fields)
|
||||||
|
sql = f"INSERT INTO weather_data ({field_names}) VALUES ({placeholders})"
|
||||||
|
values = [data[field] for field in fields]
|
||||||
|
cursor.execute(sql, values)
|
||||||
|
conn.commit()
|
||||||
|
else:
|
||||||
|
pprint(data)
|
||||||
|
|
||||||
|
del data
|
||||||
|
del fields
|
||||||
|
gc.collect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = mariadb.connect(
|
conn = mariadb.connect(
|
||||||
@ -29,25 +57,23 @@ try:
|
|||||||
except mariadb.Error as e:
|
except mariadb.Error as e:
|
||||||
logger.error('DB_ERR: ' + str(e))
|
logger.error('DB_ERR: ' + str(e))
|
||||||
raise e
|
raise e
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
ports = serial.tools.list_ports.comports()
|
|
||||||
available_ports = {}
|
|
||||||
|
|
||||||
for port in ports:
|
try:
|
||||||
if port.serial_number == '0001':
|
ports = serial.tools.list_ports.comports()
|
||||||
available_ports[port.name] = port.vid
|
available_ports = {}
|
||||||
|
|
||||||
devices = [VantagePro(port) for port in available_ports.keys()]
|
for port in ports:
|
||||||
while True:
|
if port.serial_number == '0001':
|
||||||
for i in range(1):
|
available_ports[port.name] = port.vid
|
||||||
if len(devices) != 0:
|
|
||||||
logger.info(devices)
|
|
||||||
else:
|
|
||||||
raise Exception('Can`t connect to device')
|
|
||||||
time.sleep(60)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error('Device_error' + str(e))
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
||||||
# todo переписать под influx, для линухи приколы сделать
|
devices = [VantagePro(port) for port in available_ports.keys()]
|
||||||
|
print(available_ports)
|
||||||
|
while True:
|
||||||
|
for i in range(len(devices)):
|
||||||
|
print(devices[i].fields)
|
||||||
|
#write_data(devices[i], 'st' + str(available_ports[list(available_ports.keys())[i]]), True)
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Device_error: ' + str(e))
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@ -1,200 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import metpy.calc
|
|
||||||
import numpy as np
|
|
||||||
import requests
|
|
||||||
import torch
|
|
||||||
import xarray as xr
|
|
||||||
from aurora import AuroraSmall, Batch, Metadata
|
|
||||||
from metpy.units import units
|
|
||||||
|
|
||||||
|
|
||||||
def get_download_paths(date):
|
|
||||||
"""Создает список путей для загрузки данных."""
|
|
||||||
download_path = Path("~/downloads/hres_0.1").expanduser()
|
|
||||||
downloads = {}
|
|
||||||
var_nums = {
|
|
||||||
"2t": "167", "10u": "165", "10v": "166", "msl": "151", "t": "130",
|
|
||||||
"u": "131", "v": "132", "q": "133", "z": "129", "slt": "043", "lsm": "172",
|
|
||||||
}
|
|
||||||
for v in ["2t", "10u", "10v", "msl", "z", "slt", "lsm"]:
|
|
||||||
downloads[download_path / date.strftime(f"surf_{v}_%Y-%m-%d.grib")] = (
|
|
||||||
f"https://data.rda.ucar.edu/ds113.1/"
|
|
||||||
f"ec.oper.an.sfc/{date.year}{date.month:02d}/ec.oper.an.sfc.128_{var_nums[v]}_{v}."
|
|
||||||
f"regn1280sc.{date.year}{date.month:02d}{date.day:02d}.grb"
|
|
||||||
)
|
|
||||||
for v in ["z", "t", "u", "v", "q"]:
|
|
||||||
for hour in [0, 6, 12, 18]:
|
|
||||||
prefix = "uv" if v in {"u", "v"} else "sc"
|
|
||||||
downloads[download_path / date.strftime(f"atmos_{v}_%Y-%m-%d_{hour:02d}.grib")] = (
|
|
||||||
f"https://data.rda.ucar.edu/ds113.1/"
|
|
||||||
f"ec.oper.an.pl/{date.year}{date.month:02d}/ec.oper.an.pl.128_{var_nums[v]}_{v}."
|
|
||||||
f"regn1280{prefix}.{date.year}{date.month:02d}{date.day:02d}{hour:02d}.grb"
|
|
||||||
)
|
|
||||||
return downloads, download_path
|
|
||||||
|
|
||||||
|
|
||||||
def download_data(downloads):
|
|
||||||
"""Скачивает файлы, если они отсутствуют в целевой директории."""
|
|
||||||
for target, source in downloads.items():
|
|
||||||
if not target.exists():
|
|
||||||
print(f"Downloading {source}")
|
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
response = requests.get(source)
|
|
||||||
response.raise_for_status()
|
|
||||||
with open(target, "wb") as f:
|
|
||||||
f.write(response.content)
|
|
||||||
print("Downloads finished!")
|
|
||||||
|
|
||||||
|
|
||||||
def load_surf(v, v_in_file, download_path, date):
|
|
||||||
"""Загружает переменные поверхностного уровня или статические переменные."""
|
|
||||||
ds = xr.open_dataset(download_path / date.strftime(f"surf_{v}_%Y-%m-%d.grib"), engine="cfgrib")
|
|
||||||
data = ds[v_in_file].values[:2]
|
|
||||||
data = data[None]
|
|
||||||
return torch.from_numpy(data)
|
|
||||||
|
|
||||||
|
|
||||||
def load_atmos(v, download_path, date, levels):
|
|
||||||
"""Загружает атмосферные переменные для заданных уровней давления."""
|
|
||||||
ds_00 = xr.open_dataset(
|
|
||||||
download_path / date.strftime(f"atmos_{v}_%Y-%m-%d_00.grib"), engine="cfgrib"
|
|
||||||
)
|
|
||||||
ds_06 = xr.open_dataset(
|
|
||||||
download_path / date.strftime(f"atmos_{v}_%Y-%m-%d_06.grib"), engine="cfgrib"
|
|
||||||
)
|
|
||||||
ds_00 = ds_00[v].sel(isobaricInhPa=list(levels))
|
|
||||||
ds_06 = ds_06[v].sel(isobaricInhPa=list(levels))
|
|
||||||
data = np.stack((ds_00.values, ds_06.values), axis=0)
|
|
||||||
data = data[None]
|
|
||||||
return torch.from_numpy(data)
|
|
||||||
|
|
||||||
|
|
||||||
def create_batch(date, levels, downloads, download_path):
|
|
||||||
"""Создает объект Batch с данными для модели."""
|
|
||||||
ds = xr.open_dataset(next(iter(downloads.keys())), engine="cfgrib")
|
|
||||||
batch = Batch(
|
|
||||||
surf_vars={
|
|
||||||
"2t": load_surf("2t", "t2m", download_path, date),
|
|
||||||
"10u": load_surf("10u", "u10", download_path, date),
|
|
||||||
"10v": load_surf("10v", "v10", download_path, date),
|
|
||||||
"msl": load_surf("msl", "msl", download_path, date),
|
|
||||||
},
|
|
||||||
static_vars={
|
|
||||||
"z": load_surf("z", "z", download_path, date)[0, 0],
|
|
||||||
"slt": load_surf("slt", "slt", download_path, date)[0, 0],
|
|
||||||
"lsm": load_surf("lsm", "lsm", download_path, date)[0, 0],
|
|
||||||
},
|
|
||||||
atmos_vars={
|
|
||||||
"t": load_atmos("t", download_path, date, levels),
|
|
||||||
"u": load_atmos("u", download_path, date, levels),
|
|
||||||
"v": load_atmos("v", download_path, date, levels),
|
|
||||||
"q": load_atmos("q", download_path, date, levels),
|
|
||||||
"z": load_atmos("z", download_path, date, levels),
|
|
||||||
},
|
|
||||||
metadata=Metadata(
|
|
||||||
lat=torch.from_numpy(ds.latitude.values),
|
|
||||||
lon=torch.from_numpy(ds.longitude.values),
|
|
||||||
time=(date.replace(hour=6),),
|
|
||||||
atmos_levels=levels,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return batch.regrid(res=0.1)
|
|
||||||
|
|
||||||
|
|
||||||
def create_batch_random(levels: tuple[int], date: tuple):
|
|
||||||
"""Создает объект Batch с рандомными данными для модели."""
|
|
||||||
return Batch(
|
|
||||||
surf_vars={k: torch.randn(1, 2, 17, 32) for k in ("2t", "10u", "10v", "msl")},
|
|
||||||
static_vars={k: torch.randn(17, 32) for k in ("lsm", "z", "slt")},
|
|
||||||
atmos_vars={k: torch.randn(1, 2, 4, 17, 32) for k in ("z", "u", "v", "t", "q")},
|
|
||||||
metadata=Metadata(
|
|
||||||
lat=torch.linspace(90, -90, 17),
|
|
||||||
lon=torch.linspace(0, 360, 32 + 1)[:-1],
|
|
||||||
time=date,
|
|
||||||
atmos_levels=levels,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_model(batch):
|
|
||||||
"""Инициализирует модель AuroraSmall и выполняет предсказание."""
|
|
||||||
model = AuroraSmall()
|
|
||||||
model.load_checkpoint("microsoft/aurora", "aurora-0.25-small-pretrained.ckpt")
|
|
||||||
model.eval()
|
|
||||||
model = model.to("cpu")
|
|
||||||
with torch.inference_mode():
|
|
||||||
prediction = model.forward(batch)
|
|
||||||
return prediction
|
|
||||||
|
|
||||||
|
|
||||||
def get_wind_speed_and_direction(prediction, batch: Batch, lat: float, lon: float):
|
|
||||||
target_lat = lat
|
|
||||||
target_lon = lon
|
|
||||||
|
|
||||||
lat_idx = torch.abs(batch.metadata.lat - target_lat).argmin()
|
|
||||||
lon_idx = torch.abs(batch.metadata.lon - target_lon).argmin()
|
|
||||||
|
|
||||||
u_values = prediction.atmos_vars["u"][:, :, :, lat_idx, lon_idx]
|
|
||||||
v_values = prediction.atmos_vars["v"][:, :, :, lat_idx, lon_idx]
|
|
||||||
wind_speeds=[]
|
|
||||||
wind_directions=[]
|
|
||||||
for i in range(u_values.numel()):
|
|
||||||
u_scalar = u_values.view(-1)[i].item() # Разворачиваем тензор в одномерный и берем элемент
|
|
||||||
v_scalar = v_values.view(-1)[i].item()
|
|
||||||
|
|
||||||
print("u value:", u_scalar)
|
|
||||||
print("v value:", v_scalar)
|
|
||||||
|
|
||||||
u_with_units = u_scalar * units("m/s")
|
|
||||||
v_with_units = v_scalar * units("m/s")
|
|
||||||
|
|
||||||
# Рассчитайте направление и скорость ветра
|
|
||||||
wind_dir = metpy.calc.wind_direction(u_with_units, v_with_units)
|
|
||||||
wind_speed = metpy.calc.wind_speed(u_with_units, v_with_units)
|
|
||||||
|
|
||||||
wind_speeds.append(wind_speed.magnitude.item())
|
|
||||||
wind_directions.append(wind_dir.magnitude.item())
|
|
||||||
|
|
||||||
return wind_speeds,wind_directions
|
|
||||||
|
|
||||||
|
|
||||||
def wind_direction_to_text(wind_dir_deg):
|
|
||||||
directions = [
|
|
||||||
"север", "северо-восток", "восток", "юго-восток",
|
|
||||||
"юг", "юго-запад", "запад", "северо-запад"
|
|
||||||
]
|
|
||||||
idx = int((wind_dir_deg + 22.5) // 45) % 8
|
|
||||||
return directions[idx]
|
|
||||||
|
|
||||||
|
|
||||||
def get_weather_predict(
|
|
||||||
dates: tuple[datetime],
|
|
||||||
latitude: float,
|
|
||||||
longitude: float,
|
|
||||||
):
|
|
||||||
levels = (100,)
|
|
||||||
batch_actual = create_batch_random(levels, dates)
|
|
||||||
prediction_actual = run_model(batch_actual)
|
|
||||||
return get_wind_speed_and_direction(prediction_actual, batch_actual, latitude, longitude)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
levels = (100,)
|
|
||||||
|
|
||||||
date1 = datetime(2024, 11, 27, 12)
|
|
||||||
date2 = datetime(2024, 11, 28, 12)
|
|
||||||
date_tuple = (date1, date2,)
|
|
||||||
# downloads, download_path = get_download_paths(date)
|
|
||||||
# download_data(downloads) # Скачиваем данные, если их нет
|
|
||||||
# batch_actual = create_batch(date, levels, downloads, download_path)
|
|
||||||
batch_actual = create_batch_random(levels, date_tuple)
|
|
||||||
prediction_actual = run_model(batch_actual)
|
|
||||||
wind_speed_and_direction = get_wind_speed_and_direction(prediction_actual, batch_actual, 50, 20)
|
|
||||||
return wind_speed_and_direction
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
print("Prediction completed!")
|
|
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 13 13" style="enable-background:new 0 0 13 13;" xml:space="preserve">
|
|
||||||
<path d="M9.5,6H7V3.5C7,3.22,6.78,3,6.5,3S6,3.22,6,3.5V6H3.5C3.22,6,3,6.22,3,6.5S3.22,7,3.5,7H6v2.5C6,9.78,6.22,10,6.5,10
|
|
||||||
S7,9.78,7,9.5V7h2.5C9.78,7,10,6.78,10,6.5S9.78,6,9.5,6z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 541 B |
@ -1,16 +0,0 @@
|
|||||||
import { FlorisPlot } from './types';
|
|
||||||
|
|
||||||
export const FLORIS_ENDPOINTS = {
|
|
||||||
getWindmillData: 'api/floris/get_windmill_data',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FLORIS_PLOTS: Record<string, FlorisPlot> = {
|
|
||||||
horizontalPlane: {
|
|
||||||
name: 'horizontal_plane',
|
|
||||||
label: 'Horizontal Plane',
|
|
||||||
},
|
|
||||||
verticalPlane: {
|
|
||||||
name: 'vertical_plane',
|
|
||||||
label: 'Vertical Plane',
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './constants';
|
|
||||||
export * from './service';
|
|
@ -1,14 +0,0 @@
|
|||||||
import { api } from '@api/api';
|
|
||||||
import { FlorisFormValues } from '@components/ux/floris-form/types';
|
|
||||||
|
|
||||||
import { FLORIS_ENDPOINTS } from './constants';
|
|
||||||
import { WindmillData } from './types';
|
|
||||||
import { getWindmillDataRequestParams } from './utils';
|
|
||||||
|
|
||||||
export const getWindmillData = (formValues: Partial<FlorisFormValues>) => {
|
|
||||||
const { park } = formValues;
|
|
||||||
const params = getWindmillDataRequestParams(formValues);
|
|
||||||
const parkPath = park ? `/${park.id}/` : '';
|
|
||||||
const url = `${FLORIS_ENDPOINTS.getWindmillData}${parkPath}?${params}`;
|
|
||||||
return api.get<WindmillData>(url);
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
export type FlorisPlot = {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WindmillData = {
|
|
||||||
data: number[][];
|
|
||||||
fileName: Record<string, string>;
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
import { FlorisFormValues } from '@components/ux/floris-form/types';
|
|
||||||
|
|
||||||
import { FLORIS_PLOTS } from './constants';
|
|
||||||
|
|
||||||
export const getWindmillDataRequestParams = (
|
|
||||||
formValues: Partial<FlorisFormValues>,
|
|
||||||
) => {
|
|
||||||
let params = '';
|
|
||||||
if (formValues.turbines) {
|
|
||||||
const layoutX = formValues.turbines
|
|
||||||
?.map((row) => `layout_x=${row.x}`)
|
|
||||||
.join('&');
|
|
||||||
const layoutY = formValues.turbines
|
|
||||||
?.map((row) => `layout_y=${row.y}`)
|
|
||||||
.join('&');
|
|
||||||
const yawAngle = formValues.turbines
|
|
||||||
?.map((row) => `yaw_angle=${row.angle}`)
|
|
||||||
.join('&');
|
|
||||||
params += `${layoutX}&${layoutY}&${yawAngle}`;
|
|
||||||
}
|
|
||||||
const plots = Object.values(FLORIS_PLOTS)
|
|
||||||
.filter((_, i) => formValues.plots?.[i])
|
|
||||||
.map((p) => `plots=${p.name}`)
|
|
||||||
.join('&');
|
|
||||||
const dateStart = `date_start=${formValues.dateFrom?.substring(0, 10)}`;
|
|
||||||
const dateEnd = `date_end=${formValues.dateTo?.substring(0, 10)}`;
|
|
||||||
params += `&${plots}&${dateStart}&${dateEnd}`;
|
|
||||||
return params;
|
|
||||||
};
|
|
@ -8,7 +8,6 @@ import {
|
|||||||
TurbineTypePage,
|
TurbineTypePage,
|
||||||
TurbineTypesPage,
|
TurbineTypesPage,
|
||||||
} from '@components/pages';
|
} from '@components/pages';
|
||||||
import { FlorisPage } from '@components/pages/floris-page/component';
|
|
||||||
import { ROUTES } from '@utils/route';
|
import { ROUTES } from '@utils/route';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
@ -25,7 +24,6 @@ export function App() {
|
|||||||
<Route path={ROUTES.turbineType.path} element={<TurbineTypePage />} />
|
<Route path={ROUTES.turbineType.path} element={<TurbineTypePage />} />
|
||||||
<Route path={ROUTES.parks.path} element={<ParksPage />} />
|
<Route path={ROUTES.parks.path} element={<ParksPage />} />
|
||||||
<Route path={ROUTES.park.path} element={<ParkPage />} />
|
<Route path={ROUTES.park.path} element={<ParkPage />} />
|
||||||
<Route path={ROUTES.floris.path} element={<FlorisPage />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import { WindmillData } from '@api/floris/types';
|
|
||||||
import { Heading } from '@components/ui';
|
|
||||||
import { FlorisForm, FlorisPlots, PowerSection } from '@components/ux';
|
|
||||||
import { useRoute } from '@utils/route';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
export function FlorisPage() {
|
|
||||||
const [data, setData] = useState<WindmillData>(null);
|
|
||||||
const [dateFrom, setDateFrom] = useState<string>(null);
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const handleFormSuccess = (data: WindmillData, dateFrom: string) => {
|
|
||||||
setData(data);
|
|
||||||
console.log(data);
|
|
||||||
setDateFrom(dateFrom);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.page}>
|
|
||||||
<Heading tag="h1">{route.title}</Heading>
|
|
||||||
<FlorisForm onSuccess={handleFormSuccess} onFail={() => {}} />
|
|
||||||
{data && (
|
|
||||||
<>
|
|
||||||
<PowerSection power={data.data} dateFrom={dateFrom} />
|
|
||||||
<FlorisPlots filenames={data.fileName} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
.page {
|
|
||||||
display: grid;
|
|
||||||
padding: 40px 20px;
|
|
||||||
gap: 30px;
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
grid-template-rows: auto auto auto auto 1fr;
|
|
||||||
}
|
|
@ -8,8 +8,8 @@ import { CheckboxGroupProps } from './types';
|
|||||||
|
|
||||||
export function CheckboxGroup<T>({
|
export function CheckboxGroup<T>({
|
||||||
name,
|
name,
|
||||||
|
value,
|
||||||
items,
|
items,
|
||||||
value = items.map(() => false),
|
|
||||||
onChange,
|
onChange,
|
||||||
getItemKey,
|
getItemKey,
|
||||||
getItemLabel,
|
getItemLabel,
|
||||||
@ -19,7 +19,7 @@ export function CheckboxGroup<T>({
|
|||||||
const classNames = clsx(styles.checkBoxGroup, styles[scale]);
|
const classNames = clsx(styles.checkBoxGroup, styles[scale]);
|
||||||
|
|
||||||
const handleChange = (index: number) => {
|
const handleChange = (index: number) => {
|
||||||
onChange?.(value.with(index, !value[index]));
|
onChange(value.with(index, !value[index]));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,9 +2,9 @@ import { Scale } from '../types';
|
|||||||
|
|
||||||
export type CheckboxGroupProps<T> = {
|
export type CheckboxGroupProps<T> = {
|
||||||
name: string;
|
name: string;
|
||||||
value?: boolean[];
|
value: boolean[];
|
||||||
items: T[];
|
items: T[];
|
||||||
onChange?: (value: boolean[]) => void;
|
onChange: (value: boolean[]) => void;
|
||||||
getItemKey: (item: T) => React.Key;
|
getItemKey: (item: T) => React.Key;
|
||||||
getItemLabel: (item: T) => string;
|
getItemLabel: (item: T) => string;
|
||||||
scale?: Scale;
|
scale?: Scale;
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
import { FLORIS_PLOTS, getWindmillData } from '@api/floris';
|
|
||||||
import { getParks, Park } from '@api/wind';
|
|
||||||
import {
|
|
||||||
Autocomplete,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
CheckboxGroup,
|
|
||||||
DateInput,
|
|
||||||
Heading,
|
|
||||||
} from '@components/ui';
|
|
||||||
import { Controller, useForm } from '@utils/form';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { FlorisTable } from '../floris-table/component';
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { FlorisFormProps, FlorisFormValues } from './types';
|
|
||||||
|
|
||||||
export function FlorisForm({
|
|
||||||
onSuccess,
|
|
||||||
onFail,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: FlorisFormProps) {
|
|
||||||
const [pending, setPending] = useState<boolean>(false);
|
|
||||||
const [parks, setParks] = useState<Park[]>([]);
|
|
||||||
const [isManualEntry, setIsManualEntry] = useState<boolean>(false);
|
|
||||||
const { control, reset, getValues } = useForm<FlorisFormValues>({});
|
|
||||||
|
|
||||||
const fetchParks = async () => {
|
|
||||||
const res = await getParks();
|
|
||||||
setParks(res.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchParks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const validate = (values: Partial<FlorisFormValues>) => {
|
|
||||||
console.log(values);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const values = getValues();
|
|
||||||
if (!validate(values)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPending(true);
|
|
||||||
const { data, error } = await getWindmillData(values);
|
|
||||||
if (data) {
|
|
||||||
onSuccess(data, values.dateFrom);
|
|
||||||
} else {
|
|
||||||
onFail(error.message);
|
|
||||||
}
|
|
||||||
setPending(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetButtonClick = () => {
|
|
||||||
reset({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManulEntryCheckboxChange = (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
const { checked } = event.target;
|
|
||||||
setIsManualEntry(checked);
|
|
||||||
if (checked) {
|
|
||||||
reset({ ...getValues(), park: undefined });
|
|
||||||
} else {
|
|
||||||
reset({ ...getValues(), turbines: undefined });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className={clsx(className, styles.form)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Heading tag="h3">Turbines properties</Heading>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.part}>
|
|
||||||
<div className={styles.dateRangeBox}>
|
|
||||||
<Controller
|
|
||||||
{...control('dateFrom')}
|
|
||||||
render={(params) => <DateInput placeholder="from" {...params} />}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
{...control('dateTo')}
|
|
||||||
render={(params) => <DateInput placeholder="to" {...params} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Checkbox
|
|
||||||
label={{ text: 'Manual entry', position: 'right' }}
|
|
||||||
onChange={handleManulEntryCheckboxChange}
|
|
||||||
/>
|
|
||||||
{isManualEntry && (
|
|
||||||
<Controller
|
|
||||||
{...control('turbines')}
|
|
||||||
render={(params) => <FlorisTable {...params} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isManualEntry && (
|
|
||||||
<Controller
|
|
||||||
{...control('park')}
|
|
||||||
render={(params) => (
|
|
||||||
<Autocomplete
|
|
||||||
options={parks}
|
|
||||||
getOptionKey={(p) => p.id}
|
|
||||||
getOptionLabel={(p) => p.name}
|
|
||||||
{...params}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
{...control('plots')}
|
|
||||||
render={(params) => (
|
|
||||||
<CheckboxGroup
|
|
||||||
items={Object.values(FLORIS_PLOTS)}
|
|
||||||
getItemKey={(i) => i.name}
|
|
||||||
getItemLabel={(i) => i.label}
|
|
||||||
label="Plots"
|
|
||||||
{...params}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.buttonBox}>
|
|
||||||
<Button type="submit" pending={pending}>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={handleResetButtonClick}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './component';
|
|
@ -1,35 +0,0 @@
|
|||||||
.form {
|
|
||||||
display: grid;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: var(--clr-layer-200);
|
|
||||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
gap: 30px;
|
|
||||||
grid-template-columns: 3fr 2fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.part {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dateRangeBox {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonBox {
|
|
||||||
display: flex;
|
|
||||||
justify-content: end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { WindmillData } from '@api/floris/types';
|
|
||||||
import { Park } from '@api/wind';
|
|
||||||
|
|
||||||
import { FlorisTableTurbine } from '../floris-table/types';
|
|
||||||
|
|
||||||
export type FlorisFormValues = {
|
|
||||||
dateFrom: string;
|
|
||||||
dateTo: string;
|
|
||||||
turbines: FlorisTableTurbine[];
|
|
||||||
plots: boolean[];
|
|
||||||
park: Park;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FlorisFormProps = {
|
|
||||||
onSuccess: (response: WindmillData, dateFrom: string) => void;
|
|
||||||
onFail: (message: string) => void;
|
|
||||||
} & React.ComponentProps<'form'>;
|
|
@ -1,24 +0,0 @@
|
|||||||
import { BASE_URL } from '@api/constants';
|
|
||||||
import { FLORIS_PLOTS } from '@api/floris';
|
|
||||||
import { Heading, Span } from '@components/ui';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { FlorisPlotsProps } from './types';
|
|
||||||
|
|
||||||
export function FlorisPlots({ filenames }: FlorisPlotsProps) {
|
|
||||||
return (
|
|
||||||
<div className={styles.plots}>
|
|
||||||
<Heading tag="h3">Plots</Heading>
|
|
||||||
{Object?.keys(filenames).map((key) => {
|
|
||||||
const url = `${BASE_URL}/api/floris/download_image/${filenames[key]}`;
|
|
||||||
return (
|
|
||||||
<div className={styles.plot}>
|
|
||||||
<Span>{FLORIS_PLOTS[key]?.label ?? '???'}</Span>
|
|
||||||
<img src={url} className={styles.image} alt="Plot" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './component';
|
|
@ -1,21 +0,0 @@
|
|||||||
.plots {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: var(--clr-layer-200);
|
|
||||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plot {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export type FlorisPlotsProps = {
|
|
||||||
filenames: Record<string, string>;
|
|
||||||
};
|
|
@ -1,58 +0,0 @@
|
|||||||
import { IconButton, Span } from '@components/ui';
|
|
||||||
import DeleteIcon from '@public/images/svg/delete.svg';
|
|
||||||
import PlusIcon from '@public/images/svg/plus.svg';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { FlorisTableRow } from './components';
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { FlorisTableProps, FlorisTableTurbine } from './types';
|
|
||||||
|
|
||||||
export function FlorisTable({ value = [], onChange }: FlorisTableProps) {
|
|
||||||
const [selectedRows, setSelectedRows] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const handleDeleteButtonClick = () => {
|
|
||||||
onChange?.(value.filter((_, i) => !selectedRows[i]));
|
|
||||||
setSelectedRows({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlusButtonClick = () => {
|
|
||||||
onChange?.([...value, { x: '', y: '', angle: '' }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowChange = (index: number, turbine: FlorisTableTurbine) => {
|
|
||||||
onChange?.(value.with(index, turbine));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowSelect = (index: number) => {
|
|
||||||
const checked = !selectedRows[index];
|
|
||||||
setSelectedRows({ ...selectedRows, [index]: checked });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.table}>
|
|
||||||
<header className={styles.header}>
|
|
||||||
<Span className={styles.span} />
|
|
||||||
<Span className={styles.span}>x</Span>
|
|
||||||
<Span className={styles.span}>y</Span>
|
|
||||||
<Span className={styles.span}>angle</Span>
|
|
||||||
</header>
|
|
||||||
{value.map((v, i) => (
|
|
||||||
<FlorisTableRow
|
|
||||||
key={i}
|
|
||||||
value={v}
|
|
||||||
onChange={(turbine) => handleRowChange(i, turbine)}
|
|
||||||
onSelect={() => handleRowSelect(i)}
|
|
||||||
selected={selectedRows[i] ?? false}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<IconButton onClick={handleDeleteButtonClick}>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton onClick={handlePlusButtonClick}>
|
|
||||||
<PlusIcon />
|
|
||||||
</IconButton>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import { Checkbox, NumberInput } from '@components/ui';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { FlorisTableTurbine } from '../../types';
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
import { FlorisTableRowProps } from './types';
|
|
||||||
|
|
||||||
export function FlorisTableRow({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onSelect,
|
|
||||||
selected,
|
|
||||||
}: FlorisTableRowProps) {
|
|
||||||
const handleChange = (number: string, key: keyof FlorisTableTurbine) => {
|
|
||||||
onChange({ ...value, [key]: number });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.row}>
|
|
||||||
<Checkbox
|
|
||||||
label={{ className: styles.checkboxLabel }}
|
|
||||||
onChange={onSelect}
|
|
||||||
checked={selected}
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
value={value.x}
|
|
||||||
onChange={(number) => handleChange(number, 'x')}
|
|
||||||
wrapper={{ className: styles.textInput }}
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
value={value.y}
|
|
||||||
onChange={(number) => handleChange(number, 'y')}
|
|
||||||
wrapper={{ className: styles.textInput }}
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
value={value.angle}
|
|
||||||
onChange={(number) => handleChange(number, 'angle')}
|
|
||||||
wrapper={{ className: styles.textInput }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './component';
|
|
@ -1,16 +0,0 @@
|
|||||||
.row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxLabel {
|
|
||||||
width: 46px;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--clr-border-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.textInput {
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: var(--clr-layer-200);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { FlorisTableTurbine } from '../../types';
|
|
||||||
|
|
||||||
export type FlorisTableRowProps = {
|
|
||||||
value: FlorisTableTurbine;
|
|
||||||
onChange: (value: FlorisTableTurbine) => void;
|
|
||||||
onSelect: () => void;
|
|
||||||
selected: boolean;
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './floris-table-row';
|
|
@ -1,32 +0,0 @@
|
|||||||
.table {
|
|
||||||
border-radius: 10px;
|
|
||||||
background-color: var(--clr-layer-200);
|
|
||||||
box-shadow: 0px 2px 2px var(--clr-shadow-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 46px 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.span {
|
|
||||||
padding: 13px;
|
|
||||||
border: 1px solid var(--clr-border-200);
|
|
||||||
background-color: var(--clr-layer-300);
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid var(--clr-border-200);
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
background-color: var(--clr-layer-300);
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
export type FlorisTableTurbine = {
|
|
||||||
x: string;
|
|
||||||
y: string;
|
|
||||||
angle: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FlorisTableProps = {
|
|
||||||
value?: FlorisTableTurbine[];
|
|
||||||
onChange?: (value: FlorisTableTurbine[]) => void;
|
|
||||||
};
|
|
@ -1,8 +1,5 @@
|
|||||||
export { FlorisForm } from './floris-form';
|
|
||||||
export { FlorisPlots } from './floris-plots';
|
|
||||||
export { Header } from './header';
|
export { Header } from './header';
|
||||||
export { ParkTurbineTable } from './park-turbine-table';
|
export { ParkTurbineTable } from './park-turbine-table';
|
||||||
export { ParkTurbines } from './park-turbines';
|
export { ParkTurbines } from './park-turbines';
|
||||||
export { PowerSection } from './power-section';
|
|
||||||
export { Sidebar } from './sidebar';
|
export { Sidebar } from './sidebar';
|
||||||
export { ThemeSelect } from './theme-select';
|
export { ThemeSelect } from './theme-select';
|
||||||
|
@ -3,5 +3,4 @@ import { ROUTES } from '@utils/route';
|
|||||||
export const NAVIGATION_LINKS = [
|
export const NAVIGATION_LINKS = [
|
||||||
{ path: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.title },
|
{ path: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.title },
|
||||||
{ path: ROUTES.parks.path, title: ROUTES.parks.title },
|
{ path: ROUTES.parks.path, title: ROUTES.parks.title },
|
||||||
{ path: ROUTES.floris.path, title: ROUTES.floris.title },
|
|
||||||
];
|
];
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import { Heading, Span } from '@components/ui';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import styles from './style.module.scss';
|
|
||||||
import { PowerSectionProps } from './types';
|
|
||||||
|
|
||||||
export function PowerSection({ power, dateFrom }: PowerSectionProps) {
|
|
||||||
const gridTemplateColumns = `repeat(${power[0].length + 1}, 1fr)`;
|
|
||||||
const date = new Date(dateFrom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={styles.section}>
|
|
||||||
<Heading tag="h3">Power, watt per hour</Heading>
|
|
||||||
<div>
|
|
||||||
<div className={styles.row} style={{ gridTemplateColumns }}>
|
|
||||||
<Span className={clsx(styles.cell, styles.mainCell)}></Span>
|
|
||||||
{power[0].map((_, i) => (
|
|
||||||
<Span className={clsx(styles.cell, styles.mainCell)} key={i}>
|
|
||||||
{i + 1}
|
|
||||||
</Span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{power.map((row, r) => {
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const dateStr = `${day}.${month}.${year}`;
|
|
||||||
date.setDate(date.getDate() + 1);
|
|
||||||
return (
|
|
||||||
<div className={styles.row} style={{ gridTemplateColumns }} key={r}>
|
|
||||||
<Span className={clsx(styles.cell, styles.mainCell)}>
|
|
||||||
{dateStr}
|
|
||||||
</Span>
|
|
||||||
{row.map((value, c) => (
|
|
||||||
<Span className={styles.cell} color="t300" key={c}>
|
|
||||||
{value}
|
|
||||||
</Span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './component';
|
|
@ -1,43 +0,0 @@
|
|||||||
.section {
|
|
||||||
display: grid;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: var(--clr-layer-200);
|
|
||||||
box-shadow: 0px 1px 2px var(--clr-shadow-100);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
.cell {
|
|
||||||
&:first-of-type {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
&:last-of-type {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
.cell {
|
|
||||||
&:first-of-type {
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
|
||||||
&:last-of-type {
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cell {
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid var(--clr-border-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainCell {
|
|
||||||
background-color: var(--clr-layer-300);
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export type PowerSectionProps = {
|
|
||||||
power: number[][];
|
|
||||||
dateFrom: string;
|
|
||||||
};
|
|
@ -7,7 +7,6 @@ export const ROUTES: Record<AppRouteName, AppRoute> = {
|
|||||||
turbineType: { path: '/turbine-types/:id', title: 'Turbine Type' },
|
turbineType: { path: '/turbine-types/:id', title: 'Turbine Type' },
|
||||||
parks: { path: '/parks', title: 'Parks' },
|
parks: { path: '/parks', title: 'Parks' },
|
||||||
park: { path: '/parks/:id', title: 'Park' },
|
park: { path: '/parks/:id', title: 'Park' },
|
||||||
floris: { path: '/floris', title: 'Floris' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routeArray = Object.values(ROUTES);
|
export const routeArray = Object.values(ROUTES);
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
export type AppRouteName =
|
export type AppRouteName = 'turbineTypes' | 'turbineType' | 'parks' | 'park';
|
||||||
| 'turbineTypes'
|
|
||||||
| 'turbineType'
|
|
||||||
| 'parks'
|
|
||||||
| 'park'
|
|
||||||
| 'floris';
|
|
||||||
|
|
||||||
export type AppRoute = {
|
export type AppRoute = {
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -26,8 +26,8 @@ class OpenMeteoClient:
|
|||||||
return responses
|
return responses
|
||||||
|
|
||||||
def process_response(self, response):
|
def process_response(self, response):
|
||||||
|
# Process hourly data
|
||||||
daily = response.Daily()
|
daily = response.Daily()
|
||||||
|
|
||||||
daily_wind_speed_10m = daily.Variables(0).ValuesAsNumpy()
|
daily_wind_speed_10m = daily.Variables(0).ValuesAsNumpy()
|
||||||
daily_wind_direction_10m = daily.Variables(1).ValuesAsNumpy()
|
daily_wind_direction_10m = daily.Variables(1).ValuesAsNumpy()
|
||||||
|
|
||||||
@ -36,4 +36,5 @@ class OpenMeteoClient:
|
|||||||
def get_weather_info(self, start_date, end_date, latitude=54.35119762746125, longitude=48.389356992149345):
|
def get_weather_info(self, start_date, end_date, latitude=54.35119762746125, longitude=48.389356992149345):
|
||||||
responses = self.fetch_weather_data(latitude, longitude, start_date, end_date)
|
responses = self.fetch_weather_data(latitude, longitude, start_date, end_date)
|
||||||
response = responses[0]
|
response = responses[0]
|
||||||
|
self.process_response(response)
|
||||||
return self.process_response(response)
|
return self.process_response(response)
|
||||||
|
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 43 KiB |
BIN
server/public/floris/035a179e-5d97-4adc-9c04-ab029eac2c41.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
server/public/floris/06af7a24-8b78-4960-ab06-38d48a3f2ca3.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/0bc8cac0-7d33-4e81-8987-e83462d315d3.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/10d49437-7cfe-4916-be63-185414713dbc.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
server/public/floris/11c6c379-5bbc-4b69-b9da-dc8ddfaa9fe0.png
Normal file
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
server/public/floris/16525ebd-7740-4d48-a8cf-8623b83735b6.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 39 KiB |
BIN
server/public/floris/1a216bab-847a-47e7-b199-8ed533b8913d.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 125 KiB |
BIN
server/public/floris/1ee71613-fc85-4268-9255-749c807eb019.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
server/public/floris/201e0a6d-9f88-4cc4-a0d9-8b70cb6733c9.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 40 KiB |
BIN
server/public/floris/21355189-7f85-45d4-ab9c-0318c641cb9e.png
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 54 KiB |
BIN
server/public/floris/227aa2c8-0fe0-4c97-a76c-3b48084720d4.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/234b2ea7-23a5-4a8d-ae77-3d1ff6c9fbfe.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
server/public/floris/23eeab0b-d1c8-4878-b279-f9ab015f86d3.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
server/public/floris/25135ffe-fff1-43d0-ab21-9a106f66a9c8.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
server/public/floris/29b9f425-d835-4349-98fe-e8b8623e0882.png
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 141 KiB |
BIN
server/public/floris/3129dcf4-e51f-4506-bef7-13a25236d74d.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/31314a1a-5f60-476a-9f16-89319faac097.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
server/public/floris/32e5bb87-b1c3-4702-a5ee-c3a20531cf9b.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/355d5282-47d5-4b0b-93e0-827a079f3f5c.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
server/public/floris/3853a22d-7b97-4332-b376-9f5bf5bc9a32.png
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 39 KiB |
BIN
server/public/floris/46bd2173-0b7b-438f-a6ec-605dae8c747a.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
server/public/floris/4a493968-6e81-4c49-afc4-22912fd8113c.png
Normal file
After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 29 KiB |
BIN
server/public/floris/4b4fca7d-3d50-4c43-a882-95dcb5097dea.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 60 KiB |
BIN
server/public/floris/536f2c1f-5ba2-4132-a4f8-198b3a08049f.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 58 KiB |
BIN
server/public/floris/5b1ea13d-614f-4c4a-8eb8-f65f5966c9a6.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
server/public/floris/5cdff5db-da5c-4b34-aa5a-77e97970f0eb.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
server/public/floris/5d907710-3ac4-4807-9643-35d203c6c865.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
server/public/floris/5fa9d11b-6b0a-4446-9fee-b4e0712ed4bf.png
Normal file
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 40 KiB |
BIN
server/public/floris/666e5339-abdf-42a2-95d2-08624c4d2dd0.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
server/public/floris/676bf294-4b74-426b-b68a-9a00f17d0933.png
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 33 KiB |
BIN
server/public/floris/6fd4366c-4a95-4000-a3c5-f3d21454cbb3.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
server/public/floris/7034891f-c964-4d31-9226-7da51dbad516.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/72730cb8-c22d-4e5f-8fab-7f11efc3722c.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/72acf344-a507-42fd-b4a1-5bb645894b03.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
server/public/floris/739023a2-c499-416c-b337-688765e9d168.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
server/public/floris/75093389-2ac3-429a-90cf-83fe44891742.png
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 47 KiB |
BIN
server/public/floris/796471b9-458b-421b-9542-540d580417b4.png
Normal file
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 56 KiB |
BIN
server/public/floris/7d5a9133-93b5-4edd-9119-519f66937c66.png
Normal file
After Width: | Height: | Size: 28 KiB |