| 
						 | 
				
			
			@ -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());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||