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", "axios": "^0.21.1",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"randomcolor": "^0.6.2",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.0" "vue-router": "^4.0.0"
}, },
@ -3666,6 +3667,11 @@
"safe-buffer": "^5.1.0" "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": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -7276,6 +7282,11 @@
"safe-buffer": "^5.1.0" "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": { "range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "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" "test": "echo \"No test specified\" && exit 0"
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.0.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"@quasar/extras": "^1.0.0",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"randomcolor": "^0.6.2",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.0" "vue-router": "^4.0.0"
}, },
"devDependencies": { "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", "@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": { "engines": {
"node": "^18 || ^16 || ^14.19", "node": "^18 || ^16 || ^14.19",

View File

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

View File

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

View File

@ -2,6 +2,9 @@ import axios from "axios";
import {API_URL} from "./constants"; import {API_URL} from "./constants";
import { refreshSegments } from "src/api/segments"; import { refreshSegments } from "src/api/segments";
import { refreshComponents } from "src/api/components"; 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 { export class RailSystem {
constructor(data) { constructor(data) {
@ -15,6 +18,7 @@ export class RailSystem {
this.websocket = null; this.websocket = null;
this.selectedComponents = []; this.selectedComponents = [];
this.loaded = false;
} }
} }
@ -57,7 +61,9 @@ export function removeRailSystem(rsStore, id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.delete(`${API_URL}/rs/${id}`) axios.delete(`${API_URL}/rs/${id}`)
.then(() => { .then(() => {
rsStore.selectedRailSystem = null; if (rsStore.selectedRailSystem !== null && rsStore.selectedRailSystem.id === id) {
rsStore.selectRailSystem(null);
}
refreshRailSystems(rsStore) refreshRailSystems(rsStore)
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
@ -65,3 +71,41 @@ export function removeRailSystem(rsStore, id) {
.catch(reject); .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"; 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; 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. * @return {Promise} A promise that resolves when a connection is established.
*/ */
export function establishWebsocketConnection(rs) { export function establishWebsocketConnection(rs) {
if (rs.websocket) {
console.log('rail system ' + rs.id + ' already has websocket')
}
return new Promise(resolve => { return new Promise(resolve => {
rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`); rs.websocket = new WebSocket(`${WS_URL}/${rs.id}`);
rs.websocket.onopen = resolve; rs.websocket.onopen = resolve;
@ -29,7 +37,7 @@ export function establishWebsocketConnection(rs) {
const id = data.cId; const id = data.cId;
const idx = rs.components.findIndex(c => c.id === id); const idx = rs.components.findIndex(c => c.id === id);
if (idx > -1) { 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) { export function closeWebsocketConnection(rs) {
return new Promise(resolve => { return new Promise(resolve => {
if (rs.websocket) { if (rs.websocket && rs.websocket.readyState !== WebSocket.CLOSED) {
rs.websocket.onclose = resolve; rs.websocket.onclose = () => {
rs.websocket = null;
resolve();
};
rs.websocket.close(); rs.websocket.close();
} else { } else {
rs.websocket = null;
resolve(); 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) { setActiveSwitchConfig(sw, configId) {
updateSwitchConfiguration(this.railSystem, 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 clickable
v-ripple v-ripple
:to="'/'" :to="'/'"
@click="rsStore.selectedRailSystem = null" @click="rsStore.selectRailSystem(null)"
> >
<q-item-section> <q-item-section>
<q-item-label>Home</q-item-label> <q-item-label>Home</q-item-label>
@ -40,7 +40,7 @@
clickable clickable
v-ripple v-ripple
:to="'/about'" :to="'/about'"
@click="rsStore.selectedRailSystem = null" @click="rsStore.selectRailSystem(null)"
> >
<q-item-section> <q-item-section>
<q-item-label>About</q-item-label> <q-item-label>About</q-item-label>

View File

@ -7,8 +7,20 @@
<p> <p>
This application was developed by <a href="https://andrewlalis.github.io">Andrew Lalis</a> 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 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>
<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> <p>
This web app was built using <a href="https://quasar.dev">Quasar</a> 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 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 and Java 17. For more technical information, please visit Rail Signal's
<a href="https://github.com/andrewlalis/RailSignalAPI">GitHub repository</a>. <a href="https://github.com/andrewlalis/RailSignalAPI">GitHub repository</a>.
</p> </p>
<div class="text-h4"><q-icon :name="fasHeart"/> Support</div>
<p> <p>
If you're enjoying this app, please consider making a <a href="https://paypal.me/andrewlalis" target="_blank">donation to 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>. Andrew's work on paypal</a>.
@ -26,8 +39,13 @@
</template> </template>
<script> <script>
import {fasHeart, fasMicrochip} from "@quasar/extras/fontawesome-v6";
export default { export default {
name: "AboutPage" name: "AboutPage",
setup() {
return {fasHeart, fasMicrochip};
}
}; };
</script> </script>

View File

@ -12,9 +12,7 @@
</p> </p>
</div> </div>
</div> </div>
<div class="row"> <index-page-section title="Introduction to Rail Networks">
<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"/> <q-img src="~assets/img/guide/layout.png"/>
<p> <p>
The above diagram illustrates all of the basic concepts you need to 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 the ability to manage these automatically, so you can use this web
interface to configure switches instead of doing it manually. interface to configure switches instead of doing it manually.
</p> </p>
</div> </index-page-section>
<div class="col-md-6 q-pa-md"> <index-page-section title="Paths and Path Nodes">
<div class="text-h4">Paths and Path Nodes</div>
<p> <p>
We mentioned segment boundaries and switches earlier, as simple We mentioned segment boundaries and switches earlier, as simple
components that you can add to your network in order to link it to the 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. each of the segment boundaries that it allows traffic between.
</li> </li>
</ul> </ul>
</div> </index-page-section>
</div> <index-page-section title="Drivers">
<div class="row"> <p>
<div class="col-md-6 q-pa-md"> While you can play around in this web app as long as you'd like, the
<div class="text-h4">Advanced Usage</div> 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"/> <q-img src="~assets/img/guide/layout2.png"/>
<p> <p>
The above diagram shows a more typical network arrangement for a large 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 segment, while on the inbound segments, the signal will show the
status of the junction segment. status of the junction segment.
</p> </p>
</div> </index-page-section>
</div>
</q-page> </q-page>
</template> </template>
<script> <script>
import IndexPageSection from "components/IndexPageSection.vue";
export default { export default {
name: "IndexPage" name: "IndexPage",
components: { IndexPageSection }
}; };
</script> </script>

View File

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

View File

@ -14,3 +14,12 @@ export function circle(ctx, x, y, r) {
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2); 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. 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 { 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); const tx = DOMMatrix.fromMatrix(worldTx);
tx.translateSelf(component.position.x, component.position.z, 0); tx.translateSelf(component.position.x, component.position.z, 0);
const s = getScaleFactor(); const s = getScaleFactor();
@ -67,17 +146,26 @@ function drawSegmentBoundary(ctx) {
ctx.fill(); ctx.fill();
} }
function drawSwitch(ctx, sw) { function drawSwitchConfigurations(ctx, worldTx, sw) {
const colors = [ const tx = DOMMatrix.fromMatrix(worldTx);
`rgba(61, 148, 66, 0.25)`, tx.translateSelf(sw.position.x, sw.position.z, 0);
`rgba(59, 22, 135, 0.25)`, const s = getScaleFactor();
`rgba(145, 17, 90, 0.25)`, tx.scaleSelf(1/s, 1/s, 1/s);
`rgba(191, 49, 10, 0.25)` tx.scaleSelf(20, 20, 20);
]; ctx.setTransform(tx);
ctx.lineWidth = 1;
for (let i = 0; i < sw.possibleConfigurations.length; i++) { for (let i = 0; i < sw.possibleConfigurations.length; i++) {
const config = sw.possibleConfigurations[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++) { for (let j = 0; j < config.nodes.length; j++) {
const node = config.nodes[j]; const node = config.nodes[j];
const diff = { const diff = {
@ -85,14 +173,17 @@ function drawSwitch(ctx, sw) {
y: sw.position.z - node.position.z, y: sw.position.z - node.position.z,
}; };
const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2)); const mag = Math.sqrt(Math.pow(diff.x, 2) + Math.pow(diff.y, 2));
diff.x = 2 * -diff.x / mag; diff.x = 3 * -diff.x / mag;
diff.y = 2 * -diff.y / mag; diff.y = 3 * -diff.y / mag;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, 0); ctx.moveTo(0, 0);
ctx.lineTo(diff.x, diff.y); ctx.lineTo(diff.x, diff.y);
ctx.stroke(); ctx.stroke();
} }
} }
}
function drawSwitch(ctx, sw) {
ctx.fillStyle = `rgb(245, 188, 66)`; ctx.fillStyle = `rgb(245, 188, 66)`;
ctx.strokeStyle = `rgb(245, 188, 66)`; ctx.strokeStyle = `rgb(245, 188, 66)`;
ctx.lineWidth = 0.2; 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. 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_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; const SCALE_INDEX_NORMAL = 7;
@ -56,59 +56,12 @@ export function draw() {
ctx.resetTransform(); ctx.resetTransform();
ctx.fillStyle = `rgb(240, 240, 240)`; ctx.fillStyle = `rgb(240, 240, 240)`;
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
const worldTx = getWorldTransform();
ctx.setTransform(worldTx);
// Draw segments! drawMap(ctx, railSystem);
const segmentPoints = new Map(); drawDebugInfo(ctx);
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++) { function drawDebugInfo(ctx) {
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.resetTransform();
ctx.fillStyle = "black"; ctx.fillStyle = "black";
ctx.strokeStyle = "black"; ctx.strokeStyle = "black";
@ -133,7 +86,7 @@ export function getScaleFactor() {
* Gets a matrix that transforms world coordinates to canvas. * Gets a matrix that transforms world coordinates to canvas.
* @returns {DOMMatrix} * @returns {DOMMatrix}
*/ */
function getWorldTransform() { export function getWorldTransform() {
const canvasRect = mapCanvas.getBoundingClientRect(); const canvasRect = mapCanvas.getBoundingClientRect();
const scale = getScaleFactor(); const scale = getScaleFactor();
const tx = new DOMMatrix(); const tx = new DOMMatrix();
@ -159,7 +112,7 @@ export function isComponentSelected(component) {
* @param {DOMPoint} p * @param {DOMPoint} p
* @returns {DOMPoint} * @returns {DOMPoint}
*/ */
function mapPointToWorld(p) { export function mapPointToWorld(p) {
return getWorldTransform().invertSelf().transformPoint(p); return getWorldTransform().invertSelf().transformPoint(p);
} }
@ -168,7 +121,7 @@ function mapPointToWorld(p) {
* @param {DOMPoint} p * @param {DOMPoint} p
* @returns {DOMPoint} * @returns {DOMPoint}
*/ */
function worldPointToMap(p) { export function worldPointToMap(p) {
return getWorldTransform().transformPoint(p); return getWorldTransform().transformPoint(p);
} }

View File

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

View File

@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
* web app's index page. * web app's index page.
*/ */
@Controller @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 { public class IndexPageController {
@GetMapping @GetMapping
public String getIndex() { 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 nl.andrewl.railsignalapi.model.component.PathNode;
import java.util.Comparator;
import java.util.List; import java.util.List;
public abstract class PathNodeResponse extends ComponentResponse { public abstract class PathNodeResponse extends ComponentResponse {
@ -9,6 +10,9 @@ public abstract class PathNodeResponse extends ComponentResponse {
public PathNodeResponse(PathNode p) { public PathNodeResponse(PathNode p) {
super(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.model.component.SegmentBoundaryNode;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
import java.util.Comparator;
import java.util.List; import java.util.List;
public class SegmentBoundaryNodeResponse extends PathNodeResponse { public class SegmentBoundaryNodeResponse extends PathNodeResponse {
@ -10,6 +11,9 @@ public class SegmentBoundaryNodeResponse extends PathNodeResponse {
public SegmentBoundaryNodeResponse(SegmentBoundaryNode n) { public SegmentBoundaryNodeResponse(SegmentBoundaryNode n) {
super(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 nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
import java.util.Comparator;
import java.util.List; import java.util.List;
public record SwitchConfigurationResponse ( public record SwitchConfigurationResponse (
@ -11,7 +12,10 @@ public record SwitchConfigurationResponse (
public SwitchConfigurationResponse(SwitchConfiguration sc) { public SwitchConfigurationResponse(SwitchConfiguration sc) {
this( this(
sc.getId(), 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 nl.andrewl.railsignalapi.model.component.Switch;
import java.util.Comparator;
import java.util.List; import java.util.List;
public class SwitchResponse extends PathNodeResponse { public class SwitchResponse extends PathNodeResponse {
@ -10,7 +11,10 @@ public class SwitchResponse extends PathNodeResponse {
public SwitchResponse(Switch s) { public SwitchResponse(Switch s) {
super(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()); this.activeConfiguration = s.getActiveConfiguration() == null ? null : new SwitchConfigurationResponse(s.getActiveConfiguration());
} }
} }

View File

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