Removed old vue project, improved live connection infrastructure.
|
@ -0,0 +1,111 @@
|
|||
# Component Drivers
|
||||
This document describes the general methodology for writing component drivers that operate within your rail system to connect it to the online system's up- and down-link services.
|
||||
|
||||
The following types of components are supported by Rail Signal:
|
||||
- `SIGNAL`
|
||||
- `SEGMENT_BOUNDARY`
|
||||
- `SWITCH`
|
||||
|
||||
The following information is generally required for any driver to be able to connect to the system and operate nominally:
|
||||
- A valid link token
|
||||
- The base URL of the system's API. Usually `http://localhost:8080` or whatever you've configured it to be.
|
||||
- Live connection information. This differs depending on what type of communication your device supports.
|
||||
- For devices with **websocket** support, you will need the base URL of the websocket. Usually `ws://localhost:8080` or whatever you've configured your server to use.
|
||||
- For devices with **TCP socket** support, you will need the hostname and port of the system's server socket. By default, this is `localhost:8081`.
|
||||
|
||||
A device is not limited to a single component, but will act as a relay for all components linked to the device's link token. Generally, there is no limit to the number of components that a single device can manage, but more components will lead to more load on the device.
|
||||
|
||||
## Live Communication
|
||||
The main purpose of component drivers is to relay real-time messages between actual component devices, and their representations in the online rail system. While multiple types of communication are available, in general, all messages are sent as JSON UTF-8 encoded strings. Devices must present their link token when they initiate the connection.
|
||||
|
||||
If the link token is valid, the connection will be initiated, and the device will immediately receive a `COMPONENT_DATA` message for each component that the token is linked to.
|
||||
|
||||
### Websocket
|
||||
Websocket connections should be made to `{BASE_WS_URL}/api/ws/component?token={token}`, where `{BASE_WS_URL}` is the base websocket URL, such as `ws://localhost:8080`, and `{token}` is the device's link token.
|
||||
|
||||
- If the link token is missing or improperly formatted, a 400 Bad Request response is given.
|
||||
- If the link token is not correct or not active or otherwise set to reject connections, a 401 Unauthorized response is given.
|
||||
|
||||
### TCP Socket
|
||||
TCP socket connections should be made to the server's TCP socket address, which by default is `localhost:8081`. The device should immediately send 2 bytes indicating the length of its token string, followed by the token string's bytes. The client should expect to receive in response a *connect message*:
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"message": "Connection established."
|
||||
}
|
||||
```
|
||||
|
||||
- If the token is invalid, a `"valid": false` is returned and the server closes the socket.
|
||||
|
||||
Note: All messages sent via TCP are sent as JSON messages with a 2-byte length header.
|
||||
|
||||
## Components
|
||||
Each device should be designed to handle multiple independent components concurrently. The device may receive messages at any time pertaining to any of the components, and the device may send messages at any time, pertaining to any of the components. Messages are only sent regarding a single component.
|
||||
|
||||
Every component message should contain at least the following two properties:
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "COMPONENT_DATA",
|
||||
...
|
||||
}
|
||||
```
|
||||
`cId` is the id of the component that this message is about. `type` is the type of message. This defines what additional structure to expect.
|
||||
|
||||
All components may receive `COMPONENT_DATA` messages. For example, the following could be a message regarding a signal:
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "COMPONENT_DATA",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"name": "my-component",
|
||||
"type": "SIGNAL",
|
||||
"online": true,
|
||||
"segment": {
|
||||
"id": 4,
|
||||
"name": "my-segment",
|
||||
"occupied": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The following sections will provide more detail about the other types of messages that can be sent and received by the different components.
|
||||
|
||||
### Signal
|
||||
Signals display the status of a connected segment, and as such can only receive data. They will receive `SEGMENT_STATUS` messages:
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "SEGMENT_STATUS",
|
||||
"sId": 4,
|
||||
"occupied": true
|
||||
}
|
||||
```
|
||||
`sId` is the id of the segment that was updated. `occupied` contains the current status of the segment.
|
||||
|
||||
### Segment Boundary
|
||||
Segment boundaries send updates as trains pass them, in order to provide information to the system about the state of connected segments.
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "SEGMENT_BOUNDARY_UPDATE",
|
||||
"toSegmentId": 3,
|
||||
"eventType": "ENTERING"
|
||||
}
|
||||
```
|
||||
`toSegmentId` is the id of the segment a train is moving towards. `eventType` is the type of boundary event. This can either be `ENTERING` if a train has just begun entering the segment, or `ENTERED` if a train has just left the boundary and completely entered the segment.
|
||||
|
||||
### Switch
|
||||
Switches can send information about their status, if it's been updated, and they can also receive messages that direct them to change their status.
|
||||
|
||||
```json
|
||||
{
|
||||
"cId": 123,
|
||||
"type": "SWITCH_UPDATE",
|
||||
"activeConfigId": 497238
|
||||
}
|
||||
```
|
||||
`activeConfigId` is the id of the switch configuration that's active. This message can be sent by either the system or the switch.
|
|
@ -1,2 +0,0 @@
|
|||
VITE_API_URL=http://localhost:8080/api
|
||||
VITE_WS_URL=ws://localhost:8080/api/ws/app
|
|
@ -1,2 +0,0 @@
|
|||
VITE_API_URL=http://localhost:8080/api
|
||||
VITE_WS_URL=ws://localhost:8080/api/ws/app
|
|
@ -1,11 +0,0 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"env": {
|
||||
"vue/setup-compiler-macros": true
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -1,35 +0,0 @@
|
|||
# railsignal-app
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env dub
|
||||
/+ dub.sdl:
|
||||
dependency "dsh" version="~>1.6.1"
|
||||
+/
|
||||
import dsh;
|
||||
|
||||
const DEST = "../src/main/resources/static";
|
||||
|
||||
void main() {
|
||||
print("Deploying Vue app to Spring's /static directory.");
|
||||
runOrQuit("vite build --base=/app/");
|
||||
rmdirRecurse(DEST);
|
||||
copyDir("./dist", DEST);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rail Signal</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"name": "railsignal-app",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build --base=/app/ --mode=development",
|
||||
"preview": "vite preview --port 5050",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.1.3",
|
||||
"pinia": "^2.0.14",
|
||||
"three": "^0.140.0",
|
||||
"vue": "^3.2.33",
|
||||
"vue-router": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.3.2",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"vite": "^2.9.8"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 840 B |
Before Width: | Height: | Size: 15 KiB |
|
@ -1 +0,0 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<AppNavbar />
|
||||
<RailSystem v-if="rsStore.selectedRailSystem !== null" :railSystem="rsStore.selectedRailSystem"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "./stores/railSystemsStore";
|
||||
import AppNavbar from "./components/AppNavbar.vue";
|
||||
import RailSystem from "./components/RailSystem.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AppNavbar,
|
||||
RailSystem
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,101 +0,0 @@
|
|||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
|
||||
export function refreshComponents(rs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rs.id}/c`)
|
||||
.then(response => {
|
||||
const previousSelectedComponentId = rs.selectedComponent ? rs.selectedComponent.id : null;
|
||||
rs.components = response.data;
|
||||
if (previousSelectedComponentId !== null) {
|
||||
const previousComponent = rs.components.find(c => c.id === previousSelectedComponentId);
|
||||
if (previousComponent) {
|
||||
rs.selectedComponent = previousComponent;
|
||||
} else {
|
||||
rs.selectedComponent = null;
|
||||
}
|
||||
} else {
|
||||
rs.selectedComponent = null;
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshSomeComponents(rs, components) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rs.id}/c/${components[i].id}`)
|
||||
.then(resp => {
|
||||
const idx = rs.components.findIndex(c => c.id === resp.data.id);
|
||||
if (idx > -1) rs.components[idx] = resp.data;
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
export function getComponent(rs, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
|
||||
.then(response => resolve(response.data))
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches through the rail system's components.
|
||||
* @param {RailSystem} rs
|
||||
* @param {string|null} searchQuery
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
export function searchComponents(rs, searchQuery) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params = {
|
||||
page: 0,
|
||||
size: 25
|
||||
};
|
||||
if (searchQuery) params.q = searchQuery;
|
||||
axios.get(`${API_URL}/rs/${rs.id}/c/search`, {params: params})
|
||||
.then(response => {
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function createComponent(rs, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/rs/${rs.id}/c`, data)
|
||||
.then(response => {
|
||||
const newComponentId = response.data.id;
|
||||
refreshComponents(rs)
|
||||
.then(() => {
|
||||
const newComponent = rs.components.find(c => c.id === newComponentId);
|
||||
if (newComponent) {
|
||||
rs.selectedComponent = newComponent;
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function removeComponent(rs, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/rs/${rs.id}/c/${id}`)
|
||||
.then(() => {
|
||||
refreshComponents(rs)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export const API_URL = import.meta.env.VITE_API_URL;
|
||||
export const WS_URL = import.meta.env.VITE_WS_URL;
|
|
@ -1,57 +0,0 @@
|
|||
import {API_URL} from "./constants";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* A token that's used by components to provide real-time up and down links.
|
||||
*/
|
||||
export class LinkToken {
|
||||
constructor(data) {
|
||||
this.id = data.id;
|
||||
this.label = data.label;
|
||||
this.components = data.components;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of link tokens in a rail system.
|
||||
* @param {RailSystem} rs
|
||||
* @return {Promise<LinkToken[]>}
|
||||
*/
|
||||
export function getTokens(rs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rs.id}/lt`)
|
||||
.then(response => {
|
||||
resolve(response.data.map(obj => new LinkToken(obj)));
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new link token.
|
||||
* @param {RailSystem} rs
|
||||
* @param {LinkToken} data
|
||||
* @return {Promise<string>} A promise that resolves to the token that was created.
|
||||
*/
|
||||
export function createLinkToken(rs, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/rs/${rs.id}/lt`, data)
|
||||
.then(response => {
|
||||
resolve(response.data.token);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a link token.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Number} tokenId
|
||||
*/
|
||||
export function deleteToken(rs, tokenId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/rs/${rs.id}/lt/${tokenId}`)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
import {refreshSomeComponents} from "./components";
|
||||
|
||||
/**
|
||||
* Updates the connections to a path node.
|
||||
* @param {RailSystem} rs The rail system to which the node belongs.
|
||||
* @param {Object} node The node to update.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateConnections(rs, node) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.patch(
|
||||
`${API_URL}/rs/${rs.id}/c/${node.id}/connectedNodes`,
|
||||
node
|
||||
)
|
||||
.then(response => {
|
||||
node.connectedNodes = response.data.connectedNodes;
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a connection to a path node.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Object} node
|
||||
* @param {Object} other
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addConnection(rs, node, other) {
|
||||
node.connectedNodes.push(other);
|
||||
return updateConnections(rs, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection from a path node.
|
||||
* @param {RailSystem} rs
|
||||
* @param {Object} node
|
||||
* @param {Object} other
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function removeConnection(rs, node, other) {
|
||||
const idx = node.connectedNodes.findIndex(n => n.id === other.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (idx > -1) {
|
||||
node.connectedNodes.splice(idx, 1);
|
||||
updateConnections(rs, node)
|
||||
.then(() => {
|
||||
const nodes = [];
|
||||
nodes.push(...node.connectedNodes);
|
||||
nodes.push(other);
|
||||
refreshSomeComponents(rs, nodes)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
|
||||
export class RailSystem {
|
||||
constructor(data) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.segments = [];
|
||||
this.components = [];
|
||||
this.websocket = null;
|
||||
this.selectedComponent = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshRailSystems(rsStore) {
|
||||
return new Promise(resolve => {
|
||||
axios.get(`${API_URL}/rs`)
|
||||
.then(response => {
|
||||
const rsItems = response.data;
|
||||
rsStore.railSystems.length = 0;
|
||||
for (let i = 0; i < rsItems.length; i++) {
|
||||
rsStore.railSystems.push(new RailSystem(rsItems[i]));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
})
|
||||
}
|
||||
|
||||
export function createRailSystem(rsStore, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/rs`, {name: name})
|
||||
.then(response => {
|
||||
const newId = response.data.id;
|
||||
refreshRailSystems(rsStore)
|
||||
.then(() => resolve(rsStore.railSystems.find(rs => rs.id === newId)))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export function removeRailSystem(rsStore, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/rs/${id}`)
|
||||
.then(() => {
|
||||
rsStore.selectedRailSystem = null;
|
||||
refreshRailSystems(rsStore)
|
||||
.then(() => resolve)
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import axios from "axios";
|
||||
import {API_URL} from "./constants";
|
||||
|
||||
/**
|
||||
* Fetches the set of segments for a rail system.
|
||||
* @param {Number} rsId
|
||||
* @returns {Promise<[Object]>}
|
||||
*/
|
||||
export function getSegments(rsId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(`${API_URL}/rs/${rsId}/s`)
|
||||
.then(response => resolve(response.data))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshSegments(rs) {
|
||||
return new Promise(resolve => {
|
||||
getSegments(rs.id)
|
||||
.then(segments => {
|
||||
rs.segments = segments;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
});
|
||||
}
|
||||
|
||||
export function createSegment(rs, name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${API_URL}/rs/${rs.id}/s`, {name: name})
|
||||
.then(() => {
|
||||
refreshSegments(rs)
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
export function removeSegment(rs, segmentId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(`${API_URL}/rs/${rs.id}/${segmentId}`)
|
||||
.then(() => {
|
||||
refreshSegments(rs)
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import {WS_URL} from "./constants";
|
||||
|
||||
/**
|
||||
* Establishes a websocket connection to the given rail system.
|
||||
* @param {RailSystem} rs
|
||||
*/
|
||||
export function establishWebsocketConnection(rs) {
|
||||
closeWebsocketConnection(rs);
|
||||
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
|
||||
rs.websocket.onopen = () => {
|
||||
console.log("Opened websocket connection to rail system " + rs.id);
|
||||
};
|
||||
rs.websocket.onclose = event => {
|
||||
if (event.code !== 1000) {
|
||||
console.warn("Lost websocket connection. Attempting to reestablish.");
|
||||
setTimeout(() => {
|
||||
establishWebsocketConnection(rs);
|
||||
}, 3000);
|
||||
}
|
||||
console.log("Closed websocket connection to rail system " + rs.id);
|
||||
};
|
||||
rs.websocket.onmessage = msg => {
|
||||
console.log(msg);
|
||||
};
|
||||
rs.websocket.onerror = error => {
|
||||
console.log(error);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the websocket connection to a rail system, if possible.
|
||||
* @param {RailSystem} rs
|
||||
*/
|
||||
export function closeWebsocketConnection(rs) {
|
||||
if (rs.websocket) {
|
||||
rs.websocket.close();
|
||||
rs.websocket = null;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,90 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 67.733332 67.733335"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="icon.svg"
|
||||
inkscape:export-filename="/home/andrew/Programming/github-andrewlalis/RailSignalAPI/railsignal-app/src/assets/icon.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.979899"
|
||||
inkscape:cx="164.88542"
|
||||
inkscape:cy="100.52253"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-229.26665)">
|
||||
<rect
|
||||
id="rect3713"
|
||||
width="52.916668"
|
||||
height="67.73333"
|
||||
x="7.4083328"
|
||||
y="229.26665"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
|
||||
rx="13.229167" />
|
||||
<rect
|
||||
rx="12.30785"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.24762905"
|
||||
y="231.24725"
|
||||
x="9.250967"
|
||||
height="63.772137"
|
||||
width="49.2314"
|
||||
id="rect4520" />
|
||||
<circle
|
||||
style="fill:#00d900;fill-opacity:1;stroke:none;stroke-width:0.65214598"
|
||||
id="path4522"
|
||||
cx="33.866665"
|
||||
cy="248.70078"
|
||||
r="10.364463" />
|
||||
<circle
|
||||
r="10.364463"
|
||||
cy="277.56586"
|
||||
cx="33.866665"
|
||||
id="circle4524"
|
||||
style="fill:#e71e00;fill-opacity:1;stroke:none;stroke-width:0.65214598" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1,87 +0,0 @@
|
|||
<template>
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand h1 mb-0">
|
||||
<img src="@/assets/icon.svg" height="24"/>
|
||||
Rail Signal
|
||||
</span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2, mb-md-0">
|
||||
<li class="nav-item me-2 mb-2 mb-md-0">
|
||||
<select
|
||||
id="railSystemSelect"
|
||||
v-model="rsStore.selectedRailSystem"
|
||||
class="form-select form-select-sm"
|
||||
@change="rsStore.onSelectedRailSystemChanged()"
|
||||
>
|
||||
<option v-for="rs in rsStore.railSystems" :key="rs.id" :value="rs">
|
||||
{{rs.name}}
|
||||
</option>
|
||||
</select>
|
||||
</li>
|
||||
<li class="nav-item me-2" v-if="rsStore.selectedRailSystem !== null">
|
||||
<button
|
||||
@click="remove()"
|
||||
class="btn btn-danger btn-sm"
|
||||
type="button"
|
||||
>
|
||||
Remove this Rail System
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addRailSystemModal"
|
||||
>
|
||||
Add Rail System
|
||||
</button>
|
||||
<AddRailSystem />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<ConfirmModal
|
||||
ref="confirmModal"
|
||||
:id="'removeRailSystemModal'"
|
||||
:title="'Confirm Rail System Removal'"
|
||||
:message="'Are you sure you want to remove this rail system? This CANNOT be undone. All data will be permanently lost.'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../stores/railSystemsStore";
|
||||
import AddRailSystemModal from "./railsystem/AddRailSystemModal.vue";
|
||||
import ConfirmModal from "./ConfirmModal.vue";
|
||||
import {refreshRailSystems, removeRailSystem} from "../api/railSystems";
|
||||
|
||||
export default {
|
||||
name: "AppNavbar",
|
||||
components: {AddRailSystem: AddRailSystemModal, ConfirmModal},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
refreshRailSystems(this.rsStore);
|
||||
},
|
||||
methods: {
|
||||
remove() {
|
||||
this.$refs.confirmModal.showConfirm()
|
||||
.then(() => removeRailSystem(this.rsStore, this.rsStore.selectedRailSystem.id));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,77 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" :id="id">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{title}}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" :id="id + '_cancel'">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" :id="id + '_yes'">Yes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Modal} from "bootstrap";
|
||||
|
||||
export default {
|
||||
name: "ConfirmModal",
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "Are you sure you want to continue?"
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "Confirm"
|
||||
}
|
||||
},
|
||||
expose: ['showConfirm'],
|
||||
methods: {
|
||||
showConfirm() {
|
||||
return new Promise(resolve => {
|
||||
console.log(this.id);
|
||||
console.log(this.title);
|
||||
const modalElement = document.getElementById(this.id);
|
||||
const modal = new Modal(modalElement);
|
||||
console.log(modal);
|
||||
|
||||
function onDismiss() {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
function onYes() {
|
||||
modalElement.addEventListener("hidden.bs.modal", function onSuccess() {
|
||||
modalElement.removeEventListener("hidden.bs.modal", onSuccess);
|
||||
resolve();
|
||||
});
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
const cancelButton = document.getElementById(this.id + "_cancel");
|
||||
cancelButton.removeEventListener("click", onDismiss);
|
||||
cancelButton.addEventListener("click", onDismiss)
|
||||
const yesButton = document.getElementById(this.id + "_yes");
|
||||
yesButton.removeEventListener("click", onYes);
|
||||
yesButton.addEventListener("click", onYes);
|
||||
modal.show(modalElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-8 p-0">
|
||||
<MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent" :railSystem="railSystem"/>
|
||||
<RailSystemPropertiesView v-if="!railSystem.selectedComponent" :railSystem="railSystem"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MapView from './railsystem/MapView.vue'
|
||||
import ComponentView from './railsystem/component/ComponentView.vue'
|
||||
import RailSystemPropertiesView from "./railsystem/RailSystemPropertiesView.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RailSystemPropertiesView,
|
||||
MapView,
|
||||
ComponentView
|
||||
},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,78 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="addRailSystemModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add New Rail System</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<label for="addRailSystemNameInput" class="form-label">Name</label>
|
||||
<input
|
||||
id="addRailSystemNameInput"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-model="formData.rsName"
|
||||
required
|
||||
/>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||
import {Modal} from "bootstrap";
|
||||
import {createRailSystem} from "../../api/railSystems";
|
||||
|
||||
export default {
|
||||
name: "AddRailSystem",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: ""
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
createRailSystem(this.rsStore, this.formData.rsName)
|
||||
.then(rs => {
|
||||
this.formData.rsName = "";
|
||||
const modal = Modal.getInstance(document.getElementById("addRailSystemModal"));
|
||||
modal.hide();
|
||||
this.rsStore.selectedRailSystem = rs;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't add the rail system: " + error.response.data.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,87 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="addSegmentModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Segment</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Add a new segment to this rail system. A <em>segment</em> is the
|
||||
basic organizational unit of any rail system. It is a section of
|
||||
the network that signals can monitor, and <em>segment boundary nodes</em>
|
||||
define the extent of the segment, and monitor trains entering and
|
||||
leaving the segment.
|
||||
</p>
|
||||
<p>
|
||||
You can think of a segment as a single, secure block of of the rail
|
||||
network that only one train may pass through at once. For example,
|
||||
a junction or station siding.
|
||||
</p>
|
||||
<form>
|
||||
<label for="addSegmentName" class="form-label">Name</label>
|
||||
<input
|
||||
id="addSegmentName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-model="formData.name"
|
||||
required
|
||||
/>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||
import {Modal} from "bootstrap";
|
||||
import {createSegment} from "../../api/segments";
|
||||
|
||||
export default {
|
||||
name: "AddSegmentModal",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: ""
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
const modal = Modal.getInstance(document.getElementById("addSegmentModal"));
|
||||
createSegment(this.rsStore.selectedRailSystem, this.formData.name)
|
||||
.then(() => {
|
||||
this.formData.name = "";
|
||||
modal.hide();
|
||||
})
|
||||
.catch(error => {
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't add the segment: " + error.response.data.message)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,115 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="createLinkTokenModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create Link Token</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Create a <em>link token</em> to link components in this rail system
|
||||
to actual devices in your system, so your world can talk to this
|
||||
system. Each link token should have a unique label that can be used
|
||||
to identify it, and a list of components that it's linked to.
|
||||
</p>
|
||||
<p>
|
||||
Note that for security purposes, the raw token that's generated is
|
||||
only shown once, and is never available again. If you lose the
|
||||
token, you must create a new one instead and delete the old one.
|
||||
</p>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="createLinkTokenLabel" class="form-label">Label</label>
|
||||
<input
|
||||
id="createLinkTokenLabel"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-model="formData.label"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="createLinkTokenComponentSelect">Select Components to Link</label>
|
||||
<ComponentSelector id="createLinkTokenComponentSelect" :railSystem="railSystem" v-model="components"/>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="token !== null" class="alert alert-success mt-2">
|
||||
Created token: {{token}}
|
||||
<br>
|
||||
<small>Copy this token now; it will not be shown again.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="reset()">Close</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="formSubmitted()"
|
||||
v-if="token == null"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {createLinkToken} from "../../api/linkTokens";
|
||||
import {RailSystem} from "../../api/railSystems";
|
||||
import ComponentSelector from "./util/ComponentSelector.vue";
|
||||
|
||||
export default {
|
||||
name: "CreateLinkTokenModal",
|
||||
components: {ComponentSelector},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
label: "",
|
||||
componentIds: []
|
||||
},
|
||||
components: [],
|
||||
warnings: [],
|
||||
token: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
this.formData.componentIds = this.components.map(c => c.id);
|
||||
createLinkToken(this.railSystem, this.formData)
|
||||
.then(token => {
|
||||
this.token = token;
|
||||
})
|
||||
.catch(error => {
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't create token: " + error.response.data.message);
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
// TODO: Fix this!! Reset doesn't work.
|
||||
this.formData.label = "";
|
||||
this.formData.componentIds = [];
|
||||
this.components = [];
|
||||
this.warnings = [];
|
||||
this.token = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import {initMap, draw} from "./mapRenderer.js";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
railSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// The first time this map is mounted, initialize the map.
|
||||
initMap(this.railSystem);
|
||||
},
|
||||
updated() {
|
||||
// Also, re-initialize any time this view is updated.
|
||||
initMap(this.railSystem);
|
||||
},
|
||||
watch: {
|
||||
railSystem: {
|
||||
handler() {
|
||||
draw();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="canvas-container" id="railSystemMapCanvasContainer">
|
||||
<canvas id="railSystemMapCanvas">
|
||||
Your browser doesn't support canvas!
|
||||
</canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.canvas-container {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,112 +0,0 @@
|
|||
<template>
|
||||
<h3>Rail System: <em>{{railSystem.name}}</em></h3>
|
||||
<SegmentsView :segments="railSystem.segments" v-if="railSystem.segments"/>
|
||||
<div class="dropdown d-inline-block me-2">
|
||||
<button class="btn btn-success btn-sm dropdown-toggle" type="button" id="railSystemAddComponentsToggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Add Component
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="railSystemAddComponentsToggle">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSegmentModal"
|
||||
>
|
||||
Add Segment
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="addSignalAllowed()">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSignalModal"
|
||||
>
|
||||
Add Signal
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="addSegmentBoundaryAllowed()">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSegmentBoundaryModal"
|
||||
>
|
||||
Add Segment Boundary
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="addSwitchAllowed()">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSwitchModal"
|
||||
>
|
||||
Add Switch
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="createLinkTokenAllowed()" class="d-inline-block">
|
||||
<button
|
||||
class="btn btn-success btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#createLinkTokenModal"
|
||||
>
|
||||
Create Link Token
|
||||
</button>
|
||||
<CreateLinkTokenModal :railSystem="railSystem" />
|
||||
</div>
|
||||
<AddSegmentModal />
|
||||
<AddSignalModal v-if="addSignalAllowed()" />
|
||||
<AddSegmentBoundaryModal v-if="addSegmentBoundaryAllowed()" />
|
||||
<AddSwitchModal v-if="addSwitchAllowed()" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SegmentsView from "./SegmentsView.vue";
|
||||
import AddSegmentModal from "./AddSegmentModal.vue";
|
||||
import AddSignalModal from "./component/AddSignalModal.vue";
|
||||
import AddSegmentBoundaryModal from "./component/AddSegmentBoundaryModal.vue";
|
||||
import AddSwitchModal from "./component/AddSwitchModal.vue";
|
||||
import CreateLinkTokenModal from "./CreateLinkTokenModal.vue";
|
||||
import {RailSystem} from "../../api/railSystems";
|
||||
|
||||
export default {
|
||||
name: "RailSystemPropertiesView",
|
||||
components: {
|
||||
CreateLinkTokenModal,
|
||||
AddSwitchModal,
|
||||
AddSegmentBoundaryModal,
|
||||
AddSignalModal,
|
||||
AddSegmentModal,
|
||||
SegmentsView
|
||||
},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addSignalAllowed() {
|
||||
return this.railSystem.segments && this.railSystem.segments.length > 0;
|
||||
},
|
||||
addSegmentBoundaryAllowed() {
|
||||
return this.railSystem.segments && this.railSystem.segments.length > 1;
|
||||
},
|
||||
addSwitchAllowed() {
|
||||
return this.railSystem.components && this.railSystem.components.length > 1;
|
||||
},
|
||||
createLinkTokenAllowed() {
|
||||
return this.railSystem.components && this.railSystem.components.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,52 +0,0 @@
|
|||
<template>
|
||||
<h5>Segments</h5>
|
||||
<input type="search" class="form-control-sm w-100 mb-1" placeholder="Filter by name" v-model="segmentNameFilter" />
|
||||
<ul class="list-group overflow-auto mb-2" style="max-height: 200px;">
|
||||
<li
|
||||
v-for="segment in filteredSegments()"
|
||||
:key="segment.id"
|
||||
class="list-group-item"
|
||||
>
|
||||
{{segment.name}}
|
||||
<button @click.prevent="removeSegment(rsStore.selectedRailSystem, segment.id)" class="btn btn-sm btn-danger float-end">Remove</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||
import {removeSegment} from "../../api/segments";
|
||||
|
||||
export default {
|
||||
name: "SegmentsView.vue",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore,
|
||||
removeSegment
|
||||
}
|
||||
},
|
||||
props: {
|
||||
segments: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
segmentNameFilter: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
filteredSegments() {
|
||||
if (this.segmentNameFilter === null || this.segmentNameFilter.trim().length === 0) return this.segments;
|
||||
const filterString = this.segmentNameFilter.trim().toLowerCase();
|
||||
return this.segments.filter(segment => segment.name.toLowerCase().includes(filterString));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,16 +0,0 @@
|
|||
export function roundedRect(ctx, x, y, w, h, r) {
|
||||
if (w < 2 * r) r = w / 2;
|
||||
if (h < 2 * r) r = h / 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x+r, y);
|
||||
ctx.arcTo(x+w, y, x+w, y+h, r);
|
||||
ctx.arcTo(x+w, y+h, x, y+h, r);
|
||||
ctx.arcTo(x, y+h, x, y, r);
|
||||
ctx.arcTo(x, y, x+w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
export function circle(ctx, x, y, r) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="addSegmentBoundaryModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Segment Boundary</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
A <em>segment boundary</em> is a component that defines a link
|
||||
between one segment and another. This component can be used to
|
||||
monitor trains entering and exiting the connected segments. Usually
|
||||
used in conjunction with signals for classic railway signalling
|
||||
systems.
|
||||
</p>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="addSignalName" class="form-label">Name</label>
|
||||
<input class="form-control" type="text" id="addSignalName" v-model="formData.name" required/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<label for="addSignalX" class="input-group-text">X</label>
|
||||
<input class="form-control" type="number" id="addSignalX" v-model="formData.position.x" required/>
|
||||
<label for="addSignalY" class="input-group-text">Y</label>
|
||||
<input class="form-control" type="number" id="addSignalY" v-model="formData.position.y" required/>
|
||||
<label for="addSignalZ" class="input-group-text">Z</label>
|
||||
<input class="form-control" type="number" id="addSignalZ" v-model="formData.position.z" required/>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="addSegmentBoundarySegmentA" class="form-label">Segment A</label>
|
||||
<select id="addSegmentBoundarySegmentA" class="form-select" v-model="formData.segmentA">
|
||||
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
|
||||
{{segment.name}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="addSegmentBoundarySegmentA" class="form-label">Segment B</label>
|
||||
<select id="addSegmentBoundarySegmentA" class="form-select" v-model="formData.segmentB">
|
||||
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
|
||||
{{segment.name}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||
import {Modal} from "bootstrap";
|
||||
import {createComponent} from "../../../api/components";
|
||||
|
||||
export default {
|
||||
name: "AddSegmentBoundaryModal",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: "",
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
segmentA: null,
|
||||
segmentB: null,
|
||||
segments: [],
|
||||
type: "SEGMENT_BOUNDARY"
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
const modal = Modal.getInstance(document.getElementById("addSegmentBoundaryModal"));
|
||||
this.formData.segments = [this.formData.segmentA, this.formData.segmentB];
|
||||
createComponent(this.rsStore.selectedRailSystem, this.formData)
|
||||
.then(() => {
|
||||
modal.hide();
|
||||
})
|
||||
.catch(error => {
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't add the segment boundary: " + error.response.data.message)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,99 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="addSignalModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Signal</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
A <em>signal</em> is a component that relays information about your
|
||||
rail system to in-world devices. Classically, rail signals show a
|
||||
lamp indicator to tell information about the segment of the network
|
||||
they're attached to.
|
||||
</p>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="addSignalName" class="form-label">Name</label>
|
||||
<input class="form-control" type="text" id="addSignalName" v-model="formData.name" required/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<label for="addSignalX" class="input-group-text">X</label>
|
||||
<input class="form-control" type="number" id="addSignalX" v-model="formData.position.x" required/>
|
||||
<label for="addSignalY" class="input-group-text">Y</label>
|
||||
<input class="form-control" type="number" id="addSignalY" v-model="formData.position.y" required/>
|
||||
<label for="addSignalZ" class="input-group-text">Z</label>
|
||||
<input class="form-control" type="number" id="addSignalZ" v-model="formData.position.z" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addSignalSegment" class="form-label">Segment</label>
|
||||
<select id="addSignalSegment" class="form-select" v-model="formData.segment">
|
||||
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
|
||||
{{segment.name}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||
import {Modal} from "bootstrap";
|
||||
import {createComponent} from "../../../api/components";
|
||||
|
||||
export default {
|
||||
name: "AddSignalModal",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: "",
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
segment: null,
|
||||
type: "SIGNAL"
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
const modal = Modal.getInstance(document.getElementById("addSignalModal"));
|
||||
createComponent(this.rsStore.selectedRailSystem, this.formData)
|
||||
.then(() => {
|
||||
modal.hide();
|
||||
})
|
||||
.catch(error => {
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't add the signal: " + error.response.data.message)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,123 +0,0 @@
|
|||
<template>
|
||||
<div class="modal fade" tabindex="-1" id="addSwitchModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Switch</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="addSwitchName" class="form-label">Name</label>
|
||||
<input class="form-control" type="text" id="addSwitchName" v-model="formData.name" required/>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<label for="addSwitchX" class="input-group-text">X</label>
|
||||
<input class="form-control" type="number" id="addSwitchX" v-model="formData.position.x" required/>
|
||||
<label for="addSwitchY" class="input-group-text">Y</label>
|
||||
<input class="form-control" type="number" id="addSwitchY" v-model="formData.position.y" required/>
|
||||
<label for="addSwitchZ" class="input-group-text">Z</label>
|
||||
<input class="form-control" type="number" id="addSwitchZ" v-model="formData.position.z" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<ul class="list-group overflow-auto" style="height: 200px;">
|
||||
<li class="list-group-item" v-for="(config, idx) in formData.possibleConfigurations" :key="idx">
|
||||
{{getConfigString(config)}}
|
||||
<button class="btn btn-sm btn-secondary" @click="formData.possibleConfigurations.splice(idx, 1)">Remove</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addSwitchConfigs" class="form-label">Select two nodes this switch can connect.</label>
|
||||
<select id="addSwitchConfigs" class="form-select" multiple v-model="formData.possibleConfigQueue">
|
||||
<option v-for="node in getEligibleNodes()" :key="node.id" :value="node">
|
||||
{{node.name}}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-sm btn-success" @click="addPossibleConfig()">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="warnings.length > 0">
|
||||
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
|
||||
{{msg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||
import {Modal} from "bootstrap";
|
||||
import {createComponent} from "../../../api/components";
|
||||
|
||||
export default {
|
||||
name: "AddSwitchModal",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: "",
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
possibleConfigurations: [],
|
||||
possibleConfigQueue: [],
|
||||
type: "SWITCH"
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
const modal = Modal.getInstance(document.getElementById("addSwitchModal"));
|
||||
createComponent(this.rsStore.selectedRailSystem, this.formData)
|
||||
.then(() => {
|
||||
modal.hide();
|
||||
})
|
||||
.catch(error => {
|
||||
this.warnings.length = 0;
|
||||
this.warnings.push("Couldn't add the signal: " + error.response.data.message)
|
||||
});
|
||||
},
|
||||
getConfigString(config) {
|
||||
return config.nodes.map(n => n.name).join(", ");
|
||||
},
|
||||
getEligibleNodes() {
|
||||
return this.rsStore.selectedRailSystem.components.filter(c => {
|
||||
if (c.connectedNodes === undefined || c.connectedNodes === null) return false;
|
||||
for (let i = 0; i < this.formData.possibleConfigurations.length; i++) {
|
||||
const config = this.formData.possibleConfigurations[i];
|
||||
for (const node in config.nodes) {
|
||||
if (node.id === c.id) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
addPossibleConfig() {
|
||||
if (this.formData.possibleConfigQueue.length < 2) return;
|
||||
this.formData.possibleConfigurations.push({nodes: this.formData.possibleConfigQueue});
|
||||
this.formData.possibleConfigQueue = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,88 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3>{{component.name}}</h3>
|
||||
<small class="text-muted">
|
||||
{{component.type}}
|
||||
</small>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th><td>{{component.id}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<td>
|
||||
<table class="table table-borderless m-0 p-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-0">X = {{component.position.x}}</td>
|
||||
<td class="p-0">Y = {{component.position.y}}</td>
|
||||
<td class="p-0">Z = {{component.position.z}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
||||
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
|
||||
<SwitchComponentView v-if="component.type === 'SWITCH'" :sw="component"/>
|
||||
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
|
||||
<button @click="remove()" class="btn btn-sm btn-danger">Remove</button>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
ref="removeConfirm"
|
||||
:id="'removeComponentModal'"
|
||||
:title="'Remove Component'"
|
||||
:message="'Are you sure you want to remove this component? It, and all associated data, will be permanently deleted.'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SignalComponentView from "./SignalComponentView.vue";
|
||||
import PathNodeComponentView from "./PathNodeComponentView.vue";
|
||||
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
|
||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||
import ConfirmModal from "../../ConfirmModal.vue";
|
||||
import SwitchComponentView from "./SwitchComponentView.vue";
|
||||
import {removeComponent} from "../../../api/components";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SwitchComponentView,
|
||||
ConfirmModal,
|
||||
SegmentBoundaryNodeComponentView,
|
||||
SignalComponentView,
|
||||
PathNodeComponentView
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
railSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
remove() {
|
||||
this.$refs.removeConfirm.showConfirm()
|
||||
.then(() => {
|
||||
removeComponent(this.rsStore.selectedRailSystem, this.component.id)
|
||||
.catch(console.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,84 +0,0 @@
|
|||
<template>
|
||||
<h5>Connected Nodes</h5>
|
||||
<ul class="list-group list-group-flush mb-2 border" v-if="pathNode.connectedNodes.length > 0" style="overflow: auto; max-height: 150px;">
|
||||
<li
|
||||
v-for="node in pathNode.connectedNodes"
|
||||
:key="node.id"
|
||||
class="list-group-item"
|
||||
>
|
||||
{{node.name}}
|
||||
<button @click="remove(node)" class="btn btn-sm btn-danger float-end">
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="pathNode.connectedNodes.length === 0">
|
||||
There are no connected nodes.
|
||||
</p>
|
||||
<form
|
||||
@submit.prevent="add(formData.nodeToAdd)"
|
||||
v-if="getEligibleConnections().length > 0"
|
||||
class="input-group mb-3"
|
||||
>
|
||||
<select v-model="formData.nodeToAdd" class="form-select form-select-sm">
|
||||
<option v-for="node in this.getEligibleConnections()" :key="node.id" :value="node">
|
||||
{{node.name}}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-success">Add Connection</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {addConnection, removeConnection} from "../../../api/paths";
|
||||
|
||||
export default {
|
||||
name: "PathNodeComponentView",
|
||||
props: {
|
||||
pathNode: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
railSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
nodeToAdd: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getEligibleConnections() {
|
||||
const nodes = [];
|
||||
for (let i = 0; i < this.railSystem.components.length; i++) {
|
||||
const c = this.railSystem.components[i];
|
||||
if (c.id !== this.pathNode.id && c.connectedNodes !== undefined && c.connectedNodes !== null) {
|
||||
let exists = false;
|
||||
for (let j = 0; j < this.pathNode.connectedNodes.length; j++) {
|
||||
if (this.pathNode.connectedNodes[j].id === c.id) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) nodes.push(c);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
remove(node) {
|
||||
removeConnection(this.railSystem, this.pathNode, node);
|
||||
},
|
||||
add(node) {
|
||||
addConnection(this.railSystem, this.pathNode, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<h5>Segments Connected</h5>
|
||||
<div class="mb-2">
|
||||
<span
|
||||
v-for="segment in node.segments"
|
||||
:key="segment.id"
|
||||
class="badge bg-secondary me-1"
|
||||
>
|
||||
{{segment.name}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SegmentBoundaryNodeComponentView",
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<h5>Signal Properties</h5>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Connected to</th>
|
||||
<td>{{signal.segment.name}}, Occupied: {{signal.segment.occupied}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SignalComponentView",
|
||||
props: {
|
||||
signal: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,44 +0,0 @@
|
|||
<template>
|
||||
<h5>Switch Configurations</h5>
|
||||
<ul
|
||||
class="list-group list-group-flush border mb-2"
|
||||
v-if="sw.possibleConfigurations.length > 0"
|
||||
style="overflow: auto; max-height: 150px;"
|
||||
>
|
||||
<li
|
||||
v-for="config in sw.possibleConfigurations"
|
||||
:key="config.id"
|
||||
class="list-group-item"
|
||||
>
|
||||
<span
|
||||
v-for="node in config.nodes"
|
||||
:key="node.id"
|
||||
class="badge bg-secondary me-1"
|
||||
>
|
||||
{{node.name}}
|
||||
</span>
|
||||
<span
|
||||
v-if="sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id"
|
||||
class="badge bg-success"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SwitchComponentView",
|
||||
props: {
|
||||
sw: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
Helper functions to actually perform rendering of different components.
|
||||
*/
|
||||
|
||||
import {getScaleFactor, isComponentHovered} from "./mapRenderer";
|
||||
import {roundedRect, circle} from "./canvasUtils";
|
||||
|
||||
export function drawComponent(ctx, worldTx, component) {
|
||||
const tx = DOMMatrix.fromMatrix(worldTx);
|
||||
tx.translateSelf(component.position.x, component.position.z, 0);
|
||||
const s = getScaleFactor();
|
||||
tx.scaleSelf(1/s, 1/s, 1/s);
|
||||
tx.scaleSelf(20, 20, 20);
|
||||
ctx.setTransform(tx);
|
||||
|
||||
if (component.type === "SIGNAL") {
|
||||
drawSignal(ctx, component);
|
||||
} else if (component.type === "SEGMENT_BOUNDARY") {
|
||||
drawSegmentBoundary(ctx, component);
|
||||
} else if (component.type === "SWITCH") {
|
||||
drawSwitch(ctx, component);
|
||||
}
|
||||
|
||||
ctx.setTransform(tx.translate(0.75, -0.75));
|
||||
if (component.online !== undefined && component.online !== null) {
|
||||
drawOnlineIndicator(ctx, component);
|
||||
}
|
||||
|
||||
ctx.setTransform(tx);
|
||||
// Draw hovered status.
|
||||
if (isComponentHovered(component)) {
|
||||
ctx.fillStyle = `rgba(255, 255, 0, 0.5)`;
|
||||
circle(ctx, 0, 0, 0.75);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawSignal(ctx) {
|
||||
roundedRect(ctx, -0.3, -0.5, 0.6, 1, 0.25);
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgb(0, 255, 0)";
|
||||
circle(ctx, 0, -0.2, 0.15);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawSegmentBoundary(ctx) {
|
||||
ctx.fillStyle = `rgb(150, 58, 224)`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -0.5);
|
||||
ctx.lineTo(-0.5, 0);
|
||||
ctx.lineTo(0, 0.5);
|
||||
ctx.lineTo(0.5, 0);
|
||||
ctx.lineTo(0, -0.5);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawSwitch(ctx, sw) {
|
||||
const colors = [
|
||||
`rgba(61, 148, 66, 0.25)`,
|
||||
`rgba(59, 22, 135, 0.25)`,
|
||||
`rgba(145, 17, 90, 0.25)`,
|
||||
`rgba(191, 49, 10, 0.25)`
|
||||
];
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
|
||||
const config = sw.possibleConfigurations[i];
|
||||
ctx.strokeStyle = colors[i];
|
||||
for (let j = 0; j < config.nodes.length; j++) {
|
||||
const node = config.nodes[j];
|
||||
const diff = {
|
||||
x: sw.position.x - node.position.x,
|
||||
y: sw.position.z - node.position.z,
|
||||
};
|
||||
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
|
||||
diff.x = 2 * -diff.x / mag;
|
||||
diff.y = 2 * -diff.y / mag;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(diff.x, diff.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = `rgb(245, 188, 66)`;
|
||||
ctx.strokeStyle = `rgb(245, 188, 66)`;
|
||||
ctx.lineWidth = 0.2;
|
||||
circle(ctx, 0, 0.3, 0.2);
|
||||
ctx.fill();
|
||||
circle(ctx, -0.3, -0.3, 0.2);
|
||||
ctx.fill();
|
||||
circle(ctx, 0.3, -0.3, 0.2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0.3);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(0.3, -0.3);
|
||||
ctx.moveTo(0, 0);
|
||||
ctx.lineTo(-0.3, -0.3);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawOnlineIndicator(ctx, component) {
|
||||
ctx.lineWidth = 0.1;
|
||||
if (component.online) {
|
||||
ctx.fillStyle = `rgba(52, 174, 235, 128)`;
|
||||
ctx.strokeStyle = `rgba(52, 174, 235, 128)`;
|
||||
} else {
|
||||
ctx.fillStyle = `rgba(153, 153, 153, 128)`;
|
||||
ctx.strokeStyle = `rgba(153, 153, 153, 128)`;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0.2, 0.125, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
for (let r = 0; r < 3; r++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawConnectedNodes(ctx, worldTx, component) {
|
||||
const s = getScaleFactor();
|
||||
ctx.lineWidth = 5 / s;
|
||||
ctx.strokeStyle = "black";
|
||||
for (let i = 0; i < component.connectedNodes.length; i++) {
|
||||
const node = component.connectedNodes[i];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(component.position.x, component.position.z);
|
||||
ctx.lineTo(node.position.x, node.position.z);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
|
@ -1,248 +0,0 @@
|
|||
/*
|
||||
This component is responsible for the rendering of a RailSystem in a 2d map
|
||||
view.
|
||||
*/
|
||||
|
||||
import {drawComponent, drawConnectedNodes} from "./drawing";
|
||||
|
||||
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
|
||||
const SCALE_INDEX_NORMAL = 7;
|
||||
const HOVER_RADIUS = 10;
|
||||
|
||||
let mapContainerDiv = null;
|
||||
let mapCanvas = null;
|
||||
let railSystem = null;
|
||||
|
||||
let mapScaleIndex = SCALE_INDEX_NORMAL;
|
||||
let mapTranslation = {x: 0, y: 0};
|
||||
let mapDragOrigin = null;
|
||||
let mapDragTranslation = null;
|
||||
let lastMousePoint = new DOMPoint(0, 0, 0, 0);
|
||||
const hoveredElements = [];
|
||||
|
||||
export function initMap(rs) {
|
||||
railSystem = rs;
|
||||
console.log("Initializing map for rail system: " + rs.name);
|
||||
hoveredElements.length = 0;
|
||||
mapCanvas = document.getElementById("railSystemMapCanvas");
|
||||
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
|
||||
mapCanvas.removeEventListener("wheel", onMouseWheel);
|
||||
mapCanvas.addEventListener("wheel", onMouseWheel);
|
||||
mapCanvas.removeEventListener("mousedown", onMouseDown);
|
||||
mapCanvas.addEventListener("mousedown", onMouseDown);
|
||||
mapCanvas.removeEventListener("mouseup", onMouseUp);
|
||||
mapCanvas.addEventListener("mouseup", onMouseUp);
|
||||
mapCanvas.removeEventListener("mousemove", onMouseMove);
|
||||
mapCanvas.addEventListener("mousemove", onMouseMove);
|
||||
|
||||
// Do an initial draw.
|
||||
draw();
|
||||
}
|
||||
|
||||
export function draw() {
|
||||
if (!(mapCanvas && railSystem && railSystem.components)) {
|
||||
console.warn("Attempted to draw map without canvas or railSystem.");
|
||||
return;
|
||||
}
|
||||
const ctx = mapCanvas.getContext("2d");
|
||||
if (mapCanvas.width !== mapContainerDiv.clientWidth) {
|
||||
mapCanvas.width = mapContainerDiv.clientWidth;
|
||||
}
|
||||
if (mapCanvas.height !== mapContainerDiv.clientHeight) {
|
||||
mapCanvas.height = mapContainerDiv.clientHeight;
|
||||
}
|
||||
const width = mapCanvas.width;
|
||||
const height = mapCanvas.height;
|
||||
ctx.resetTransform();
|
||||
ctx.fillStyle = `rgb(240, 240, 240)`;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
const worldTx = getWorldTransform();
|
||||
ctx.setTransform(worldTx);
|
||||
|
||||
// Draw segments!
|
||||
const segmentPoints = new Map();
|
||||
railSystem.segments.forEach(segment => segmentPoints.set(segment.id, []));
|
||||
for (let i = 0; i < railSystem.components.length; i++) {
|
||||
const c = railSystem.components[i];
|
||||
if (c.type === "SEGMENT_BOUNDARY") {
|
||||
for (let j = 0; j < c.segments.length; j++) {
|
||||
segmentPoints.get(c.segments[j].id).push({x: c.position.x, y: c.position.z});
|
||||
}
|
||||
}
|
||||
}
|
||||
railSystem.segments.forEach(segment => {
|
||||
const points = segmentPoints.get(segment.id);
|
||||
const avgPoint = {x: 0, y: 0};
|
||||
points.forEach(point => {
|
||||
avgPoint.x += point.x;
|
||||
avgPoint.y += point.y;
|
||||
});
|
||||
avgPoint.x /= points.length;
|
||||
avgPoint.y /= points.length;
|
||||
let r = 5;
|
||||
points.forEach(point => {
|
||||
const dist2 = Math.pow(avgPoint.x - point.x, 2) + Math.pow(avgPoint.y - point.y, 2);
|
||||
if (dist2 > r * r) {
|
||||
r = Math.sqrt(dist2);
|
||||
}
|
||||
});
|
||||
ctx.fillStyle = `rgba(200, 200, 200, 0.25)`;
|
||||
const p = worldPointToMap(new DOMPoint(avgPoint.x, avgPoint.y, 0, 0));
|
||||
const s = getScaleFactor();
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x / s, p.y / s, r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
||||
ctx.font = "3px Sans-Serif";
|
||||
ctx.fillText(`${segment.name}`, p.x / s, p.y / s);
|
||||
});
|
||||
|
||||
for (let i = 0; i < railSystem.components.length; i++) {
|
||||
const c = railSystem.components[i];
|
||||
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
|
||||
drawConnectedNodes(ctx, worldTx, c);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < railSystem.components.length; i++) {
|
||||
drawComponent(ctx, worldTx, railSystem.components[i]);
|
||||
}
|
||||
|
||||
// Draw debug info.
|
||||
ctx.resetTransform();
|
||||
ctx.fillStyle = "black";
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.font = "10px Sans-Serif";
|
||||
const lastWorldPoint = mapPointToWorld(lastMousePoint);
|
||||
const lines = [
|
||||
"Scale factor: " + getScaleFactor(),
|
||||
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
|
||||
`Components: ${railSystem.components.length}`,
|
||||
`Hovered elements: ${hoveredElements.length}`
|
||||
]
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
ctx.fillText(lines[i], 10, 20 + (i * 15));
|
||||
}
|
||||
}
|
||||
|
||||
export function getScaleFactor() {
|
||||
return SCALE_VALUES[mapScaleIndex];
|
||||
}
|
||||
|
||||
function getWorldTransform() {
|
||||
const canvasRect = mapCanvas.getBoundingClientRect();
|
||||
const scale = getScaleFactor();
|
||||
const tx = new DOMMatrix();
|
||||
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
|
||||
tx.scaleSelf(scale, scale, scale);
|
||||
tx.translateSelf(mapTranslation.x, mapTranslation.y, 0);
|
||||
if (mapDragOrigin !== null && mapDragTranslation !== null) {
|
||||
tx.translateSelf(mapDragTranslation.x, mapDragTranslation.y, 0);
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
export function isComponentHovered(component) {
|
||||
for (let i = 0; i < hoveredElements.length; i++) {
|
||||
if (hoveredElements[i].id === component.id) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point on the map coordinates to world coordinates.
|
||||
* @param {DOMPoint} p
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
function mapPointToWorld(p) {
|
||||
return getWorldTransform().invertSelf().transformPoint(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a point in the world to map coordinates.
|
||||
* @param {DOMPoint} p
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
function worldPointToMap(p) {
|
||||
return getWorldTransform().transformPoint(p);
|
||||
}
|
||||
|
||||
/*
|
||||
EVENT HANDLING
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {WheelEvent} event
|
||||
*/
|
||||
function onMouseWheel(event) {
|
||||
const s = event.deltaY;
|
||||
if (s > 0) {
|
||||
mapScaleIndex = Math.max(0, mapScaleIndex - 1);
|
||||
} else if (s < 0) {
|
||||
mapScaleIndex = Math.min(SCALE_VALUES.length - 1, mapScaleIndex + 1);
|
||||
}
|
||||
draw();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseDown(event) {
|
||||
const p = getMousePoint(event);
|
||||
mapDragOrigin = {x: p.x, y: p.y};
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (mapDragTranslation !== null) {
|
||||
mapTranslation.x += mapDragTranslation.x;
|
||||
mapTranslation.y += mapDragTranslation.y;
|
||||
}
|
||||
if (hoveredElements.length === 1) {
|
||||
railSystem.selectedComponent = hoveredElements[0];
|
||||
} else {
|
||||
railSystem.selectedComponent = null;
|
||||
}
|
||||
mapDragOrigin = null;
|
||||
mapDragTranslation = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function onMouseMove(event) {
|
||||
const p = getMousePoint(event);
|
||||
lastMousePoint = p;
|
||||
if (mapDragOrigin !== null) {
|
||||
const scale = getScaleFactor();
|
||||
const dx = p.x - mapDragOrigin.x;
|
||||
const dy = p.y - mapDragOrigin.y;
|
||||
mapDragTranslation = {x: dx / scale, y: dy / scale};
|
||||
} else {
|
||||
hoveredElements.length = 0;
|
||||
// Populate with list of hovered elements.
|
||||
for (let i = 0; i < railSystem.components.length; i++) {
|
||||
const c = railSystem.components[i];
|
||||
const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1);
|
||||
const mapComponentPoint = worldPointToMap(componentPoint);
|
||||
const dist2 = (p.x - mapComponentPoint.x) * (p.x - mapComponentPoint.x) + (p.y - mapComponentPoint.y) * (p.y - mapComponentPoint.y);
|
||||
if (dist2 < HOVER_RADIUS * HOVER_RADIUS) {
|
||||
hoveredElements.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the point at which the user clicked on the map.
|
||||
* @param {MouseEvent} event
|
||||
* @returns {DOMPoint}
|
||||
*/
|
||||
function getMousePoint(event) {
|
||||
const rect = mapCanvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
return new DOMPoint(x, y, 0, 1);
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
<template>
|
||||
<div class="border p-2">
|
||||
<input type="text" class="form-control mb-2" placeholder="Search for components" v-model="searchQuery" @keyup="refreshComponents()" />
|
||||
<ul class="list-group list-group-flush" style="overflow: auto; max-height: 200px">
|
||||
<li
|
||||
class="list-group-item"
|
||||
v-for="component in possibleComponents"
|
||||
:key="component.id"
|
||||
>
|
||||
{{component.name}}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="badge btn btn-sm btn-success float-end"
|
||||
@click="selectComponent(component)"
|
||||
v-if="!isComponentSelected(component)"
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
<span v-if="isComponentSelected(component)" class="badge bg-secondary float-end">Selected</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="selectedComponents.length > 0" class="mt-2">
|
||||
<span
|
||||
class="badge bg-secondary me-1 mb-1"
|
||||
v-for="component in selectedComponents"
|
||||
:key="component.id"
|
||||
>
|
||||
{{component.name}}
|
||||
<button class="badge rounded-pill bg-danger" @click="deselectComponent(component)">
|
||||
X
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
type="button"
|
||||
v-if="selectedComponents.length > 1"
|
||||
@click="selectedComponents.length = 0"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {RailSystem} from "../../../api/railSystems";
|
||||
import {searchComponents} from "../../../api/components";
|
||||
|
||||
export default {
|
||||
name: "ComponentSelector",
|
||||
props: {
|
||||
railSystem: {
|
||||
type: RailSystem,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
searchQuery: null,
|
||||
selectedComponents: [],
|
||||
possibleComponents: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.refreshComponents();
|
||||
},
|
||||
methods: {
|
||||
refreshComponents() {
|
||||
searchComponents(this.railSystem, this.searchQuery)
|
||||
.then(page => this.possibleComponents = page.content);
|
||||
},
|
||||
isComponentSelected(component) {
|
||||
return this.selectedComponents.some(c => c.id === component.id);
|
||||
},
|
||||
selectComponent(component) {
|
||||
if (this.isComponentSelected(component)) return;
|
||||
this.selectedComponents.push(component);
|
||||
this.selectedComponents.sort((a, b) => {
|
||||
const nameA = a.name.toUpperCase();
|
||||
const nameB = b.name.toUpperCase();
|
||||
if (nameA < nameB) return -1;
|
||||
if (nameA > nameB) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.$emit("update:modelValue", this.selectedComponents);
|
||||
},
|
||||
deselectComponent(component) {
|
||||
const idx = this.selectedComponents.findIndex(c => c.id === component.id);
|
||||
if (idx > -1) {
|
||||
this.selectedComponents.splice(idx, 1);
|
||||
this.$emit("update:modelValue", this.selectedComponents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,11 +0,0 @@
|
|||
import {createApp} from 'vue';
|
||||
import {createPinia} from 'pinia';
|
||||
import App from './App.vue'
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "bootstrap";
|
||||
|
||||
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
app.use(pinia);
|
||||
app.mount('#app')
|
|
@ -1,32 +0,0 @@
|
|||
import {defineStore} from "pinia";
|
||||
import {refreshSegments} from "../api/segments"
|
||||
import {refreshComponents} from "../api/components";
|
||||
import {closeWebsocketConnection, establishWebsocketConnection} from "../api/websocket";
|
||||
|
||||
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||
state: () => ({
|
||||
/**
|
||||
* @type {RailSystem[]}
|
||||
*/
|
||||
railSystems: [],
|
||||
/**
|
||||
* @type {RailSystem | null}
|
||||
*/
|
||||
selectedRailSystem: null
|
||||
}),
|
||||
actions: {
|
||||
onSelectedRailSystemChanged() {
|
||||
this.railSystems.forEach(rs => closeWebsocketConnection(rs));
|
||||
if (!this.selectedRailSystem) return;
|
||||
refreshSegments(this.selectedRailSystem);
|
||||
refreshComponents(this.selectedRailSystem);
|
||||
establishWebsocketConnection(this.selectedRailSystem);
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
rsId() {
|
||||
if (this.selectedRailSystem === null) return null;
|
||||
return this.selectedRailSystem.id;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({command, mode}) => {
|
||||
return {
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
base: mode === "production" ? "/app/" : undefined
|
||||
}
|
||||
})
|
|
@ -3,7 +3,7 @@ package nl.andrewl.railsignalapi.live;
|
|||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* A downlink connection to one or more components (linked by a {@link nl.andrewl.railsignalapi.model.LinkToken}
|
||||
* A downlink connection to one or more components (linked by a {@link nl.andrewl.railsignalapi.model.LinkToken})
|
||||
* which we can send messages to.
|
||||
*/
|
||||
public abstract class ComponentDownlink {
|
||||
|
|
|
@ -4,6 +4,8 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentDataMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentMessage;
|
||||
import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -31,13 +33,18 @@ public class ComponentDownlinkService {
|
|||
* @param downlink The downlink to register.
|
||||
*/
|
||||
@Transactional
|
||||
public synchronized void registerDownlink(ComponentDownlink downlink) {
|
||||
public synchronized void registerDownlink(ComponentDownlink downlink) throws Exception {
|
||||
Set<Component> components = tokenRepository.findById(downlink.getTokenId()).orElseThrow().getComponents();
|
||||
componentDownlinks.put(downlink, components.stream().map(Component::getId).collect(Collectors.toSet()));
|
||||
for (var c : components) {
|
||||
c.setOnline(true);
|
||||
componentRepository.save(c);
|
||||
appUpdateService.sendComponentUpdate(c.getRailSystem().getId(), c.getId());
|
||||
|
||||
// Immediately send a data message to the downlink and app for each component that comes online.
|
||||
var msg = new ComponentDataMessage(c);
|
||||
downlink.send(msg);
|
||||
appUpdateService.sendUpdate(c.getRailSystem().getId(), msg);
|
||||
|
||||
Set<ComponentDownlink> downlinks = downlinksByCId.computeIfAbsent(c.getId(), aLong -> new HashSet<>());
|
||||
downlinks.add(downlink);
|
||||
}
|
||||
|
@ -97,4 +104,8 @@ public class ComponentDownlinkService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(ComponentMessage msg) {
|
||||
sendMessage(msg.cId, msg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package nl.andrewl.railsignalapi.live;
|
|||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentUplinkMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SegmentBoundaryUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.service.SegmentService;
|
||||
|
@ -22,7 +22,7 @@ public class ComponentUplinkMessageHandler {
|
|||
private final SegmentService segmentService;
|
||||
|
||||
@Transactional
|
||||
public void messageReceived(ComponentUplinkMessage msg) {
|
||||
public void messageReceived(ComponentMessage msg) {
|
||||
if (msg instanceof SegmentBoundaryUpdateMessage sb) {
|
||||
segmentService.onBoundaryUpdate(sb);
|
||||
} else if (msg instanceof SwitchUpdateMessage sw) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
|
||||
|
||||
/**
|
||||
* A message that's sent to devices which contains the full component response
|
||||
* for a specific component.
|
||||
*/
|
||||
@Getter
|
||||
public class ComponentDataMessage extends ComponentMessage {
|
||||
private final ComponentResponse data;
|
||||
|
||||
public ComponentDataMessage(Component c) {
|
||||
super(c.getId(), "COMPONENT_DATA");
|
||||
this.data = ComponentResponse.of(c);
|
||||
}
|
||||
}
|
|
@ -3,18 +3,32 @@ package nl.andrewl.railsignalapi.live.dto;
|
|||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* The parent class for all uplink messages that can be sent by connected
|
||||
* components.
|
||||
* Base class for all messages that will be sent to components.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = SegmentBoundaryUpdateMessage.class, name = "sb"),
|
||||
@JsonSubTypes.Type(value = SwitchUpdateMessage.class, name = "sw")
|
||||
@JsonSubTypes.Type(value = SegmentBoundaryUpdateMessage.class, name = "SEGMENT_BOUNDARY_UPDATE"),
|
||||
@JsonSubTypes.Type(value = SwitchUpdateMessage.class, name = "SWITCH_UPDATE"),
|
||||
@JsonSubTypes.Type(value = ErrorMessage.class, name = "ERROR")
|
||||
})
|
||||
public abstract class ComponentUplinkMessage {
|
||||
@NoArgsConstructor
|
||||
public abstract class ComponentMessage {
|
||||
/**
|
||||
* The id of the component that this message is for.
|
||||
*/
|
||||
public long cId;
|
||||
|
||||
/**
|
||||
* The type of message.
|
||||
*/
|
||||
public String type;
|
||||
|
||||
public ComponentMessage(long cId, String type) {
|
||||
this.cId = cId;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
/**
|
||||
* A message that's sent regarding an error that occurred.
|
||||
*/
|
||||
public class ErrorMessage extends ComponentMessage {
|
||||
public String message;
|
||||
|
||||
public ErrorMessage(long cId, String message) {
|
||||
super(cId, "ERROR");
|
||||
this.message = message;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Message that's sent by segment boundaries when a train crosses it.
|
||||
*/
|
||||
public class SegmentBoundaryUpdateMessage extends ComponentUplinkMessage {
|
||||
@NoArgsConstructor
|
||||
public class SegmentBoundaryUpdateMessage extends ComponentMessage {
|
||||
/**
|
||||
* The id of the segment that a train detected by the segment boundary is
|
||||
* moving towards.
|
||||
|
|
|
@ -1,6 +1,26 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
public record SegmentStatusMessage (
|
||||
long cId,
|
||||
boolean occupied
|
||||
) {}
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* A message that's sent to signal components regarding a change in a segment's
|
||||
* status.
|
||||
*/
|
||||
@Getter
|
||||
public class SegmentStatusMessage extends ComponentMessage {
|
||||
/**
|
||||
* The id of the segment that updated.
|
||||
*/
|
||||
private final long sId;
|
||||
|
||||
/**
|
||||
* Whether the segment is occupied.
|
||||
*/
|
||||
private final boolean occupied;
|
||||
|
||||
public SegmentStatusMessage(long cId, long sId, boolean occupied) {
|
||||
super(cId, "SEGMENT_STATUS");
|
||||
this.sId = sId;
|
||||
this.occupied = occupied;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
import java.util.Set;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Message that's sent by a switch when its active configuration is updated.
|
||||
*/
|
||||
public class SwitchUpdateMessage extends ComponentUplinkMessage {
|
||||
@NoArgsConstructor
|
||||
public class SwitchUpdateMessage extends ComponentMessage {
|
||||
/**
|
||||
* A set of path node ids that represents the active configuration.
|
||||
* The id of the configuration that's active.
|
||||
*/
|
||||
public Set<Long> configuration;
|
||||
public long activeConfigId;
|
||||
|
||||
public SwitchUpdateMessage(long cId, long activeConfigId) {
|
||||
super(cId, "SWITCH_UPDATE");
|
||||
this.activeConfigId = activeConfigId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package nl.andrewl.railsignalapi.live.tcp_socket;
|
||||
|
||||
/**
|
||||
* A message that's sent in response to a client's attempt to connect.
|
||||
* @param valid Whether the connection is valid.
|
||||
* @param message A message.
|
||||
*/
|
||||
public record ConnectMessage(
|
||||
boolean valid,
|
||||
String message
|
||||
|
|
|
@ -4,7 +4,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import nl.andrewl.railsignalapi.live.ComponentDownlink;
|
||||
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
||||
import nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentUplinkMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentMessage;
|
||||
import nl.andrewl.railsignalapi.util.JsonUtils;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
|
@ -43,10 +43,20 @@ public class TcpLinkManager extends ComponentDownlink implements Runnable {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
downlinkService.registerDownlink(this);
|
||||
try {
|
||||
downlinkService.registerDownlink(this);
|
||||
} catch (Exception e) {
|
||||
log.error("An error occurred while registering a downlink.");
|
||||
try {
|
||||
shutdown();
|
||||
} catch (IOException ex) {
|
||||
log.error("An error occurred while shutting down a downlink.", ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (!socket.isClosed()) {
|
||||
try {
|
||||
var msg = JsonUtils.readMessage(in, ComponentUplinkMessage.class);
|
||||
var msg = JsonUtils.readMessage(in, ComponentMessage.class);
|
||||
uplinkMessageHandler.messageReceived(msg);
|
||||
} catch (IOException e) {
|
||||
log.warn("An error occurred while receiving an uplink message.", e);
|
||||
|
|
|
@ -53,6 +53,11 @@ public class AppUpdateService {
|
|||
log.info("De-registered an app websocket session.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an update to any connected apps.
|
||||
* @param rsId The id of the rail system that the update pertains to.
|
||||
* @param msg The message to send.
|
||||
*/
|
||||
public synchronized void sendUpdate(long rsId, Object msg) {
|
||||
Set<WebSocketSession> sessionsForRs = sessions.get(rsId);
|
||||
if (sessionsForRs != null) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
||||
import nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentUplinkMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.ComponentMessage;
|
||||
import nl.andrewl.railsignalapi.util.JsonUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
@ -13,6 +13,8 @@ import org.springframework.web.socket.TextMessage;
|
|||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Handler for websocket connections that components open to send and receive
|
||||
* real-time updates from the server.
|
||||
|
@ -28,12 +30,21 @@ public class ComponentWebsocketHandler extends TextWebSocketHandler {
|
|||
@Transactional(readOnly = true)
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
long tokenId = (long) session.getAttributes().get("tokenId");
|
||||
componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session));
|
||||
try {
|
||||
componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session));
|
||||
} catch (Exception e) {
|
||||
log.error("An error occurred while registering a new websocket downlink.", e);
|
||||
try {
|
||||
session.close();
|
||||
} catch (IOException ex) {
|
||||
log.error("An error occurred while closing a websocket downlink.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
var msg = JsonUtils.readMessage(message.getPayload(), ComponentUplinkMessage.class);
|
||||
var msg = JsonUtils.readMessage(message.getPayload(), ComponentMessage.class);
|
||||
uplinkMessageHandler.messageReceived(msg);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,7 @@ import lombok.Setter;
|
|||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A switch is a component that directs traffic between several connected
|
||||
|
@ -38,12 +36,4 @@ public class Switch extends PathNode {
|
|||
this.possibleConfigurations = possibleConfigurations;
|
||||
this.activeConfiguration = activeConfiguration;
|
||||
}
|
||||
|
||||
public Optional<SwitchConfiguration> findConfiguration(Set<Long> pathNodeIds) {
|
||||
for (var config : possibleConfigurations) {
|
||||
Set<Long> configNodeIds = config.getNodes().stream().map(Component::getId).collect(Collectors.toSet());
|
||||
if (pathNodeIds.equals(configNodeIds)) return Optional.of(config);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ public class WebConfig implements WebMvcConfigurer {
|
|||
// Configure resource handlers to use the /app directory for all vue frontend stuff.
|
||||
registry.addResourceHandler("/app/**")
|
||||
.addResourceLocations("classpath:/app/");
|
||||
|
||||
// Configure resource handlers for driver files.
|
||||
registry.addResourceHandler("/driver/**")
|
||||
.addResourceLocations("classpath:/driver/");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -68,11 +68,15 @@ public class SegmentService {
|
|||
|
||||
private void sendSegmentOccupiedStatus(Segment segment) {
|
||||
for (var signal : segment.getSignals()) {
|
||||
downlinkService.sendMessage(signal.getId(), new SegmentStatusMessage(signal.getId(), segment.isOccupied()));
|
||||
downlinkService.sendMessage(signal.getId(), new SegmentStatusMessage(signal.getId(), segment.getId(), segment.isOccupied()));
|
||||
appUpdateService.sendComponentUpdate(segment.getRailSystem().getId(), signal.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles updates from segment boundary components.
|
||||
* @param msg The update message.
|
||||
*/
|
||||
@Transactional
|
||||
public void onBoundaryUpdate(SegmentBoundaryUpdateMessage msg) {
|
||||
var segmentBoundary = segmentBoundaryRepository.findById(msg.cId)
|
||||
|
|
|
@ -3,9 +3,11 @@ package nl.andrewl.railsignalapi.service;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
||||
import nl.andrewl.railsignalapi.live.dto.ErrorMessage;
|
||||
import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
|
||||
import nl.andrewl.railsignalapi.model.component.Switch;
|
||||
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
@ -23,11 +25,16 @@ public class SwitchService {
|
|||
@Transactional
|
||||
public void onSwitchUpdate(SwitchUpdateMessage msg) {
|
||||
switchRepository.findById(msg.cId).ifPresent(sw -> {
|
||||
sw.findConfiguration(msg.configuration).ifPresent(config -> {
|
||||
sw.setActiveConfiguration(config);
|
||||
switchRepository.save(sw);
|
||||
appUpdateService.sendComponentUpdate(sw.getRailSystem().getId(), sw.getId());
|
||||
});
|
||||
sw.getPossibleConfigurations().stream()
|
||||
.filter(c -> c.getId().equals(msg.activeConfigId))
|
||||
.findFirst()
|
||||
.ifPresentOrElse(config -> {
|
||||
sw.setActiveConfiguration(config);
|
||||
switchRepository.save(sw);
|
||||
appUpdateService.sendComponentUpdate(sw.getRailSystem().getId(), sw.getId());
|
||||
}, () -> {
|
||||
downlinkService.sendMessage(new ErrorMessage(sw.getId(), "Invalid active config id."));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ spring.jpa.open-in-view=false
|
|||
spring.datasource.hikari.maximum-pool-size=5
|
||||
spring.datasource.hikari.minimum-idle=1
|
||||
|
||||
spring.thymeleaf.enabled=false
|
||||
|
||||
server.port=8080
|
||||
server.tomcat.threads.max=10
|
||||
server.tomcat.threads.min-spare=2
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
function tableLength(t)
|
||||
local c = 0
|
||||
for _ in pairs(t) do
|
||||
c = c + 1
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
function startsWith(str, s)
|
||||
return str:find(s, 1, true) == 1
|
||||
end
|
||||
|
||||
function readNum(validationFunction)
|
||||
local func = validationFunction or function (n)
|
||||
return n ~= nil
|
||||
end
|
||||
local num = nil
|
||||
while true do
|
||||
local s = io.read()
|
||||
if s ~= nil then num = tonumber(s) end
|
||||
if not func(num) then
|
||||
print("The number you entered is not valid.")
|
||||
else
|
||||
return num
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function readStr(validationFunction)
|
||||
local func = validationFunction or function (s)
|
||||
return s ~= nil and string.len(s) > 0
|
||||
end
|
||||
while true do
|
||||
local str = io.read()
|
||||
if func(str) then
|
||||
return str
|
||||
else
|
||||
print("The string you entered is not valid.")
|
||||
end
|
||||
end
|
||||
end
|