mirror of
https://github.com/Wessel/Roommapper.git
synced 2026-07-21 23:37:11 +02:00
fix: WebUI files werden niet aan de commit toegevoegd
This commit is contained in:
42
src/Web/package.json
Normal file
42
src/Web/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "roommapper",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.3.7",
|
||||
"@babel/plugin-transform-private-property-in-object": "^7.24.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"antd": "^5.18.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"sass": "^1.77.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start src/index.jsx",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
12098
src/Web/pnpm-lock.yaml
generated
Normal file
12098
src/Web/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
src/Web/src/components/controlField.jsx
Normal file
60
src/Web/src/components/controlField.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Button, Card } from 'antd';
|
||||
|
||||
import { API_ENDPOINT } from '../constants';
|
||||
|
||||
// Crude way of making an enum.
|
||||
const tasks = Object.freeze({
|
||||
START: 0,
|
||||
STOP: 1,
|
||||
MAP: 2,
|
||||
});
|
||||
|
||||
export default class ControlField extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Save the state of the "message" field.
|
||||
this.state = {
|
||||
result: null
|
||||
};
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
// Upload error messages to the "Output Message" field.
|
||||
this.setState({ result: error.message });
|
||||
};
|
||||
|
||||
handleClick(task) {
|
||||
// Parse the pressed button and send the corresponding task to the server.
|
||||
task = task === tasks.START ? 'start' : task === tasks.STOP ? 'stop' : 'map';
|
||||
fetch(`${API_ENDPOINT}/roomba/control`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ task: task })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => this.setState({ result: JSON.stringify(data) }))
|
||||
.catch(this.handleError);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='control'>
|
||||
<div className='control-buttons'>
|
||||
<Button type='primary' onClick={() => this.handleClick(tasks.START)}>Start</Button>
|
||||
<Button type='primary' onClick={() => this.handleClick(tasks.MAP)}>Map</Button>
|
||||
<Button type='primary' danger onClick={() => this.handleClick(tasks.STOP)}>Stop</Button>
|
||||
</div>
|
||||
<Card
|
||||
className='output-field'
|
||||
title="Output Message"
|
||||
bordered={false}
|
||||
>
|
||||
<pre>{this.state.result}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
72
src/Web/src/components/layout.jsx
Normal file
72
src/Web/src/components/layout.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { HeatMapOutlined } from '@ant-design/icons';
|
||||
import { Layout, Menu, theme } from 'antd';
|
||||
|
||||
import Controlfield from './controlField';
|
||||
import MapCanvas from './mapCanvas';
|
||||
|
||||
const { Content, Sider } = Layout;
|
||||
|
||||
// Sidebar menu items
|
||||
const menuItems = [
|
||||
{
|
||||
key: 'map',
|
||||
icon: React.createElement(HeatMapOutlined),
|
||||
label: 'Map',
|
||||
}
|
||||
];
|
||||
|
||||
// I.v.m. het gebruik van de "antd" UI library moet er gebruik gemaakt worden
|
||||
// van een "hook", deze react onderdelen kunnen niet gebruikt worden in een
|
||||
// class en dus is dit het enigste niet-OOP gedeelte van de frontend.
|
||||
const App = () => {
|
||||
const { token: { colorBgContainer, borderRadiusLG } } = theme.useToken();
|
||||
return (
|
||||
<Layout hasSider>
|
||||
<Sider
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
theme='dark'
|
||||
>
|
||||
<Menu theme="dark" mode="inline" defaultSelectedKeys={['map']} items={menuItems} />
|
||||
</Sider>
|
||||
<Layout
|
||||
theme='dark'
|
||||
style={{ marginLeft: 200 }}
|
||||
>
|
||||
<Content
|
||||
style={{ margin: '24px 16px 0', overflow: 'initial' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: 24,
|
||||
textAlign: 'left',
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Controlfield />
|
||||
<div style={{ 'marginLeft': '50px' }}>
|
||||
<MapCanvas
|
||||
width={600}
|
||||
height={600}
|
||||
onError={MapCanvas.handleError}
|
||||
className='map-canvas'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
115
src/Web/src/components/mapCanvas.jsx
Normal file
115
src/Web/src/components/mapCanvas.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { Select, Input, Button, Form } from 'antd';
|
||||
import { API_ENDPOINT } from '../constants';
|
||||
|
||||
export default class MapCanvas extends React.Component {
|
||||
defaultWidth = 300;
|
||||
defaultHeight = 300;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Due to `draw`'s asynchronous behavior, we need to bind `this` to the
|
||||
// handler functionin order for it to be able to be used inside of `render`.
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
// Get a reference to the canvas element so we can draw on it
|
||||
this.canvasRef = React.createRef();
|
||||
this.state = {
|
||||
inputValue: '8a97354d-d4ac-43c6-8b32-528cc24e3ede',
|
||||
searchOption: 'id',
|
||||
canvasWidth: props.width || this.defaultWidth,
|
||||
canvasHeight: props.height || this.defaultHeight,
|
||||
};
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
await this.draw();
|
||||
}
|
||||
|
||||
// This is a lifecycle method that is called after the component has been
|
||||
// rendered to the DOM. This is where we will draw the initial state
|
||||
// on the canvas.
|
||||
componentDidMount() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
async getLinePoints(search = 'id', field = '8a97354d-d4ac-43c6-8b32-528cc24e3ede') {
|
||||
try {
|
||||
const { searchOption, inputValue } = this.state;
|
||||
const url = `${API_ENDPOINT}/database?${searchOption}=${inputValue}`;
|
||||
const data = await (await fetch(url)).json();
|
||||
|
||||
// Combine all found sets into a singular array.
|
||||
const map = [];
|
||||
if (!Array.isArray(data) || data.length < 1) return map;
|
||||
data.forEach((set) => set.Objects.forEach((coord) => map.push(coord)));
|
||||
|
||||
return map;
|
||||
} catch (ex) {
|
||||
this.props.onError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
async draw() {
|
||||
// Get the 2D context from the canvas element
|
||||
const ctx = this.canvasRef.current.getContext('2d');
|
||||
// Get the points to draw from the server
|
||||
const points = await this.getLinePoints();
|
||||
|
||||
// Set the font for the text we will draw on the canvas
|
||||
ctx.font = "24px Comic Sans MS";
|
||||
|
||||
// Clear the canvas before drawing
|
||||
ctx.clearRect(0, 0, this.state.canvasWidth, this.state.canvasHeight);
|
||||
|
||||
// If there are no points, display a message and return
|
||||
if (!points || points.length < 1) {
|
||||
ctx.fillText("No valid entry found.", 10, this.state.canvasHeight / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop trough all coordinates, draw a dot at each point to form a top-down
|
||||
// view of the objects.
|
||||
points.forEach(point => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(point[1], point[0], 1, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { searchOption, inputValue } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form onFinish={this.handleSubmit} className='map-choice'>
|
||||
<Select
|
||||
className='map-selector'
|
||||
value={searchOption}
|
||||
onChange={(val) => this.setState({ searchOption: val })}
|
||||
>
|
||||
<Select value="id">ID</Select>
|
||||
<Select value="version">Version</Select>
|
||||
<Select value="date">Date</Select>
|
||||
<Select value="all">All</Select>
|
||||
</Select>
|
||||
<Input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(evt) => this.setState({ inputValue: evt.target.value })}
|
||||
disabled={searchOption === 'all'}
|
||||
/>
|
||||
<Button type="default" htmlType="submit">Update Map</Button>
|
||||
</Form>
|
||||
<div>
|
||||
<canvas
|
||||
ref={this.canvasRef}
|
||||
width={this.state.canvasWidth}
|
||||
height={this.state.canvasHeight}
|
||||
{...this.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
1
src/Web/src/constants.jsx
Normal file
1
src/Web/src/constants.jsx
Normal file
@@ -0,0 +1 @@
|
||||
export const API_ENDPOINT = 'http://127.0.0.1:5000/api/v1';
|
||||
7
src/Web/src/index.jsx
Normal file
7
src/Web/src/index.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import Layout from './components/layout';
|
||||
import './scss/index.scss';
|
||||
|
||||
ReactDOM
|
||||
.createRoot(document.getElementById('root'))
|
||||
.render(<Layout />);
|
||||
Reference in New Issue
Block a user