Added single page for signal system.

This commit is contained in:
Andrew Lalis 2021-11-23 20:53:44 +01:00
parent cbbf74ee4a
commit 6edb2e4912
12 changed files with 366 additions and 6 deletions

View File

@ -25,6 +25,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>

View File

@ -24,13 +24,17 @@ public class Signal {
@OneToMany(mappedBy = "signal", orphanRemoval = true, cascade = CascadeType.ALL)
private Set<SignalBranchConnection> branchConnections;
@Embedded
private Position position;
@Column(nullable = false)
@Setter
private boolean online = false;
public Signal(RailSystem railSystem, String name, Set<SignalBranchConnection> branchConnections) {
public Signal(RailSystem railSystem, String name, Position position, Set<SignalBranchConnection> branchConnections) {
this.railSystem = railSystem;
this.name = name;
this.position = position;
this.branchConnections = branchConnections;
}
}

View File

@ -4,6 +4,8 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@NoArgsConstructor
@ -22,9 +24,13 @@ public class SignalBranchConnection {
@Enumerated(EnumType.STRING)
private Direction direction;
@ManyToMany
private Set<SignalBranchConnection> reachableSignalConnections;
public SignalBranchConnection(Signal signal, Branch branch, Direction direction) {
this.signal = signal;
this.branch = branch;
this.direction = direction;
reachableSignalConnections = new HashSet<>();
}
}

View File

@ -0,0 +1,14 @@
package nl.andrewl.railsignalapi.page;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(path = "/")
public class IndexPageController {
@GetMapping
public String getIndex() {
return "index";
}
}

View File

@ -2,10 +2,15 @@ package nl.andrewl.railsignalapi.rest;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
}

View File

