diff --git a/quasar-app/package-lock.json b/quasar-app/package-lock.json index 4f59a6a..870c3cc 100644 --- a/quasar-app/package-lock.json +++ b/quasar-app/package-lock.json @@ -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", diff --git a/quasar-app/package.json b/quasar-app/package.json index 435218d..1a6f09f 100644 --- a/quasar-app/package.json +++ b/quasar-app/package.json @@ -11,24 +11,25 @@ "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", "npm": ">= 6.13.4", "yarn": ">= 1.21.1" } -} \ No newline at end of file +} diff --git a/quasar-app/quasar.config.js b/quasar-app/quasar.config.js index 7a455a5..9be06bc 100644 --- a/quasar-app/quasar.config.js +++ b/quasar-app/quasar.config.js @@ -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 diff --git a/quasar-app/src/App.vue b/quasar-app/src/App.vue index 38442ee..8baf947 100644 --- a/quasar-app/src/App.vue +++ b/quasar-app/src/App.vue @@ -3,7 +3,7 @@ + + diff --git a/quasar-app/src/components/rs/SelectedComponentView.vue b/quasar-app/src/components/rs/SelectedComponentView.vue index a7fbb08..f63c11a 100644 --- a/quasar-app/src/components/rs/SelectedComponentView.vue +++ b/quasar-app/src/components/rs/SelectedComponentView.vue @@ -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; } } }; diff --git a/quasar-app/src/layouts/MainLayout.vue b/quasar-app/src/layouts/MainLayout.vue index 398edf3..15dbbf7 100644 --- a/quasar-app/src/layouts/MainLayout.vue +++ b/quasar-app/src/layouts/MainLayout.vue @@ -30,7 +30,7 @@ clickable v-ripple :to="'/'" - @click="rsStore.selectedRailSystem = null" + @click="rsStore.selectRailSystem(null)" > Home @@ -40,7 +40,7 @@ clickable v-ripple :to="'/about'" - @click="rsStore.selectedRailSystem = null" + @click="rsStore.selectRailSystem(null)" > About diff --git a/quasar-app/src/pages/AboutPage.vue b/quasar-app/src/pages/AboutPage.vue index e884b97..9063369 100644 --- a/quasar-app/src/pages/AboutPage.vue +++ b/quasar-app/src/pages/AboutPage.vue @@ -7,8 +7,20 @@

This application was developed by Andrew Lalis 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. + Theoretically, 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.

+

+ 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 create a new issue + on the GitHub repository. +

+
Technologies

This web app was built using Quasar and VueJS. The API that powers this @@ -16,6 +28,7 @@ and Java 17. For more technical information, please visit Rail Signal's GitHub repository.

+
Support

If you're enjoying this app, please consider making a donation to Andrew's work on paypal. @@ -26,8 +39,13 @@ diff --git a/quasar-app/src/pages/IndexPage.vue b/quasar-app/src/pages/IndexPage.vue index 38d2074..c4ae25a 100644 --- a/quasar-app/src/pages/IndexPage.vue +++ b/quasar-app/src/pages/IndexPage.vue @@ -12,110 +12,117 @@

-
-
-
Introduction to Rail Networks
- -

- The above diagram illustrates all of the basic concepts you need to - know in order to build and manage your rail networks. -

-

- Each rail system can be conceptually split up into lots of small - segments, 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! -

-

- At the places where segments meet, we see a segment boundary - 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. -

-

- Now that we've covered segments and segment boundaries, we can now - display a segment's status using a signal. A signal - is a device that is linked to a segment, and whenever the segment's - status updates (when a train enters or leaves it), 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. -

-

- Finally, unless you're just making a boring single-line loop, you'll - most likely have some switches - 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. -

-
-
-
Paths and Path Nodes
-

- 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. -

-

- Behind the scenes, your Rail Signal models your network as a set of - path nodes, 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. -

-
    -
  • - 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. -
  • -
  • - Switches are connected to nodes based on their set of defined - configurations. In the example diagram, our switch allows two - possible configurations: -
      -
    • Between the blue and purple segments.
    • -
    • Between the green and purple segments.
    • -
    - This implies that our switch node is connected to three other nodes: - each of the segment boundaries that it allows traffic between. -
  • -
-
-
-
-
-
Advanced Usage
- -

- 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. -

-
-
- + + +

+ The above diagram illustrates all of the basic concepts you need to + know in order to build and manage your rail networks. +

+

+ Each rail system can be conceptually split up into lots of small + segments, 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! +

+

+ At the places where segments meet, we see a segment boundary + 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. +

+

+ Now that we've covered segments and segment boundaries, we can now + display a segment's status using a signal. A signal + is a device that is linked to a segment, and whenever the segment's + status updates (when a train enters or leaves it), 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. +

+

+ Finally, unless you're just making a boring single-line loop, you'll + most likely have some switches + 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. +

+
+ +

+ 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. +

+

+ Behind the scenes, your Rail Signal models your network as a set of + path nodes, 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. +

+
    +
  • + 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. +
  • +
  • + Switches are connected to nodes based on their set of defined + configurations. In the example diagram, our switch allows two + possible configurations: +
      +
    • Between the blue and purple segments.
    • +
    • Between the green and purple segments.
    • +
    + This implies that our switch node is connected to three other nodes: + each of the segment boundaries that it allows traffic between. +
  • +
+
+ +

+ 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 driver, 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. +

+
+ + +

+ 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. +

+
diff --git a/quasar-app/src/pages/RailSystem.vue b/quasar-app/src/pages/RailSystem.vue index f73bc7e..195ea8d 100644 --- a/quasar-app/src/pages/RailSystem.vue +++ b/quasar-app/src/pages/RailSystem.vue @@ -1,6 +1,6 @@ @@ -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; + } } }; diff --git a/quasar-app/src/render/canvasUtils.js b/quasar-app/src/render/canvasUtils.js index 726b394..fbad3a5 100644 --- a/quasar-app/src/render/canvasUtils.js +++ b/quasar-app/src/render/canvasUtils.js @@ -13,4 +13,13 @@ export function roundedRect(ctx, x, y, w, h, r) { export function circle(ctx, x, y, r) { ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); -} \ No newline at end of file +} + +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; + } +} diff --git a/quasar-app/src/render/drawing.js b/quasar-app/src/render/drawing.js index 81d8f59..c722f20 100644 --- a/quasar-app/src/render/drawing.js +++ b/quasar-app/src/render/drawing.js @@ -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) { diff --git a/quasar-app/src/render/mapRenderer.js b/quasar-app/src/render/mapRenderer.js index 57e666e..450334b 100644 --- a/quasar-app/src/render/mapRenderer.js +++ b/quasar-app/src/render/mapRenderer.js @@ -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); } diff --git a/quasar-app/src/stores/railSystemsStore.js b/quasar-app/src/stores/railSystemsStore.js index d86195b..31488df 100644 --- a/quasar-app/src/stores/railSystemsStore.js +++ b/quasar-app/src/stores/railSystemsStore.js @@ -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)); diff --git a/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java b/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java index 02dfce2..e8db1f0 100644 --- a/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java +++ b/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java @@ -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() { diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java index 3d2bfee..503d4c2 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/PathNodeResponse.java @@ -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(); } } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java index 1af60e4..2d84737 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SegmentBoundaryNodeResponse.java @@ -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(); } } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java index eab8df2..1e2e58d 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchConfigurationResponse.java @@ -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() ); } } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java index 33ad8a9..81fce8f 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SwitchResponse.java @@ -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()); } } diff --git a/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java b/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java index 8c56af0..5218095 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java @@ -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;