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,
|
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 |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 22 KiB |