added stuff
This commit is contained in:
parent
2fbc22af0d
commit
e608e2ba8c
1
pom.xml
1
pom.xml
|
@ -40,6 +40,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
|
<version>2.1.212</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -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,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>RailSignal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "railsignal-app",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --port 5050",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.27.2",
|
||||||
|
"pinia": "^2.0.14",
|
||||||
|
"three": "^0.140.0",
|
||||||
|
"vue": "^3.2.33",
|
||||||
|
"vue-router": "^4.0.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^2.3.1",
|
||||||
|
"eslint": "^8.5.0",
|
||||||
|
"eslint-plugin-vue": "^8.2.0",
|
||||||
|
"vite": "^2.9.5"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<h1>RailSignal</h1>
|
||||||
|
</header>
|
||||||
|
<RailSystemsManager />
|
||||||
|
|
||||||
|
<RailSystem
|
||||||
|
v-if="rsStore.selectedRailSystem !== null"
|
||||||
|
:railSystem="rsStore.selectedRailSystem"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RailSystem from "./components/RailSystem.vue";
|
||||||
|
import RailSystemsManager from "./components/RailSystemsManager.vue";
|
||||||
|
import {useRailSystemsStore} from "./stores/railSystemsStore";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
RailSystem,
|
||||||
|
RailSystemsManager
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const rsStore = useRailSystemsStore();
|
||||||
|
return {
|
||||||
|
rsStore
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<h2>{{railSystem.name}}</h2>
|
||||||
|
<RsMap :railSystem="railSystem" />
|
||||||
|
<RsComponent v-if="selectedComponent !== null" :component="selectedComponent" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RsMap from './railsystem/MapView.vue'
|
||||||
|
import RsComponent from './railsystem/Component.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
RsMap,
|
||||||
|
RsComponent
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
railSystem: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedComponent: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<select v-model="rsStore.selectedRailSystem">
|
||||||
|
<option v-for="rs in rsStore.railSystems" :key="rs.id" :value="rs">
|
||||||
|
{{rs.name}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="rsStore.railSystems.length === 0">
|
||||||
|
There are no rail systems.
|
||||||
|
</p>
|
||||||
|
<button v-if="rsStore.selectedRailSystem !== null" @click="rsStore.removeRailSystem(rsStore.selectedRailSystem)">
|
||||||
|
Remove this Rail System
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3>Create a New Rail System</h3>
|
||||||
|
<form>
|
||||||
|
<label for="rsNameInput">Name</label>
|
||||||
|
<input id="rsNameInput" type="text" v-model="formData.rsName"/>
|
||||||
|
<button type="submit" @click.prevent="formSubmitted">Submit</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {useRailSystemsStore} from "../stores/railSystemsStore";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "RailSystemsManager.vue",
|
||||||
|
setup() {
|
||||||
|
const rsStore = useRailSystemsStore();
|
||||||
|
return {
|
||||||
|
rsStore
|
||||||
|
};
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
rsName: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formSubmitted() {
|
||||||
|
this.rsStore.createRailSystem(this.formData.rsName)
|
||||||
|
.then(() => {
|
||||||
|
this.formData.rsName = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.rsStore.refreshRailSystems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<label for="addSegmentName">Name</label>
|
||||||
|
<input type="text" v-model="formData.segmentName" />
|
||||||
|
<button>Add</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "AddSegment",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
segmentName: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rs-component">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rs-component {
|
||||||
|
width: 20%;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
railSystem: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas>
|
||||||
|
Your browser doesn't support canvas!
|
||||||
|
</canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
canvas {
|
||||||
|
border: 1px solid black;
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia();
|
||||||
|
app.use(pinia);
|
||||||
|
|
||||||
|
app.mount('#app')
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const useRailSystemsStore = defineStore('RailSystemsStore', {
|
||||||
|
state: () => ({
|
||||||
|
railSystems: [],
|
||||||
|
selectedRailSystem: null,
|
||||||
|
selectedComponent: null
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
refreshRailSystems() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.get(import.meta.env.VITE_API_URL + "/rs")
|
||||||
|
.then(response => {
|
||||||
|
this.railSystems = response.data;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createRailSystem(name) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.post(
|
||||||
|
import.meta.env.VITE_API_URL + "/rs",
|
||||||
|
{name: name}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.refreshRailSystems()
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(error => reject(error));
|
||||||
|
})
|
||||||
|
.catch(error => reject(error));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeRailSystem(rs) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios.delete(import.meta.env.VITE_API_URL + "/rs/" + rs.id)
|
||||||
|
.then(() => {
|
||||||
|
this.refreshRailSystems()
|
||||||
|
.then(() => resolve)
|
||||||
|
.catch(error => reject(error));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -2,10 +2,27 @@ package nl.andrewl.railsignalapi.dao;
|
||||||
|
|
||||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
import nl.andrewl.railsignalapi.model.component.Component;
|
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.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ComponentRepository extends JpaRepository<Component, Long> {
|
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);
|
void deleteAllByRailSystem(RailSystem rs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,16 @@ import nl.andrewl.railsignalapi.model.Segment;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface SegmentRepository extends JpaRepository<Segment, Long> {
|
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);
|
void deleteAllByRailSystem(RailSystem rs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package nl.andrewl.railsignalapi.dao;
|
||||||
|
|
||||||
|
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
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> {
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
|
||||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,7 +28,7 @@ public class Segment {
|
||||||
/**
|
/**
|
||||||
* A unique name for this segment.
|
* A unique name for this segment.
|
||||||
*/
|
*/
|
||||||
@Column(unique = true)
|
@Column
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,9 +40,16 @@ public class Segment {
|
||||||
/**
|
/**
|
||||||
* The set of nodes from which trains can enter and exit this segment.
|
* The set of nodes from which trains can enter and exit this segment.
|
||||||
*/
|
*/
|
||||||
@ManyToMany(mappedBy = "segments", cascade = CascadeType.ALL)
|
@ManyToMany(mappedBy = "segments")
|
||||||
private Set<SegmentBoundaryNode> boundaryNodes;
|
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
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o == this) return true;
|
if (o == this) return true;
|
||||||
|
|
|
@ -37,9 +37,15 @@ public abstract class Component {
|
||||||
/**
|
/**
|
||||||
* A human-readable name for the component.
|
* A human-readable name for the component.
|
||||||
*/
|
*/
|
||||||
@Column(unique = true)
|
@Column
|
||||||
private String name;
|
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
|
* Whether this component is online, meaning that an in-world device is
|
||||||
* currently connected to relay information regarding this component.
|
* currently connected to relay information regarding this component.
|
||||||
|
@ -48,10 +54,11 @@ public abstract class Component {
|
||||||
@Setter
|
@Setter
|
||||||
private boolean online = false;
|
private boolean online = false;
|
||||||
|
|
||||||
public Component(RailSystem railSystem, Position position, String name) {
|
public Component(RailSystem railSystem, Position position, String name, ComponentType type) {
|
||||||
this.railSystem = railSystem;
|
this.railSystem = railSystem;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -59,4 +66,13 @@ public abstract class Component {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
return o instanceof Component c && this.id != null && this.id.equals(c.id);
|
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
|
||||||
|
}
|
|
@ -19,15 +19,15 @@ import java.util.Set;
|
||||||
@Inheritance(strategy = InheritanceType.JOINED)
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
public class PathNode extends Component {
|
public abstract class PathNode extends Component {
|
||||||
/**
|
/**
|
||||||
* The set of nodes that this one is connected to.
|
* The set of nodes that this one is connected to.
|
||||||
*/
|
*/
|
||||||
@ManyToMany
|
@ManyToMany
|
||||||
private Set<PathNode> connectedNodes;
|
private Set<PathNode> connectedNodes;
|
||||||
|
|
||||||
public PathNode(RailSystem railSystem, Position position, String name, Set<PathNode> connectedNodes) {
|
public PathNode(RailSystem railSystem, Position position, String name, ComponentType type, Set<PathNode> connectedNodes) {
|
||||||
super(railSystem, position, name);
|
super(railSystem, position, name, type);
|
||||||
this.connectedNodes = connectedNodes;
|
this.connectedNodes = connectedNodes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class SegmentBoundaryNode extends PathNode {
|
||||||
private Set<Segment> segments;
|
private Set<Segment> segments;
|
||||||
|
|
||||||
public SegmentBoundaryNode(RailSystem railSystem, Position position, String name, Set<PathNode> connectedNodes, Set<Segment> segments) {
|
public SegmentBoundaryNode(RailSystem railSystem, Position position, String name, Set<PathNode> connectedNodes, Set<Segment> segments) {
|
||||||
super(railSystem, position, name, connectedNodes);
|
super(railSystem, position, name, ComponentType.SEGMENT_BOUNDARY, connectedNodes);
|
||||||
this.segments = segments;
|
this.segments = segments;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package nl.andrewl.railsignalapi.model.component;
|
package nl.andrewl.railsignalapi.model.component;
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.railsignalapi.model.Direction;
|
|
||||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
import nl.andrewl.railsignalapi.model.Segment;
|
import nl.andrewl.railsignalapi.model.Segment;
|
||||||
|
|
||||||
import javax.persistence.*;
|
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
|
* A signal is a component that relays the status of a connected segment to
|
||||||
|
@ -15,29 +17,16 @@ import javax.persistence.*;
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
public class Signal extends Component {
|
public class Signal extends Component {
|
||||||
/**
|
|
||||||
* The direction this signal is facing. This is the direction in which the
|
|
||||||
* signal connects to a branch.
|
|
||||||
* <pre>
|
|
||||||
* |-segment A-|-segment B-|
|
|
||||||
* ===================== <- Rail
|
|
||||||
* ]+ ---> Signal is facing East, and shows status on
|
|
||||||
* its western side. It is connected to segment B.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private Direction direction;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The segment that this signal connects to.
|
* The segment that this signal connects to.
|
||||||
*/
|
*/
|
||||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
private Segment segment;
|
private Segment segment;
|
||||||
|
|
||||||
public Signal(RailSystem railSystem, Position position, String name, Segment segment, Direction direction) {
|
public Signal(RailSystem railSystem, Position position, String name, Segment segment) {
|
||||||
super(railSystem, position, name);
|
super(railSystem, position, name, ComponentType.SIGNAL);
|
||||||
this.segment = segment;
|
this.segment = segment;
|
||||||
this.direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package nl.andrewl.railsignalapi.model.component;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -22,8 +24,16 @@ public class Switch extends PathNode {
|
||||||
private Set<SwitchConfiguration> possibleConfigurations;
|
private Set<SwitchConfiguration> possibleConfigurations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The switch configuration that this switch is currently in.
|
* The switch configuration that this switch is currently in. If null, then
|
||||||
|
* we don't know what configuration the switch is in.
|
||||||
*/
|
*/
|
||||||
@OneToOne(optional = false, fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
@Setter
|
||||||
private SwitchConfiguration activeConfiguration;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package nl.andrewl.railsignalapi.model.component;
|
package nl.andrewl.railsignalapi.model.component;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
|
@ -10,7 +12,8 @@ import java.util.Set;
|
||||||
* as an active configuration in the linked switch component.
|
* as an active configuration in the linked switch component.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
public class SwitchConfiguration {
|
public class SwitchConfiguration {
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
|
@ -28,4 +31,17 @@ public class SwitchConfiguration {
|
||||||
*/
|
*/
|
||||||
@ManyToMany(fetch = FetchType.EAGER)
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
private Set<PathNode> nodes;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.component.ComponentResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.component.SimpleComponentResponse;
|
||||||
|
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<SimpleComponentResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest;
|
||||||
|
|
||||||
|
public record SegmentPayload(String name) {
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
||||||
|
import nl.andrewl.railsignalapi.service.SegmentService;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package nl.andrewl.railsignalapi.rest;
|
package nl.andrewl.railsignalapi.rest;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.EnableWebMvc;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
@ -13,4 +14,13 @@ public class WebConfig implements WebMvcConfigurer {
|
||||||
registry.addResourceHandler("/static/**")
|
registry.addResourceHandler("/static/**")
|
||||||
.addResourceLocations("classpath:/static/");
|
.addResourceLocations("classpath:/static/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOrigins("*")
|
||||||
|
.allowedMethods("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest.dto;
|
||||||
|
|
||||||
|
import nl.andrewl.railsignalapi.model.Segment;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.component.SegmentBoundaryNodeResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.component.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,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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package nl.andrewl.railsignalapi.rest.dto.component;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 = 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,129 @@
|
||||||
|
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.component.ComponentResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.component.SimpleComponentResponse;
|
||||||
|
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<SimpleComponentResponse> getComponents(long rsId) {
|
||||||
|
var rs = railSystemRepository.findById(rsId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
return componentRepository.findAllByRailSystem(rs).stream().map(SimpleComponentResponse::new).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 && 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));
|
||||||
|
}
|
||||||
|
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 void remove(long rsId, long componentId) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import java.util.List;
|
||||||
public class RailSystemService {
|
public class RailSystemService {
|
||||||
private final RailSystemRepository railSystemRepository;
|
private final RailSystemRepository railSystemRepository;
|
||||||
private final SegmentRepository segmentRepository;
|
private final SegmentRepository segmentRepository;
|
||||||
private final ComponentRepository componentRepository;
|
private final ComponentRepository<?> componentRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<RailSystemResponse> getRailSystems() {
|
public List<RailSystemResponse> getRailSystems() {
|
||||||
|
@ -29,10 +29,13 @@ public class RailSystemService {
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public RailSystemResponse createRailSystem(RailSystemCreationPayload payload) {
|
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.");
|
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));
|
return new RailSystemResponse(railSystemRepository.save(rs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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.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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,246 +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.model.component.SignalBranchConnection;
|
|
||||||
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.andrewl.railsignalapi.service.SignalService;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.socket.CloseStatus;
|
import org.springframework.web.socket.CloseStatus;
|
||||||
import org.springframework.web.socket.TextMessage;
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
@ -18,7 +17,6 @@ import java.util.Set;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SignalWebSocketHandler extends TextWebSocketHandler {
|
public class SignalWebSocketHandler extends TextWebSocketHandler {
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
private final SignalService signalService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
|
@ -31,19 +29,19 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
||||||
for (var idStr : signalIdHeader.split(",")) {
|
for (var idStr : signalIdHeader.split(",")) {
|
||||||
ids.add(Long.parseLong(idStr.trim()));
|
ids.add(Long.parseLong(idStr.trim()));
|
||||||
}
|
}
|
||||||
signalService.registerSignalWebSocketSession(ids, session);
|
//signalService.registerSignalWebSocketSession(ids, session);
|
||||||
log.info("Connection established with signals {}.", ids);
|
log.info("Connection established with signals {}.", ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||||
var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
|
var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
|
||||||
signalService.handleSignalUpdate(msg);
|
//signalService.handleSignalUpdate(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||||
signalService.deregisterSignalWebSocketSession(session);
|
//signalService.deregisterSignalWebSocketSession(session);
|
||||||
log.info("Closed connection {}. Status: {}", session.getId(), status.toString());
|
log.info("Closed connection {}. Status: {}", session.getId(), status.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
function worldTransform() {
|
|
||||||
const canvasRect = railMapCanvas[0].getBoundingClientRect();
|
|
||||||
const scale = SCALE_VALUES[canvasScaleIndex];
|
|
||||||
let tx = new DOMMatrix();
|
|
||||||
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
|
|
||||||
tx.scaleSelf(scale, scale, scale);
|
|
||||||
tx.translateSelf(canvasTranslation.x, canvasTranslation.y, 0);
|
|
||||||
if (canvasDragOrigin !== null && canvasDragTranslation !== null) {
|
|
||||||
tx.translateSelf(canvasDragTranslation.x, canvasDragTranslation.y, 0);
|
|
||||||
}
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
function canvasPointToWorld(p) {
|
|
||||||
return worldTransform().invertSelf().transformPoint(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
function worldPointToCanvas(p) {
|
|
||||||
return worldTransform().transformPoint(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
function signalTransform(worldTx, signal) {
|
|
||||||
let tx = DOMMatrix.fromMatrix(worldTx);
|
|
||||||
tx.translateSelf(signal.position.x, signal.position.z, 0);
|
|
||||||
let direction = signal.branchConnections[0].direction;
|
|
||||||
if (direction === "EAST" || direction === "SOUTH" || direction === "SOUTH_EAST" || direction === "SOUTH_WEST") {
|
|
||||||
direction = signal.branchConnections[1].direction;
|
|
||||||
}
|
|
||||||
if (direction === undefined || direction === null || direction === "") {
|
|
||||||
direction = "NORTH";
|
|
||||||
}
|
|
||||||
let angle = 0;
|
|
||||||
if (direction === "NORTH") {
|
|
||||||
angle = 90;
|
|
||||||
} else if (direction === "NORTH_WEST") {
|
|
||||||
angle = 45;
|
|
||||||
} else if (direction === "NORTH_EAST") {
|
|
||||||
angle = 135;
|
|
||||||
}
|
|
||||||
tx.rotateSelf(0, 0, angle);
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawRailSystem() {
|
|
||||||
let ctx = railMapCanvas[0].getContext("2d");
|
|
||||||
ctx.resetTransform();
|
|
||||||
ctx.clearRect(0, 0, railMapCanvas.width(), railMapCanvas.height());
|
|
||||||
const worldTx = worldTransform();
|
|
||||||
ctx.setTransform(worldTx);
|
|
||||||
railSystem.signals.forEach(signal => {
|
|
||||||
drawReachableConnections(ctx, signal);
|
|
||||||
});
|
|
||||||
railSystem.signals.forEach(signal => {
|
|
||||||
ctx.setTransform(signalTransform(worldTx, signal));
|
|
||||||
drawSignal(ctx, signal);
|
|
||||||
});
|
|
||||||
ctx.resetTransform();
|
|
||||||
ctx.fillStyle = 'black';
|
|
||||||
ctx.strokeStyle = 'black';
|
|
||||||
ctx.font = '24px Serif';
|
|
||||||
let textLine = 0;
|
|
||||||
hoveredElements.forEach(element => {
|
|
||||||
ctx.strokeText(element.name, 10, 20 + textLine * 20);
|
|
||||||
ctx.fillText(element.name, 10, 20 + textLine * 20);
|
|
||||||
textLine += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSignal(ctx, signal) {
|
|
||||||
if (signal.online) {
|
|
||||||
ctx.fillStyle = 'black';
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = 'gray';
|
|
||||||
}
|
|
||||||
ctx.scale(2, 2);
|
|
||||||
ctx.fillRect(-0.5, -0.5, 1, 1);
|
|
||||||
let northWesterlyCon = signal.branchConnections[0];
|
|
||||||
let southEasterlyCon = signal.branchConnections[1];
|
|
||||||
if (northWesterlyCon.direction === "EAST" || northWesterlyCon.direction === "SOUTH" || northWesterlyCon.direction === "SOUTH_WEST" || northWesterlyCon.direction === "SOUTH_EAST") {
|
|
||||||
let tmp = northWesterlyCon;
|
|
||||||
northWesterlyCon = southEasterlyCon;
|
|
||||||
southEasterlyCon = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = getSignalColor(signal, southEasterlyCon.branch.status);
|
|
||||||
ctx.fillRect(-0.75, -0.4, 0.3, 0.8);
|
|
||||||
ctx.fillStyle = getSignalColor(signal, northWesterlyCon.branch.status);
|
|
||||||
ctx.fillRect(0.45, -0.4, 0.3, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSignalColor(signal, branchStatus) {
|
|
||||||
if (!signal.online) return 'rgb(0, 0, 255)';
|
|
||||||
if (branchStatus === "FREE") {
|
|
||||||
return 'rgb(0, 255, 0)';
|
|
||||||
} else if (branchStatus === "OCCUPIED") {
|
|
||||||
return 'rgb(255, 0, 0)';
|
|
||||||
} else {
|
|
||||||
return 'rgb(0, 0, 255)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws lines indicating reachable paths between this signal and others, with arrows for directionality.
|
|
||||||
function drawReachableConnections(ctx, signal) {
|
|
||||||
ctx.strokeStyle = 'black';
|
|
||||||
ctx.lineWidth = 0.25;
|
|
||||||
signal.branchConnections.forEach(connection => {
|
|
||||||
ctx.resetTransform();
|
|
||||||
connection.reachableSignalConnections.forEach(reachableCon => {
|
|
||||||
const dx = reachableCon.signalPosition.x - signal.position.x;
|
|
||||||
const dy = reachableCon.signalPosition.z - signal.position.z;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
let tx = worldTransform();
|
|
||||||
tx.translateSelf(signal.position.x, signal.position.z, 0);
|
|
||||||
const angle = Math.atan2(dy, dx) * 180 / Math.PI - 90;
|
|
||||||
tx.rotateSelf(0, 0, angle);
|
|
||||||
ctx.setTransform(tx);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, 0);
|
|
||||||
ctx.lineTo(0, dist);
|
|
||||||
const arrowEnd = 5;
|
|
||||||
const arrowWidth = 0.5;
|
|
||||||
const arrowLength = 1;
|
|
||||||
ctx.lineTo(0, arrowEnd);
|
|
||||||
ctx.lineTo(arrowWidth, arrowEnd - arrowLength);
|
|
||||||
ctx.lineTo(-arrowWidth, arrowEnd - arrowLength);
|
|
||||||
ctx.lineTo(0, arrowEnd);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRailSystemBoundingBox() {
|
|
||||||
let min = {x: Number.MAX_SAFE_INTEGER, z: Number.MAX_SAFE_INTEGER};
|
|
||||||
let max = {x: Number.MIN_SAFE_INTEGER, z: Number.MIN_SAFE_INTEGER};
|
|
||||||
railSystem.signals.forEach(signal => {
|
|
||||||
let p = signal.position;
|
|
||||||
if (p.x < min.x) min.x = p.x;
|
|
||||||
if (p.z < min.z) min.z = p.z;
|
|
||||||
if (p.x > max.x) max.x = p.x;
|
|
||||||
if (p.z > max.z) max.z = p.z;
|
|
||||||
});
|
|
||||||
return {x: min.x, y: min.z, width: Math.abs(max.x - min.x), height: Math.abs(max.z - min.z)};
|
|
||||||
}
|
|
|
@ -1,388 +0,0 @@
|
||||||
const $ = jQuery;
|
|
||||||
|
|
||||||
let railSystemSelect;
|
|
||||||
let railMapCanvas;
|
|
||||||
let railSystem = null;
|
|
||||||
let detailPanel = null;
|
|
||||||
|
|
||||||
let canvasTranslation = {x: 0, y: 0};
|
|
||||||
let canvasDragOrigin = null;
|
|
||||||
let canvasDragTranslation = null;
|
|
||||||
let hoveredElements = [];
|
|
||||||
|
|
||||||
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;
|
|
||||||
let canvasScaleIndex = SCALE_INDEX_NORMAL;
|
|
||||||
|
|
||||||
$(document).ready(() => {
|
|
||||||
railSystemSelect = $('#railSystemSelect');
|
|
||||||
railSystemSelect.change(railSystemChanged);
|
|
||||||
|
|
||||||
railMapCanvas = $('#railMapCanvas');
|
|
||||||
railMapCanvas.on('wheel', onCanvasMouseWheel);
|
|
||||||
railMapCanvas.mousedown(onCanvasMouseDown);
|
|
||||||
railMapCanvas.mouseup(onCanvasMouseUp);
|
|
||||||
railMapCanvas.mousemove(onCanvasMouseMove);
|
|
||||||
|
|
||||||
$('#addRailSystemInput').on("input", () => {
|
|
||||||
$('#addRailSystemButton').prop("disabled", $('#addRailSystemInput').val() === "");
|
|
||||||
});
|
|
||||||
$('#addRailSystemButton').click(addRailSystem);
|
|
||||||
$('#removeRailSystemButton').click(deleteRailSystem);
|
|
||||||
|
|
||||||
detailPanel = $('#railMapDetailPanel');
|
|
||||||
|
|
||||||
refreshRailSystems(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle mouse scrolling within the context of the canvas.
|
|
||||||
function onCanvasMouseWheel(event) {
|
|
||||||
let s = event.originalEvent.deltaY;
|
|
||||||
if (s > 0) {
|
|
||||||
canvasScaleIndex = Math.max(0, canvasScaleIndex - 1);
|
|
||||||
} else if (s < 0) {
|
|
||||||
canvasScaleIndex = Math.min(SCALE_VALUES.length - 1, canvasScaleIndex + 1);
|
|
||||||
}
|
|
||||||
drawRailSystem();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle mouse clicks on the canvas.
|
|
||||||
function onCanvasMouseDown(event) {
|
|
||||||
const p = getMousePoint(event);
|
|
||||||
canvasDragOrigin = {x: p.x, y: p.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle mouse release on the canvas, which stops dragging or indicates that the user may have clicked on something.
|
|
||||||
function onCanvasMouseUp(event) {
|
|
||||||
if (canvasDragTranslation !== null) {
|
|
||||||
canvasTranslation.x += canvasDragTranslation.x;
|
|
||||||
canvasTranslation.y += canvasDragTranslation.y;
|
|
||||||
} else {
|
|
||||||
const p = getMousePoint(event);
|
|
||||||
let signalClicked = false;
|
|
||||||
railSystem.signals.forEach(signal => {
|
|
||||||
const sp = new DOMPoint(signal.position.x, signal.position.z, 0, 1);
|
|
||||||
const canvasSp = worldPointToCanvas(sp);
|
|
||||||
const dist = Math.sqrt(Math.pow(p.x - canvasSp.x, 2) + Math.pow(p.y - canvasSp.y, 2));
|
|
||||||
if (dist < 5) {
|
|
||||||
console.log(signal);
|
|
||||||
onSignalSelected(signal);
|
|
||||||
signalClicked = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!signalClicked) {
|
|
||||||
onSignalSelected(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
canvasDragOrigin = null;
|
|
||||||
canvasDragTranslation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle mouse motion over the canvas. This is for dragging and hovering over items.
|
|
||||||
function onCanvasMouseMove(event) {
|
|
||||||
const rect = railMapCanvas[0].getBoundingClientRect();
|
|
||||||
const x = event.clientX - rect.left;
|
|
||||||
const y = event.clientY - rect.top;
|
|
||||||
if (canvasDragOrigin !== null) {
|
|
||||||
const scale = SCALE_VALUES[canvasScaleIndex];
|
|
||||||
const dx = x - canvasDragOrigin.x;
|
|
||||||
const dy = y - canvasDragOrigin.y;
|
|
||||||
canvasDragTranslation = {x: dx / scale, y: dy / scale};
|
|
||||||
drawRailSystem();
|
|
||||||
} else {
|
|
||||||
hoveredElements = [];
|
|
||||||
const p = getMousePoint(event);
|
|
||||||
railSystem.signals.forEach(signal => {
|
|
||||||
const sp = new DOMPoint(signal.position.x, signal.position.z, 0, 1);
|
|
||||||
const canvasSp = worldPointToCanvas(sp);
|
|
||||||
const dist = Math.sqrt(Math.pow(p.x - canvasSp.x, 2) + Math.pow(p.y - canvasSp.y, 2));
|
|
||||||
if (dist < 5) {
|
|
||||||
hoveredElements.push(signal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
drawRailSystem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMousePoint(event) {
|
|
||||||
const rect = railMapCanvas[0].getBoundingClientRect();
|
|
||||||
const x = event.clientX - rect.left;
|
|
||||||
const y = event.clientY - rect.top;
|
|
||||||
return new DOMPoint(x, y, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function railSystemChanged() {
|
|
||||||
detailPanel.empty();
|
|
||||||
railSystem = {};
|
|
||||||
railSystem.id = railSystemSelect.val();
|
|
||||||
$.get("/api/railSystems/" + railSystem.id + "/signals")
|
|
||||||
.done(signals => {
|
|
||||||
railSystem.signals = signals;
|
|
||||||
let bb = getRailSystemBoundingBox();
|
|
||||||
canvasTranslation.x = -1 * (bb.x + (bb.width / 2));
|
|
||||||
canvasTranslation.y = -1 * (bb.y + (bb.height / 2));
|
|
||||||
canvasScaleIndex = SCALE_INDEX_NORMAL;
|
|
||||||
drawRailSystem();
|
|
||||||
window.setInterval(railSystemUpdated, 1000);
|
|
||||||
});
|
|
||||||
$.get("/api/railSystems/" + railSystem.id + "/branches")
|
|
||||||
.done(branches => {
|
|
||||||
railSystem.branches = branches;
|
|
||||||
const branchSelects = $('.js_branch_list');
|
|
||||||
branchSelects.empty();
|
|
||||||
railSystem.branches.forEach(branch => {
|
|
||||||
branchSelects.append($('<option value="' + branch.name + '"></option>'))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the current rail system's information from the API.
|
|
||||||
function railSystemUpdated() {
|
|
||||||
$.get("/api/railSystems/" + railSystem.id + "/signals")
|
|
||||||
.done(signals => {
|
|
||||||
railSystem.signals = signals;
|
|
||||||
drawRailSystem();
|
|
||||||
});
|
|
||||||
$.get("/api/railSystems/" + railSystem.id + "/branches")
|
|
||||||
.done(branches => {
|
|
||||||
railSystem.branches = branches;
|
|
||||||
const branchSelects = $('.js_branch_list');
|
|
||||||
branchSelects.empty();
|
|
||||||
railSystem.branches.forEach(branch => {
|
|
||||||
branchSelects.append($('<option value="' + branch.name + '"></option>'))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectSignalById(id) {
|
|
||||||
railSystem.signals.forEach(signal => {
|
|
||||||
if (signal.id === id) {
|
|
||||||
onSignalSelected(signal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSignalSelected(signal) {
|
|
||||||
detailPanel.empty();
|
|
||||||
if (signal !== null) {
|
|
||||||
const tpl = Handlebars.compile($('#signalTemplate').html());
|
|
||||||
detailPanel.html(tpl(signal));
|
|
||||||
signal.branchConnections.forEach(con => {
|
|
||||||
const select = $('#signalPotentialConnectionsSelect-' + con.id);
|
|
||||||
$.get("/api/railSystems/" + railSystem.id + "/branches/" + con.branch.id + "/signals")
|
|
||||||
.done(signals => {
|
|
||||||
signals = signals.filter(s => s.id !== signal.id);
|
|
||||||
let connections = [];
|
|
||||||
signals.forEach(s => {
|
|
||||||
s.branchConnections
|
|
||||||
.filter(c => c.branch.id === con.branch.id && !con.reachableSignalConnections.some(rc => rc.connectionId === c.id))
|
|
||||||
.forEach(potentialConnection => {
|
|
||||||
potentialConnection.signalName = s.name;
|
|
||||||
potentialConnection.signalId = s.id;
|
|
||||||
connections.push(potentialConnection);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
select.empty();
|
|
||||||
const row = $('#signalPotentialConnectionsRow-' + con.id);
|
|
||||||
row.toggle(connections.length > 0);
|
|
||||||
connections.forEach(c => {
|
|
||||||
select.append($(`<option value="${c.id}">${c.signalName} via ${c.direction} connection ${c.id}</option>`))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRailSystem() {
|
|
||||||
let name = $('#addRailSystemInput').val().trim();
|
|
||||||
$.post({
|
|
||||||
url: "/api/railSystems",
|
|
||||||
data: JSON.stringify({name: name}),
|
|
||||||
contentType: "application/json"
|
|
||||||
})
|
|
||||||
.done((response) => {
|
|
||||||
refreshRailSystems();
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
$('#addRailSystemInput').val("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteRailSystem() {
|
|
||||||
if (railSystem !== null && railSystem.id) {
|
|
||||||
confirm("Are you sure you want to permanently remove rail system " + railSystem.id + "?")
|
|
||||||
.then(() => {
|
|
||||||
$.ajax({
|
|
||||||
url: "/api/railSystems/" + railSystem.id,
|
|
||||||
type: "DELETE"
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
refreshRailSystems(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshRailSystems(selectFirst) {
|
|
||||||
$.get("/api/railSystems")
|
|
||||||
.done(railSystems => {
|
|
||||||
railSystemSelect.empty();
|
|
||||||
railSystems.forEach(railSystem => {
|
|
||||||
let option = $(`<option value="${railSystem.id}">${railSystem.name} - ID: ${railSystem.id}</option>`)
|
|
||||||
railSystemSelect.append(option);
|
|
||||||
});
|
|
||||||
if (selectFirst) {
|
|
||||||
railSystemSelect.val(railSystems[0].id);
|
|
||||||
railSystemSelect.change();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewSignal() {
|
|
||||||
const modalElement = $('#addSignalModal');
|
|
||||||
const form = $('#addSignalForm');
|
|
||||||
form.validate();
|
|
||||||
if (!form.valid()) return;
|
|
||||||
const data = {
|
|
||||||
name: $('#addSignalName').val().trim(),
|
|
||||||
position: {
|
|
||||||
x: $('#addSignalPositionX').val(),
|
|
||||||
y: $('#addSignalPositionY').val(),
|
|
||||||
z: $('#addSignalPositionZ').val()
|
|
||||||
},
|
|
||||||
branchConnections: [
|
|
||||||
{
|
|
||||||
direction: $('#addSignalFirstConnectionDirection').val(),
|
|
||||||
name: $('#addSignalFirstConnectionBranch').val()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
direction: $('#addSignalSecondConnectionDirection').val(),
|
|
||||||
name: $('#addSignalSecondConnectionBranch').val()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
const modal = bootstrap.Modal.getInstance(modalElement[0]);
|
|
||||||
modal.hide();
|
|
||||||
modalElement.on("hidden.bs.modal", () => {
|
|
||||||
confirm("Are you sure you want to add this new signal to the system?")
|
|
||||||
.then(() => {
|
|
||||||
$.post({
|
|
||||||
url: "/api/railSystems/" + railSystem.id + "/signals",
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
contentType: "application/json"
|
|
||||||
})
|
|
||||||
.done(() => {
|
|
||||||
form.trigger("reset");
|
|
||||||
railSystemUpdated();
|
|
||||||
})
|
|
||||||
.fail((response) => {
|
|
||||||
console.error(response);
|
|
||||||
$('#addSignalAlertsContainer').append($('<div class="alert alert-danger">An error occurred.</div>'));
|
|
||||||
modal.show();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
form.trigger("reset");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSignal(signalId) {
|
|
||||||
confirm(`Are you sure you want to remove signal ${signalId}? This cannot be undone.`)
|
|
||||||
.then(() => {
|
|
||||||
$.ajax({
|
|
||||||
url: `/api/railSystems/${railSystem.id}/signals/${signalId}`,
|
|
||||||
type: "DELETE"
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
railSystemUpdated();
|
|
||||||
})
|
|
||||||
.fail((response) => {
|
|
||||||
console.error(response);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeReachableConnection(signalId, fromId, toId) {
|
|
||||||
confirm(`Are you sure you want to remove the connection from ${fromId} to ${toId} from signal ${signalId}?`)
|
|
||||||
.then(() => {
|
|
||||||
$.get(`/api/railSystems/${railSystem.id}/signals/${signalId}`)
|
|
||||||
.done(signal => {
|
|
||||||
let connections = [];
|
|
||||||
signal.branchConnections.forEach(con => {
|
|
||||||
con.reachableSignalConnections.forEach(reachableCon => {
|
|
||||||
connections.push({from: con.id, to: reachableCon.connectionId});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
connections = connections.filter(c => !(c.from === fromId && c.to === toId));
|
|
||||||
$.post({
|
|
||||||
url: `/api/railSystems/${railSystem.id}/signals/${signal.id}/signalConnections`,
|
|
||||||
data: JSON.stringify({connections: connections}),
|
|
||||||
contentType: "application/json"
|
|
||||||
})
|
|
||||||
.done(() => {
|
|
||||||
railSystemUpdated();
|
|
||||||
})
|
|
||||||
.fail((response) => {
|
|
||||||
console.error(response);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addReachableConnectionBtn(signalId, fromId) {
|
|
||||||
const select = $('#signalPotentialConnectionsSelect-' + fromId);
|
|
||||||
const toId = select.val();
|
|
||||||
if (toId) {
|
|
||||||
addReachableConnection(signalId, fromId, toId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addReachableConnection(signalId, fromId, toId) {
|
|
||||||
confirm(`Are you sure you want to add a connection from ${fromId} to ${toId} from signal ${signalId}?`)
|
|
||||||
.then(() => {
|
|
||||||
$.get(`/api/railSystems/${railSystem.id}/signals/${signalId}`)
|
|
||||||
.done(signal => {
|
|
||||||
let connections = [];
|
|
||||||
signal.branchConnections.forEach(con => {
|
|
||||||
con.reachableSignalConnections.forEach(reachableCon => {
|
|
||||||
connections.push({from: con.id, to: reachableCon.connectionId});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (!connections.find(c => c.from === fromId && c.to === toId)) {
|
|
||||||
connections.push({from: fromId, to: toId});
|
|
||||||
$.post({
|
|
||||||
url: `/api/railSystems/${railSystem.id}/signals/${signal.id}/signalConnections`,
|
|
||||||
data: JSON.stringify({connections: connections}),
|
|
||||||
contentType: "application/json"
|
|
||||||
})
|
|
||||||
.done(() => {
|
|
||||||
railSystemChanged();
|
|
||||||
})
|
|
||||||
.fail((response) => {
|
|
||||||
console.error(response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirm(message) {
|
|
||||||
const modalElement = $('#confirmModal');
|
|
||||||
if (message) {
|
|
||||||
$('#confirmModalBody').html(message);
|
|
||||||
} else {
|
|
||||||
$('#confirmModalBody').html("Are you sure you want to continue?");
|
|
||||||
}
|
|
||||||
const modal = new bootstrap.Modal(modalElement[0], {keyboard: false});
|
|
||||||
modal.show();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
$('#confirmModalOkButton').click(() => {
|
|
||||||
modalElement.on("hidden.bs.modal", () => resolve());
|
|
||||||
});
|
|
||||||
$('#confirmModalCancelButton').click(() => {
|
|
||||||
modalElement.on("hidden.bs.modal", () => reject());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,209 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>RailSignal</title>
|
|
||||||
|
|
||||||
<link href="/static/style/main.css" rel="stylesheet"/>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<h1>RailSignal</h1>
|
|
||||||
<p class="lead">Stay in control of your rails.</p>
|
|
||||||
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-auto">
|
|
||||||
<label for="railSystemSelect">Select a rail system:</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<select id="railSystemSelect" class="form-select">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" placeholder="Or add a new system" id="addRailSystemInput"/>
|
|
||||||
<button class="btn btn-success" type="button" id="addRailSystemButton" disabled>Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<button class="btn btn-danger" type="button" id="removeRailSystemButton">Remove Selected System</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSignalModal">Add Signal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-4"/>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div id="railMapCanvasParent" class="col">
|
|
||||||
<canvas id="railMapCanvas" width="800" height="500" class="border"></canvas>
|
|
||||||
</div>
|
|
||||||
<div id="railMapDetailPanel" class="col p-2 border">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<script id="signalTemplate" type="text/x-handlebars-template">
|
|
||||||
<h3>{{name}}</h3>
|
|
||||||
<hr>
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-sm-3">ID</dt>
|
|
||||||
<dd class="col-sm-9">{{id}}</dd>
|
|
||||||
<dt class="col-sm-3">Position</dt>
|
|
||||||
<dd class="col-sm-9">{{position.x}}, {{position.y}}, {{position.z}}</dd>
|
|
||||||
<dt class="col-sm-3">Online</dt>
|
|
||||||
<dd class="col-sm-9">{{online}}</dd>
|
|
||||||
</dl>
|
|
||||||
<h4>Connections</h4>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{{#each branchConnections}}
|
|
||||||
<h5>{{this.direction}} <small class="text-muted">ID: {{this.id}}</small></h5>
|
|
||||||
<p>
|
|
||||||
On branch <span class="badge bg-dark">{{this.branch.name}} <small class="text-muted">ID: {{this.branch.id}}</small></span>
|
|
||||||
</p>
|
|
||||||
<h6>Connected to Signals:</h6>
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
{{#each this.reachableSignalConnections}}
|
|
||||||
<li>
|
|
||||||
<a onclick="selectSignalById({{this.signalId}})" href="#">{{this.signalName}}</a>
|
|
||||||
via {{this.direction}} <small class="text-muted">ID: {{this.connectionId}}</small>
|
|
||||||
<span
|
|
||||||
class="btn btn-sm btn-secondary js_removeSignalConnection"
|
|
||||||
onclick="removeReachableConnection({{../../id}}, {{../id}}, {{this.connectionId}})"
|
|
||||||
>Remove</span>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
<div class="row" id="signalPotentialConnectionsRow-{{this.id}}">
|
|
||||||
<div class="col-auto">
|
|
||||||
<select id="signalPotentialConnectionsSelect-{{this.id}}" class="form-select form-select-sm">
|
|
||||||
{{#each this.potentialSignalConnections}}
|
|
||||||
<option value="{{this.connectionId}}">{{this.signalName}} via {{this.branchName}} on {{this.direction}} connection</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" onclick="addReachableConnectionBtn({{../id}}, {{this.id}})">Add Connection</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="removeSignal({{id}})">Remove this Signal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="modal fade" tabindex="-1" id="confirmModal" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 id="confirmModalHeader">Confirm</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p id="confirmModalBody">Are you sure you want to continue?</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="confirmModalCancelButton">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="confirmModalOkButton">Ok</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal fade" tabindex="-1" id="addSignalModal" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Add Signal</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Fill in the following data to add a new signal to the system.</p>
|
|
||||||
<form id="addSignalForm">
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<input id="addSignalName" type="text" class="form-control" placeholder="Name" aria-label="Name" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<h6>Position</h6>
|
|
||||||
<div class="col">
|
|
||||||
<input id="addSignalPositionX" class="form-control" placeholder="X" aria-label="X" type="number" step="0.1" required>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<input id="addSignalPositionY" class="form-control" placeholder="Y" aria-label="Y" type="number" step="0.1" required>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<input id="addSignalPositionZ" class="form-control" placeholder="Z" aria-label="Z" type="number" step="0.1" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h6>Connections</h6>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<select id="addSignalFirstConnectionDirection" class="form-select" required>
|
|
||||||
<option value="N">North</option>
|
|
||||||
<option value="S">South</option>
|
|
||||||
<option value="E">East</option>
|
|
||||||
<option value="W">West</option>
|
|
||||||
<option value="NW">NorthWest</option>
|
|
||||||
<option value="NE">NorthEast</option>
|
|
||||||
<option value="SW">SouthWest</option>
|
|
||||||
<option value="SE">SouthEast</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<input id="addSignalFirstConnectionBranch" type="text" class="form-control" placeholder="Branch Name" list="addSignalFirstConnectionBranchList" required>
|
|
||||||
<datalist id="addSignalFirstConnectionBranchList" class="js_branch_list">
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<select id="addSignalSecondConnectionDirection" class="form-select" required>
|
|
||||||
<option value="N">North</option>
|
|
||||||
<option value="S">South</option>
|
|
||||||
<option value="E">East</option>
|
|
||||||
<option value="W">West</option>
|
|
||||||
<option value="NW">NorthWest</option>
|
|
||||||
<option value="NE">NorthEast</option>
|
|
||||||
<option value="SW">SouthWest</option>
|
|
||||||
<option value="SE">SouthEast</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<input id="addSignalSecondConnectionBranch" type="text" class="form-control" placeholder="Branch Name" list="addSignalSecondConnectionBranchList" required>
|
|
||||||
<datalist id="addSignalSecondConnectionBranchList" class="js_branch_list">
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="addSignalAlertsContainer"></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" onclick="addNewSignal()">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.19.3/dist/jquery.validate.min.js" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.min.js" integrity="sha512-RNLkV3d+aLtfcpEyFG8jRbnWHxUqVZozacROI4J2F1sTaDqo1dPQYs01OMi1t1w9Y2FdbSCDSQ2ZVdAC8bzgAg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
||||||
|
|
||||||
<script src="/static/js/drawing.js"></script>
|
|
||||||
<script src="/static/js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue