Fixed switch UI updating, websocket bugs, and context switching.

This commit is contained in:
Andrew Lalis 2022-06-01 15:54:29 +02:00
parent c16627e7d3
commit bd6c87149a
22 changed files with 470 additions and 252 deletions

View File

@ -12,6 +12,7 @@
"axios": "^0.21.1",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"randomcolor": "^0.6.2",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
},
@ -3666,6 +3667,11 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/randomcolor": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.6.2.tgz",
"integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A=="
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -7276,6 +7282,11 @@
"safe-buffer": "^5.1.0"
}
},
"randomcolor": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.6.2.tgz",
"integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",

View File

@ -11,20 +11,21 @@
"test": "echo \"No test specified\" && exit 0"
},
"dependencies": {
"@quasar/extras": "^1.0.0",
"axios": "^0.21.1",
"pinia": "^2.0.11",
"@quasar/extras": "^1.0.0",
"quasar": "^2.6.0",
"randomcolor": "^0.6.2",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
},
"devDependencies": {
"eslint": "^8.10.0",
"eslint-plugin-vue": "^8.5.0",
"eslint-config-prettier": "^8.1.0",
"prettier": "^2.5.1",
"@quasar/app-vite": "^1.0.0",
"autoprefixer": "^10.4.2"
"autoprefixer": "^10.4.2",
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^8.5.0",
"prettier": "^2.5.1"
},
"engines": {
"node": "^18 || ^16 || ^14.19",

View File

@ -31,8 +31,7 @@ module.exports = configure(function (ctx) {
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: [
'axios',
'axios'
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css

View File

@ -3,7 +3,7 @@
</template>
<script>
import { defineComponent } from 'vue'
import { defineComponent } from "vue";
export default defineComponent({
name: 'App'

View File

@ -2,6 +2,9 @@ import axios from "axios";
import {API_URL} from "./constants";
import { refreshSegments } from "src/api/segments";
import { refreshComponents } from "src/api/components";
import { closeWebsocketConnection, establishWebsocketConnection } from "src/api/websocket";
import { refreshLinkTokens } from "src/api/linkTokens";
import { refreshSettings } from "src/api/settings";
export class RailSystem {
constructor(data) {
@ -15,6 +18,7 @@ export class RailSystem {
this.websocket = null;
this.selectedComponents = [];
this.loaded = false;
}
}
@ -57,7 +61,9 @@ export function removeRailSystem(rsStore, id) {
return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${id}`)
.then(() => {
rsStore.selectedRailSystem = null;
if (rsStore.selectedRailSystem !== null && rsStore.selectedRailSystem.id === id) {
rsStore.selectRailSystem(null);
}
refreshRailSystems(rsStore)
.then(resolve)
.catch(reject);
@ -65,3 +71,41 @@ export function removeRailSystem(rsStore, id) {
.catch(reject);
});
}
/**
* Loads all data for a rail system. This is generally done when a rail system
* is selected.
* @param {RailSystem} rs
*/
export async function loadData(rs) {
console.log("Loading rail system " + rs.id);
await closeWebsocketConnection(rs);
console.log("Closed websocket connection to " + rs.id);
const updatePromises = [];
updatePromises.push(refreshSegments(rs));
updatePromises.push(refreshComponents(rs));
updatePromises.push(refreshLinkTokens(rs));
updatePromises.push(refreshSettings(rs));
await Promise.all(updatePromises);
await establishWebsocketConnection(rs);
console.log("Finished loading rail system " + rs.id);
rs.loaded = true;
}
/**
* Unloads all data for a rail system. This is generally done when the user
* navigates away from a rail system's page.
* @param {RailSystem} rs
* @returns {Promise}
*/
export async function unloadData(rs) {
console.log("Unloading data for rail system " + rs.id);
await closeWebsocketConnection(rs);
rs.segments = [];
rs.components = [];
rs.linkTokens = [];
rs.selectedComponents = [];
rs.settings = null;
rs.loaded = false;
console.log("Finished unloading data for rail system " + rs.id);
}

View File

@ -1,5 +1,10 @@
import { WS_URL } from "./constants";
/**
* The time to wait before attempting to reconnect if a websocket connection is
* abruptly closed.
* @type {number}
*/
const WS_RECONNECT_TIMEOUT = 3000;
/**
@ -8,6 +13,9 @@ const WS_RECONNECT_TIMEOUT = 3000;
* @return {Promise} A promise that resolves when a connection is established.
*/
export function establishWebsocketConnection(rs) {
if (rs.websocket) {
console.log('rail system ' + rs.id + ' already has websocket')
}
return new Promise(resolve => {
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
rs.websocket.onopen = resolve;
@ -29,7 +37,7 @@ export function establishWebsocketConnection(rs) {
const id = data.cId;
const idx = rs.components.findIndex(c => c.id === id);
if (idx > -1) {
rs.components[idx] = data.data;
Object.assign(rs.components[idx], data.data);
}
}
};
@ -46,12 +54,15 @@ export function establishWebsocketConnection(rs) {
*/
export function closeWebsocketConnection(rs) {
return new Promise(resolve => {
if (rs.websocket) {
rs.websocket.onclose = resolve;
if (rs.websocket && rs.websocket.readyState !== WebSocket.CLOSED) {
rs.websocket.onclose = () => {
rs.websocket = null;
resolve();
};
rs.websocket.close();
} else {
rs.websocket = null;
resolve();
}
});
}

View File

@ -0,0 +1,24 @@
<template>
<div class="row">
<div class="col-md-6 q-pa-md">
<div class="text-h4">{{title}}</div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "IndexPageSection",
props: {
title: {
type: String,
required: true
}
}
};
</script>
<style scoped>
</style>

View File

@ -176,6 +176,9 @@ export default {
},
setActiveSwitchConfig(sw, configId) {
updateSwitchConfiguration(this.railSystem, sw, configId);
},
isConfigActive(sw, config) {
return sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id;
}
}
};

View File

@ -30,7 +30,7 @@
clickable
v-ripple
:to="'/'"
@click="rsStore.selectedRailSystem = null"
@click="rsStore.selectRailSystem(null)"
>
<q-item-section>
<q-item-label>Home</q-item-label>
@ -40,7 +40,7 @@
clickable
v-ripple
:to="'/about'"
@click="rsStore.selectedRailSystem = null"
@click="rsStore.selectRailSystem(null)"
>
<q-item-section>
<q-item-label>About</q-item-label>

View File

@ -7,8 +7,20 @@
<p>
This application was developed by <a href="https://andrewlalis.github.io">Andrew Lalis</a>
after several years of tinkering with various ways to automate rail
networks in Minecraft.
networks in Minecraft. However, Rail Signal was designed from the
ground up to be free from the constraints of any particular system.
<em>Theoretically</em>, you could use Rail Signal to manage a real-world
rail network, although I wouldn't recommend it, due to the complex and
secure nature of real-world networks.
</p>
<p>
Of course, Rail Signal was originally designed for Minecraft, so we've
started by adding Rail Signal compatible drivers for Computercraft
and Immersive Railroading by default. If you have a different system
and would like to integrate it with Rail Signal, please <a href="https://github.com/andrewlalis/RailSignalAPI/issues" target="_blank">create a new issue</a>
on the GitHub repository.
</p>
<div class="text-h4"><q-icon :name="fasMicrochip"/> Technologies</div>
<p>
This web app was built using <a href="https://quasar.dev">Quasar</a>
and <a href="https://vuejs.org/">VueJS</a>. The API that powers this
@ -16,6 +28,7 @@
and Java 17. For more technical information, please visit Rail Signal's
<a href="https://github.com/andrewlalis/RailSignalAPI">GitHub repository</a>.
</p>
<div class="text-h4"><q-icon :name="fasHeart"/> Support</div>
<p>
If you're enjoying this app, please consider making a <a href="https://paypal.me/andrewlalis" target="_blank">donation to
Andrew's work on paypal</a>.
@ -26,8 +39,13 @@
</template>
<script>
import {fasHeart, fasMicrochip} from "@quasar/extras/fontawesome-v6";
export default {
name: "AboutPage"
name: "AboutPage",
setup() {
return {fasHeart, fasMicrochip};
}
};
</script>

View File

@ -12,9 +12,7 @@
</p>
</div>
</div>
<div class="row">
<div class="col-md-6 q-pa-md">
<div class="text-h4">Introduction to Rail Networks</div>
<index-page-section title="Introduction to Rail Networks">
<q-img src="~assets/img/guide/layout.png"/>
<p>
The above diagram illustrates all of the basic concepts you need to
@ -56,9 +54,8 @@
the ability to manage these automatically, so you can use this web
interface to configure switches instead of doing it manually.
</p>
</div>
<div class="col-md-6 q-pa-md">
<div class="text-h4">Paths and Path Nodes</div>
</index-page-section>
<index-page-section title="Paths and Path Nodes">
<p>
We mentioned segment boundaries and switches earlier, as simple
components that you can add to your network in order to link it to the
@ -89,11 +86,21 @@
each of the segment boundaries that it allows traffic between.
</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-md-6 q-pa-md">
<div class="text-h4">Advanced Usage</div>
</index-page-section>
<index-page-section title="Drivers">
<p>
While you can play around in this web app as long as you'd like, the
main point is to connect to an external rail system. That's done through
a <strong>driver</strong>, which is a dedicated piece of code that sends
and receives messages from the Rail Signal server. Usually, driver
software will be installed into physical components in your system, like
signals and trackside detectors, and switch levers. It's the responsibility
of driver software to tell Rail Signal when a train crosses a segment
boundary, or when a switch updates, or anything else it should know
about.
</p>
</index-page-section>
<index-page-section title="Advanced Usage">
<q-img src="~assets/img/guide/layout2.png"/>
<p>
The above diagram shows a more typical network arrangement for a large
@ -107,15 +114,15 @@
segment, while on the inbound segments, the signal will show the
status of the junction segment.
</p>
</div>
</div>
</index-page-section>
</q-page>
</template>
<script>
import IndexPageSection from "components/IndexPageSection.vue";
export default {
name: "IndexPage"
name: "IndexPage",
components: { IndexPageSection }
};
</script>

View File

@ -1,6 +1,6 @@
<template>
<q-page>
<div v-if="railSystem">
<div v-if="railSystem && railSystem.loaded">
<q-tabs
v-model="panel"
align="left"
@ -25,6 +25,10 @@
<router-view />
</div>
<q-inner-loading
:showing="!railSystem || !railSystem.loaded"
label="Loading rail system..."
/>
</q-page>
</template>
@ -33,6 +37,7 @@ import { useRailSystemsStore } from "stores/railSystemsStore";
import MapView from "components/rs/MapView.vue";
import SegmentsView from "components/rs/SegmentsView.vue";
import SettingsView from "components/rs/SettingsView.vue";
import { loadData, unloadData } from "src/api/railSystems";
export default {
name: "RailSystemPage",
@ -41,23 +46,45 @@ export default {
return {
panel: "map",
railSystem: null,
linkTokens: []
loading: false
}
},
beforeRouteEnter(to, from, next) {
const id = parseInt(to.params.id);
setup() {
const rsStore = useRailSystemsStore();
rsStore.selectRailSystem(id).then(() => {
next(vm => vm.railSystem = rsStore.selectedRailSystem);
});
return {rsStore};
},
beforeRouteUpdate(to, from) {
const id = parseInt(to.params.id);
const rsStore = useRailSystemsStore();
rsStore.selectRailSystem(id).then(() => {
this.railSystem = rsStore.selectedRailSystem;
});
mounted() {
this.updateRailSystem();
},
created() {
this.$watch(
() => this.$route.params,
this.updateRailSystem,
{
immediate: true
}
)
},
methods: {
async updateRailSystem() {
if (this.loading) return;
this.loading = true;
console.log(">>>> updating rail system.")
if (this.railSystem) {
this.rsStore.selectedRailSystem = null;
await unloadData(this.railSystem);
}
if (this.$route.params.id) {
const newRsId = parseInt(this.$route.params.id);
const rs = this.rsStore.railSystems.find(r => r.id === newRsId);
if (rs) {
this.railSystem = rs;
this.rsStore.selectedRailSystem = rs;
await loadData(rs);
}
}
this.loading = false;
}
}
};
</script>

View File

@ -14,3 +14,12 @@ export function circle(ctx, x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
}
export function mulberry32(a) {
return function() {
let t = a += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}

View File

@ -2,10 +2,89 @@
Helper functions to actually perform rendering of different components.
*/
import { getScaleFactor, isComponentHovered, isComponentSelected } from "./mapRenderer";
import { getScaleFactor, getWorldTransform, isComponentHovered, isComponentSelected } from "./mapRenderer";
import { circle, roundedRect } from "./canvasUtils";
import randomColor from "randomcolor";
export function drawComponent(ctx, worldTx, component) {
export function drawMap(ctx, rs) {
const worldTx = getWorldTransform();
ctx.setTransform(worldTx);
drawSegments(ctx, rs);
drawNodeConnections(ctx, rs, worldTx);
drawComponents(ctx, rs, worldTx);
}
function drawSegments(ctx, rs) {
const segmentPoints = new Map();
rs.segments.forEach(segment => segmentPoints.set(segment.id, []));
for (let i = 0; i < rs.components.length; i++) {
const c = rs.components[i];
if (c.type === "SEGMENT_BOUNDARY") {
for (let j = 0; j < c.segments.length; j++) {
segmentPoints.get(c.segments[j].id).push({x: c.position.x, y: c.position.z});
}
}
}
for (let i = 0; i < rs.segments.length; i++) {
const color = randomColor({ luminosity: 'light', format: 'rgb', seed: rs.segments[i].id });
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = 5;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.font = "3px Sans-Serif";
const points = segmentPoints.get(rs.segments[i].id);
if (points.length === 0) continue;
const avgPoint = {x: points[0].x, y: points[0].y};
if (points.length === 1) {
circle(ctx, points[0].x, points[0].y, 5);
ctx.fill();
} else {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let j = 1; j < points.length; j++) {
ctx.lineTo(points[j].x, points[j].y);
avgPoint.x += points[j].x;
avgPoint.y += points[j].y;
}
avgPoint.x /= points.length;
avgPoint.y /= points.length;
ctx.lineTo(points[0].x, points[0].y);
ctx.fill();
ctx.stroke();
}
// Draw the segment name.
ctx.fillStyle = randomColor({luminosity: 'dark', format: 'rgb', seed: rs.segments[i].id});
ctx.fillText(rs.segments[i].name, avgPoint.x, avgPoint.y);
}
}
function drawNodeConnections(ctx, rs, worldTx) {
for (let i = 0; i < rs.components.length; i++) {
const c = rs.components[i];
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
drawConnectedNodes(ctx, worldTx, c);
}
}
}
function drawComponents(ctx, rs, worldTx) {
// Draw switch configurations first
for (let i = 0; i < rs.components.length; i++) {
if (rs.components[i].type === "SWITCH") {
drawSwitchConfigurations(ctx, worldTx, rs.components[i]);
}
}
for (let i = 0; i < rs.components.length; i++) {
drawComponent(ctx, worldTx, rs.components[i]);
}
}
function drawComponent(ctx, worldTx, component) {
const tx = DOMMatrix.fromMatrix(worldTx);
tx.translateSelf(component.position.x, component.position.z, 0);
const s = getScaleFactor();
@ -67,17 +146,26 @@ function drawSegmentBoundary(ctx) {
ctx.fill();
}
function drawSwitch(ctx, sw) {
const colors = [
`rgba(61, 148, 66, 0.25)`,
`rgba(59, 22, 135, 0.25)`,
`rgba(145, 17, 90, 0.25)`,
`rgba(191, 49, 10, 0.25)`
];
ctx.lineWidth = 1;
function drawSwitchConfigurations(ctx, worldTx, sw) {
const tx = DOMMatrix.fromMatrix(worldTx);
tx.translateSelf(sw.position.x, sw.position.z, 0);
const s = getScaleFactor();
tx.scaleSelf(1/s, 1/s, 1/s);
tx.scaleSelf(20, 20, 20);
ctx.setTransform(tx);
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
const config = sw.possibleConfigurations[i];
ctx.strokeStyle = colors[i];
ctx.strokeStyle = randomColor({
seed: config.id,
format: 'rgb',
luminosity: 'bright'
});
if (sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id) {
ctx.lineWidth = 0.6;
} else {
ctx.lineWidth = 0.3;
}
for (let j = 0; j < config.nodes.length; j++) {
const node = config.nodes[j];
const diff = {
@ -85,14 +173,17 @@ function drawSwitch(ctx, sw) {
y: sw.position.z - node.position.z,
};
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
diff.x = 2 * -diff.x / mag;
diff.y = 2 * -diff.y / mag;
diff.x = 3 * -diff.x / mag;
diff.y = 3 * -diff.y / mag;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(diff.x, diff.y);
ctx.stroke();
}
}
}
function drawSwitch(ctx, sw) {
ctx.fillStyle = `rgb(245, 188, 66)`;
ctx.strokeStyle = `rgb(245, 188, 66)`;
ctx.lineWidth = 0.2;

View File

@ -3,7 +3,7 @@ This component is responsible for the rendering of a RailSystem in a 2d map
view.
*/
import {drawComponent, drawConnectedNodes} from "./drawing";
import { drawMap } from "./drawing";
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
const SCALE_INDEX_NORMAL = 7;
@ -56,59 +56,12 @@ export function draw() {
ctx.resetTransform();
ctx.fillStyle = `rgb(240, 240, 240)`;
ctx.fillRect(0, 0, width, height);
const worldTx = getWorldTransform();
ctx.setTransform(worldTx);
// Draw segments!
const segmentPoints = new Map();
railSystem.segments.forEach(segment => segmentPoints.set(segment.id, []));
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
if (c.type === "SEGMENT_BOUNDARY") {
for (let j = 0; j < c.segments.length; j++) {
segmentPoints.get(c.segments[j].id).push({x: c.position.x, y: c.position.z});
}
}
}
railSystem.segments.forEach(segment => {
const points = segmentPoints.get(segment.id);
const avgPoint = {x: 0, y: 0};
points.forEach(point => {
avgPoint.x += point.x;
avgPoint.y += point.y;
});
avgPoint.x /= points.length;
avgPoint.y /= points.length;
let r = 5;
points.forEach(point => {
const dist2 = Math.pow(avgPoint.x - point.x, 2) + Math.pow(avgPoint.y - point.y, 2);
if (dist2 > r * r) {
r = Math.sqrt(dist2);
}
});
ctx.fillStyle = `rgba(200, 200, 200, 0.25)`;
const p = worldPointToMap(new DOMPoint(avgPoint.x, avgPoint.y, 0, 0));
const s = getScaleFactor();
ctx.beginPath();
ctx.arc(p.x / s, p.y / s, r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
ctx.font = "3px Sans-Serif";
ctx.fillText(`${segment.name}`, p.x / s, p.y / s);
});
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
drawConnectedNodes(ctx, worldTx, c);
}
drawMap(ctx, railSystem);
drawDebugInfo(ctx);
}
for (let i = 0; i < railSystem.components.length; i++) {
drawComponent(ctx, worldTx, railSystem.components[i]);
}
// Draw debug info.
function drawDebugInfo(ctx) {
ctx.resetTransform();
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
@ -133,7 +86,7 @@ export function getScaleFactor() {
* Gets a matrix that transforms world coordinates to canvas.
* @returns {DOMMatrix}
*/
function getWorldTransform() {
export function getWorldTransform() {
const canvasRect = mapCanvas.getBoundingClientRect();
const scale = getScaleFactor();
const tx = new DOMMatrix();
@ -159,7 +112,7 @@ export function isComponentSelected(component) {
* @param {DOMPoint} p
* @returns {DOMPoint}
*/
function mapPointToWorld(p) {
export function mapPointToWorld(p) {
return getWorldTransform().invertSelf().transformPoint(p);
}
@ -168,7 +121,7 @@ function mapPointToWorld(p) {
* @param {DOMPoint} p
* @returns {DOMPoint}
*/
function worldPointToMap(p) {
export function worldPointToMap(p) {
return getWorldTransform().transformPoint(p);
}

View File

@ -20,20 +20,22 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
actions: {
/**
* Updates the selected rail system.
* @param rsId {Number} The new rail system id.
* @param rsId {Number | null} The new rail system id.
* @returns {Promise} A promise that resolves when the new rail system is
* fully loaded and ready.
*/
selectRailSystem(rsId) {
// Close any existing websocket connections prior to refreshing.
const wsClosePromises = [];
if (this.selectedRailSystem) {
if (this.selectedRailSystem !== null) {
wsClosePromises.push(closeWebsocketConnection(this.selectedRailSystem));
}
if (rsId === null) return Promise.all(wsClosePromises);
return new Promise(resolve => {
Promise.all(wsClosePromises).then(() => {
refreshRailSystems(this).then(() => {
const rs = this.railSystems.find(r => r.id === rsId);
console.log(rs);
const updatePromises = [];
updatePromises.push(refreshSegments(rs));
updatePromises.push(refreshComponents(rs));

View File

@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
* web app's index page.
*/
@Controller
@RequestMapping(path = {"/", "/app", "/app/about", "/home", "/index.html", "/index"})
@RequestMapping(path = {"/", "/app", "/app/about", "/app/rail-systems/*", "/home", "/index.html", "/index"})
public class IndexPageController {
@GetMapping
public String getIndex() {

View File

@ -2,6 +2,7 @@ package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.PathNode;
import java.util.Comparator;
import java.util.List;
public abstract class PathNodeResponse extends ComponentResponse {
@ -9,6 +10,9 @@ public abstract class PathNodeResponse extends ComponentResponse {
public PathNodeResponse(PathNode p) {
super(p);
this.connectedNodes = p.getConnectedNodes().stream().map(SimpleComponentResponse::new).toList();
this.connectedNodes = p.getConnectedNodes().stream()
.sorted(Comparator.comparing(PathNode::getName))
.map(SimpleComponentResponse::new)
.toList();
}
}

View File

@ -3,6 +3,7 @@ package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
import java.util.Comparator;
import java.util.List;
public class SegmentBoundaryNodeResponse extends PathNodeResponse {
@ -10,6 +11,9 @@ public class SegmentBoundaryNodeResponse extends PathNodeResponse {
public SegmentBoundaryNodeResponse(SegmentBoundaryNode n) {
super(n);
this.segments = n.getSegments().stream().map(SegmentResponse::new).toList();
this.segments = n.getSegments().stream()
.map(SegmentResponse::new)
.sorted(Comparator.comparing(sr -> sr.name))
.toList();
}
}

View File

@ -2,6 +2,7 @@ package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
import java.util.Comparator;
import java.util.List;
public record SwitchConfigurationResponse (
@ -11,7 +12,10 @@ public record SwitchConfigurationResponse (
public SwitchConfigurationResponse(SwitchConfiguration sc) {
this(
sc.getId(),
sc.getNodes().stream().map(SimpleComponentResponse::new).toList()
sc.getNodes().stream()
.map(SimpleComponentResponse::new)
.sorted(Comparator.comparing(SimpleComponentResponse::name))
.toList()
);
}
}

View File

@ -2,6 +2,7 @@ package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.Switch;
import java.util.Comparator;
import java.util.List;
public class SwitchResponse extends PathNodeResponse {
@ -10,7 +11,10 @@ public class SwitchResponse extends PathNodeResponse {
public SwitchResponse(Switch s) {
super(s);
this.possibleConfigurations = s.getPossibleConfigurations().stream().map(SwitchConfigurationResponse::new).toList();
this.possibleConfigurations = s.getPossibleConfigurations().stream()
.map(SwitchConfigurationResponse::new)
.sorted(Comparator.comparing(SwitchConfigurationResponse::id))
.toList();
this.activeConfiguration = s.getActiveConfiguration() == null ? null : new SwitchConfigurationResponse(s.getActiveConfiguration());
}
}

View File

@ -1,6 +1,7 @@
package nl.andrewl.railsignalapi.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nl.andrewl.railsignalapi.dao.ComponentRepository;
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
import nl.andrewl.railsignalapi.dao.SegmentRepository;
@ -24,6 +25,7 @@ import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class SegmentService {
private final SegmentRepository segmentRepository;
private final RailSystemRepository railSystemRepository;