Compare commits
19 Commits
main
...
front-2-fi
Author | SHA1 | Date | |
---|---|---|---|
130962a582 | |||
b3d053b06f | |||
f2224ed6b0 | |||
faa9bb3e26 | |||
325fe7c6f5 | |||
a229d96eda | |||
9602a1206c | |||
39cd0d0860 | |||
3ffc4467bb | |||
5b8de6374d | |||
eb686935d6 | |||
cd59459e43 | |||
27e4a475bd | |||
834ca2adfe | |||
58ccbf9ef9 | |||
f30661c709 | |||
7022fdb50b | |||
cf0ff194d2 | |||
737af8e01d |
@ -1,11 +1,10 @@
|
|||||||
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',
|
||||||
@ -13,37 +12,10 @@ 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()
|
||||||
def write_data(device, station, send=True):
|
console_handler.setLevel(logging.DEBUG)
|
||||||
try:
|
console_handler.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
|
||||||
#device.parse()
|
logger.addHandler(console_handler)
|
||||||
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(
|
||||||
@ -57,23 +29,25 @@ 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 = {}
|
||||||
|
|
||||||
try:
|
for port in ports:
|
||||||
ports = serial.tools.list_ports.comports()
|
if port.serial_number == '0001':
|
||||||
available_ports = {}
|
available_ports[port.name] = port.vid
|
||||||
|
|
||||||
for port in ports:
|
devices = [VantagePro(port) for port in available_ports.keys()]
|
||||||
if port.serial_number == '0001':
|
while True:
|
||||||
available_ports[port.name] = port.vid
|
for i in range(1):
|
||||||
|
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)
|
||||||
|
|
||||||
devices = [VantagePro(port) for port in available_ports.keys()]
|
# todo переписать под influx, для линухи приколы сделать
|
||||||
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
|
|
||||||
|
|
||||||
|
200
davisAPI/prediction.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
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!")
|
7
front/public/images/svg/plus.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?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>
|
After Width: | Height: | Size: 541 B |
16
front/src/api/floris/constants.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
2
front/src/api/floris/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './constants';
|
||||||
|
export * from './service';
|
14
front/src/api/floris/service.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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);
|
||||||
|
};
|
9
front/src/api/floris/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type FlorisPlot = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WindmillData = {
|
||||||
|
data: number[][];
|
||||||
|
fileName: Record<string, string>;
|
||||||
|
};
|
29
front/src/api/floris/utils.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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,6 +8,7 @@ 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';
|
||||||
@ -24,6 +25,7 @@ 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="*"
|
||||||
|
32
front/src/components/pages/floris-page/component.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
.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;
|
||||||
|
144
front/src/components/ux/floris-form/component.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
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
front/src/components/ux/floris-form/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
35
front/src/components/ux/floris-form/styles.module.scss
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.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;
|
||||||
|
}
|
17
front/src/components/ux/floris-form/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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'>;
|
24
front/src/components/ux/floris-plots/component.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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
front/src/components/ux/floris-plots/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
21
front/src/components/ux/floris-plots/styles.module.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.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;
|
||||||
|
}
|
3
front/src/components/ux/floris-plots/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type FlorisPlotsProps = {
|
||||||
|
filenames: Record<string, string>;
|
||||||
|
};
|
58
front/src/components/ux/floris-table/component.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
@ -0,0 +1,16 @@
|
|||||||
|
.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;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import { FlorisTableTurbine } from '../../types';
|
||||||
|
|
||||||
|
export type FlorisTableRowProps = {
|
||||||
|
value: FlorisTableTurbine;
|
||||||
|
onChange: (value: FlorisTableTurbine) => void;
|
||||||
|
onSelect: () => void;
|
||||||
|
selected: boolean;
|
||||||
|
};
|
1
front/src/components/ux/floris-table/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './floris-table-row';
|
32
front/src/components/ux/floris-table/styles.module.scss
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.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);
|
||||||
|
}
|
10
front/src/components/ux/floris-table/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type FlorisTableTurbine = {
|
||||||
|
x: string;
|
||||||
|
y: string;
|
||||||
|
angle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FlorisTableProps = {
|
||||||
|
value?: FlorisTableTurbine[];
|
||||||
|
onChange?: (value: FlorisTableTurbine[]) => void;
|
||||||
|
};
|
@ -1,5 +1,8 @@
|
|||||||
|
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,4 +3,5 @@ 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 },
|
||||||
];
|
];
|
||||||
|
46
front/src/components/ux/power-section/component.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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
front/src/components/ux/power-section/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './component';
|
43
front/src/components/ux/power-section/style.module.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.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);
|
||||||
|
}
|
4
front/src/components/ux/power-section/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type PowerSectionProps = {
|
||||||
|
power: number[][];
|
||||||
|
dateFrom: string;
|
||||||
|
};
|
@ -7,6 +7,7 @@ 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,4 +1,9 @@
|
|||||||
export type AppRouteName = 'turbineTypes' | 'turbineType' | 'parks' | 'park';
|
export type AppRouteName =
|
||||||
|
| '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,5 +36,4 @@ 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)
|
||||||
|
BIN
server/public/floris/0056f6b4-f82f-4523-a224-cc8f3b0a8bb9.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
server/public/floris/0078bd46-5c5e-4846-bf6a-cc7ca66f9f2a.png
Normal file
After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
server/public/floris/132a4bd3-0cf3-47d2-829c-6a1cd6bdb62f.png
Normal file
After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 16 KiB |
BIN
server/public/floris/184fb09f-4bae-42a3-9691-fad201030766.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
server/public/floris/1865d480-0673-4409-b7b8-443dbcc2a6ed.png
Normal file
After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 16 KiB |
BIN
server/public/floris/1a93a8a0-97ba-462d-b3fa-023c5fb940f2.png
Normal file
After Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 22 KiB |
BIN
server/public/floris/20afd6df-3575-4184-b53d-70e8cb99e871.png
Normal file
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
server/public/floris/21a3b1bf-39d4-4f98-abcb-f1b988593b69.png
Normal file
After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/2e4d202a-cb4d-421f-b866-c8634ece6a06.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
server/public/floris/2faae1b6-f86a-463c-a146-0fa29bcc913e.png
Normal file
After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/43eef41b-d370-4898-95b6-3eee69bb5b2a.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
server/public/floris/45fae614-3485-46aa-b073-0b03f48ddd24.png
Normal file
After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 27 KiB |
BIN
server/public/floris/4ad0d954-1fd0-4492-b9de-492b814cb125.png
Normal file
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 22 KiB |
BIN
server/public/floris/4c2f36c1-2f28-4c11-99a5-a2a3d5be8cca.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/4dfceaf3-2a70-4b93-bfb7-e49dcf2a9ae9.png
Normal file
After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 22 KiB |
BIN
server/public/floris/55cbbbf6-1b3d-41d1-87b3-efe223f15504.png
Normal file
After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
server/public/floris/609bc34a-224b-4d3c-a1fe-404fbb5e4fb0.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
server/public/floris/6172e811-ed32-484b-8e01-71fb8506ead6.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
server/public/floris/66169646-0d6a-43ef-90e0-a4abc49e63c4.png
Normal file
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB |
BIN
server/public/floris/67963f1a-1ae9-458f-a84f-32290e0e3c06.png
Normal file
After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
server/public/floris/7579a4ab-bbc5-4bef-9e21-6adf73774a78.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
server/public/floris/75b3486c-8a18-409e-8fc2-6b7dedc9acaf.png
Normal file
After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 29 KiB |
BIN
server/public/floris/7b1a29ff-e1ef-4223-a9af-67d04e034c97.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
server/public/floris/7b47a009-d93d-4b87-9dc2-1e8a9dcfe0ae.png
Normal file
After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 28 KiB |