Added improved rendering and signal detail panel.

This commit is contained in:
Andrew Lalis 2021-11-24 12:01:36 +01:00
parent 6edb2e4912
commit 4546993f0f
13 changed files with 295 additions and 54 deletions

View File

@ -0,0 +1,9 @@
package nl.andrewl.railsignalapi.dao;
import nl.andrewl.railsignalapi.model.SignalBranchConnection;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SignalBranchConnectionRepository extends JpaRepository<SignalBranchConnection, Long> {
}

View File

@ -18,7 +18,8 @@ public interface SignalRepository extends JpaRepository<Signal, Long> {
@Query("SELECT DISTINCT s FROM Signal s " + @Query("SELECT DISTINCT s FROM Signal s " +
"LEFT JOIN s.branchConnections bc " + "LEFT JOIN s.branchConnections bc " +
"WHERE bc.branch = :branch") "WHERE bc.branch = :branch " +
"ORDER BY s.name")
List<Signal> findAllConnectedToBranch(Branch branch); List<Signal> findAllConnectedToBranch(Branch branch);
List<Signal> findAllByRailSystemOrderByName(RailSystem railSystem); List<Signal> findAllByRailSystemOrderByName(RailSystem railSystem);

View File

@ -8,5 +8,41 @@ public enum Direction {
NORTH_WEST, NORTH_WEST,
NORTH_EAST, NORTH_EAST,
SOUTH_WEST, SOUTH_WEST,
SOUTH_EAST SOUTH_EAST;
public Direction opposite() {
return switch (this) {
case NORTH -> SOUTH;
case SOUTH -> NORTH;
case EAST -> WEST;
case WEST -> EAST;
case NORTH_EAST -> SOUTH_WEST;
case NORTH_WEST -> SOUTH_EAST;
case SOUTH_EAST -> NORTH_WEST;
case SOUTH_WEST -> NORTH_EAST;
};
}
public boolean isOpposite(Direction other) {
return this.opposite().equals(other);
}
public static Direction parse(String s) {
s = s.trim().toUpperCase();
try {
return Direction.valueOf(s);
} catch (IllegalArgumentException e) {
return switch (s) {
case "N" -> NORTH;
case "S" -> SOUTH;
case "E" -> EAST;
case "W" -> WEST;
case "NW" -> NORTH_WEST;
case "NE" -> NORTH_EAST;
case "SW" -> SOUTH_WEST;
case "SE" -> SOUTH_EAST;
default -> throw new IllegalArgumentException("Invalid direction: " + s);
};
}
}
} }

View File

@ -5,12 +5,13 @@ import lombok.NoArgsConstructor;
import javax.persistence.*; import javax.persistence.*;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects;
import java.util.Set; import java.util.Set;
@Entity @Entity
@NoArgsConstructor @NoArgsConstructor
@Getter @Getter
public class SignalBranchConnection { public class SignalBranchConnection implements Comparable<SignalBranchConnection> {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@ -33,4 +34,24 @@ public class SignalBranchConnection {
this.direction = direction; this.direction = direction;
reachableSignalConnections = new HashSet<>(); reachableSignalConnections = new HashSet<>();
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
return this.id != null &&
o instanceof SignalBranchConnection sbc && sbc.getId() != null &&
this.id.equals(sbc.getId());
}
@Override
public int hashCode() {
return Objects.hashCode(id);
}
@Override
public int compareTo(SignalBranchConnection o) {
int c = Long.compare(this.getSignal().getId(), o.getSignal().getId());
if (c != 0) return c;
return this.direction.compareTo(o.getDirection());
}
} }

View File

