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,110 +12,117 @@
</p>
</div>
</div>
<div class="row">
<div class="col-md-6 q-pa-md">
<div class="text-h4">Introduction to Rail Networks</div>
<q-img src="~assets/img/guide/layout.png"/>
<p>
The above diagram illustrates all of the basic concepts you need to
know in order to build and manage your rail networks.
</p>
<p>
Each rail system can be conceptually split up into lots of small
<strong>segments</strong>, each of which represents a single part of
the network that a single train should go through at once. For
example, in our diagram, each shaded area is a segment. We only want
one train to go through the junction at once, or there might be a
crash!
</p>
<p>
At the places where segments meet, we see a <strong style="color: #963ae0">segment boundary</strong>
which is also denoted with a red dotted line for convenience. This is
a physical point on a track where trains travel from one segment to
another. What's special about segment boundaries is that they're where
we can used devices to track trains moving in and out of segments. To
put it simply, imagine there's a little computer next to each segment
boundary point that sends a message saying, "Hey! A train just passed!"
every time that it detects a train going over it.
</p>
<p>
Now that we've covered segments and segment boundaries, we can now
display a segment's status using a <strong>signal</strong>. A signal
is a device that is linked to a segment, and whenever the segment's
status updates (<em>when a train enters or leaves it</em>), the signal
will be updated as well. Usually, signals are placed near the segment
boundary, so that approaching trains know whether they're safe to
continue, but with Rail Signal, you can place a signal anywhere, and
connect it to any segment.
</p>
<p>
Finally, unless you're just making a boring single-line loop, you'll
most likely have some <strong style="color: #f5bc42">switches</strong>
in your network. Switches are just sections of rail that allow trains
to choose between two different paths to take. Rail Signal gives you
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>
<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
internet. There's more to it than that, however.
</p>
<p>
Behind the scenes, your Rail Signal models your network as a set of
<strong>path nodes</strong>, where each node can be connected to any
other number of nodes. A train travels through your network by moving
from node to node, until it reaches its desired destination. Both the
segment boundary and switch are types of path nodes.
</p>
<ul>
<li>
Segment boundaries may only be connected to at most two nodes. This
is because a segment boundary is fundamentally just a point on a
single rail line.
</li>
<li>
Switches are connected to nodes based on their set of defined
configurations. In the example diagram, our switch allows two
possible configurations:
<ul>
<li>Between the <strong style="color: #3cadab">blue</strong> and <strong style="color: #7169b4">purple</strong> segments.</li>
<li>Between the <strong style="color: #81d07b">green</strong> and <strong style="color: #7169b4">purple</strong> segments.</li>
</ul>
This implies that our switch node is connected to three other nodes:
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>
<q-img src="~assets/img/guide/layout2.png"/>
<p>
The above diagram shows a more typical network arrangement for a large
scale, two-way mainline. Here, we see that each side of the main line
has its own segment, so that trains can travel past each other without
issue. We make the entire junction a single segment, so that only one
train can pass through at a time. More advanced setups might have
separate segments for bypass lines to avoid traffic jams. Beside each
entrance and exit to the junction, we've placed a signal. On the
outbound segments, the signal will report the status of the outbound
segment, while on the inbound segments, the signal will show the
status of the junction segment.
</p>
</div>
</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
know in order to build and manage your rail networks.
</p>
<p>
Each rail system can be conceptually split up into lots of small
<strong>segments</strong>, each of which represents a single part of
the network that a single train should go through at once. For
example, in our diagram, each shaded area is a segment. We only want
one train to go through the junction at once, or there might be a
crash!
</p>
<p>
At the places where segments meet, we see a <strong style="color: #963ae0">segment boundary</strong>
which is also denoted with a red dotted line for convenience. This is
a physical point on a track where trains travel from one segment to
another. What's special about segment boundaries is that they're where
we can used devices to track trains moving in and out of segments. To
put it simply, imagine there's a little computer next to each segment
boundary point that sends a message saying, "Hey! A train just passed!"
every time that it detects a train going over it.
</p>
<p>
Now that we've covered segments and segment boundaries, we can now
display a segment's status using a <strong>signal</strong>. A signal
is a device that is linked to a segment, and whenever the segment's
status updates (<em>when a train enters or leaves it</em>), the signal
will be updated as well. Usually, signals are placed near the segment
boundary, so that approaching trains know whether they're safe to
continue, but with Rail Signal, you can place a signal anywhere, and
connect it to any segment.
</p>
<p>
Finally, unless you're just making a boring single-line loop, you'll
most likely have some <strong style="color: #f5bc42">switches</strong>
in your network. Switches are just sections of rail that allow trains
to choose between two different paths to take. Rail Signal gives you
the ability to manage these automatically, so you can use this web
interface to configure switches instead of doing it manually.
</p>
</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
internet. There's more to it than that, however.
</p>
<p>
Behind the scenes, your Rail Signal models your network as a set of
<strong>path nodes</strong>, where each node can be connected to any
other number of nodes. A train travels through your network by moving
from node to node, until it reaches its desired destination. Both the
segment boundary and switch are types of path nodes.
</p>
<ul>
<li>
Segment boundaries may only be connected to at most two nodes. This
is because a segment boundary is fundamentally just a point on a
single rail line.
</li>
<li>
Switches are connected to nodes based on their set of defined
configurations. In the example diagram, our switch allows two
possible configurations:
<ul>
<li>Between the <strong style="color: #3cadab">blue</strong> and <strong style="color: #7169b4">purple</strong> segments.</li>
<li>Between the <strong style="color: #81d07b">green</strong> and <strong style="color: #7169b4">purple</strong> segments.</li>
</ul>
This implies that our switch node is connected to three other nodes:
each of the segment boundaries that it allows traffic between.
</li>
</ul>
</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
scale, two-way mainline. Here, we see that each side of the main line
has its own segment, so that trains can travel past each other without
issue. We make the entire junction a single segment, so that only one
train can pass through at a time. More advanced setups might have
separate segments for bypass lines to avoid traffic jams. Beside each
entrance and exit to the junction, we've placed a signal. On the
outbound segments, the signal will report the status of the outbound
segment, while on the inbound segments, the signal will show the
status of the junction segment.
</p>
</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,48 +146,60 @@ 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;
for (let i = 0; i < sw.possibleConfigurations.length; i++) {
const config = sw.possibleConfigurations[i];
ctx.strokeStyle = colors[i];
for (let j = 0; j < config.nodes.length; j++) {
const node = config.nodes[j];
const diff = {
x: sw.position.x - node.position.x,
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;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(diff.x, diff.y);
ctx.stroke();
}
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 = 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;
}
ctx.fillStyle = `rgb(245, 188, 66)`;
ctx.strokeStyle = `rgb(245, 188, 66)`;
ctx.lineWidth = 0.2;
circle(ctx, 0, 0.3, 0.2);
ctx.fill();
circle(ctx, -0.3, -0.3, 0.2);
ctx.fill();
circle(ctx, 0.3, -0.3, 0.2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, 0.3);
ctx.lineTo(0, 0);
ctx.lineTo(0.3, -0.3);
ctx.moveTo(0, 0);
ctx.lineTo(-0.3, -0.3);
ctx.stroke();
for (let j = 0; j < config.nodes.length; j++) {
const node = config.nodes[j];
const diff = {
x: sw.position.x - node.position.x,
y: sw.position.z - node.position.z,
};
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
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;
circle(ctx, 0, 0.3, 0.2);
ctx.fill();
circle(ctx, -0.3, -0.3, 0.2);
ctx.fill();
circle(ctx, 0.3, -0.3, 0.2);
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, 0.3);
ctx.lineTo(0, 0);
ctx.lineTo(0.3, -0.3);
ctx.moveTo(0, 0);
ctx.lineTo(-0.3, -0.3);
ctx.stroke();
}
function drawLabel(ctx, lbl) {

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,73 +56,26 @@ 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);
});
drawMap(ctx, railSystem);
drawDebugInfo(ctx);
}
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);
}
}
for (let i = 0; i < railSystem.components.length; i++) {
drawComponent(ctx, worldTx, railSystem.components[i]);
}
// Draw debug info.
ctx.resetTransform();
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
ctx.font = "10px Sans-Serif";
const lastWorldPoint = mapPointToWorld(lastMousePoint);
const lines = [
"Scale factor: " + getScaleFactor(),
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
`Components: ${railSystem.components.length}`,
`Hovered elements: ${hoveredElements.length}`
]
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 10, 20 + (i * 15));
}
function drawDebugInfo(ctx) {
ctx.resetTransform();
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
ctx.font = "10px Sans-Serif";
const lastWorldPoint = mapPointToWorld(lastMousePoint);
const lines = [
"Scale factor: " + getScaleFactor(),
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
`Components: ${railSystem.components.length}`,
`Hovered elements: ${hoveredElements.length}`
]
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 10, 20 + (i * 15));
}
}
export function getScaleFactor() {
@ -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;