Fixed switch UI updating, websocket bugs, and context switching.
This commit is contained in:
parent
c16627e7d3
commit
bd6c87149a
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue