|
@ -34,3 +34,6 @@ build/
|
|||
|
||||
*.mv.db
|
||||
*.trace.db
|
||||
|
||||
src/main/resources/app
|
||||
/build_system
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env dub
|
||||
/+ dub.sdl:
|
||||
dependency "dsh" version="~>1.6.1"
|
||||
+/
|
||||
|
||||
/**
|
||||
* This script will build the Rail Signal Vue app, then bundle it into this
|
||||
* Spring project's files under src/main/resources/app/, and will then build
|
||||
* this project into a jar file.
|
||||
*/
|
||||
module build_system;
|
||||
|
||||
import dsh;
|
||||
|
||||
const DIST = "./src/main/resources/app";
|
||||
|
||||
void main(string[] args) {
|
||||
print("Building RailSignalAPI");
|
||||
chdir("railsignal-app");
|
||||
print("Building app...");
|
||||
runOrQuit("npm run build");
|
||||
print("Copying dist to %s", DIST);
|
||||
chdir("..");
|
||||
removeIfExists(DIST);
|
||||
mkdir(DIST);
|
||||
copyDir("railsignal-app/dist", DIST);
|
||||
print("Building API...");
|
||||
runOrQuit("mvn clean package spring-boot:repackage");
|
||||
print("Build complete!");
|
||||
|
||||
if (args.length > 1 && args[1] == "run") {
|
||||
string f = findFile("target", "^.+\\.jar$", false);
|
||||
if (f == null) {
|
||||
error("Could not find jar file!");
|
||||
} else {
|
||||
print("Running the program.");
|
||||
run("java -jar " ~ f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
pom.xml
|
@ -5,7 +5,7 @@
|
|||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.6.0</version>
|
||||
<version>2.6.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>nl.andrewl</groupId>
|
||||
|
@ -40,6 +40,7 @@
|
|||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.1.212</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=http://localhost:8080/api
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=http://localhost:8080/api
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"env": {
|
||||
"vue/setup-compiler-macros": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# 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?
|
|
@ -0,0 +1,35 @@
|
|||
# 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
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
#!/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);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!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>
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 422 B |
After Width: | Height: | Size: 840 B |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1 @@
|
|||
{"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"}
|
|
@ -0,0 +1,30 @@
|
|||
<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>
|
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,90 @@
|
|||
<?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>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,91 @@
|
|||
<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"
|
||||
>
|
||||
<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="removeRailSystem()"
|
||||
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";
|
||||
|
||||
export default {
|
||||
name: "AppNavbar",
|
||||
components: {AddRailSystem: AddRailSystemModal, ConfirmModal},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
rsStore.$subscribe(mutation => {
|
||||
const evt = mutation.events;
|
||||
if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
|
||||
rsStore.fetchSelectedRailSystemData();
|
||||
}
|
||||
});
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.rsStore.refreshRailSystems();
|
||||
},
|
||||
methods: {
|
||||
removeRailSystem() {
|
||||
this.$refs.confirmModal.showConfirm()
|
||||
.then(() => this.rsStore.removeRailSystem(this.rsStore.selectedRailSystem));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
<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>
|
|
@ -0,0 +1,36 @@
|
|||
<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>
|
|
@ -0,0 +1,77 @@
|
|||
<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";
|
||||
|
||||
export default {
|
||||
name: "AddRailSystem",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: ""
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
this.rsStore.createRailSystem(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>
|
|
@ -0,0 +1,86 @@
|
|||
<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";
|
||||
|
||||
export default {
|
||||
name: "AddSegmentModal",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
name: ""
|
||||
},
|
||||
warnings: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formSubmitted() {
|
||||
const modal = Modal.getInstance(document.getElementById("addSegmentModal"));
|
||||
this.rsStore.addSegment(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>
|
|
@ -0,0 +1,47 @@
|
|||
<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>
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<h3>Rail System: <em>{{railSystem.name}}</em></h3>
|
||||
<SegmentsView />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm me-2"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSegmentModal"
|
||||
>
|
||||
Add Segment
|
||||
</button>
|
||||
<AddSegmentModal />
|
||||
<span v-if="railSystem.segments && railSystem.segments.length > 0">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm me-2"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSignalModal"
|
||||
>
|
||||
Add Signal
|
||||
</button>
|
||||
<AddSignalModal />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm me-2"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSegmentBoundaryModal"
|
||||
>
|
||||
Add Segment Boundary
|
||||
</button>
|
||||
<AddSegmentBoundaryModal />
|
||||
</span>
|
||||
<span v-if="railSystem.components && railSystem.components.length > 1">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addSwitchModal"
|
||||
>
|
||||
Add Switch
|
||||
</button>
|
||||
<AddSwitchModal />
|
||||
</span>
|
||||
</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";
|
||||
|
||||
export default {
|
||||
name: "RailSystemPropertiesView",
|
||||
components: {
|
||||
AddSwitchModal,
|
||||
AddSegmentBoundaryModal,
|
||||
AddSignalModal,
|
||||
AddSegmentModal,
|
||||
SegmentsView
|
||||
},
|
||||
props: {
|
||||
railSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<h5>Segments</h5>
|
||||
<ul class="list-group overflow-auto mb-2" style="max-height: 200px;">
|
||||
<li
|
||||
v-for="segment in rsStore.selectedRailSystem.segments"
|
||||
:key="segment.id"
|
||||
class="list-group-item"
|
||||
>
|
||||
{{segment.name}}
|
||||
<button @click.prevent="rsStore.removeSegment(segment.id)" class="btn btn-sm btn-danger float-end">Remove</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useRailSystemsStore} from "../../stores/railSystemsStore";
|
||||
|
||||
export default {
|
||||
name: "SegmentsView.vue",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,16 @@
|
|||
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);
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
<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";
|
||||
|
||||
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];
|
||||
this.rsStore.addComponent(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>
|
|
@ -0,0 +1,98 @@
|
|||
<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";
|
||||
|
||||
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"));
|
||||
this.rsStore.addComponent(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>
|
|
@ -0,0 +1,122 @@
|
|||
<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">Segment</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";
|
||||
|
||||
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"));
|
||||
this.rsStore.addComponent(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>
|
|
@ -0,0 +1,82 @@
|
|||
<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>
|
||||
<tr>
|
||||
<th>Online</th><td>{{component.online}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
||||
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
|
||||
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
|
||||
<button @click="removeComponent()" 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";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmModal,
|
||||
SegmentBoundaryNodeComponentView,
|
||||
SignalComponentView,
|
||||
PathNodeComponentView
|
||||
},
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
railSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeComponent() {
|
||||
this.$refs.removeConfirm.showConfirm()
|
||||
.then(() => this.rsStore.removeComponent(this.component.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<h5>Connected Nodes</h5>
|
||||
<table class="table" v-if="pathNode.connectedNodes.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="node in pathNode.connectedNodes" :key="node.id">
|
||||
<td>{{node.name}}</td>
|
||||
<td>
|
||||
<button
|
||||
@click="rsStore.removeConnection(pathNode, node)"
|
||||
class="btn btn-sm btn-danger"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-if="pathNode.connectedNodes.length === 0">
|
||||
There are no connected nodes.
|
||||
</p>
|
||||
<form
|
||||
@submit.prevent="rsStore.addConnection(pathNode, 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 {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||
|
||||
export default {
|
||||
name: "PathNodeComponentView",
|
||||
setup() {
|
||||
const rsStore = useRailSystemsStore();
|
||||
return {
|
||||
rsStore
|
||||
};
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<h5>Segments Connected</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="segment in node.segments" :key="segment.id">
|
||||
<td>{{segment.name}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SegmentBoundaryNodeComponentView",
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<h5>Signal Properties</h5>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Connected to</th>
|
||||
<td>{{signal.segment.name}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SignalComponentView",
|
||||
props: {
|
||||
signal: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
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);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
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')
|
|
@ -0,0 +1,163 @@
|
|||
import { defineStore } from "pinia";
|
||||
import axios from "axios";
|
||||
|
||||
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||
state: () => ({
|
||||
railSystems: [],
|
||||
/**
|
||||
* @type {{segments: [Object], components: [Object], selectedComponent: Object} | null}
|
||||
*/
|
||||
selectedRailSystem: null,
|
||||
apiUrl: import.meta.env.VITE_API_URL
|
||||
}),
|
||||
actions: {
|
||||
refreshRailSystems() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(this.apiUrl + "/rs")
|
||||
.then(response => {
|
||||
this.railSystems = response.data;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
createRailSystem(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(this.apiUrl + "/rs", {name: name})
|
||||
.then(response => {
|
||||
const newId = response.data.id;
|
||||
this.refreshRailSystems()
|
||||
.then(() => resolve(this.railSystems.find(rs => rs.id === newId)))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
},
|
||||
removeRailSystem(rs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.delete(this.apiUrl + "/rs/" + rs.id)
|
||||
.then(() => {
|
||||
this.selectedRailSystem = null;
|
||||
this.refreshRailSystems()
|
||||
.then(() => resolve)
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
})
|
||||
},
|
||||
refreshSegments(rs) {
|
||||
return new Promise(resolve => {
|
||||
axios.get(`${this.apiUrl}/rs/${rs.id}/s`)
|
||||
.then(response => {
|
||||
rs.segments = response.data;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
refreshAllComponents(rs) {
|
||||
return new Promise(resolve => {
|
||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c`)
|
||||
.then(response => {
|
||||
rs.selectedComponent = null;
|
||||
rs.components = response.data;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
fetchSelectedRailSystemData() {
|
||||
if (!this.selectedRailSystem) return;
|
||||
this.refreshSegments(this.selectedRailSystem);
|
||||
this.refreshAllComponents(this.selectedRailSystem);
|
||||
},
|
||||
addSegment(name) {
|
||||
const rs = this.selectedRailSystem;
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${this.apiUrl}/rs/${rs.id}/s`, {name: name})
|
||||
.then(() => {
|
||||
this.refreshSegments(rs)
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
},
|
||||
removeSegment(id) {
|
||||
const rs = this.selectedRailSystem;
|
||||
axios.delete(`${this.apiUrl}/rs/${rs.id}/s/${id}`)
|
||||
.then(() => this.refreshSegments(rs))
|
||||
.catch(error => console.log(error));
|
||||
},
|
||||
addComponent(data) {
|
||||
const rs = this.selectedRailSystem;
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data)
|
||||
.then(() => {
|
||||
this.refreshAllComponents(rs)
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
},
|
||||
removeComponent(id) {
|
||||
const rs = this.selectedRailSystem;
|
||||
axios.delete(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
|
||||
.then(() => this.refreshAllComponents(rs))
|
||||
.catch(error => console.log(error));
|
||||
},
|
||||
fetchComponentData(component) {
|
||||
return new Promise(resolve => {
|
||||
const rs = this.selectedRailSystem;
|
||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${component.id}`)
|
||||
.then(response => resolve(response.data))
|
||||
.catch(error => console.log(error));
|
||||
});
|
||||
},
|
||||
refreshComponents(components) {
|
||||
const rs = this.selectedRailSystem;
|
||||
for (let i = 0; i < components.length; i++) {
|
||||
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${components[i].id}`)
|
||||
.then(resp => {
|
||||
const idx = this.selectedRailSystem.components.findIndex(c => c.id === resp.data.id);
|
||||
if (idx > -1) this.selectedRailSystem.components[idx] = resp.data;
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
},
|
||||
updateConnections(pathNode) {
|
||||
const rs = this.selectedRailSystem;
|
||||
return new Promise(resolve => {
|
||||
axios.patch(
|
||||
`${this.apiUrl}/rs/${rs.id}/c/${pathNode.id}/connectedNodes`,
|
||||
pathNode
|
||||
)
|
||||
.then(response => {
|
||||
pathNode.connectedNodes = response.data.connectedNodes;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => console.log(error));
|
||||
});
|
||||
},
|
||||
addConnection(pathNode, other) {
|
||||
pathNode.connectedNodes.push(other);
|
||||
this.updateConnections(pathNode)
|
||||
.then(() => {
|
||||
this.refreshComponents(pathNode.connectedNodes);
|
||||
});
|
||||
},
|
||||
removeConnection(pathNode, other) {
|
||||
const idx = pathNode.connectedNodes.findIndex(n => n.id === other.id);
|
||||
if (idx > -1) {
|
||||
pathNode.connectedNodes.splice(idx, 1);
|
||||
this.updateConnections(pathNode)
|
||||
.then(() => {
|
||||
const nodes = [];
|
||||
nodes.push(pathNode.connectedNodes);
|
||||
nodes.push(other);
|
||||
this.refreshComponents(nodes);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,18 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Branch;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface BranchRepository extends JpaRepository<Branch, Long> {
|
||||
Optional<Branch> findByIdAndRailSystem(long id, RailSystem railSystem);
|
||||
Optional<Branch> findByIdAndRailSystemId(long id, long railSystemId);
|
||||
Optional<Branch> findByNameAndRailSystem(String name, RailSystem railSystem);
|
||||
List<Branch> findAllByRailSystemOrderByName(RailSystem railSystem);
|
||||
List<Branch> findAllByNameAndRailSystem(String name, RailSystem railSystem);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.model.component.Position;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ComponentRepository<T extends Component> extends JpaRepository<T, Long> {
|
||||
Optional<T> findByIdAndRailSystemId(long id, long rsId);
|
||||
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
||||
|
||||
@Query("SELECT c FROM Component c " +
|
||||
"WHERE c.railSystem = :rs AND " +
|
||||
"c.position.x >= :#{#lower.x} AND c.position.y >= :#{#lower.y} AND c.position.z >= :#{#lower.z} AND " +
|
||||
"c.position.x <= :#{#upper.x} AND c.position.y <= :#{#upper.y} AND c.position.z <= :#{#upper.z}")
|
||||
List<T> findAllInBounds(RailSystem rs, Position lower, Position upper);
|
||||
|
||||
List<T> findAllByRailSystem(RailSystem rs);
|
||||
|
||||
void deleteAllByRailSystem(RailSystem rs);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Label;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface LabelRepository extends JpaRepository<Label, Long> {
|
||||
List<Label> findAllByRailSystem(RailSystem rs);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SegmentRepository extends JpaRepository<Segment, Long> {
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
||||
|
||||
List<Segment> findAllByRailSystemId(long rsId);
|
||||
|
||||
Optional<Segment> findByIdAndRailSystemId(long id, long rsId);
|
||||
|
||||
void deleteAllByRailSystem(RailSystem rs);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.SignalBranchConnection;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SignalBranchConnectionRepository extends JpaRepository<SignalBranchConnection, Long> {
|
||||
}
|
|
@ -1,26 +1,8 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Branch;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Signal;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SignalRepository extends JpaRepository<Signal, Long> {
|
||||
Optional<Signal> findByIdAndRailSystem(long id, RailSystem railSystem);
|
||||
Optional<Signal> findByIdAndRailSystemId(long id, long railSystemId);
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem railSystem);
|
||||
|
||||
@Query("SELECT DISTINCT s FROM Signal s " +
|
||||
"LEFT JOIN s.branchConnections bc " +
|
||||
"WHERE bc.branch = :branch " +
|
||||
"ORDER BY s.name")
|
||||
List<Signal> findAllConnectedToBranch(Branch branch);
|
||||
|
||||
List<Signal> findAllByRailSystemOrderByName(RailSystem railSystem);
|
||||
public interface SignalRepository extends ComponentRepository<Signal> {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.PathNode;
|
||||
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SwitchConfigurationRepository extends JpaRepository<SwitchConfiguration, Long> {
|
||||
void deleteAllByNodesContaining(PathNode p);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Switch;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SwitchRepository extends ComponentRepository<Switch> {
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.dao;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Train;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TrainRepository extends JpaRepository<Train, Long> {
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class Branch {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private RailSystem railSystem;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Setter
|
||||
private BranchStatus status;
|
||||
|
||||
@OneToMany(mappedBy = "branch", orphanRemoval = true)
|
||||
private Set<SignalBranchConnection> signalConnections;
|
||||
|
||||
public Branch(RailSystem railSystem, String name, BranchStatus status) {
|
||||
this.railSystem = railSystem;
|
||||
this.name = name;
|
||||
this.status = status;
|
||||
this.signalConnections = new HashSet<>();
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
public enum BranchStatus {
|
||||
FREE,
|
||||
OCCUPIED
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
/**
|
||||
* A cardinal direction, useful for some components.
|
||||
*/
|
||||
public enum Direction {
|
||||
NORTH,
|
||||
SOUTH,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
/**
|
||||
* A simple label element that allows text to be placed in the rail system
|
||||
* model.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class Label {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private RailSystem railSystem;
|
||||
|
||||
@Column(nullable = false, length = 63)
|
||||
private String text;
|
||||
|
||||
public Label(RailSystem rs, String text) {
|
||||
this.railSystem = rs;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,9 @@ import javax.persistence.Entity;
|
|||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
|
||||
/**
|
||||
* Represents a closed system that contains a collection of components.
|
||||
*/
|
||||
@Entity
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
|
@ -16,6 +19,9 @@ public class RailSystem {
|
|||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* The name of this system.
|
||||
*/
|
||||
@Column(nullable = false, unique = true)
|
||||
private String name;
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
|
||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents a traversable segment of a rail system that components can
|
||||
* connect to.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class Segment {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private RailSystem railSystem;
|
||||
|
||||
/**
|
||||
* A unique name for this segment.
|
||||
*/
|
||||
@Column
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The signals that are connected to this branch.
|
||||
*/
|
||||
@OneToMany(mappedBy = "segment")
|
||||
private Set<Signal> signals;
|
||||
|
||||
/**
|
||||
* The set of nodes from which trains can enter and exit this segment.
|
||||
*/
|
||||
@ManyToMany(mappedBy = "segments")
|
||||
private Set<SegmentBoundaryNode> boundaryNodes;
|
||||
|
||||
public Segment(RailSystem railSystem, String name) {
|
||||
this.railSystem = railSystem;
|
||||
this.name = name;
|
||||
this.signals = new HashSet<>();
|
||||
this.boundaryNodes = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) return true;
|
||||
return o instanceof Segment s && this.id != null && this.id.equals(s.id);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class Signal {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private RailSystem railSystem;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "signal", orphanRemoval = true, cascade = CascadeType.ALL)
|
||||
private Set<SignalBranchConnection> branchConnections;
|
||||
|
||||
@Embedded
|
||||
private Position position;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Setter
|
||||
private boolean online = false;
|
||||
|
||||
public Signal(RailSystem railSystem, String name, Position position, Set<SignalBranchConnection> branchConnections) {
|
||||
this.railSystem = railSystem;
|
||||
this.name = name;
|
||||
this.position = position;
|
||||
this.branchConnections = branchConnections;
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class SignalBranchConnection implements Comparable<SignalBranchConnection> {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
private Signal signal;
|
||||
|
||||
@ManyToOne(optional = false, cascade = CascadeType.PERSIST)
|
||||
private Branch branch;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Direction direction;
|
||||
|
||||
@ManyToMany
|
||||
private Set<SignalBranchConnection> reachableSignalConnections;
|
||||
|
||||
public SignalBranchConnection(Signal signal, Branch branch, Direction direction) {
|
||||
this.signal = signal;
|
||||
this.branch = branch;
|
||||
this.direction = direction;
|
||||
reachableSignalConnections = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
return this.id != null &&
|
||||
o instanceof SignalBranchConnection sbc && sbc.getId() != null &&
|
||||
this.id.equals(sbc.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SignalBranchConnection o) {
|
||||
int c = Long.compare(this.getSignal().getId(), o.getSignal().getId());
|
||||
if (c != 0) return c;
|
||||
return this.direction.compareTo(o.getDirection());
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class Switch {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private RailSystem railSystem;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Embedded
|
||||
private Position position;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int state;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int maxStates;
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Entity
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
public class Train {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
private Branch currentBranch;
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
/**
|
||||
* Represents a physical component of the rail system that the API can interact
|
||||
* with, and send or receive data from. For example, a signal, switch, or
|
||||
* detector.
|
||||
*/
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public abstract class Component {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* The rail system that this component belongs to.
|
||||
*/
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private RailSystem railSystem;
|
||||
|
||||
/**
|
||||
* The position of this component in the system.
|
||||
*/
|
||||
@Embedded
|
||||
private Position position;
|
||||
|
||||
/**
|
||||
* A human-readable name for the component. This must be unique among all
|
||||
* components in the rail system.
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The type of this component.
|
||||
*/
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
private ComponentType type;
|
||||
|
||||
/**
|
||||
* Whether this component is online, meaning that an in-world device is
|
||||
* currently connected to relay information regarding this component.
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
@Setter
|
||||
private boolean online = false;
|
||||
|
||||
public Component(RailSystem railSystem, Position position, String name, ComponentType type) {
|
||||
this.railSystem = railSystem;
|
||||
this.position = position;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
return o instanceof Component c && this.id != null && this.id.equals(c.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(id);
|
||||
if (name != null) sb.append('[').append(name).append(']');
|
||||
sb.append(String.format("@[x=%.1f,y=%.1f,z=%.1f]", position.getX(), position.getY(), position.getZ()));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
public enum ComponentType {
|
||||
SIGNAL,
|
||||
SWITCH,
|
||||
SEGMENT_BOUNDARY
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A node that, together with other nodes, forms a path that trains can follow
|
||||
* to traverse through segments in the rail system.
|
||||
*/
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public abstract class PathNode extends Component {
|
||||
/**
|
||||
* The set of nodes that this one is connected to.
|
||||
*/
|
||||
@ManyToMany(cascade = CascadeType.DETACH)
|
||||
private Set<PathNode> connectedNodes;
|
||||
|
||||
public PathNode(RailSystem railSystem, Position position, String name, ComponentType type, Set<PathNode> connectedNodes) {
|
||||
super(railSystem, position, name, type);
|
||||
this.connectedNodes = connectedNodes;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.railsignalapi.model;
|
||||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
@ -6,6 +6,9 @@ import lombok.NoArgsConstructor;
|
|||
|
||||
import javax.persistence.Embeddable;
|
||||
|
||||
/**
|
||||
* A three-dimensional position for a component within a system.
|
||||
*/
|
||||
@Embeddable
|
||||
@Data
|
||||
@AllArgsConstructor
|
|
@ -0,0 +1,32 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.ManyToMany;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Component that relays information about trains traversing from one segment
|
||||
* to another. It links exactly two segments together at a specific point.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class SegmentBoundaryNode extends PathNode {
|
||||
/**
|
||||
* The set of segments that this boundary node connects. This should
|
||||
* generally always have exactly two segments.
|
||||
*/
|
||||
@ManyToMany
|
||||
private Set<Segment> segments;
|
||||
|
||||
public SegmentBoundaryNode(RailSystem railSystem, Position position, String name, Set<PathNode> connectedNodes, Set<Segment> segments) {
|
||||
super(railSystem, position, name, ComponentType.SEGMENT_BOUNDARY, connectedNodes);
|
||||
this.segments = segments;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.FetchType;
|
||||
import javax.persistence.ManyToOne;
|
||||
|
||||
/**
|
||||
* A signal is a component that relays the status of a connected segment to
|
||||
* some sort of in-world representation, whether that be a certain light
|
||||
* color, or electrical signal.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class Signal extends Component {
|
||||
/**
|
||||
* The segment that this signal connects to.
|
||||
*/
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private Segment segment;
|
||||
|
||||
public Signal(RailSystem railSystem, Position position, String name, Segment segment) {
|
||||
super(railSystem, position, name, ComponentType.SIGNAL);
|
||||
this.segment = segment;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A switch is a component that directs traffic between several connected
|
||||
* segments.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class Switch extends PathNode {
|
||||
/**
|
||||
* The set of all possible configurations that this switch can be in.
|
||||
*/
|
||||
@OneToMany(mappedBy = "switchComponent", orphanRemoval = true, cascade = CascadeType.ALL)
|
||||
private Set<SwitchConfiguration> possibleConfigurations;
|
||||
|
||||
/**
|
||||
* The switch configuration that this switch is currently in. If null, then
|
||||
* we don't know what configuration the switch is in.
|
||||
*/
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@Setter
|
||||
private SwitchConfiguration activeConfiguration;
|
||||
|
||||
public Switch(RailSystem railSystem, Position position, String name, Set<PathNode> connectedNodes, Set<SwitchConfiguration> possibleConfigurations, SwitchConfiguration activeConfiguration) {
|
||||
super(railSystem, position, name, ComponentType.SWITCH, connectedNodes);
|
||||
this.possibleConfigurations = possibleConfigurations;
|
||||
this.activeConfiguration = activeConfiguration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A possible connection that can be made between path nodes, if this is set
|
||||
* as an active configuration in the linked switch component.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class SwitchConfiguration {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* The switch component that this configuration belongs to.
|
||||
*/
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private Switch switchComponent;
|
||||
|
||||
/**
|
||||
* The set of nodes that this switch configuration connects. This should
|
||||
* be almost always a set of two nodes.
|
||||
*/
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
private Set<PathNode> nodes;
|
||||
|
||||
public SwitchConfiguration(Switch switchComponent, Set<PathNode> nodes) {
|
||||
this.switchComponent = switchComponent;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) return true;
|
||||
return o instanceof SwitchConfiguration sc &&
|
||||
sc.switchComponent.equals(this.switchComponent) &&
|
||||
sc.nodes.equals(this.nodes);
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping(path = "/")
|
||||
@RequestMapping(path = {"/", "/app", "/home", "/index.html", "/index"})
|
||||
public class IndexPageController {
|
||||
@GetMapping
|
||||
public String getIndex() {
|
||||
return "index";
|
||||
return "forward:/app/index.html";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.rest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.rest.dto.BranchResponse;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
|
||||
import nl.andrewl.railsignalapi.service.BranchService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/railSystems/{rsId}/branches")
|
||||
@RequiredArgsConstructor
|
||||
public class BranchesController {
|
||||
private final BranchService branchService;
|
||||
|
||||
@GetMapping
|
||||
public List<BranchResponse> getAllBranches(@PathVariable long rsId) {
|
||||
return branchService.getAllBranches(rsId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{branchId}")
|
||||
public BranchResponse getBranch(@PathVariable long rsId, @PathVariable long branchId) {
|
||||
return branchService.getBranch(rsId, branchId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{branchId}/signals")
|
||||
public List<SignalResponse> getBranchSignals(@PathVariable long rsId, @PathVariable long branchId) {
|
||||
return branchService.getConnectedSignals(rsId, branchId);
|
||||
}
|
||||
|
||||
@DeleteMapping(path = "/{branchId}")
|
||||
public ResponseEntity<?> deleteBranch(@PathVariable long rsId, @PathVariable long branchId) {
|
||||
branchService.deleteBranch(rsId, branchId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package nl.andrewl.railsignalapi.rest;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
|
||||
import nl.andrewl.railsignalapi.service.ComponentService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/rs/{rsId}/c")
|
||||
@RequiredArgsConstructor
|
||||
public class ComponentsApiController {
|
||||
private final ComponentService componentService;
|
||||
|
||||
@GetMapping
|
||||
public List<ComponentResponse> getAllComponents(@PathVariable long rsId) {
|
||||
return componentService.getComponents(rsId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{cId}")
|
||||
public ComponentResponse getComponent(@PathVariable long rsId, @PathVariable long cId) {
|
||||
return componentService.getComponent(rsId, cId);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody ObjectNode data) {
|
||||
return componentService.create(rsId, data);
|
||||
}
|
||||
|
||||
@DeleteMapping(path = "/{cId}")
|
||||
public ResponseEntity<Void> removeComponent(@PathVariable long rsId, @PathVariable long cId) {
|
||||
componentService.removeComponent(rsId, cId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PatchMapping(path = "/{cId}/connectedNodes")
|
||||
public ComponentResponse updateConnectedNodes(@PathVariable long rsId, @PathVariable long cId, @RequestBody PathNodeUpdatePayload payload) {
|
||||
return componentService.updatePath(rsId, cId, payload);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.*;
|
|||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/railSystems")
|
||||
@RequestMapping(path = "/api/rs")
|
||||
@RequiredArgsConstructor
|
||||
public class RailSystemsApiController {
|
||||
private final RailSystemService railSystemService;
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package nl.andrewl.railsignalapi.rest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
||||
import nl.andrewl.railsignalapi.service.SegmentService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/rs/{rsId}/s")
|
||||
@RequiredArgsConstructor
|
||||
public class SegmentsApiController {
|
||||
private final SegmentService segmentService;
|
||||
|
||||
@GetMapping
|
||||
public List<SegmentResponse> getSegments(@PathVariable long rsId) {
|
||||
return segmentService.getSegments(rsId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{sId}")
|
||||
public FullSegmentResponse getSegment(@PathVariable long rsId, @PathVariable long sId) {
|
||||
return segmentService.getSegment(rsId, sId);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public FullSegmentResponse createSegment(@PathVariable long rsId, @RequestBody SegmentPayload payload) {
|
||||
return segmentService.create(rsId, payload);
|
||||
}
|
||||
|
||||
@DeleteMapping(path = "/{sId}")
|
||||
public ResponseEntity<Void> removeSegment(@PathVariable long rsId, @PathVariable long sId) {
|
||||
segmentService.remove(rsId, sId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.rest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalConnectionsUpdatePayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalCreationPayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
|
||||
import nl.andrewl.railsignalapi.service.SignalService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/railSystems/{rsId}/signals")
|
||||
@RequiredArgsConstructor
|
||||
public class SignalsApiController {
|
||||
private final SignalService signalService;
|
||||
|
||||
@PostMapping
|
||||
public SignalResponse createSignal(@PathVariable long rsId, @RequestBody SignalCreationPayload payload) {
|
||||
return signalService.createSignal(rsId, payload);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<SignalResponse> getSignals(@PathVariable long rsId) {
|
||||
return signalService.getAllSignals(rsId);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/{sigId}")
|
||||
public SignalResponse getSignal(@PathVariable long rsId, @PathVariable long sigId) {
|
||||
return signalService.getSignal(rsId, sigId);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/{sigId}/signalConnections")
|
||||
public SignalResponse updateSignalConnections(@PathVariable long rsId, @PathVariable long sigId, @RequestBody SignalConnectionsUpdatePayload payload) {
|
||||
return signalService.updateSignalBranchConnections(rsId, sigId, payload);
|
||||
}
|
||||
|
||||
@DeleteMapping(path = "/{sigId}")
|
||||
public ResponseEntity<?> deleteSignal(@PathVariable long rsId, @PathVariable long sigId) {
|
||||
signalService.deleteSignal(rsId, sigId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package nl.andrewl.railsignalapi.rest;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
@ -10,7 +11,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||
public class WebConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/static/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
// Configure resource handlers to use the /app directory for all vue frontend stuff.
|
||||
registry.addResourceHandler("/app/**")
|
||||
.addResourceLocations("classpath:/app/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods("*");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Branch;
|
||||
|
||||
public record BranchResponse(
|
||||
long id,
|
||||
String name,
|
||||
String status
|
||||
) {
|
||||
public BranchResponse(Branch branch) {
|
||||
this(branch.getId(), branch.getName(), branch.getStatus().name());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
import nl.andrewl.railsignalapi.rest.dto.component.out.SegmentBoundaryNodeResponse;
|
||||
import nl.andrewl.railsignalapi.rest.dto.component.out.SignalResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FullSegmentResponse extends SegmentResponse {
|
||||
public List<SignalResponse> signals;
|
||||
public List<SegmentBoundaryNodeResponse> boundaryNodes;
|
||||
|
||||
public FullSegmentResponse(Segment s) {
|
||||
super(s);
|
||||
this.signals = s.getSignals().stream().map(SignalResponse::new).toList();
|
||||
this.boundaryNodes = s.getBoundaryNodes().stream().map(SegmentBoundaryNodeResponse::new).toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PathNodeUpdatePayload {
|
||||
public List<NodeIdObj> connectedNodes;
|
||||
public static class NodeIdObj {
|
||||
public long id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
public record SegmentPayload(String name) {
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
|
||||
public class SegmentResponse {
|
||||
public long id;
|
||||
public String name;
|
||||
|
||||
public SegmentResponse(long id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public SegmentResponse(Segment s) {
|
||||
this(s.getId(), s.getName());
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SignalConnectionsUpdatePayload(
|
||||
List<ConnectionData> connections
|
||||
) {
|
||||
public static record ConnectionData(long from, long to) {}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Position;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SignalCreationPayload(
|
||||
String name,
|
||||
Position position,
|
||||
List<BranchData> branchConnections
|
||||
) {
|
||||
public static record BranchData(String direction, String name, Long id) {}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.Position;
|
||||
import nl.andrewl.railsignalapi.model.Signal;
|
||||
import nl.andrewl.railsignalapi.model.SignalBranchConnection;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public record SignalResponse(
|
||||
long id,
|
||||
String name,
|
||||
Position position,
|
||||
List<ConnectionData> branchConnections,
|
||||
boolean online
|
||||
) {
|
||||
public SignalResponse(Signal signal) {
|
||||
this(
|
||||
signal.getId(),
|
||||
signal.getName(),
|
||||
signal.getPosition(),
|
||||
signal.getBranchConnections().stream()
|
||||
.sorted(Comparator.comparing(SignalBranchConnection::getDirection))
|
||||
.map(ConnectionData::new)
|
||||
.toList(),
|
||||
signal.isOnline()
|
||||
);
|
||||
}
|
||||
|
||||
public static record ConnectionData(
|
||||
long id,
|
||||
String direction,
|
||||
BranchResponse branch,
|
||||
List<ReachableConnectionData> reachableSignalConnections
|
||||
) {
|
||||
public ConnectionData(SignalBranchConnection c) {
|
||||
this(
|
||||
c.getId(),
|
||||
c.getDirection().name(),
|
||||
new BranchResponse(c.getBranch()),
|
||||
c.getReachableSignalConnections().stream()
|
||||
.map(cc -> new ReachableConnectionData(
|
||||
cc.getId(),
|
||||
cc.getDirection().name(),
|
||||
cc.getSignal().getId(),
|
||||
cc.getSignal().getName(),
|
||||
cc.getSignal().getPosition()
|
||||
))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
public static record ReachableConnectionData(
|
||||
long connectionId,
|
||||
String direction,
|
||||
long signalId,
|
||||
String signalName,
|
||||
Position signalPosition
|
||||
) {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.in;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Position;
|
||||
|
||||
public abstract class ComponentPayload {
|
||||
public String name;
|
||||
public String type;
|
||||
public Position position;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.in;
|
||||
|
||||
public class SignalPayload extends ComponentPayload {
|
||||
public long segmentId;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.*;
|
||||
|
||||
public abstract class ComponentResponse {
|
||||
public long id;
|
||||
public Position position;
|
||||
public String name;
|
||||
public String type;
|
||||
public boolean online;
|
||||
|
||||
public ComponentResponse(Component c) {
|
||||
this.id = c.getId();
|
||||
this.position = c.getPosition();
|
||||
this.name = c.getName();
|
||||
this.type = c.getType().name();
|
||||
this.online = c.isOnline();
|
||||
}
|
||||
|
||||
public static ComponentResponse of(Component c) {
|
||||
return switch (c.getType()) {
|
||||
case SIGNAL -> new SignalResponse((Signal) c);
|
||||
case SWITCH -> new SwitchResponse((Switch) c);
|
||||
case SEGMENT_BOUNDARY -> new SegmentBoundaryNodeResponse((SegmentBoundaryNode) c);
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.PathNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class PathNodeResponse extends ComponentResponse {
|
||||
public List<SimpleComponentResponse> connectedNodes;
|
||||
|
||||
public PathNodeResponse(PathNode p) {
|
||||
super(p);
|
||||
this.connectedNodes = p.getConnectedNodes().stream().map(SimpleComponentResponse::new).toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SegmentBoundaryNodeResponse extends PathNodeResponse {
|
||||
public List<SegmentResponse> segments;
|
||||
|
||||
public SegmentBoundaryNodeResponse(SegmentBoundaryNode n) {
|
||||
super(n);
|
||||
this.segments = n.getSegments().stream().map(SegmentResponse::new).toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
||||
|
||||
public class SignalResponse extends ComponentResponse {
|
||||
public SegmentResponse segment;
|
||||
public SignalResponse(Signal s) {
|
||||
super(s);
|
||||
this.segment = new SegmentResponse(s.getSegment());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.model.component.Position;
|
||||
|
||||
public record SimpleComponentResponse (
|
||||
long id,
|
||||
Position position,
|
||||
String name,
|
||||
String type,
|
||||
boolean online
|
||||
) {
|
||||
public SimpleComponentResponse(Component c) {
|
||||
this(
|
||||
c.getId(),
|
||||
c.getPosition(),
|
||||
c.getName(),
|
||||
c.getType().name(),
|
||||
c.isOnline()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SwitchConfigurationResponse (
|
||||
long id,
|
||||
List<SimpleComponentResponse> nodes
|
||||
) {
|
||||
public SwitchConfigurationResponse(SwitchConfiguration sc) {
|
||||
this(
|
||||
sc.getId(),
|
||||
sc.getNodes().stream().map(SimpleComponentResponse::new).toList()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
||||
|
||||
import nl.andrewl.railsignalapi.model.component.Switch;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SwitchResponse extends PathNodeResponse {
|
||||
public List<SwitchConfigurationResponse> possibleConfigurations;
|
||||
public SwitchConfigurationResponse activeConfiguration;
|
||||
|
||||
public SwitchResponse(Switch s) {
|
||||
super(s);
|
||||
this.possibleConfigurations = s.getPossibleConfigurations().stream().map(SwitchConfigurationResponse::new).toList();
|
||||
this.activeConfiguration = s.getActiveConfiguration() == null ? null : new SwitchConfigurationResponse(s.getActiveConfiguration());
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.dao.BranchRepository;
|
||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SignalRepository;
|
||||
import nl.andrewl.railsignalapi.rest.dto.BranchResponse;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BranchService {
|
||||
private final BranchRepository branchRepository;
|
||||
private final RailSystemRepository railSystemRepository;
|
||||
private final SignalRepository signalRepository;
|
||||
|
||||
@Transactional
|
||||
public void deleteBranch(long rsId, long branchId) {
|
||||
var branch = branchRepository.findByIdAndRailSystemId(branchId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!branch.getSignalConnections().isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Branch should not be connected to any signals.");
|
||||
}
|
||||
branchRepository.delete(branch);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BranchResponse> getAllBranches(long rsId) {
|
||||
var rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return branchRepository.findAllByRailSystemOrderByName(rs).stream()
|
||||
.map(BranchResponse::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BranchResponse getBranch(long rsId, long branchId) {
|
||||
var branch = branchRepository.findByIdAndRailSystemId(branchId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return new BranchResponse(branch);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SignalResponse> getConnectedSignals(long rsId, long branchId) {
|
||||
var branch = branchRepository.findByIdAndRailSystemId(branchId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return signalRepository.findAllConnectedToBranch(branch).stream()
|
||||
.map(SignalResponse::new)
|
||||
.toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package nl.andrewl.railsignalapi.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SegmentRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SwitchConfigurationRepository;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
import nl.andrewl.railsignalapi.model.component.*;
|
||||
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ComponentService {
|
||||
private final ComponentRepository<Component> componentRepository;
|
||||
private final RailSystemRepository railSystemRepository;
|
||||
private final SegmentRepository segmentRepository;
|
||||
private final SwitchConfigurationRepository switchConfigurationRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ComponentResponse> getComponents(long rsId) {
|
||||
var rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return componentRepository.findAllByRailSystem(rs).stream().map(ComponentResponse::of).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ComponentResponse getComponent(long rsId, long componentId) {
|
||||
var c = componentRepository.findByIdAndRailSystemId(componentId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return ComponentResponse.of(c);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void removeComponent(long rsId, long componentId) {
|
||||
var c = componentRepository.findByIdAndRailSystemId(componentId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
// If this is a path node, check for and remove any switch configurations that use it.
|
||||
if (c instanceof PathNode p) {
|
||||
switchConfigurationRepository.deleteAllByNodesContaining(p);
|
||||
}
|
||||
componentRepository.delete(c);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ComponentResponse create(long rsId, ObjectNode data) {
|
||||
RailSystem rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
String type = data.get("type").asText();
|
||||
Position pos = new Position();
|
||||
pos.setX(data.get("position").get("x").asDouble());
|
||||
pos.setY(data.get("position").get("y").asDouble());
|
||||
pos.setZ(data.get("position").get("z").asDouble());
|
||||
String name = data.get("name").asText();
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required name.");
|
||||
}
|
||||
|
||||
if (componentRepository.existsByNameAndRailSystem(name, rs)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with that name already exists.");
|
||||
}
|
||||
|
||||
Component c = switch (type) {
|
||||
case "SIGNAL" -> createSignal(rs, pos, name, data);
|
||||
case "SWITCH" -> createSwitch(rs, pos, name, data);
|
||||
case "SEGMENT_BOUNDARY" -> createSegmentBoundary(rs, pos, name, data);
|
||||
default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported component type: " + type);
|
||||
};
|
||||
c = componentRepository.save(c);
|
||||
return ComponentResponse.of(c);
|
||||
}
|
||||
|
||||
private Component createSignal(RailSystem rs, Position pos, String name, ObjectNode data) {
|
||||
long segmentId = data.get("segment").get("id").asLong();
|
||||
Segment segment = segmentRepository.findById(segmentId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return new Signal(rs, pos, name, segment);
|
||||
}
|
||||
|
||||
private Component createSwitch(RailSystem rs, Position pos, String name, ObjectNode data) {
|
||||
Switch s = new Switch(rs, pos, name, new HashSet<>(), new HashSet<>(), null);
|
||||
for (JsonNode configJson : data.withArray("possibleConfigurations")) {
|
||||
Set<PathNode> pathNodes = new HashSet<>();
|
||||
for (JsonNode pathNodeJson : configJson.withArray("nodes")) {
|
||||
long pathNodeId = pathNodeJson.get("id").asLong();
|
||||
Component c = componentRepository.findById(pathNodeId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (c instanceof PathNode pathNode) {
|
||||
pathNodes.add(pathNode);
|
||||
s.getConnectedNodes().add(pathNode);
|
||||
} else {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + pathNodeId + " does not refer to a PathNode component.");
|
||||
}
|
||||
}
|
||||
s.getPossibleConfigurations().add(new SwitchConfiguration(s, pathNodes));
|
||||
}
|
||||
if (s.getPossibleConfigurations().size() < 2) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least two switch configurations are needed.");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private Component createSegmentBoundary(RailSystem rs, Position pos, String name, ObjectNode data) {
|
||||
ArrayNode segmentsNode = data.withArray("segments");
|
||||
Set<Segment> segments = new HashSet<>();
|
||||
for (JsonNode segmentNode : segmentsNode) {
|
||||
long segmentId = segmentNode.get("id").asLong();
|
||||
Segment segment = segmentRepository.findById(segmentId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
segments.add(segment);
|
||||
}
|
||||
if (segments.size() < 1 || segments.size() > 2) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid number of segments.");
|
||||
}
|
||||
return new SegmentBoundaryNode(rs, pos, name, new HashSet<>(), segments);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) {
|
||||
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!(c instanceof PathNode p)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component is not a PathNode.");
|
||||
Set<PathNode> newNodes = new HashSet<>();
|
||||
for (var nodeObj : payload.connectedNodes) {
|
||||
long id = nodeObj.id;
|
||||
var c1 = componentRepository.findByIdAndRailSystemId(id, rsId);
|
||||
if (c1.isPresent() && c1.get() instanceof PathNode pn) {
|
||||
newNodes.add(pn);
|
||||
} else {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with id " + id + " is not a PathNode in the same rail system.");
|
||||
}
|
||||
}
|
||||
|
||||
Set<PathNode> nodesToRemove = new HashSet<>(p.getConnectedNodes());
|
||||
nodesToRemove.removeAll(newNodes);
|
||||
|
||||
Set<PathNode> nodesToAdd = new HashSet<>(newNodes);
|
||||
nodesToAdd.removeAll(p.getConnectedNodes());
|
||||
|
||||
p.getConnectedNodes().removeAll(nodesToRemove);
|
||||
p.getConnectedNodes().addAll(nodesToAdd);
|
||||
for (var node : nodesToRemove) {
|
||||
node.getConnectedNodes().remove(p);
|
||||
}
|
||||
for (var node : nodesToAdd) {
|
||||
node.getConnectedNodes().add(p);
|
||||
}
|
||||
componentRepository.saveAll(nodesToRemove);
|
||||
componentRepository.saveAll(nodesToAdd);
|
||||
p = componentRepository.save(p);
|
||||
return ComponentResponse.of(p);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package nl.andrewl.railsignalapi.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.dao.BranchRepository;
|
||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SignalRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SegmentRepository;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
import nl.andrewl.railsignalapi.rest.dto.RailSystemCreationPayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.RailSystemResponse;
|
||||
|
@ -19,8 +19,8 @@ import java.util.List;
|
|||
@RequiredArgsConstructor
|
||||
public class RailSystemService {
|
||||
private final RailSystemRepository railSystemRepository;
|
||||
private final SignalRepository signalRepository;
|
||||
private final BranchRepository branchRepository;
|
||||
private final SegmentRepository segmentRepository;
|
||||
private final ComponentRepository<?> componentRepository;
|
||||
|
||||
@Transactional
|
||||
public List<RailSystemResponse> getRailSystems() {
|
||||
|
@ -29,10 +29,13 @@ public class RailSystemService {
|
|||
|
||||
@Transactional
|
||||
public RailSystemResponse createRailSystem(RailSystemCreationPayload payload) {
|
||||
if (railSystemRepository.existsByName(payload.name())) {
|
||||
if (payload.name() == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required name.");
|
||||
if (payload.name().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Name cannot be blank.");
|
||||
String name = payload.name().trim();
|
||||
if (railSystemRepository.existsByName(name)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "A rail system with that name already exists.");
|
||||
}
|
||||
RailSystem rs = new RailSystem(payload.name());
|
||||
RailSystem rs = new RailSystem(name);
|
||||
return new RailSystemResponse(railSystemRepository.save(rs));
|
||||
}
|
||||
|
||||
|
@ -40,10 +43,8 @@ public class RailSystemService {
|
|||
public void delete(long rsId) {
|
||||
var rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
var signals = signalRepository.findAllByRailSystemOrderByName(rs);
|
||||
signalRepository.deleteAll(signals);
|
||||
var branches = branchRepository.findAllByRailSystemOrderByName(rs);
|
||||
branchRepository.deleteAll(branches);
|
||||
componentRepository.deleteAllByRailSystem(rs);
|
||||
segmentRepository.deleteAllByRailSystem(rs);
|
||||
railSystemRepository.delete(rs);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package nl.andrewl.railsignalapi.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SegmentRepository;
|
||||
import nl.andrewl.railsignalapi.model.Segment;
|
||||
import nl.andrewl.railsignalapi.model.component.Component;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SegmentService {
|
||||
private final SegmentRepository segmentRepository;
|
||||
private final RailSystemRepository railSystemRepository;
|
||||
private final ComponentRepository<Component> componentRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SegmentResponse> getSegments(long rsId) {
|
||||
return segmentRepository.findAllByRailSystemId(rsId).stream().map(SegmentResponse::new).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FullSegmentResponse getSegment(long rsId, long segmentId) {
|
||||
var segment = segmentRepository.findByIdAndRailSystemId(segmentId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return new FullSegmentResponse(segment);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FullSegmentResponse create(long rsId, SegmentPayload payload) {
|
||||
var rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
String name = payload.name();
|
||||
if (segmentRepository.existsByNameAndRailSystem(name, rs)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Segment with that name already exists.");
|
||||
}
|
||||
Segment segment = segmentRepository.save(new Segment(rs, name));
|
||||
return new FullSegmentResponse(segment);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void remove(long rsId, long segmentId) {
|
||||
var segment = segmentRepository.findByIdAndRailSystemId(segmentId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
componentRepository.deleteAll(segment.getSignals());
|
||||
componentRepository.deleteAll(segment.getBoundaryNodes());
|
||||
segmentRepository.delete(segment);
|
||||
}
|
||||
}
|
|
@ -1,245 +0,0 @@
|
|||
package nl.andrewl.railsignalapi.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.dao.BranchRepository;
|
||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SignalBranchConnectionRepository;
|
||||
import nl.andrewl.railsignalapi.dao.SignalRepository;
|
||||
import nl.andrewl.railsignalapi.model.*;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalConnectionsUpdatePayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalCreationPayload;
|
||||
import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
|
||||
import nl.andrewl.railsignalapi.websocket.BranchUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.websocket.SignalUpdateMessage;
|
||||
import nl.andrewl.railsignalapi.websocket.SignalUpdateType;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SignalService {
|
||||
private final RailSystemRepository railSystemRepository;
|
||||
private final SignalRepository signalRepository;
|
||||
private final BranchRepository branchRepository;
|
||||
private final SignalBranchConnectionRepository signalBranchConnectionRepository;
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final Map<WebSocketSession, Set<Long>> signalWebSocketSessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Transactional
|
||||
public SignalResponse createSignal(long rsId, SignalCreationPayload payload) {
|
||||
var rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Rail system not found."));
|
||||
if (signalRepository.existsByNameAndRailSystem(payload.name(), rs)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Signal " + payload.name() + " already exists.");
|
||||
}
|
||||
if (payload.branchConnections().size() != 2) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Exactly two branch connections must be provided.");
|
||||
}
|
||||
// Ensure that the directions of the connections are opposite each other.
|
||||
Direction dir1 = Direction.parse(payload.branchConnections().get(0).direction());
|
||||
Direction dir2 = Direction.parse(payload.branchConnections().get(1).direction());
|
||||
if (!dir1.isOpposite(dir2)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Branch connections must be opposite each other.");
|
||||
|
||||
Set<SignalBranchConnection> branchConnections = new HashSet<>();
|
||||
Signal signal = new Signal(rs, payload.name(), payload.position(), branchConnections);
|
||||
for (var branchData : payload.branchConnections()) {
|
||||
Branch branch;
|
||||
if (branchData.id() != null) {
|
||||
branch = this.branchRepository.findByIdAndRailSystem(branchData.id(), rs)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Branch id " + branchData.id() + " is invalid."));
|
||||
} else {
|
||||
branch = this.branchRepository.findByNameAndRailSystem(branchData.name(), rs)
|
||||
.orElse(new Branch(rs, branchData.name(), BranchStatus.FREE));
|
||||
}
|
||||
Direction dir = Direction.parse(branchData.direction());
|
||||
branchConnections.add(new SignalBranchConnection(signal, branch, dir));
|
||||
}
|
||||
signal = signalRepository.save(signal);
|
||||
return new SignalResponse(signal);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SignalResponse updateSignalBranchConnections(long rsId, long sigId, SignalConnectionsUpdatePayload payload) {
|
||||
var signal = signalRepository.findByIdAndRailSystemId(sigId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
for (var c : payload.connections()) {
|
||||
var fromConnection = signalBranchConnectionRepository.findById(c.from())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Could not find signal branch connection: " + c.from()));
|
||||
if (!fromConnection.getSignal().getId().equals(signal.getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Can only update signal branch connections originating from the specified signal.");
|
||||
}
|
||||
var toConnection = signalBranchConnectionRepository.findById(c.to())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Could not find signal branch connection: " + c.to()));
|
||||
if (!fromConnection.getBranch().getId().equals(toConnection.getBranch().getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Signal branch connections can only path via a mutual branch.");
|
||||
}
|
||||
fromConnection.getReachableSignalConnections().add(toConnection);
|
||||
signalBranchConnectionRepository.save(fromConnection);
|
||||
}
|
||||
for (var con : signal.getBranchConnections()) {
|
||||
Set<SignalBranchConnection> connectionsToRemove = new HashSet<>();
|
||||
for (var reachableCon : con.getReachableSignalConnections()) {
|
||||
if (!payload.connections().contains(new SignalConnectionsUpdatePayload.ConnectionData(con.getId(), reachableCon.getId()))) {
|
||||
connectionsToRemove.add(reachableCon);
|
||||
}
|
||||
}
|
||||
con.getReachableSignalConnections().removeAll(connectionsToRemove);
|
||||
signalBranchConnectionRepository.save(con);
|
||||
}
|
||||
// Reload the signal.
|
||||
signal = signalRepository.findById(signal.getId()).orElseThrow();
|
||||
return new SignalResponse(signal);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void registerSignalWebSocketSession(Set<Long> signalIds, WebSocketSession session) {
|
||||
this.signalWebSocketSessions.put(session, signalIds);
|
||||
// Instantly send a data packet so that the signals are up-to-date.
|
||||
RailSystem rs = null;
|
||||
for (var signalId : signalIds) {
|
||||
var signal = signalRepository.findById(signalId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid signal id."));
|
||||
if (rs == null) {
|
||||
rs = signal.getRailSystem();
|
||||
} else if (!rs.getId().equals(signal.getRailSystem().getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot open signal websocket session for signals from different rail systems.");
|
||||
}
|
||||
for (var branchConnection : signal.getBranchConnections()) {
|
||||
try {
|
||||
session.sendMessage(new TextMessage(mapper.writeValueAsString(
|
||||
new BranchUpdateMessage(
|
||||
branchConnection.getBranch().getId(),
|
||||
branchConnection.getBranch().getStatus().name()
|
||||
)
|
||||
)));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
signal.setOnline(true);
|
||||
signalRepository.save(signal);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deregisterSignalWebSocketSession(WebSocketSession session) {
|
||||
var ids = this.signalWebSocketSessions.remove(session);
|
||||
if (ids != null) {
|
||||
for (var signalId : ids) {
|
||||
signalRepository.findById(signalId).ifPresent(signal -> {
|
||||
signal.setOnline(false);
|
||||
signalRepository.save(signal);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WebSocketSession getSignalWebSocketSession(long signalId) {
|
||||
for (var entry : signalWebSocketSessions.entrySet()) {
|
||||
if (entry.getValue().contains(signalId)) return entry.getKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void handleSignalUpdate(SignalUpdateMessage updateMessage) {
|
||||
var signal = signalRepository.findById(updateMessage.signalId())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
Branch fromBranch = null;
|
||||
Branch toBranch = null;
|
||||
for (var con : signal.getBranchConnections()) {
|
||||
if (con.getBranch().getId() == updateMessage.fromBranchId()) {
|
||||
fromBranch = con.getBranch();
|
||||
}
|
||||
if (con.getBranch().getId() == updateMessage.toBranchId()) {
|
||||
toBranch = con.getBranch();
|
||||
}
|
||||
}
|
||||
if (fromBranch == null || toBranch == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid branches.");
|
||||
}
|
||||
SignalUpdateType updateType = SignalUpdateType.valueOf(updateMessage.type().trim().toUpperCase());
|
||||
if (updateType == SignalUpdateType.BEGIN && toBranch.getStatus() != BranchStatus.FREE) {
|
||||
log.warn("Warning! Train is entering a non-free branch {}.", toBranch.getName());
|
||||
}
|
||||
if (toBranch.getStatus() != BranchStatus.OCCUPIED) {
|
||||
log.info("Updating branch {} status from {} to {}.", toBranch.getName(), toBranch.getStatus(), BranchStatus.OCCUPIED);
|
||||
toBranch.setStatus(BranchStatus.OCCUPIED);
|
||||
branchRepository.save(toBranch);
|
||||
broadcastToConnectedSignals(toBranch);
|
||||
}
|
||||
if (updateType == SignalUpdateType.END) {
|
||||
if (fromBranch.getStatus() != BranchStatus.FREE) {
|
||||
log.info("Updating branch {} status from {} to {}.", fromBranch.getName(), fromBranch.getStatus(), BranchStatus.FREE);
|
||||
fromBranch.setStatus(BranchStatus.FREE);
|
||||
branchRepository.save(fromBranch);
|
||||
broadcastToConnectedSignals(fromBranch);
|
||||
}
|
||||
} else if (updateType == SignalUpdateType.BEGIN) {
|
||||
if (fromBranch.getStatus() != BranchStatus.OCCUPIED) {
|
||||
log.info("Updating branch {} status from {} to {}.", fromBranch.getName(), fromBranch.getStatus(), BranchStatus.OCCUPIED);
|
||||
fromBranch.setStatus(BranchStatus.OCCUPIED);
|
||||
branchRepository.save(fromBranch);
|
||||
broadcastToConnectedSignals(fromBranch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastToConnectedSignals(Branch branch) {
|
||||
try {
|
||||
WebSocketMessage<String> msg = new TextMessage(mapper.writeValueAsString(
|
||||
new BranchUpdateMessage(branch.getId(), branch.getStatus().name())
|
||||
));
|
||||
signalRepository.findAllConnectedToBranch(branch).stream()
|
||||
.map(s -> getSignalWebSocketSession(s.getId()))
|
||||
.filter(Objects::nonNull).distinct()
|
||||
.forEach(session -> {
|
||||
try {
|
||||
session.sendMessage(msg);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public SignalResponse getSignal(long rsId, long sigId) {
|
||||
var s = signalRepository.findByIdAndRailSystemId(sigId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return new SignalResponse(s);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SignalResponse> getAllSignals(long rsId) {
|
||||
var rs = railSystemRepository.findById(rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Rail system not found."));
|
||||
return signalRepository.findAllByRailSystemOrderByName(rs).stream()
|
||||
.map(SignalResponse::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteSignal(long rsId, long sigId) {
|
||||
var s = signalRepository.findByIdAndRailSystemId(sigId, rsId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
signalRepository.delete(s);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package nl.andrewl.railsignalapi.websocket;
|
|||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.service.SignalService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
|
@ -18,7 +17,6 @@ import java.util.Set;
|
|||
@Slf4j
|
||||
public class SignalWebSocketHandler extends TextWebSocketHandler {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final SignalService signalService;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
|
@ -31,19 +29,19 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
|||
for (var idStr : signalIdHeader.split(",")) {
|
||||
ids.add(Long.parseLong(idStr.trim()));
|
||||
}
|
||||
signalService.registerSignalWebSocketSession(ids, session);
|
||||
//signalService.registerSignalWebSocketSession(ids, session);
|
||||
log.info("Connection established with signals {}.", ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
|
||||
signalService.handleSignalUpdate(msg);
|
||||
//signalService.handleSignalUpdate(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
signalService.deregisterSignalWebSocketSession(session);
|
||||
//signalService.deregisterSignalWebSocketSession(session);
|
||||
log.info("Closed connection {}. Status: {}", session.getId(), status.toString());
|
||||
}
|
||||
}
|
||||
|
|