Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
eb686935d6 | |||
cd59459e43 | |||
27e4a475bd | |||
834ca2adfe | |||
58ccbf9ef9 | |||
f30661c709 |
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,
|
||||
TurbineTypesPage,
|
||||
} from '@components/pages';
|
||||
import { FlorisPage } from '@components/pages/floris-page/component';
|
||||
import { ROUTES } from '@utils/route';
|
||||
import React from 'react';
|
||||
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.parks.path} element={<ParksPage />} />
|
||||
<Route path={ROUTES.park.path} element={<ParkPage />} />
|
||||
<Route path={ROUTES.floris.path} element={<FlorisPage />} />
|
||||
</Route>
|
||||
<Route
|
||||
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>({
|
||||
name,
|
||||
value,
|
||||
items,
|
||||
value = items.map(() => false),
|
||||
onChange,
|
||||
getItemKey,
|
||||
getItemLabel,
|
||||
@ -19,7 +19,7 @@ export function CheckboxGroup<T>({
|
||||
const classNames = clsx(styles.checkBoxGroup, styles[scale]);
|
||||
|
||||
const handleChange = (index: number) => {
|
||||
onChange(value.with(index, !value[index]));
|
||||
onChange?.(value.with(index, !value[index]));
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -2,9 +2,9 @@ import { Scale } from '../types';
|
||||
|
||||
export type CheckboxGroupProps<T> = {
|
||||
name: string;
|
||||
value: boolean[];
|
||||
value?: boolean[];
|
||||
items: T[];
|
||||
onChange: (value: boolean[]) => void;
|
||||
onChange?: (value: boolean[]) => void;
|
||||
getItemKey: (item: T) => React.Key;
|
||||
getItemLabel: (item: T) => string;
|
||||
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 { ParkTurbineTable } from './park-turbine-table';
|
||||
export { ParkTurbines } from './park-turbines';
|
||||
export { PowerSection } from './power-section';
|
||||
export { Sidebar } from './sidebar';
|
||||
export { ThemeSelect } from './theme-select';
|
||||
|
@ -3,4 +3,5 @@ import { ROUTES } from '@utils/route';
|
||||
export const NAVIGATION_LINKS = [
|
||||
{ path: ROUTES.turbineTypes.path, title: ROUTES.turbineTypes.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' },
|
||||
parks: { path: '/parks', title: 'Parks' },
|
||||
park: { path: '/parks/:id', title: 'Park' },
|
||||
floris: { path: '/floris', title: 'Floris' },
|
||||
};
|
||||
|
||||
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 = {
|
||||
path: string;
|
||||
|
@ -26,8 +26,8 @@ class OpenMeteoClient:
|
||||
return responses
|
||||
|
||||
def process_response(self, response):
|
||||
# Process hourly data
|
||||
daily = response.Daily()
|
||||
|
||||
daily_wind_speed_10m = daily.Variables(0).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):
|
||||
responses = self.fetch_weather_data(latitude, longitude, start_date, end_date)
|
||||
response = responses[0]
|
||||
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 |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 22 KiB |