added stuff
This commit is contained in:
parent
2fbc22af0d
commit
e608e2ba8c
1
pom.xml
1
pom.xml
|
@ -40,6 +40,7 @@
|
|||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.1.212</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=http://localhost:8080/api
|
|
@ -0,0 +1,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.component.Component;
|
||||
import nl.andrewl.railsignalapi.model.component.Position;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ComponentRepository 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);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,16 @@ import nl.andrewl.railsignalapi.model.Segment;
|
|||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SegmentRepository extends JpaRepository<Segment, Long> {
|
||||
boolean existsByNameAndRailSystem(String name, RailSystem rs);
|
||||
|
||||
List<Segment> findAllByRailSystemId(long rsId);
|
||||
|
||||
Optional<Segment> findByIdAndRailSystemId(long id, long rsId);
|
||||
|
||||
void deleteAllByRailSystem(RailSystem rs);
|
||||
}
|
||||
|
|
|
@ -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 javax.persistence.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -27,7 +28,7 @@ public class Segment {
|
|||
/**
|
||||
* A unique name for this segment.
|
||||
*/
|
||||
@Column(unique = true)
|
||||
@Column
|
||||
private String name;
|
||||
|
||||
/**
|
||||
|
@ -39,9 +40,16 @@ public class 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;
|
||||
|
||||
public Segment(RailSystem railSystem, String name) {
|
||||
this.railSystem = railSystem;
|
||||
this.name = name;
|
||||
this.signals = new HashSet<>();
|
||||
this.boundaryNodes = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) return true;
|
||||
|
|
|
@ -37,9 +37,15 @@ public abstract class Component {
|
|||
/**
|
||||
* A human-readable name for the component.
|
||||
*/
|
||||
@Column(unique = true)
|
||||
@Column
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The type of this component.
|
||||
*/
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
private ComponentType type;
|
||||
|
||||
/**
|
||||
* Whether this component is online, meaning that an in-world device is
|
||||
* currently connected to relay information regarding this component.
|
||||
|
@ -48,10 +54,11 @@ public abstract class Component {
|
|||
@Setter
|
||||
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.position = position;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,4 +66,13 @@ public abstract class Component {
|
|||
if (this == o) return true;
|
||||
return o instanceof Component c && this.id != null && this.id.equals(c.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(id);
|
||||
if (name != null) sb.append('[').append(name).append(']');
|
||||
sb.append(String.format("@[x=%.1f,y=%.1f,z=%.1f]", position.getX(), position.getY(), position.getZ()));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
public enum ComponentType {
|
||||
SIGNAL,
|
||||
SWITCH,
|
||||
SEGMENT_BOUNDARY
|
||||
}
|
|
@ -19,15 +19,15 @@ import java.util.Set;
|
|||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class PathNode extends Component {
|
||||
public abstract class PathNode extends Component {
|
||||
/**
|
||||
* The set of nodes that this one is connected to.
|
||||
*/
|
||||
@ManyToMany
|
||||
private Set<PathNode> connectedNodes;
|
||||
|
||||
public PathNode(RailSystem railSystem, Position position, String name, Set<PathNode> connectedNodes) {
|
||||
super(railSystem, position, name);
|
||||
public PathNode(RailSystem railSystem, Position position, String name, ComponentType type, Set<PathNode> connectedNodes) {
|
||||
super(railSystem, position, name, type);
|
||||
this.connectedNodes = connectedNodes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ public class SegmentBoundaryNode extends PathNode {
|
|||
private 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package nl.andrewl.railsignalapi.model.component;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.railsignalapi.model.Direction;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
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
|
||||
|
@ -15,29 +17,16 @@ import javax.persistence.*;
|
|||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
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.
|
||||
*/
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
private Segment segment;
|
||||
|
||||
public Signal(RailSystem railSystem, Position position, String name, Segment segment, Direction direction) {
|
||||
super(railSystem, position, name);
|
||||
public Signal(RailSystem railSystem, Position position, String name, Segment segment) {
|
||||
super(railSystem, position, name, ComponentType.SIGNAL);
|
||||
this.segment = segment;
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package nl.andrewl.railsignalapi.model.component;
|
|||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Set;
|
||||
|
@ -22,8 +24,16 @@ public class Switch extends PathNode {
|
|||
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;
|
||||
|
||||
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;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
@ -10,7 +12,8 @@ import java.util.Set;
|
|||
* as an active configuration in the linked switch component.
|
||||
*/
|
||||
@Entity
|
||||
@NoArgsConstructor
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@Getter
|
||||
public class SwitchConfiguration {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
|
@ -28,4 +31,17 @@ public class SwitchConfiguration {
|
|||
*/
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
private Set<PathNode> nodes;
|
||||
|
||||
public SwitchConfiguration(Switch switchComponent, Set<PathNode> nodes) {
|
||||
this.switchComponent = switchComponent;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) return true;
|
||||
return o instanceof SwitchConfiguration sc &&
|
||||
sc.switchComponent.equals(this.switchComponent) &&
|
||||
sc.nodes.equals(this.nodes);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
@ -13,4 +14,13 @@ public class WebConfig implements WebMvcConfigurer {
|
|||
registry.addResourceHandler("/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 {
|
||||
private final RailSystemRepository railSystemRepository;
|
||||
private final SegmentRepository segmentRepository;
|
||||
private final ComponentRepository componentRepository;
|
||||
private final ComponentRepository<?> componentRepository;
|
||||
|
||||
@Transactional
|
||||
public List<RailSystemResponse> getRailSystems() {
|
||||
|
@ -29,10 +29,13 @@ public class RailSystemService {
|
|||
|
||||
@Transactional
|
||||
public RailSystemResponse createRailSystem(RailSystemCreationPayload payload) {
|
||||
if (railSystemRepository.existsByName(payload.name())) {
|
||||
if (payload.name() == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required name.");
|
||||
if (payload.name().isBlank()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Name cannot be blank.");
|
||||
String name = payload.name().trim();
|
||||
if (railSystemRepository.existsByName(name)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "A rail system with that name already exists.");
|
||||
}
|
||||
RailSystem rs = new RailSystem(payload.name());
|
||||
RailSystem rs = new RailSystem(name);
|
||||
return new RailSystemResponse(railSystemRepository.save(rs));
|
||||
}
|
||||
|
||||
|
|
|
@ -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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nl.andrewl.railsignalapi.service.SignalService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
|
@ -18,7 +17,6 @@ import java.util.Set;
|
|||
@Slf4j
|
||||
public class SignalWebSocketHandler extends TextWebSocketHandler {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final SignalService signalService;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
|
@ -31,19 +29,19 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
|||
for (var idStr : signalIdHeader.split(",")) {
|
||||
ids.add(Long.parseLong(idStr.trim()));
|
||||
}
|
||||
signalService.registerSignalWebSocketSession(ids, session);
|
||||
//signalService.registerSignalWebSocketSession(ids, session);
|
||||
log.info("Connection established with signals {}.", ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
|
||||
signalService.handleSignalUpdate(msg);
|
||||
//signalService.handleSignalUpdate(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
signalService.deregisterSignalWebSocketSession(session);
|
||||
//signalService.deregisterSignalWebSocketSession(session);
|
||||
log.info("Closed connection {}. Status: {}", session.getId(), status.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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