Added improvements to advanced rail system components.

This commit is contained in:
Andrew Lalis 2022-06-01 20:31:50 +02:00
parent 3c9554ebba
commit cb8eed835a
14 changed files with 722 additions and 239 deletions

166
banner.svg Normal file
View File

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1280"
height="640"
viewBox="0 0 338.66666 169.33334"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="banner.svg"
inkscape:export-filename="/home/andrew/Pictures/railsignalbanner.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="-19.654761 : -18.142858 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="338.66666 : 84.66667 : 1"
inkscape:persp3d-origin="169.33333 : 56.444447 : 1"
id="perspective815" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="845.17179"
inkscape:cy="205.3419"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-127.66666)">
<rect
style="fill:#659a54;fill-opacity:1;stroke:none;stroke-width:12.70000076;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect870"
width="338.66666"
height="169.33333"
x="0"
y="127.66666" />
<rect
style="fill:#454e44;fill-opacity:1;stroke:none;stroke-width:12.70000076;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect911"
width="338.66666"
height="111.125"
x="-1.110223e-16"
y="185.875" />
<g
id="g852"
transform="rotate(90,151.19047,228.96429)">
<path
inkscape:connector-curvature="0"
id="path835"
d="m 122.84226,146.37648 h 92.98215"
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 122.84226,212.9003 h 92.98215"
id="path837"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path839"
d="m 122.84226,246.1622 h 92.98215"
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 122.84226,179.63839 h 92.98215"
id="path841"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path843"
d="m 122.84226,279.42411 h 92.98215"
style="fill:none;stroke:#4a2b00;stroke-width:12.69999981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path831"
d="M 148.35565,291.70833 V 129.93452"
style="fill:none;stroke:#989898;stroke-width:8.46666718;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#989898;stroke-width:8.46666718;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 190.31102,291.70833 V 129.93452"
id="path833"
inkscape:connector-curvature="0" />
</g>
<flowRoot
xml:space="preserve"
id="flowRoot913"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.57599614,0,0,0.57599614,-10.203222,101.60397)"><flowRegion
id="flowRegion915"
style="fill:#ffffff;fill-opacity:1;stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"><rect
id="rect917"
width="1102.8572"
height="162.85715"
x="51.42857"
y="34.285713"
style="fill:#ffffff;fill-opacity:1;stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></flowRegion><flowPara
id="flowPara919"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:96px;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono Bold';stroke:#102f07;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">Rail Signal</flowPara></flowRoot> <g
id="g933"
transform="translate(123.59822)">
<path
inkscape:connector-curvature="0"
id="path921"
d="m 169.33333,245.97321 v 46.49107"
style="fill:none;stroke:#000000;stroke-width:16.93333435;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
ry="2.6458333"
rx="2.6458333"
y="196.08035"
x="152.13544"
height="51.026794"
width="34.395802"
id="rect923"
style="fill:#000000;fill-opacity:1;stroke:#bababa;stroke-width:4.23333359;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="6.0476193"
cy="212.33333"
cx="169.33333"
id="path925"
style="fill:#1cd500;fill-opacity:1;stroke:none;stroke-width:8.46666718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#d50000;fill-opacity:1;stroke:none;stroke-width:8.46666718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle927"
cx="169.33333"
cy="228.5863"
r="6.0476193" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -98,6 +98,18 @@ export function removeComponent(rs, id) {
}); });
} }
export function updateComponent(rs, component) {
return new Promise((resolve, reject) => {
axios.patch(`${API_URL}/rs/${rs.id}/c/${component.id}`, component)
.then(() => {
refreshComponents(rs)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
}
export function updateSwitchConfiguration(rs, sw, configId) { export function updateSwitchConfiguration(rs, sw, configId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.post( axios.post(

View File

@ -62,7 +62,7 @@ export function removeRailSystem(rsStore, id) {
axios.delete(`${API_URL}/rs/${id}`) axios.delete(`${API_URL}/rs/${id}`)
.then(() => { .then(() => {
if (rsStore.selectedRailSystem !== null && rsStore.selectedRailSystem.id === id) { if (rsStore.selectedRailSystem !== null && rsStore.selectedRailSystem.id === id) {
rsStore.selectRailSystem(null); rsStore.selectedRailSystem = null;
} }
refreshRailSystems(rsStore) refreshRailSystems(rsStore)
.then(resolve) .then(resolve)

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-md-8" id="railSystemMapCanvasContainer"> <div class="col-md-8" id="railSystemMapCanvasContainer">
<canvas id="railSystemMapCanvas" height="800"> <canvas id="railSystemMapCanvas" height="600">
Your browser doesn't support canvas. Your browser doesn't support canvas.
</canvas> </canvas>
</div> </div>
<q-scroll-area class="col-md-4" v-if="railSystem.selectedComponents.length > 0"> <q-scroll-area class="col-md-4" v-if="railSystem.selectedComponents.length > 0">
<div class="row" v-for="component in railSystem.selectedComponents" :key="component.id"> <div class="row" v-for="component in railSystem.selectedComponents" :key="component.id">
<div class="col full-width"> <div class="col full-width">
<selected-component-view :component="component" :rail-system="railSystem" /> <selected-component-view :component="component"/>
</div> </div>
</div> </div>
@ -182,7 +182,6 @@ import { RailSystem } from "src/api/railSystems";
import {draw, initMap} from "src/render/mapRenderer"; import {draw, initMap} from "src/render/mapRenderer";
import SelectedComponentView from "components/rs/SelectedComponentView.vue"; import SelectedComponentView from "components/rs/SelectedComponentView.vue";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import { createComponent } from "src/api/components";
import AddComponentDialog from "components/rs/add_component/AddComponentDialog.vue"; import AddComponentDialog from "components/rs/add_component/AddComponentDialog.vue";
export default { export default {

View File

@ -1,184 +1,37 @@
<template> <template>
<div class="q-pa-md"> <label-component-view
<div class="text-h6">{{component.name}}</div> :label="component"
<q-list bordered> v-if="component.type === 'LABEL'"
<q-item clickable> />
<q-item-section> <signal-component-view
<q-item-label>{{component.type}}</q-item-label> :signal="component"
<q-item-label caption>Id: {{component.id}}</q-item-label> v-if="component.type === 'SIGNAL'"
</q-item-section> />
<q-item-section v-if="component.online === true" top side> <segment-boundary-component-view
<q-chip color="positive" text-color="white">Online</q-chip> :segment-boundary="component"
</q-item-section> v-if="component.type === 'SEGMENT_BOUNDARY'"
<q-item-section v-if="component.online === false" top side> />
<q-chip color="negative" text-color="white">Offline</q-chip> <switch-component-view
</q-item-section> :sw="component"
</q-item> v-if="component.type === 'SWITCH'"
<q-item clickable>
<q-item-section>
<q-item-label>Position</q-item-label>
<q-item-label caption>
X: {{component.position.x}},
Y: {{component.position.y}},
Z: {{component.position.z}}
</q-item-label>
</q-item-section>
</q-item>
<!-- Signal info -->
<q-expansion-item
id="signalInfo"
label="Signal Information"
v-if="component.type === 'SIGNAL'"
:content-inset-level="0.5"
switch-toggle-side
expand-separator
>
<q-list>
<segment-list-item v-for="segment in [component.segment]" :key="segment.id" :segment="segment" />
</q-list>
</q-expansion-item>
<!-- Path node info -->
<q-expansion-item
label="Connected Nodes"
v-if="component.connectedNodes !== undefined && component.connectedNodes !== null"
:content-inset-level="0.5"
switch-toggle-side
expand-separator
class="q-gutter-md"
>
<q-list>
<q-item
v-for="node in component.connectedNodes"
:key="node.id"
clickable
v-ripple
@click="select(node)"
>
<q-item-section>
<q-item-label>{{node.name}}</q-item-label>
<q-item-label caption>Id: {{node.id}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
<!-- Segment boundary info -->
<q-expansion-item
label="Connected Segments"
v-if="component.type === 'SEGMENT_BOUNDARY'"
:content-inset-level="0.5"
switch-toggle-side
expand-separator
>
<q-list>
<segment-list-item v-for="segment in component.segments" :key="segment.id" :segment="segment"/>
</q-list>
</q-expansion-item>
<!-- Switch info -->
<q-expansion-item
label="Switch Information"
v-if="component.type === 'SWITCH'"
:content-inset-level="0.5"
switch-toggle-side
expand-separator
>
<q-list>
<q-item
v-for="config in component.possibleConfigurations"
:key="config.id"
>
<q-item-section>
<q-item-label class="q-gutter-sm">
<q-chip
v-for="node in config.nodes"
:key="node.id"
dense
size="sm"
:label="node.name"
clickable
@click="select(node)"
/> />
</q-item-label>
</q-item-section>
<q-item-section v-if="component.activeConfiguration === null || component.activeConfiguration.id !== config.id" side>
<q-btn dense size="sm" color="positive" @click="setActiveSwitchConfig(component, config.id)">Set Active</q-btn>
</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
<q-item>
<q-item-section side>
<q-btn color="warning" label="Remove" size="sm" @click="remove(component)"/>
</q-item-section>
</q-item>
</q-list>
</div>
</template> </template>
<script> <script>
import { RailSystem } from "src/api/railSystems"; import SignalComponentView from "components/rs/component_views/SignalComponentView.vue";
import { useRailSystemsStore } from "stores/railSystemsStore"; import SegmentBoundaryComponentView from "components/rs/component_views/SegmentBoundaryComponentView.vue";
import SegmentListItem from "components/rs/SegmentListItem.vue"; import LabelComponentView from "components/rs/component_views/LabelComponentView.vue";
import { useQuasar } from "quasar"; import SwitchComponentView from "components/rs/component_views/SwitchComponentView.vue";
import { removeComponent, updateSwitchConfiguration } from "src/api/components";
export default { export default {
name: "SelectedComponentView", name: "SelectedComponentView",
components: { SegmentListItem }, components: {
setup() { SwitchComponentView,
const rsStore = useRailSystemsStore(); LabelComponentView, SegmentBoundaryComponentView, SignalComponentView },
const quasar = useQuasar();
return {rsStore, quasar};
},
props: { props: {
component: { component: {
type: Object, type: Object,
required: true required: true
},
railSystem: {
type: RailSystem,
required: true
}
},
methods: {
select(component) {
const c = this.rsStore.selectedRailSystem.components.find(cp => cp.id === component.id);
if (c) {
this.rsStore.selectedRailSystem.selectedComponents.length = 0;
this.rsStore.selectedRailSystem.selectedComponents.push(c);
}
},
remove(component) {
this.quasar.dialog({
title: "Confirm Removal",
message: "Are you sure you want to remove this component? This cannot be undone.",
cancel: true
}).onOk(() => {
removeComponent(this.railSystem, component.id)
.then(() => {
this.quasar.notify({
color: "positive",
message: "Component has been removed."
});
})
.catch(error => {
this.quasar.notify({
color: "negative",
message: "An error occurred: " + error.response.data.message
});
});
});
},
setActiveSwitchConfig(sw, configId) {
updateSwitchConfiguration(this.railSystem, sw, configId);
},
isConfigActive(sw, config) {
return sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id;
} }
} }
}; };

View File

@ -0,0 +1,94 @@
<template>
<div class="q-pa-md">
<div class="text-h6">{{component.name}}</div>
<q-list bordered>
<q-item clickable>
<q-item-section>
<q-item-label>{{component.type}}</q-item-label>
<q-item-label caption>Id: {{component.id}}</q-item-label>
</q-item-section>
<q-item-section v-if="component.online === true" top side>
<q-chip color="positive" text-color="white">Online</q-chip>
</q-item-section>
<q-item-section v-if="component.online === false" top side>
<q-chip color="negative" text-color="white">Offline</q-chip>
</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>
<q-item-label>Position</q-item-label>
<q-item-label caption>
X: {{component.position.x}},
Y: {{component.position.y}},
Z: {{component.position.z}}
</q-item-label>
</q-item-section>
</q-item>
<q-separator/>
<slot></slot>
<q-item>
<q-item-section side>
<q-btn color="negative" label="Remove" size="sm" @click="remove(component)"/>
</q-item-section>
</q-item>
</q-list>
</div>
</template>
<script>
import {removeComponent} from "src/api/components";
import {useQuasar} from "quasar";
import {useRailSystemsStore} from "stores/railSystemsStore";
export default {
name: "BaseComponentView",
props: {
component: {
type: Object,
required: true
}
},
setup() {
const rsStore = useRailSystemsStore();
const quasar = useQuasar();
return {rsStore, quasar};
},
methods: {
select(component) {
const c = this.rsStore.selectedRailSystem.components.find(cp => cp.id === component.id);
if (c) {
this.rsStore.selectedRailSystem.selectedComponents.length = 0;
this.rsStore.selectedRailSystem.selectedComponents.push(c);
}
},
remove(component) {
this.quasar.dialog({
title: "Confirm Removal",
message: "Are you sure you want to remove this component? This cannot be undone.",
cancel: true
}).onOk(() => {
removeComponent(this.railSystem, component.id)
.then(() => {
this.quasar.notify({
color: "positive",
message: "Component has been removed."
});
})
.catch(error => {
this.quasar.notify({
color: "negative",
message: "An error occurred: " + error.response.data.message
});
});
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,28 @@
<template>
<base-component-view :component="label">
<q-item-label header>Label</q-item-label>
<q-item>
<q-item-section>
<q-item-label>{{label.text}}</q-item-label>
</q-item-section>
</q-item>
</base-component-view>
</template>
<script>
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
export default {
name: "LabelComponentView",
components: {BaseComponentView},
props: {
label: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,125 @@
<template>
<q-item-label header>Connected Path Nodes</q-item-label>
<q-item>
<q-item-section>
<q-item-label class="q-gutter-sm">
<q-chip
v-for="node in pathNode.connectedNodes"
:key="node.id"
dense
size="sm"
:label="node.name"
/>
</q-item-label>
</q-item-section>
</q-item>
<q-item dense v-if="editable">
<q-btn size="sm" color="accent" label="Edit Connected Nodes" @click="showDialog"/>
<q-dialog v-model="dialog.visible" style="min-width: 400px" @hide="reset">
<q-card>
<q-form @submit="onSubmit" @reset="reset">
<q-card-section>
<div class="text-h6">Edit Connected Nodes</div>
<p>
Update the nodes that this path node is connected to. These
connections define how the system understands your rail network,
and are used for routing and analytics.
</p>
<p v-if="pathNode.type ==='SEGMENT_BOUNDARY'">
You're editing a <strong>segment boundary</strong> node. This
means that you can only have at most <strong>2</strong> nodes
connected to it.
</p>
</q-card-section>
<q-card-section>
<q-select
v-model="dialog.selectedNodes"
:options="getEligibleNodes()"
multiple
:option-value="node => node"
:option-label="node => node.name"
use-chips
stack-label
label="Nodes"
/>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" type="reset" @click="dialog.visible = false"/>
<q-btn flat label="Edit" type="submit"/>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</q-item>
</template>
<script>
import {useRailSystemsStore} from "stores/railSystemsStore";
import {useQuasar} from "quasar";
import {updateComponent} from "src/api/components";
export default {
name: "PathNodeItem",
props: {
pathNode: {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false
}
},
setup() {
const rsStore = useRailSystemsStore();
const quasar = useQuasar();
return {rsStore, quasar};
},
data() {
return {
dialog: {
visible: false,
selectedNodes: []
}
}
},
methods: {
showDialog() {
this.dialog.selectedNodes = this.pathNode.connectedNodes.slice();
this.dialog.visible = true;
},
reset() {
this.dialog.selectedNodes.length = 0;
},
onSubmit() {
const data = {...this.pathNode};
data.connectedNodes = this.dialog.selectedNodes;
updateComponent(this.rsStore.selectedRailSystem, data)
.then(() => {
this.dialog.visible = false;
this.quasar.notify({
color: "positive",
message: "Path node updated."
});
})
.catch(error => {
console.log(error);
this.quasar.notify({
color: "negative",
message: "An error occurred: " + error.response.data.message
});
});
},
getEligibleNodes() {
return this.rsStore.selectedRailSystem.components.filter(c => {
return (c.connectedNodes !== null && c.connectedNodes !== undefined) &&
this.dialog.selectedNodes.findIndex(node => node.id === c.id) === -1;
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,35 @@
<template>
<base-component-view :component="segmentBoundary">
<q-item-label header>Connected Segments</q-item-label>
<q-item>
<q-item-section
v-for="segment in segmentBoundary.segments"
:key="segment.id"
>
<q-item-label>{{segment.name}}</q-item-label>
<q-item-label caption>ID: {{segment.id}}</q-item-label>
</q-item-section>
</q-item>
<q-separator/>
<path-node-item :path-node="segmentBoundary" :editable="true"/>
</base-component-view>
</template>
<script>
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
import PathNodeItem from "components/rs/component_views/PathNodeItem.vue";
export default {
name: "SegmentBoundaryComponentView",
components: {PathNodeItem, BaseComponentView},
props: {
segmentBoundary: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,25 @@
<template>
<base-component-view :component="signal">
<q-item-label header>Connected to Segment</q-item-label>
<segment-list-item :segment="signal.segment"/>
</base-component-view>
</template>
<script>
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
import SegmentListItem from "components/rs/SegmentListItem.vue";
export default {
name: "SignalComponentView",
components: {SegmentListItem, BaseComponentView},
props: {
signal: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,200 @@
<template>
<base-component-view :component="sw">
<q-item-label header>Switch Configurations</q-item-label>
<q-list>
<q-item
v-for="config in sw.possibleConfigurations"
:key="config.id"
dense
>
<q-item-section>
<q-item-label class="q-gutter-sm">
<q-chip
v-for="node in config.nodes"
:key="node.id"
dense
size="sm"
:label="node.name"
clickable
/>
</q-item-label>
<q-item-label caption>Configuration #{{config.id}}</q-item-label>
</q-item-section>
<q-item-section v-if="!isConfigActive(config)" side top>
<q-btn dense size="sm" color="positive" @click="setActiveSwitchConfig(config.id)">Set Active</q-btn>
</q-item-section>
</q-item>
<q-item>
<q-btn size="sm" color="accent" label="Edit Configurations" @click="showDialog"/>
<q-dialog v-model="dialog.visible" style="min-width: 400px" @hide="reset">
<q-card>
<q-form @submit="onSubmit" @reset="reset">
<q-card-section>
<div class="text-h6">Edit Switch Configurations</div>
<p>
Edit the possible configurations for this switch.
</p>
</q-card-section>
<q-card-section>
<div class="row">
<q-list>
<q-item
v-for="config in dialog.configurations"
:key="config.key"
>
<q-item-section>
<q-item-label>
<q-chip
v-for="node in config.nodes"
:key="node.id"
:label="node.name"
/>
</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn size="12px" flat dense round icon="delete" @click="removeConfig(config)"/>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="row">
<div class="col-sm-6">
<q-select
v-model="dialog.pathNode1"
:options="getEligibleSwitchNodes(dialog.pathNode2)"
:option-value="node => node"
:option-label="node => node.name"
label="First Path Node"
/>
</div>
<div class="col-sm-6">
<q-select
v-model="dialog.pathNode2"
:options="getEligibleSwitchNodes(dialog.pathNode1)"
:option-value="node => node"
:option-label="node => node.name"
label="Second Path Node"
/>
</div>
</div>
<div class="row">
<q-btn label="Add Configuration" @click="addConfiguration" v-if="canAddConfig(dialog.pathNode1, dialog.pathNode2)"/>
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" type="reset" @click="dialog.visible = false"/>
<q-btn flat label="Edit" type="submit"/>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</q-item>
</q-list>
<q-separator/>
<path-node-item :path-node="sw"/>
</base-component-view>
</template>
<script>
import BaseComponentView from "components/rs/component_views/BaseComponentView.vue";
import {updateComponent, updateSwitchConfiguration} from "src/api/components";
import PathNodeItem from "components/rs/component_views/PathNodeItem.vue";
import {useRailSystemsStore} from "stores/railSystemsStore";
import {useQuasar} from "quasar";
export default {
name: "SwitchComponentView",
components: {PathNodeItem, BaseComponentView},
props: {
sw: {
type: Object,
required: true
}
},
setup() {
const rsStore = useRailSystemsStore();
const quasar = useQuasar();
return {rsStore, quasar};
},
data() {
return {
dialog: {
visible: false,
configurations: [],
pathNode1: null,
pathNode2: null
}
}
},
methods: {
setActiveSwitchConfig(configId) {
updateSwitchConfiguration(this.railSystem, this.sw, configId);
},
isConfigActive(config) {
return this.sw.activeConfiguration !== null && this.sw.activeConfiguration.id === config.id;
},
showDialog() {
this.dialog.configurations = this.sw.possibleConfigurations.slice();
this.dialog.configurations.forEach(cfg => {
cfg.key = cfg.nodes[0].id + '_' + cfg.nodes[1].id
});
this.dialog.visible = true;
},
reset() {
this.dialog.pathNode1 = null;
this.dialog.pathNode2 = null;
this.dialog.configurations.length = 0;
},
onSubmit() {
const data = {...this.sw};
data.possibleConfigurations = this.dialog.configurations;
data.activeConfiguration = null;
updateComponent(this.rsStore.selectedRailSystem, data)
.then(() => {
this.dialog.visible = false;
this.quasar.notify({
color: "positive",
message: "Switch configurations updated."
});
})
.catch(error => {
console.log(error);
this.quasar.notify({
color: "negative",
message: "An error occurred: " + error.response.data.message
});
});
},
getEligibleSwitchNodes(exclude) {
return this.rsStore.selectedRailSystem.components.filter(c => {
return (c.connectedNodes !== undefined && c.connectedNodes !== null) &&
(exclude === null || c.id !== exclude.id);
});
},
canAddConfig(n1, n2) {
return this.dialog.configurations.length < 2 &&
n1 !== null && n2 !== null &&
n1.id !== n2.id &&
!this.dialog.configurations.some(config => {
return config.nodes.every(node => {
return node.id === n1.id || node.id === n2.id;
});
});
},
addConfiguration() {
this.dialog.configurations.push({
nodes: [this.dialog.pathNode1, this.dialog.pathNode2],
key: this.dialog.pathNode1.id + '_' + this.dialog.pathNode2.id
});
},
removeConfig(config) {
const idx = this.dialog.configurations.findIndex(cfg => cfg.key === config.key);
if (idx === -1) return;
this.dialog.configurations.splice(idx, 1);
}
}
}
</script>
<style scoped>
</style>

View File

@ -26,22 +26,12 @@
bordered bordered
> >
<rail-systems-list :rail-systems="rsStore.railSystems" /> <rail-systems-list :rail-systems="rsStore.railSystems" />
<q-item <q-item clickable v-ripple :to="'/'">
clickable
v-ripple
:to="'/'"
@click="rsStore.selectRailSystem(null)"
>
<q-item-section> <q-item-section>
<q-item-label>Home</q-item-label> <q-item-label>Home</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item <q-item clickable v-ripple :to="'/about'">
clickable
v-ripple
:to="'/about'"
@click="rsStore.selectRailSystem(null)"
>
<q-item-section> <q-item-section>
<q-item-label>About</q-item-label> <q-item-label>About</q-item-label>
</q-item-section> </q-item-section>
@ -68,7 +58,6 @@ export default defineComponent({
setup () { setup () {
const rsStore = useRailSystemsStore() const rsStore = useRailSystemsStore()
const leftDrawerOpen = ref(false) const leftDrawerOpen = ref(false)
return { return {
rsStore, rsStore,
leftDrawerOpen, leftDrawerOpen,

View File

@ -71,8 +71,11 @@ function drawDebugInfo(ctx) {
"Scale factor: " + getScaleFactor(), "Scale factor: " + getScaleFactor(),
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`, `(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
`Components: ${railSystem.components.length}`, `Components: ${railSystem.components.length}`,
`Hovered elements: ${hoveredElements.length}` `Hovered components: ${hoveredElements.length}`
] ]
for (let i = 0; i < hoveredElements.length; i++) {
lines.push(" " + hoveredElements[i].name);
}
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 10, 20 + (i * 15)); ctx.fillText(lines[i], 10, 20 + (i * 15));
} }

View File

@ -1,10 +1,4 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import { refreshSegments } from "../api/segments";
import { refreshComponents } from "../api/components";
import { closeWebsocketConnection, establishWebsocketConnection } from "../api/websocket";
import { refreshRailSystems } from "src/api/railSystems";
import { refreshLinkTokens } from "src/api/linkTokens";
import { refreshSettings } from "src/api/settings";
export const useRailSystemsStore = defineStore('RailSystemsStore', { export const useRailSystemsStore = defineStore('RailSystemsStore', {
state: () => ({ state: () => ({
@ -16,45 +10,5 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
* @type {RailSystem | null} * @type {RailSystem | null}
*/ */
selectedRailSystem: null selectedRailSystem: null
}), })
actions: {
/**
* Updates the selected rail system.
* @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 !== 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));
updatePromises.push(refreshLinkTokens(rs));
updatePromises.push(refreshSettings(rs));
updatePromises.push(establishWebsocketConnection(rs));
Promise.all(updatePromises).then(() => {
this.selectedRailSystem = rs;
resolve();
});
});
});
});
}
},
getters: {
rsId() {
if (this.selectedRailSystem === null) return null;
return this.selectedRailSystem.id;
}
}
}); });