@ -2,6 +2,7 @@ package nl.andrewl.railsignalapi.rest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.rest.dto.BranchResponse; import nl.andrewl.railsignalapi.rest.dto.BranchResponse;
import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
import nl.andrewl.railsignalapi.service.BranchService; import nl.andrewl.railsignalapi.service.BranchService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -19,6 +20,16 @@ public class BranchesController {
return branchService.getAllBranches(rsId); return branchService.getAllBranches(rsId);
} }
@GetMapping(path = "/{branchId}")
public BranchResponse getBranch(@PathVariable long rsId, @PathVariable long branchId) {
return branchService.getBranch(rsId, branchId);
}
@GetMapping(path = "/{branchId}/signals")
public List<SignalResponse> getBranchSignals(@PathVariable long rsId, @PathVariable long branchId) {
return branchService.getConnectedSignals(rsId, branchId);
}
@DeleteMapping(path = "/{branchId}") @DeleteMapping(path = "/{branchId}")
public ResponseEntity<?> deleteBranch(@PathVariable long rsId, @PathVariable long branchId) { public ResponseEntity<?> deleteBranch(@PathVariable long rsId, @PathVariable long branchId) {
branchService.deleteBranch(rsId, branchId); branchService.deleteBranch(rsId, branchId);

View File

@ -1,6 +1,7 @@
package nl.andrewl.railsignalapi.rest; package nl.andrewl.railsignalapi.rest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.rest.dto.SignalConnectionsUpdatePayload;
import nl.andrewl.railsignalapi.rest.dto.SignalCreationPayload; import nl.andrewl.railsignalapi.rest.dto.SignalCreationPayload;
import nl.andrewl.railsignalapi.rest.dto.SignalResponse; import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
import nl.andrewl.railsignalapi.service.SignalService; import nl.andrewl.railsignalapi.service.SignalService;
@ -30,6 +31,11 @@ public class SignalsApiController {
return signalService.getSignal(rsId, sigId); return signalService.getSignal(rsId, sigId);
} }
@PostMapping(path = "/{sigId}/signalConnections")
public SignalResponse updateSignalConnections(@PathVariable long rsId, @PathVariable long sigId, @RequestBody SignalConnectionsUpdatePayload payload) {
return signalService.updateSignalBranchConnections(rsId, sigId, payload);
}
@DeleteMapping(path = "/{sigId}") @DeleteMapping(path = "/{sigId}")
public ResponseEntity<?> deleteSignal(@PathVariable long rsId, @PathVariable long sigId) { public ResponseEntity<?> deleteSignal(@PathVariable long rsId, @PathVariable long sigId) {
signalService.deleteSignal(rsId, sigId); signalService.deleteSignal(rsId, sigId);

View File

@ -0,0 +1,9 @@
package nl.andrewl.railsignalapi.rest.dto;
import java.util.List;
public record SignalConnectionsUpdatePayload(
List<ConnectionData> connections
) {
public static record ConnectionData(long from, long to) {}
}

View File

@ -41,6 +41,7 @@ public record SignalResponse(
c.getReachableSignalConnections().stream() c.getReachableSignalConnections().stream()
.map(cc -> new ReachableConnectionData( .map(cc -> new ReachableConnectionData(
cc.getId(), cc.getId(),
cc.getDirection().name(),
cc.getSignal().getId(), cc.getSignal().getId(),
cc.getSignal().getName(), cc.getSignal().getName(),
cc.getSignal().getPosition() cc.getSignal().getPosition()
@ -51,6 +52,7 @@ public record SignalResponse(
public static record ReachableConnectionData( public static record ReachableConnectionData(
long connectionId, long connectionId,
String direction,
long signalId, long signalId,
String signalName, String signalName,
Position signalPosition Position signalPosition

View File

@ -3,7 +3,9 @@ package nl.andrewl.railsignalapi.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.dao.BranchRepository; import nl.andrewl.railsignalapi.dao.BranchRepository;
import nl.andrewl.railsignalapi.dao.RailSystemRepository; 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.BranchResponse;
import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -16,6 +18,7 @@ import java.util.List;
public class BranchService { public class BranchService {
private final BranchRepository branchRepository; private final BranchRepository branchRepository;
private final RailSystemRepository railSystemRepository; private final RailSystemRepository railSystemRepository;
private final SignalRepository signalRepository;
@Transactional @Transactional
public void deleteBranch(long rsId, long branchId) { public void deleteBranch(long rsId, long branchId) {
@ -35,4 +38,20 @@ public class BranchService {
.map(BranchResponse::new) .map(BranchResponse::new)
.toList(); .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();
}
} }

View File

@ -6,8 +6,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import nl.andrewl.railsignalapi.dao.BranchRepository; import nl.andrewl.railsignalapi.dao.BranchRepository;
import nl.andrewl.railsignalapi.dao.RailSystemRepository; import nl.andrewl.railsignalapi.dao.RailSystemRepository;
import nl.andrewl.railsignalapi.dao.SignalBranchConnectionRepository;
import nl.andrewl.railsignalapi.dao.SignalRepository; import nl.andrewl.railsignalapi.dao.SignalRepository;
import nl.andrewl.railsignalapi.model.*; import nl.andrewl.railsignalapi.model.*;
import nl.andrewl.railsignalapi.rest.dto.SignalConnectionsUpdatePayload;
import nl.andrewl.railsignalapi.rest.dto.SignalCreationPayload; import nl.andrewl.railsignalapi.rest.dto.SignalCreationPayload;
import nl.andrewl.railsignalapi.rest.dto.SignalResponse; import nl.andrewl.railsignalapi.rest.dto.SignalResponse;
import nl.andrewl.railsignalapi.websocket.BranchUpdateMessage; import nl.andrewl.railsignalapi.websocket.BranchUpdateMessage;
@ -32,6 +34,7 @@ public class SignalService {
private final RailSystemRepository railSystemRepository; private final RailSystemRepository railSystemRepository;
private final SignalRepository signalRepository; private final SignalRepository signalRepository;
private final BranchRepository branchRepository; private final BranchRepository branchRepository;
private final SignalBranchConnectionRepository signalBranchConnectionRepository;
private final ObjectMapper mapper = new ObjectMapper(); private final ObjectMapper mapper = new ObjectMapper();
private final Map<WebSocketSession, Set<Long>> signalWebSocketSessions = new ConcurrentHashMap<>(); private final Map<WebSocketSession, Set<Long>> signalWebSocketSessions = new ConcurrentHashMap<>();
@ -43,6 +46,14 @@ public class SignalService {
if (signalRepository.existsByNameAndRailSystem(payload.name(), rs)) { if (signalRepository.existsByNameAndRailSystem(payload.name(), rs)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Signal " + payload.name() + " already exists."); 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<>(); Set<SignalBranchConnection> branchConnections = new HashSet<>();
Signal signal = new Signal(rs, payload.name(), payload.position(), branchConnections); Signal signal = new Signal(rs, payload.name(), payload.position(), branchConnections);
for (var branchData : payload.branchConnections()) { for (var branchData : payload.branchConnections()) {
@ -61,6 +72,39 @@ public class SignalService {
return new SignalResponse(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 @Transactional
public void registerSignalWebSocketSession(Set<Long> signalIds, WebSocketSession session) { public void registerSignalWebSocketSession(Set<Long> signalIds, WebSocketSession session) {
this.signalWebSocketSessions.put(session, signalIds); this.signalWebSocketSessions.put(session, signalIds);

View File

@ -11,26 +11,30 @@ function worldTransform() {
return tx; return tx;
} }
function mousePointToWorld(event) { function canvasPointToWorld(p) {
const rect = railMapCanvas[0].getBoundingClientRect(); return worldTransform().invertSelf().transformPoint(p);
const x = event.clientX - rect.left; }
const y = event.clientY - rect.top;
return worldTransform().invertSelf().transformPoint(new DOMPoint(x, y, 0, 1)); function worldPointToCanvas(p) {
return worldTransform().transformPoint(p);
} }
function signalTransform(worldTx, signal) { function signalTransform(worldTx, signal) {
let tx = DOMMatrix.fromMatrix(worldTx); let tx = DOMMatrix.fromMatrix(worldTx);
tx.translateSelf(signal.position.x, signal.position.z, 0); tx.translateSelf(signal.position.x, signal.position.z, 0);
let direction = signal.branchConnections[0].direction; 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 === "") { if (direction === undefined || direction === null || direction === "") {
direction = "NORTH"; direction = "NORTH";
} }
let angle = 0; let angle = 0;
if (direction === "NORTH" || direction === "SOUTH") { if (direction === "NORTH") {
angle = 90; angle = 90;
} else if (direction === "NORTH_WEST" || direction === "SOUTH_EAST") { } else if (direction === "NORTH_WEST") {
angle = 45; angle = 45;
} else if (direction === "NORTH_EAST" || direction === "SOUTH_WEST") { } else if (direction === "NORTH_EAST") {
angle = 135; angle = 135;
} }
tx.rotateSelf(0, 0, angle); tx.rotateSelf(0, 0, angle);
@ -56,8 +60,6 @@ function drawRailSystem() {
ctx.font = '24px Serif'; ctx.font = '24px Serif';
let textLine = 0; let textLine = 0;
hoveredElements.forEach(element => { hoveredElements.forEach(element => {
console.log('printing!')
console.log(element);
ctx.strokeText(element.name, 10, 20 + textLine * 20); ctx.strokeText(element.name, 10, 20 + textLine * 20);
ctx.fillText(element.name, 10, 20 + textLine * 20); ctx.fillText(element.name, 10, 20 + textLine * 20);
textLine += 1; textLine += 1;
@ -72,17 +74,17 @@ function drawSignal(ctx, signal) {
} }
ctx.scale(2, 2); ctx.scale(2, 2);
ctx.fillRect(-0.5, -0.5, 1, 1); ctx.fillRect(-0.5, -0.5, 1, 1);
let firstCon = signal.branchConnections[0]; let northWesterlyCon = signal.branchConnections[0];
let secondCon = signal.branchConnections[1]; let southEasterlyCon = signal.branchConnections[1];
if (firstCon === "EAST" || firstCon === "SOUTH" || firstCon === "SOUTH_WEST" || firstCon === "SOUTH_EAST") { if (northWesterlyCon.direction === "EAST" || northWesterlyCon.direction === "SOUTH" || northWesterlyCon.direction === "SOUTH_WEST" || northWesterlyCon.direction === "SOUTH_EAST") {
let tmp = firstCon; let tmp = northWesterlyCon;
firstCon = secondCon; northWesterlyCon = southEasterlyCon;
secondCon = tmp; southEasterlyCon = tmp;
} }
ctx.fillStyle = getSignalColor(signal, firstCon.branch.status); ctx.fillStyle = getSignalColor(signal, southEasterlyCon.branch.status);
ctx.fillRect(-0.75, -0.4, 0.3, 0.8); ctx.fillRect(-0.75, -0.4, 0.3, 0.8);
ctx.fillStyle = getSignalColor(signal, secondCon.branch.status); ctx.fillStyle = getSignalColor(signal, northWesterlyCon.branch.status);
ctx.fillRect(0.45, -0.4, 0.3, 0.8); ctx.fillRect(0.45, -0.4, 0.3, 0.8);
} }
@ -97,15 +99,33 @@ function getSignalColor(signal, branchStatus) {
} }
} }
// Draws lines indicating reachable paths between this signal and others, with arrows for directionality.
function drawReachableConnections(ctx, signal) { function drawReachableConnections(ctx, signal) {
ctx.strokeStyle = 'black'; ctx.strokeStyle = 'black';
ctx.lineWidth = 0.25; ctx.lineWidth = 0.25;
signal.branchConnections.forEach(connection => { signal.branchConnections.forEach(connection => {
ctx.resetTransform();
connection.reachableSignalConnections.forEach(reachableCon => { 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.beginPath();
ctx.moveTo(signal.position.x, signal.position.z); ctx.moveTo(0, 0);
ctx.lineTo(reachableCon.signalPosition.x, reachableCon.signalPosition.z); 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.stroke();
ctx.fill();
}); });
}); });
} }

View File

@ -9,8 +9,8 @@ let canvasDragOrigin = null;
let canvasDragTranslation = null; let canvasDragTranslation = null;
let hoveredElements = []; 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_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 = 5; const SCALE_INDEX_NORMAL = 7;
let canvasScaleIndex = SCALE_INDEX_NORMAL; let canvasScaleIndex = SCALE_INDEX_NORMAL;
$(document).ready(() => { $(document).ready(() => {
@ -18,10 +18,10 @@ $(document).ready(() => {
railSystemSelect.change(railSystemChanged); railSystemSelect.change(railSystemChanged);
railMapCanvas = $('#railMapCanvas'); railMapCanvas = $('#railMapCanvas');
railMapCanvas.on('wheel', onMouseWheel); railMapCanvas.on('wheel', onCanvasMouseWheel);
railMapCanvas.mousedown(onMouseDown); railMapCanvas.mousedown(onCanvasMouseDown);
railMapCanvas.mouseup(onMouseUp); railMapCanvas.mouseup(onCanvasMouseUp);
railMapCanvas.mousemove(onMouseMove); railMapCanvas.mousemove(onCanvasMouseMove);
$.get("/api/railSystems") $.get("/api/railSystems")
.done(railSystems => { .done(railSystems => {
@ -34,7 +34,8 @@ $(document).ready(() => {
}); });
}); });
function onMouseWheel(event) { // Handle mouse scrolling within the context of the canvas.
function onCanvasMouseWheel(event) {
let s = event.originalEvent.deltaY; let s = event.originalEvent.deltaY;
if (s > 0) { if (s > 0) {
canvasScaleIndex = Math.max(0, canvasScaleIndex - 1); canvasScaleIndex = Math.max(0, canvasScaleIndex - 1);
@ -45,33 +46,40 @@ function onMouseWheel(event) {
event.stopPropagation(); event.stopPropagation();
} }
function onMouseDown(event) { // Handle mouse clicks on the canvas.
const rect = railMapCanvas[0].getBoundingClientRect(); function onCanvasMouseDown(event) {
const x = event.clientX - rect.left; const p = getMousePoint(event);
const y = event.clientY - rect.top; canvasDragOrigin = {x: p.x, y: p.y};
canvasDragOrigin = {x: x, y: y};
} }
function onMouseUp(event) { // 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) { if (canvasDragTranslation !== null) {
canvasTranslation.x += canvasDragTranslation.x; canvasTranslation.x += canvasDragTranslation.x;
canvasTranslation.y += canvasDragTranslation.y; canvasTranslation.y += canvasDragTranslation.y;
} else { } else {
const p = mousePointToWorld(event); const p = getMousePoint(event);
let signalClicked = false;
railSystem.signals.forEach(signal => { railSystem.signals.forEach(signal => {
const sp = new DOMPoint(signal.position.x, signal.position.z, 0, 1); 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)); const canvasSp = worldPointToCanvas(sp);
if (dist < 1) { const dist = Math.sqrt(Math.pow(p.x - canvasSp.x, 2) + Math.pow(p.y - canvasSp.y, 2));
if (dist < 5) {
console.log(signal); console.log(signal);
$('#testingText').val(JSON.stringify(signal, null, 2)); onSignalSelected(signal);
signalClicked = true;
} }
}); });
if (!signalClicked) {
onSignalSelected(null);
}
} }
canvasDragOrigin = null; canvasDragOrigin = null;
canvasDragTranslation = null; canvasDragTranslation = null;
} }
function onMouseMove(event) { // Handle mouse motion over the canvas. This is for dragging and hovering over items.
function onCanvasMouseMove(event) {
const rect = railMapCanvas[0].getBoundingClientRect(); const rect = railMapCanvas[0].getBoundingClientRect();
const x = event.clientX - rect.left; const x = event.clientX - rect.left;
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
@ -83,11 +91,12 @@ function onMouseMove(event) {
drawRailSystem(); drawRailSystem();
} else { } else {
hoveredElements = []; hoveredElements = [];
const p = mousePointToWorld(event); const p = getMousePoint(event);
railSystem.signals.forEach(signal => { railSystem.signals.forEach(signal => {
const sp = new DOMPoint(signal.position.x, signal.position.z, 0, 1); 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)); const canvasSp = worldPointToCanvas(sp);
if (dist < 1) { const dist = Math.sqrt(Math.pow(p.x - canvasSp.x, 2) + Math.pow(p.y - canvasSp.y, 2));
if (dist < 5) {
hoveredElements.push(signal); hoveredElements.push(signal);
} }
}); });
@ -95,6 +104,13 @@ function onMouseMove(event) {
} }
} }
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() { function railSystemChanged() {
railSystem = {}; railSystem = {};
railSystem.id = railSystemSelect.val(); railSystem.id = railSystemSelect.val();
@ -102,14 +118,30 @@ function railSystemChanged() {
.done(signals => { .done(signals => {
railSystem.signals = signals; railSystem.signals = signals;
let bb = getRailSystemBoundingBox(); let bb = getRailSystemBoundingBox();
// canvasTranslation.x = -1 * (bb.x + (bb.width / 2)); canvasTranslation.x = -1 * (bb.x + (bb.width / 2));
// canvasTranslation.y = -1 * (bb.y + (bb.height / 2)); canvasTranslation.y = -1 * (bb.y + (bb.height / 2));
// canvasScaleIndex = SCALE_INDEX_NORMAL; canvasScaleIndex = SCALE_INDEX_NORMAL;
drawRailSystem(); drawRailSystem();
window.setTimeout(railSystemChanged, 1000);
}); });
$.get("/api/railSystems/" + railSystem.id + "/branches") $.get("/api/railSystems/" + railSystem.id + "/branches")
.done(branches => { .done(branches => {
railSystem.branches = branches; railSystem.branches = branches;
}); });
} }
function selectSignalById(id) {
railSystem.signals.forEach(signal => {
if (signal.id === id) {
onSignalSelected(signal);
}
});
}
function onSignalSelected(signal) {
const dp = $('#railMapDetailPanel');
dp.empty();
if (signal !== null) {
const tpl = Handlebars.compile($('#signalTemplate').html());
dp.html(tpl(signal));
}
}

View File

@ -37,21 +37,52 @@
<hr class="my-4"/> <hr class="my-4"/>
<div class="row"> <div class="row">
<div id="railMapCanvasParent" class="col border p-0"> <div id="railMapCanvasParent" class="col">
<canvas id="railMapCanvas" width="1000" height="500"></canvas> <canvas id="railMapCanvas" width="800" height="500" class="border"></canvas>
</div>
<div id="railMapDetailPanel" class="col p-2 border">
</div> </div>
</div> </div>
</div>
<div class="row"> <div style="display: none;">
<textarea id="testingText"></textarea> <script id="signalTemplate" type="text/x-handlebars-template">
</div> <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">{{this.id}}</small></h5>
<dl class="row">
<dt class="col-sm-3">Branch</dt>
<dd class="col-sm-9">{{this.branch.name}} <small class="text-muted">{{this.branch.id}}</small></dd>
{{#if this.reachableSignalConnections.length}}
<dt class="col-sm-3">Reachable Signals</dt>
<dd class="col-sm-9">
{{#each this.reachableSignalConnections}}
<p><a onclick="selectSignalById({{this.signalId}})" href="#">{{this.signalName}}</a> <small class="text-muted">{{this.signalId}}</small> via {{this.direction}} connection {{this.connectionId}}</p>
{{/each}}
</dd>
{{/if}}
</dl>
{{/each}}
</script>
</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@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="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/drawing.js"></script>
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
</body> </body>
</html> </html>