mirror of
https://github.com/XFox111/MuiCharts.git
synced 2026-04-22 06:51:05 +03:00
- Implemented actual API for frontend
- Reorganized and refactored frontend project
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=http://localhost:5152
|
||||||
@@ -32,7 +32,7 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-unused-vars": "warn",
|
"@typescript-eslint/no-unused-vars": "warn",
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
|
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
"@typescript-eslint/no-unused-vars": "warn"
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Use the official Node.js 20 image as the base image
|
# Use the official Node.js 20 image as the base image
|
||||||
FROM node:20 as builder
|
FROM node:20 as builder
|
||||||
|
|
||||||
|
ARG API_URL=http://localhost:5152
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ RUN yarn install
|
|||||||
# Copy the app source code to the working directory
|
# Copy the app source code to the working directory
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo "VITE_API_URL=${API_URL}" > .env.production
|
||||||
|
|
||||||
# Build the app
|
# Build the app
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/d3-scale": "^4.0.8",
|
"@types/d3-scale": "^4.0.8",
|
||||||
|
"@types/node": "^20.11.20",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
|||||||
+70
-23
@@ -4,9 +4,10 @@ import { useEffect, useState } from "react";
|
|||||||
import useStyles from "./App.styles";
|
import useStyles from "./App.styles";
|
||||||
import ChartSkeleton from "./Components/ChartSkeleton";
|
import ChartSkeleton from "./Components/ChartSkeleton";
|
||||||
import TrackChart from "./Components/TrackChart";
|
import TrackChart from "./Components/TrackChart";
|
||||||
import IPoint from "./Data/IPoint";
|
import ApiEndpoints from "./Data/Api/ApiEndpoints";
|
||||||
import ITrack from "./Data/ITrack";
|
import IPoint from "./Data/Api/Models/IPoint";
|
||||||
import LoadMockData from "./Data/LoadMockData";
|
import ITrack from "./Data/Api/Models/ITrack";
|
||||||
|
import { GeneratePoints, GenerateTracks } from "./Data/MockDataGenerator";
|
||||||
|
|
||||||
const theme: Theme = createTheme();
|
const theme: Theme = createTheme();
|
||||||
|
|
||||||
@@ -20,25 +21,68 @@ function App(): JSX.Element
|
|||||||
const [zoom, setZoom] = useState<number[]>([0, 1]);
|
const [zoom, setZoom] = useState<number[]>([0, 1]);
|
||||||
const sx = useStyles();
|
const sx = useStyles();
|
||||||
|
|
||||||
const loadData = () =>
|
|
||||||
{
|
|
||||||
setLoading(true);
|
|
||||||
console.log("Loading data...");
|
|
||||||
const newData = LoadMockData(true);
|
|
||||||
setData(newData);
|
|
||||||
setZoom([0, Math.min(newData.tracks.length + 1, 20)]);
|
|
||||||
|
|
||||||
new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
.then(() => setLoading(false))
|
|
||||||
.catch(console.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
loadData();
|
void LoadDataAsync();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleZoomChange = (_: unknown, newValue: number | number[]) =>
|
async function LoadDataAsync(): Promise<void>
|
||||||
|
{
|
||||||
|
setLoading(true);
|
||||||
|
const { Points, Tracks } = new ApiEndpoints();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const tracks: ITrack[] = await Tracks.GetAllTracksAsync();
|
||||||
|
let points: IPoint[] = [];
|
||||||
|
|
||||||
|
if (tracks.length > 0)
|
||||||
|
points = await Points.GetPointsArrayAsync([
|
||||||
|
...tracks.map(t => t.firstId),
|
||||||
|
tracks[tracks.length - 1].secondId
|
||||||
|
]);
|
||||||
|
|
||||||
|
setData({ tracks, points });
|
||||||
|
setZoom([0, Math.min(tracks.length + 1, 20)]);
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error("Failed to load data:", error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
console.log("Data loaded")
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function RecreateDataAsync(): Promise<void>
|
||||||
|
{
|
||||||
|
setLoading(true);
|
||||||
|
const { Points, Tracks } = new ApiEndpoints();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const points: IPoint[] = GeneratePoints(120);
|
||||||
|
const tracks: ITrack[] = GenerateTracks(points);
|
||||||
|
|
||||||
|
await Points.ImportPointsAsync(points);
|
||||||
|
await Tracks.ImportTracksAsync(tracks);
|
||||||
|
|
||||||
|
await LoadDataAsync();
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error("Failed to recreate data:", error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
console.log("Data recreated")
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function OnZoomChange(_: unknown, newValue: number | number[]): void
|
||||||
{
|
{
|
||||||
const value: number[] = newValue as number[];
|
const value: number[] = newValue as number[];
|
||||||
|
|
||||||
@@ -46,10 +90,13 @@ function App(): JSX.Element
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (value[1] - value[0] > 50)
|
if (value[1] - value[0] > 50)
|
||||||
|
{
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setTimeout(() => setLoading(false), 500);
|
||||||
|
}
|
||||||
|
|
||||||
setZoom(value);
|
setZoom(value);
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={ theme }>
|
<ThemeProvider theme={ theme }>
|
||||||
@@ -58,21 +105,21 @@ function App(): JSX.Element
|
|||||||
<Box sx={ sx.root }>
|
<Box sx={ sx.root }>
|
||||||
<TrackChart
|
<TrackChart
|
||||||
tracks={ data.tracks } points={ data.points }
|
tracks={ data.tracks } points={ data.points }
|
||||||
zoom={ zoom } onProcessingComplete={ () => setLoading(false) } />
|
zoom={ zoom } />
|
||||||
|
|
||||||
<Container sx={ sx.controls }>
|
<Container sx={ sx.controls }>
|
||||||
{ !isLoading &&
|
{ !isLoading &&
|
||||||
<Slider
|
<Slider
|
||||||
min={ 0 } max={ data.tracks.length + 1 }
|
min={ 0 } max={ data.tracks.length + 1 }
|
||||||
defaultValue={ zoom } onChangeCommitted={ handleZoomChange }
|
defaultValue={ zoom } onChangeCommitted={ OnZoomChange }
|
||||||
valueLabelDisplay="auto" />
|
valueLabelDisplay="auto" />
|
||||||
}
|
}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained" color="inherit" endIcon={ <RefreshIcon /> }
|
variant="contained" color="inherit" endIcon={ <RefreshIcon /> }
|
||||||
onClick={ loadData } disabled={ isLoading }>
|
onClick={ () => void RecreateDataAsync() } disabled={ isLoading }>
|
||||||
|
|
||||||
Refresh
|
Recreate
|
||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { SxProps } from "@mui/system";
|
import { SxProps } from "@mui/system";
|
||||||
import * as xc from "@mui/x-charts";
|
import * as xc from "@mui/x-charts";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import IPoint from "../Data/Api/Models/IPoint";
|
||||||
|
import MaxSpeed from "../Data/Api/Models/MaxSpeed";
|
||||||
import IChartPoint from "../Data/IChartPoint";
|
import IChartPoint from "../Data/IChartPoint";
|
||||||
import IPoint from "../Data/IPoint";
|
import ITrack from "../Data/Api/Models/ITrack";
|
||||||
import ITrack from "../Data/ITrack";
|
|
||||||
import MaxSpeed from "../Data/MaxSpeed";
|
|
||||||
import { TooltipProps, TracklineSeries } from "../Data/TrackChartDataProps";
|
import { TooltipProps, TracklineSeries } from "../Data/TrackChartDataProps";
|
||||||
import CartesianGrid from "./CartesianGrid";
|
import CartesianGrid from "./CartesianGrid";
|
||||||
import TrackLinePlot from "./TrackLinePlot";
|
import TrackLinePlot from "./TrackLinePlot";
|
||||||
@@ -18,14 +18,12 @@ interface IProps
|
|||||||
points: IPoint[];
|
points: IPoint[];
|
||||||
/** The zoom levels (start, end). */
|
/** The zoom levels (start, end). */
|
||||||
zoom: number[];
|
zoom: number[];
|
||||||
/** A callback for when the processing is complete. */
|
|
||||||
onProcessingComplete?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chart of the track.
|
* A chart of the track.
|
||||||
*/
|
*/
|
||||||
function TrackChart({ tracks, points, zoom, ...props }: IProps): JSX.Element
|
function TrackChart({ tracks, points, zoom }: IProps): JSX.Element
|
||||||
{
|
{
|
||||||
const [dataset, setDataset] = useState<IChartPoint[]>([]);
|
const [dataset, setDataset] = useState<IChartPoint[]>([]);
|
||||||
const [xTicks, setXTicks] = useState<number[]>([]);
|
const [xTicks, setXTicks] = useState<number[]>([]);
|
||||||
@@ -84,10 +82,7 @@ function TrackChart({ tracks, points, zoom, ...props }: IProps): JSX.Element
|
|||||||
setXTicks(data.map(i => i.distance));
|
setXTicks(data.map(i => i.distance));
|
||||||
|
|
||||||
console.warn("Reflow!");
|
console.warn("Reflow!");
|
||||||
|
}, [tracks, points]);
|
||||||
setTimeout(() => props.onProcessingComplete?.(), 500);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [tracks, points, props.onProcessingComplete]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SxProps } from "@mui/system";
|
|||||||
import { LinePlot, useDrawingArea, useXScale } from "@mui/x-charts";
|
import { LinePlot, useDrawingArea, useXScale } from "@mui/x-charts";
|
||||||
import { ScaleLinear } from "d3-scale";
|
import { ScaleLinear } from "d3-scale";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import MaxSpeed from "../Data/MaxSpeed";
|
import MaxSpeed from "../Data/Api/Models/MaxSpeed";
|
||||||
import IChartPoint from "../Data/IChartPoint";
|
import IChartPoint from "../Data/IChartPoint";
|
||||||
|
|
||||||
// Remarks:
|
// Remarks:
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Points from "./Points";
|
||||||
|
import Tracks from "./Tracks";
|
||||||
|
|
||||||
|
const apiUrl: string = import.meta.env.VITE_API_URL as string;
|
||||||
|
|
||||||
|
export default class ApiEndpoints
|
||||||
|
{
|
||||||
|
public Points: Points;
|
||||||
|
public Tracks: Tracks;
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
{
|
||||||
|
this.Points = new Points(apiUrl);
|
||||||
|
this.Tracks = new Tracks(apiUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import IPoint from "../../Models/IPoint";
|
||||||
|
|
||||||
|
export default interface IGetPointsResponse
|
||||||
|
{
|
||||||
|
points: IPoint[],
|
||||||
|
totalCount: number,
|
||||||
|
count: number,
|
||||||
|
page: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class UpsertPointRequest
|
||||||
|
{
|
||||||
|
public name: string;
|
||||||
|
public height: number;
|
||||||
|
|
||||||
|
constructor(name: string, height: number)
|
||||||
|
{
|
||||||
|
if (name.length < 1)
|
||||||
|
throw new Error("Name must be at least 1 character long");
|
||||||
|
|
||||||
|
if (height !== Math.floor(height))
|
||||||
|
throw new Error("Height must be an integer");
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import MaxSpeed from "../../Models/MaxSpeed";
|
||||||
|
import Surface from "../../Models/Surface";
|
||||||
|
|
||||||
|
export default class UpsertTrackRequest
|
||||||
|
{
|
||||||
|
public firstId: number;
|
||||||
|
public secondId: number;
|
||||||
|
public distance: number;
|
||||||
|
public surface: Surface;
|
||||||
|
public maxSpeed: MaxSpeed;
|
||||||
|
|
||||||
|
constructor(firstId: number, secondId: number, distance: number, surface: number, maxSpeed: number)
|
||||||
|
{
|
||||||
|
if (firstId < 0)
|
||||||
|
throw new Error("First ID must be at least 0");
|
||||||
|
|
||||||
|
if (secondId < 0)
|
||||||
|
throw new Error("Second ID must be at least 0");
|
||||||
|
|
||||||
|
if (distance < 1)
|
||||||
|
throw new Error("Distance must be greater than 0");
|
||||||
|
|
||||||
|
this.firstId = firstId;
|
||||||
|
this.secondId = secondId;
|
||||||
|
this.distance = distance;
|
||||||
|
this.surface = surface;
|
||||||
|
this.maxSpeed = maxSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import IGetPointsResponse from "./Contracts/Point/IGetPointsResponse";
|
||||||
|
import UpsertPointRequest from "./Contracts/Point/UpsertPointRequest";
|
||||||
|
import IPoint from "./Models/IPoint";
|
||||||
|
|
||||||
|
export default class Points
|
||||||
|
{
|
||||||
|
private apiUrl: string;
|
||||||
|
|
||||||
|
constructor(apiUrl: string)
|
||||||
|
{
|
||||||
|
this.apiUrl = apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async CreatePointAsync(request: UpsertPointRequest): Promise<IPoint>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + "/points", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: IPoint = await response.json() as IPoint;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetPointsArrayAsync(ids: number[]): Promise<IPoint[]>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + "/points/array", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(ids)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: IPoint[] = await response.json() as IPoint[];
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetPointsAsync(page?: number, count?: number): Promise<IGetPointsResponse>
|
||||||
|
{
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (page)
|
||||||
|
params.append("page", page.toString());
|
||||||
|
|
||||||
|
if (count)
|
||||||
|
params.append("count", count.toString());
|
||||||
|
|
||||||
|
const response: Response = await fetch(this.apiUrl + `/points?${params.toString()}`);
|
||||||
|
const data: IGetPointsResponse = await response.json() as IGetPointsResponse;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetPointAsync(id: number): Promise<IPoint>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + `/points/${id}`);
|
||||||
|
const data: IPoint = await response.json() as IPoint;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UpsertPointAsync(id: number, request: UpsertPointRequest): Promise<IPoint | null>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + `/points/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const data: IPoint = await response.json() as IPoint;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async DeletePointAsync(id: number): Promise<void>
|
||||||
|
{
|
||||||
|
await fetch(this.apiUrl + `/points/${id}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ImportPointsAsync(points: IPoint[]): Promise<IPoint[]>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + "/points/import", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(points)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: IPoint[] = await response.json() as IPoint[];
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import UpsertTrackRequest from "./Contracts/Track/UpsertTrackRequest";
|
||||||
|
import ITrack from "./Models/ITrack";
|
||||||
|
|
||||||
|
export default class Tracks
|
||||||
|
{
|
||||||
|
private apiUrl: string;
|
||||||
|
|
||||||
|
constructor(apiUrl: string)
|
||||||
|
{
|
||||||
|
this.apiUrl = apiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async CreateTrackAsync(request: UpsertTrackRequest): Promise<ITrack>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + "/tracks", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: ITrack = await response.json() as ITrack;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetTrackAsync(firstId: number, secondId: number): Promise<ITrack>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + `/tracks/${firstId}/${secondId}`);
|
||||||
|
const data: ITrack = await response.json() as ITrack;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async GetAllTracksAsync(): Promise<ITrack[]>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + "/tracks");
|
||||||
|
const data: ITrack[] = await response.json() as ITrack[];
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UpsertTrackAsync(
|
||||||
|
firstId: number,
|
||||||
|
secondId: number,
|
||||||
|
request: UpsertTrackRequest
|
||||||
|
): Promise<ITrack | null>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + `/tracks/${firstId}/${secondId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const data: ITrack = await response.json() as ITrack;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async DeleteTrackAsync(firstId: number, secondId: number): Promise<void>
|
||||||
|
{
|
||||||
|
await fetch(this.apiUrl + `/tracks/${firstId}/${secondId}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ImportTracksAsync(tracks: ITrack[]): Promise<ITrack[]>
|
||||||
|
{
|
||||||
|
const response: Response = await fetch(this.apiUrl + "/tracks/import", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(tracks)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: ITrack[] = await response.json() as ITrack[];
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* A class that generates random numbers from a Gaussian distribution.
|
||||||
|
*/
|
||||||
|
export default class GaussianGenerator
|
||||||
|
{
|
||||||
|
private mean: number;
|
||||||
|
private stdDev: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a GaussianGenerator object with the specified mean and standard deviation.
|
||||||
|
* @param mean The mean of the Gaussian distribution.
|
||||||
|
* @param stdDev The standard deviation of the Gaussian distribution.
|
||||||
|
*/
|
||||||
|
constructor(mean: number, stdDev: number)
|
||||||
|
{
|
||||||
|
this.mean = mean;
|
||||||
|
this.stdDev = stdDev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the next random number from the Gaussian distribution.
|
||||||
|
* @returns The next random number from the Gaussian distribution.
|
||||||
|
*/
|
||||||
|
NextGaussian(): number
|
||||||
|
{
|
||||||
|
let u1 = 0, u2 = 0;
|
||||||
|
while (u1 === 0) u1 = Math.random(); // Converting [0,1) to (0,1)
|
||||||
|
while (u2 === 0) u2 = Math.random();
|
||||||
|
const randStdNormal = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
||||||
|
const randNormal = this.mean + this.stdDev * randStdNormal; // random normal(mean,stdDev^2)
|
||||||
|
return Math.floor(randNormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an array of random numbers from the Gaussian distribution.
|
||||||
|
* @param n The number of random numbers to generate.
|
||||||
|
* @returns An array of random numbers from the Gaussian distribution.
|
||||||
|
*/
|
||||||
|
Generate(n: number): number[]
|
||||||
|
{
|
||||||
|
const arr = [];
|
||||||
|
for (let i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
arr.push(this.NextGaussian());
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import MaxSpeed from "./MaxSpeed";
|
import MaxSpeed from "./Api/Models/MaxSpeed";
|
||||||
import Surface from "./Surface";
|
import Surface from "./Api/Models/Surface";
|
||||||
|
|
||||||
/** Represents an aggregated point on the chart. */
|
/** Represents an aggregated point on the chart. */
|
||||||
export default interface IChartPoint
|
export default interface IChartPoint
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import IPoint from "./IPoint";
|
|
||||||
import ITrack from "./ITrack";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a random number between min and max
|
|
||||||
* @param min The minimum value (inclusive)
|
|
||||||
* @param max The maximum value (exclusive)
|
|
||||||
* @returns A random number between min and max
|
|
||||||
*/
|
|
||||||
const getRandom = (min: number, max: number): number =>
|
|
||||||
Math.floor(Math.random() * (max - min)) + min;
|
|
||||||
|
|
||||||
const pointNames: string[] =
|
|
||||||
[
|
|
||||||
"Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India",
|
|
||||||
"Juliett", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo",
|
|
||||||
"Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu"
|
|
||||||
];
|
|
||||||
|
|
||||||
function LoadMockData(largeCount?: boolean): { tracks: ITrack[], points: IPoint[]; }
|
|
||||||
{
|
|
||||||
const count: number = getRandom(10, 20) * (largeCount ? 10 : 1);
|
|
||||||
const points: IPoint[] = [];
|
|
||||||
const tracks: ITrack[] = [];
|
|
||||||
|
|
||||||
// Generate random data
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
points.push({
|
|
||||||
id: i,
|
|
||||||
name: `${pointNames[getRandom(0, pointNames.length)]}-${i}`,
|
|
||||||
height: getRandom(50, 200)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < count - 1; i++)
|
|
||||||
{
|
|
||||||
tracks.push({
|
|
||||||
firstId: points[i].id,
|
|
||||||
secondId: points[i + 1].id,
|
|
||||||
distance: getRandom(1000, 2000),
|
|
||||||
surface: getRandom(0, 3),
|
|
||||||
maxSpeed: getRandom(0, 3)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { tracks, points };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LoadMockData;
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import IPoint from "./Api/Models/IPoint";
|
||||||
|
import ITrack from "./Api/Models/ITrack";
|
||||||
|
import MaxSpeed from "./Api/Models/MaxSpeed";
|
||||||
|
import Surface from "./Api/Models/Surface";
|
||||||
|
import GaussianGenerator from "./GaussianGenerator";
|
||||||
|
|
||||||
|
export function GeneratePoints(count: number): IPoint[]
|
||||||
|
{
|
||||||
|
const generator = new GaussianGenerator(160, 10);
|
||||||
|
|
||||||
|
const points: IPoint[] = generator.Generate(count).map((height, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name: `${pointNames[index % pointNames.length]}-${index + 1}`,
|
||||||
|
height
|
||||||
|
}));
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenerateTracks(points: IPoint[]): ITrack[]
|
||||||
|
{
|
||||||
|
const generator = new GaussianGenerator(2000, 500);
|
||||||
|
|
||||||
|
const tracks: ITrack[] = generator.Generate(points.length - 1).map((distance, index) => ({
|
||||||
|
firstId: points[index].id,
|
||||||
|
secondId: points[index + 1].id,
|
||||||
|
distance,
|
||||||
|
surface: getRandom(0, 3) as Surface,
|
||||||
|
maxSpeed: getRandom(0, 3) as MaxSpeed
|
||||||
|
}));
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random number between min and max
|
||||||
|
* @param min The minimum value (inclusive)
|
||||||
|
* @param max The maximum value (exclusive)
|
||||||
|
* @returns A random number between min and max
|
||||||
|
*/
|
||||||
|
const getRandom = (min: number, max: number): number =>
|
||||||
|
Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
|
||||||
|
const pointNames: string[] =
|
||||||
|
[
|
||||||
|
"Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India",
|
||||||
|
"Juliett", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo",
|
||||||
|
"Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu"
|
||||||
|
];
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { green, grey, orange, red, yellow } from "@mui/material/colors";
|
import { green, grey, orange, red, yellow } from "@mui/material/colors";
|
||||||
import { AllSeriesType, ChartsAxisContentProps } from "@mui/x-charts";
|
import { AllSeriesType, ChartsAxisContentProps } from "@mui/x-charts";
|
||||||
|
import MaxSpeed from "./Api/Models/MaxSpeed";
|
||||||
|
import Surface from "./Api/Models/Surface";
|
||||||
import IChartPoint from "./IChartPoint";
|
import IChartPoint from "./IChartPoint";
|
||||||
import MaxSpeed from "./MaxSpeed";
|
|
||||||
import Surface from "./Surface";
|
|
||||||
|
|
||||||
/** Props for rendering trackline segments. */
|
/** Props for rendering trackline segments. */
|
||||||
export const TracklineSeries: AllSeriesType[] = [
|
export const TracklineSeries: AllSeriesType[] = [
|
||||||
|
|||||||
@@ -859,6 +859,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||||
|
|
||||||
|
"@types/node@^20.11.20":
|
||||||
|
version "20.11.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.20.tgz#f0a2aee575215149a62784210ad88b3a34843659"
|
||||||
|
integrity sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
|
||||||
@@ -2906,6 +2913,11 @@ unbox-primitive@^1.0.2:
|
|||||||
has-symbols "^1.0.3"
|
has-symbols "^1.0.3"
|
||||||
which-boxed-primitive "^1.0.2"
|
which-boxed-primitive "^1.0.2"
|
||||||
|
|
||||||
|
undici-types@~5.26.4:
|
||||||
|
version "5.26.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||||
|
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||||
|
|
||||||
update-browserslist-db@^1.0.13:
|
update-browserslist-db@^1.0.13:
|
||||||
version "1.0.13"
|
version "1.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
|
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
|
||||||
|
|||||||
Reference in New Issue
Block a user