@ -1,9 +1,12 @@
package nl.andrewl.railsignalapi.rest.dto;
import nl.andrewl.railsignalapi.model.Position;
import java.util.List;
public record SignalCreationPayload(
String name,
Position position,
List<BranchData> branchConnections
) {
public static record BranchData(String direction, String name, Long id) {}

View File

@ -1,5 +1,6 @@
package nl.andrewl.railsignalapi.rest.dto;
import nl.andrewl.railsignalapi.model.Position;
import nl.andrewl.railsignalapi.model.Signal;
import nl.andrewl.railsignalapi.model.SignalBranchConnection;
@ -9,6 +10,7 @@ import java.util.List;
public record SignalResponse(
long id,
String name,
Position position,
List<ConnectionData> branchConnections,
boolean online
) {
@ -16,6 +18,7 @@ public record SignalResponse(
this(
signal.getId(),
signal.getName(),
signal.getPosition(),
signal.getBranchConnections().stream()
.sorted(Comparator.comparing(SignalBranchConnection::getDirection))
.map(ConnectionData::new)
@ -25,11 +28,32 @@ public record SignalResponse(
}
public static record ConnectionData(
long id,
String direction,
BranchResponse branch
BranchResponse branch,
List<ReachableConnectionData> reachableSignalConnections
) {
public ConnectionData(SignalBranchConnection c) {
this(c.getDirection().name(), new BranchResponse(c.getBranch()));
this(
c.getId(),
c.getDirection().name(),
new BranchResponse(c.getBranch()),
c.getReachableSignalConnections().stream()
.map(cc -> new ReachableConnectionData(
cc.getId(),
cc.getSignal().getId(),
cc.getSignal().getName(),
cc.getSignal().getPosition()
))
.toList()
);
}
public static record ReachableConnectionData(
long connectionId,
long signalId,
String signalName,
Position signalPosition
) {}
}
}

View File

@ -44,7 +44,7 @@ public class SignalService {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Signal " + payload.name() + " already exists.");
}
Set<SignalBranchConnection> branchConnections = new HashSet<>();
Signal signal = new Signal(rs, payload.name(), branchConnections);
Signal signal = new Signal(rs, payload.name(), payload.position(), branchConnections);
for (var branchData : payload.branchConnections()) {
Branch branch;
if (branchData.id() != null) {
@ -61,7 +61,7 @@ public class SignalService {
return new SignalResponse(signal);
}
@Transactional(readOnly = true)
@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.

View File

@ -0,0 +1,124 @@
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 mousePointToWorld(event) {
const rect = railMapCanvas[0].getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return worldTransform().invertSelf().transformPoint(new DOMPoint(x, y, 0, 1));
}
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 === undefined || direction === null || direction === "") {
direction = "NORTH";
}
let angle = 0;
if (direction === "NORTH" || direction === "SOUTH") {
angle = 90;
} else if (direction === "NORTH_WEST" || direction === "SOUTH_EAST") {
angle = 45;
} else if (direction === "NORTH_EAST" || direction === "SOUTH_WEST") {
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 => {
console.log('printing!')
console.log(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 firstCon = signal.branchConnections[0];
let secondCon = signal.branchConnections[1];
if (firstCon === "EAST" || firstCon === "SOUTH" || firstCon === "SOUTH_WEST" || firstCon === "SOUTH_EAST") {
let tmp = firstCon;
firstCon = secondCon;
secondCon = tmp;
}
ctx.fillStyle = getSignalColor(signal, firstCon.branch.status);
ctx.fillRect(-0.75, -0.4, 0.3, 0.8);
ctx.fillStyle = getSignalColor(signal, secondCon.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)';
}
}
function drawReachableConnections(ctx, signal) {
ctx.strokeStyle = 'black';
ctx.lineWidth = 0.25;
signal.branchConnections.forEach(connection => {
connection.reachableSignalConnections.forEach(reachableCon => {
ctx.beginPath();
ctx.moveTo(signal.position.x, signal.position.z);
ctx.lineTo(reachableCon.signalPosition.x, reachableCon.signalPosition.z);
ctx.stroke();
});
});
}
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)};
}

View File

@ -0,0 +1,115 @@
const $ = jQuery;
let railSystemSelect;
let railMapCanvas;
let railSystem = null;
let canvasTranslation = {x: 0, y: 0};
let canvasDragOrigin = null;
let canvasDragTranslation = null;
let hoveredElements = [];
const SCALE_VALUES = [0.01, 0.1, 1.0, 1.25, 1.5, 2.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 = 5;
let canvasScaleIndex = SCALE_INDEX_NORMAL;
$(document).ready(() => {
railSystemSelect = $('#railSystemSelect');
railSystemSelect.change(railSystemChanged);
railMapCanvas = $('#railMapCanvas');
railMapCanvas.on('wheel', onMouseWheel);
railMapCanvas.mousedown(onMouseDown);
railMapCanvas.mouseup(onMouseUp);
railMapCanvas.mousemove(onMouseMove);
$.get("/api/railSystems")
.done(railSystems => {
railSystems.forEach(railSystem => {
let option = $('<option value="' + railSystem.id + '">' + railSystem.name + '</option>')
railSystemSelect.append(option);
});
railSystemSelect.val(railSystems[0].id);
railSystemSelect.change();
});
});
function onMouseWheel(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();
}
function onMouseDown(event) {
const rect = railMapCanvas[0].getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
canvasDragOrigin = {x: x, y: y};
}
function onMouseUp(event) {
if (canvasDragTranslation !== null) {
canvasTranslation.x += canvasDragTranslation.x;
canvasTranslation.y += canvasDragTranslation.y;
} else {
const p = mousePointToWorld(event);
railSystem.signals.forEach(signal => {
const sp = new DOMPoint(signal.position.x, signal.position.z, 0, 1);
const dist = Math.sqrt(Math.pow(p.x - sp.x, 2) + Math.pow(p.y - sp.y, 2));
if (dist < 1) {
console.log(signal);
$('#testingText').val(JSON.stringify(signal, null, 2));
}
});
}
canvasDragOrigin = null;
canvasDragTranslation = null;
}
function onMouseMove(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 = mousePointToWorld(event);
railSystem.signals.forEach(signal => {
const sp = new DOMPoint(signal.position.x, signal.position.z, 0, 1);
const dist = Math.sqrt(Math.pow(p.x - sp.x, 2) + Math.pow(p.y - sp.y, 2));
if (dist < 1) {
hoveredElements.push(signal);
}
});
drawRailSystem();
}
}
function railSystemChanged() {
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.setTimeout(railSystemChanged, 1000);
});
$.get("/api/railSystems/" + railSystem.id + "/branches")
.done(branches => {
railSystem.branches = branches;
});
}

View File

View File

@ -0,0 +1,57 @@
<!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" disabled>Remove Selected System</button>
</div>
</div>
<hr class="my-4"/>
<div class="row">
<div id="railMapCanvasParent" class="col border p-0">
<canvas id="railMapCanvas" width="1000" height="500"></canvas>
</div>
</div>
<div class="row">
<textarea id="testingText"></textarea>
</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/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="/static/js/drawing.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>