Added modals, icon, and better formatting.

This commit is contained in:
Andrew Lalis 2022-05-08 18:17:51 +02:00
parent a02758ecd4
commit ba409985e5
40 changed files with 1173 additions and 372 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RailSignal</title> <title>Rail Signal</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -8,8 +8,9 @@
"name": "railsignal-app", "name": "railsignal-app",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "^4.6.1", "bootstrap": "^5.1.3",
"pinia": "^2.0.14", "pinia": "^2.0.14",
"three": "^0.140.0", "three": "^0.140.0",
"vue": "^3.2.33", "vue": "^3.2.33",
@ -73,6 +74,15 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true "dev": true
}, },
"node_modules/@popperjs/core": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.2.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.2.tgz",
@ -287,16 +297,15 @@
"dev": true "dev": true
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "4.6.1", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==", "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/bootstrap" "url": "https://opencollective.com/bootstrap"
}, },
"peerDependencies": { "peerDependencies": {
"jquery": "1.9.1 - 3", "@popperjs/core": "^2.10.2"
"popper.js": "^1.16.1"
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
@ -1262,12 +1271,6 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true "dev": true
}, },
"node_modules/jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
"peer": true
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -1520,17 +1523,6 @@
} }
} }
}, },
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.13", "version": "8.4.13",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz",
@ -1978,6 +1970,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true "dev": true
}, },
"@popperjs/core": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
},
"@vitejs/plugin-vue": { "@vitejs/plugin-vue": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.2.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.2.tgz",
@ -2162,9 +2159,9 @@
"dev": true "dev": true
}, },
"bootstrap": { "bootstrap": {
"version": "4.6.1", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==", "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
"requires": {} "requires": {}
}, },
"brace-expansion": { "brace-expansion": {
@ -2781,12 +2778,6 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true "dev": true
}, },
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
"peer": true
},
"js-yaml": { "js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -2967,12 +2958,6 @@
} }
} }
}, },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"peer": true
},
"postcss": { "postcss": {
"version": "8.4.13", "version": "8.4.13",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz",

View File

@ -8,7 +8,9 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "^5.1.3",
"pinia": "^2.0.14", "pinia": "^2.0.14",
"three": "^0.140.0", "three": "^0.140.0",
"vue": "^3.2.33", "vue": "^3.2.33",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -1,21 +1,17 @@
<template> <template>
<header> <AppNavbar />
<h1>RailSignal</h1>
</header>
<RailSystemsManager />
<RailSystem v-if="rsStore.selectedRailSystem !== null" :railSystem="rsStore.selectedRailSystem"/> <RailSystem v-if="rsStore.selectedRailSystem !== null" :railSystem="rsStore.selectedRailSystem"/>
</template> </template>
<script> <script>
import RailSystem from "./components/RailSystem.vue";
import RailSystemsManager from "./components/RailSystemsManager.vue";
import {useRailSystemsStore} from "./stores/railSystemsStore"; import {useRailSystemsStore} from "./stores/railSystemsStore";
import AppNavbar from "./components/AppNavbar.vue";
import RailSystem from "./components/RailSystem.vue";
export default { export default {
components: { components: {
RailSystem, AppNavbar,
RailSystemsManager RailSystem
}, },
setup() { setup() {
const rsStore = useRailSystemsStore(); const rsStore = useRailSystemsStore();

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,90 @@
<?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="256"
height="256"
viewBox="0 0 67.733332 67.733335"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="icon.svg"
inkscape:export-filename="/home/andrew/Programming/github-andrewlalis/RailSignalAPI/railsignal-app/src/assets/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="164.88542"
inkscape:cy="100.52253"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:pagecheckerboard="true"
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,-229.26665)">
<rect
id="rect3713"
width="52.916668"
height="67.73333"
x="7.4083328"
y="229.26665"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
rx="13.229167" />
<rect
rx="12.30785"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.24762905"
y="231.24725"
x="9.250967"
height="63.772137"
width="49.2314"
id="rect4520" />
<circle
style="fill:#00d900;fill-opacity:1;stroke:none;stroke-width:0.65214598"
id="path4522"
cx="33.866665"
cy="248.70078"
r="10.364463" />
<circle
r="10.364463"
cy="277.56586"
cx="33.866665"
id="circle4524"
style="fill:#e71e00;fill-opacity:1;stroke:none;stroke-width:0.65214598" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,91 @@
<template>
<nav class="navbar navbar-expand-md navbar-light bg-light">
<div class="container-fluid">
<span class="navbar-brand h1 mb-0">
<img src="@/assets/icon.svg" height="24"/>
Rail Signal
</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2, mb-md-0">
<li class="nav-item me-2 mb-2 mb-md-0">
<select
id="railSystemSelect"
v-model="rsStore.selectedRailSystem"
class="form-select form-select-sm"
>
<option v-for="rs in rsStore.railSystems" :key="rs.id" :value="rs">
{{rs.name}}
</option>
</select>
</li>
<li class="nav-item me-2" v-if="rsStore.selectedRailSystem !== null">
<button
@click="removeRailSystem()"
class="btn btn-danger btn-sm"
type="button"
>
Remove this Rail System
</button>
</li>
<li class="nav-item">
<button
type="button"
class="btn btn-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#addRailSystemModal"
>
Add Rail System
</button>
<AddRailSystem />
</li>
</ul>
</div>
</div>
</nav>
<ConfirmModal
ref="confirmModal"
:id="'removeRailSystemModal'"
:title="'Confirm Rail System Removal'"
:message="'Are you sure you want to remove this rail system? This CANNOT be undone. All data will be permanently lost.'"
/>
</template>
<script>
import {useRailSystemsStore} from "../stores/railSystemsStore";
import AddRailSystemModal from "./railsystem/AddRailSystemModal.vue";
import ConfirmModal from "./ConfirmModal.vue";
export default {
name: "AppNavbar",
components: {AddRailSystem: AddRailSystemModal, ConfirmModal},
setup() {
const rsStore = useRailSystemsStore();
rsStore.$subscribe(mutation => {
const evt = mutation.events;
if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
rsStore.fetchSelectedRailSystemData();
}
});
return {
rsStore
};
},
mounted() {
this.rsStore.refreshRailSystems();
},
methods: {
removeRailSystem() {
this.$refs.confirmModal.showConfirm()
.then(() => this.rsStore.removeRailSystem(this.rsStore.selectedRailSystem));
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,77 @@
<template>
<div class="modal fade" tabindex="-1" :id="id">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{title}}</h5>
</div>
<div class="modal-body">
<p>{{message}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" :id="id + '_cancel'">Cancel</button>
<button type="button" class="btn btn-primary" :id="id + '_yes'">Yes</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {Modal} from "bootstrap";
export default {
name: "ConfirmModal",
props: {
id: {
type: String,
required: true
},
message: {
type: String,
required: false,
default: "Are you sure you want to continue?"
},
title: {
type: String,
required: false,
default: "Confirm"
}
},
expose: ['showConfirm'],
methods: {
showConfirm() {
return new Promise(resolve => {
console.log(this.id);
console.log(this.title);
const modalElement = document.getElementById(this.id);
const modal = new Modal(modalElement);
console.log(modal);
function onDismiss() {
modal.hide();
}
function onYes() {
modalElement.addEventListener("hidden.bs.modal", function onSuccess() {
modalElement.removeEventListener("hidden.bs.modal", onSuccess);
resolve();
});
modal.hide();
}
const cancelButton = document.getElementById(this.id + "_cancel");
cancelButton.removeEventListener("click", onDismiss);
cancelButton.addEventListener("click", onDismiss)
const yesButton = document.getElementById(this.id + "_yes");
yesButton.removeEventListener("click", onYes);
yesButton.addEventListener("click", onYes);
modal.show(modalElement);
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,26 +1,25 @@
<template> <template>
<h2>{{railSystem.name}}</h2> <div class="container-fluid">
<div> <div class="row">
<MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" /> <div class="col-md-8 p-0">
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent" :railSystem="railSystem"/> <MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" />
</div>
<div class="col-md-4">
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent" :railSystem="railSystem"/>
<RailSystemPropertiesView v-if="!railSystem.selectedComponent" :railSystem="railSystem"/>
</div>
</div>
</div> </div>
<SegmentsView />
<AddSignal v-if="railSystem.segments && railSystem.segments.length > 0" />
<AddSegmentBoundary v-if="railSystem.segments && railSystem.segments.length > 0" />
</template> </template>
<script> <script>
import MapView from './railsystem/MapView.vue' import MapView from './railsystem/MapView.vue'
import ComponentView from './railsystem/component/ComponentView.vue' import ComponentView from './railsystem/component/ComponentView.vue'
import SegmentsView from "./railsystem/SegmentsView.vue"; import RailSystemPropertiesView from "./railsystem/RailSystemPropertiesView.vue";
import AddSignal from "./railsystem/component/AddSignal.vue";
import AddSegmentBoundary from "./railsystem/component/AddSegmentBoundary.vue";
export default { export default {
components: { components: {
AddSignal, RailSystemPropertiesView,
AddSegmentBoundary,
SegmentsView,
MapView, MapView,
ComponentView ComponentView
}, },

View File

@ -1,62 +0,0 @@
<template>
<select v-model="rsStore.selectedRailSystem">
<option v-for="rs in rsStore.railSystems" :key="rs.id" :value="rs">
{{rs.name}}
</option>
</select>
<p v-if="rsStore.railSystems.length === 0">
There are no rail systems.
</p>
<button v-if="rsStore.selectedRailSystem !== null" @click="rsStore.removeRailSystem(rsStore.selectedRailSystem)">
Remove this Rail System
</button>
<h3>Create a New Rail System</h3>
<form>
<label for="rsNameInput">Name</label>
<input id="rsNameInput" type="text" v-model="formData.rsName"/>
<button type="submit" @click.prevent="formSubmitted">Submit</button>
</form>
</template>
<script>
import {useRailSystemsStore} from "../stores/railSystemsStore";
export default {
name: "RailSystemsManager.vue",
setup() {
const rsStore = useRailSystemsStore();
rsStore.$subscribe(mutation => {
const evt = mutation.events;
if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
rsStore.fetchSelectedRailSystemData();
}
});
return {
rsStore
};
},
data() {
return {
formData: {
rsName: ""
}
}
},
methods: {
formSubmitted() {
this.rsStore.createRailSystem(this.formData.rsName)
.then(() => {
this.formData.rsName = "";
});
}
},
mounted() {
this.rsStore.refreshRailSystems();
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,77 @@
<template>
<div class="modal fade" tabindex="-1" id="addRailSystemModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Rail System</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<label for="addRailSystemNameInput" class="form-label">Name</label>
<input
id="addRailSystemNameInput"
class="form-control"
type="text"
v-model="formData.rsName"
required
/>
</form>
<div v-if="warnings.length > 0">
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
{{msg}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {useRailSystemsStore} from "../../stores/railSystemsStore";
import {Modal} from "bootstrap";
export default {
name: "AddRailSystem",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: ""
},
warnings: []
};
},
methods: {
formSubmitted() {
this.rsStore.createRailSystem(this.formData.rsName)
.then(rs => {
this.formData.rsName = "";
const modal = Modal.getInstance(document.getElementById("addRailSystemModal"));
modal.hide();
this.rsStore.selectedRailSystem = rs;
})
.catch(error => {
console.log(error);
this.warnings.length = 0;
this.warnings.push("Couldn't add the rail system: " + error.response.data.message);
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,33 +0,0 @@
<template>
<h4>Add Segment</h4>
<form @submit.prevent="rsStore.addSegment(this.formData.segmentName)">
<label for="addSegmentName">Name</label>
<input type="text" v-model="formData.segmentName" />
<button type="submit">Add</button>
</form>
</template>
<script>
import {useRailSystemsStore} from "../../stores/railSystemsStore";
export default {
name: "AddSegment",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
segmentName: ""
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,86 @@
<template>
<div class="modal fade" tabindex="-1" id="addSegmentModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Segment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Add a new segment to this rail system. A <em>segment</em> is the
basic organizational unit of any rail system. It is a section of
the network that signals can monitor, and <em>segment boundary nodes</em>
define the extent of the segment, and monitor trains entering and
leaving the segment.
</p>
<p>
You can think of a segment as a single, secure block of of the rail
network that only one train may pass through at once. For example,
a junction or station siding.
</p>
<form>
<label for="addSegmentName" class="form-label">Name</label>
<input
id="addSegmentName"
class="form-control"
type="text"
v-model="formData.name"
required
/>
</form>
<div v-if="warnings.length > 0">
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
{{msg}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {useRailSystemsStore} from "../../stores/railSystemsStore";
import {Modal} from "bootstrap";
export default {
name: "AddSegmentModal",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: ""
},
warnings: []
};
},
methods: {
formSubmitted() {
const modal = Modal.getInstance(document.getElementById("addSegmentModal"));
this.rsStore.addSegment(this.formData.name)
.then(() => {
this.formData.name = "";
modal.hide();
})
.catch(error => {
this.warnings.length = 0;
this.warnings.push("Couldn't add the segment: " + error.response.data.message)
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,5 +1,5 @@
<script> <script>
import {initMap} from "./mapRenderer.js"; import {initMap, draw} from "./mapRenderer.js";
export default { export default {
props: { props: {
@ -15,18 +15,33 @@ export default {
updated() { updated() {
// Also, re-initialize any time this view is updated. // Also, re-initialize any time this view is updated.
initMap(this.railSystem); initMap(this.railSystem);
},
watch: {
railSystem: {
handler() {
draw();
},
deep: true
}
} }
} }
</script> </script>
<template> <template>
<canvas id="railSystemMapCanvas" width="1000" height="600"> <div class="canvas-container" id="railSystemMapCanvasContainer">
<canvas id="railSystemMapCanvas">
Your browser doesn't support canvas! Your browser doesn't support canvas!
</canvas> </canvas>
</div>
</template> </template>
<style> <style>
.canvas-container {
width: 100%;
height: 800px;
}
canvas { canvas {
border: 1px solid black; width: 100%;
height: 100%;
} }
</style> </style>

View File

@ -0,0 +1,73 @@
<template>
<h3>Rail System: <em>{{railSystem.name}}</em></h3>
<SegmentsView />
<button
type="button"
class="btn btn-success btn-sm me-2"
data-bs-toggle="modal"
data-bs-target="#addSegmentModal"
>
Add Segment
</button>
<AddSegmentModal />
<span v-if="railSystem.segments && railSystem.segments.length > 0">
<button
type="button"
class="btn btn-success btn-sm me-2"
data-bs-toggle="modal"
data-bs-target="#addSignalModal"
>
Add Signal
</button>
<AddSignalModal />
<button
type="button"
class="btn btn-success btn-sm me-2"
data-bs-toggle="modal"
data-bs-target="#addSegmentBoundaryModal"
>
Add Segment Boundary
</button>
<AddSegmentBoundaryModal />
</span>
<span v-if="railSystem.components && railSystem.components.length > 1">
<button
type="button"
class="btn btn-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#addSwitchModal"
>
Add Switch
</button>
<AddSwitchModal />
</span>
</template>
<script>
import SegmentsView from "./SegmentsView.vue";
import AddSegmentModal from "./AddSegmentModal.vue";
import AddSignalModal from "./component/AddSignalModal.vue";
import AddSegmentBoundaryModal from "./component/AddSegmentBoundaryModal.vue";
import AddSwitchModal from "./component/AddSwitchModal.vue";
export default {
name: "RailSystemPropertiesView",
components: {
AddSwitchModal,
AddSegmentBoundaryModal,
AddSignalModal,
AddSegmentModal,
SegmentsView
},
props: {
railSystem: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,22 +1,22 @@
<template> <template>
<h3>Segments</h3> <h5>Segments</h5>
<ul> <ul class="list-group overflow-auto mb-2" style="max-height: 200px;">
<li v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id"> <li
{{segment.name}} <button @click.prevent="rsStore.removeSegment(segment.id)">Remove</button> v-for="segment in rsStore.selectedRailSystem.segments"
:key="segment.id"
class="list-group-item"
>
{{segment.name}}
<button @click.prevent="rsStore.removeSegment(segment.id)" class="btn btn-sm btn-danger float-end">Remove</button>
</li> </li>
</ul> </ul>
<AddSegment />
</template> </template>
<script> <script>
import AddSegment from "./AddSegment.vue";
import {useRailSystemsStore} from "../../stores/railSystemsStore"; import {useRailSystemsStore} from "../../stores/railSystemsStore";
export default { export default {
name: "SegmentsView.vue", name: "SegmentsView.vue",
components: {
AddSegment
},
setup() { setup() {
const rsStore = useRailSystemsStore(); const rsStore = useRailSystemsStore();
return { return {

View File

@ -1,76 +0,0 @@
<template>
<h4>Add Segment Boundary</h4>
<form @submit.prevent="submit()">
<div>
<label for="addSBX">X</label>
<input type="number" id="addSBX" v-model="formData.position.x" required/>
</div>
<div>
<label for="addSBY">Y</label>
<input type="number" id="addSBY" v-model="formData.position.y" required/>
</div>
<div>
<label for="addSBZ">Z</label>
<input type="number" id="addSBZ" v-model="formData.position.z" required/>
</div>
<div>
<label for="addSBName">Name</label>
<input type="text" id="addSBName" v-model="formData.name"/>
</div>
<div>
<label for="addSBSegmentA">Segment A</label>
<select id="addSBSegmentA" v-model="formData.segmentA">
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
{{segment.id}} | {{segment.name}}
</option>
</select>
<label for="addSBSegmentB">Segment B</label>
<select id="addSBSegmentB" v-model="formData.segmentB">
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
{{segment.id}} | {{segment.name}}
</option>
</select>
</div>
<button type="submit">Submit</button>
</form>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
export default {
name: "AddSegmentBoundary.vue",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: "",
position: {
x: 0,
y: 0,
z: 0
},
segmentA: null,
segmentB: null,
segments: [],
type: "SEGMENT_BOUNDARY"
}
}
},
methods: {
submit() {
this.formData.segments = [this.formData.segmentA, this.formData.segmentB];
this.rsStore.addComponent(this.formData);
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,112 @@
<template>
<div class="modal fade" tabindex="-1" id="addSegmentBoundaryModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Segment Boundary</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
A <em>segment boundary</em> is a component that defines a link
between one segment and another. This component can be used to
monitor trains entering and exiting the connected segments. Usually
used in conjunction with signals for classic railway signalling
systems.
</p>
<form>
<div class="mb-3">
<label for="addSignalName" class="form-label">Name</label>
<input class="form-control" type="text" id="addSignalName" v-model="formData.name" required/>
</div>
<div class="input-group mb-3">
<label for="addSignalX" class="input-group-text">X</label>
<input class="form-control" type="number" id="addSignalX" v-model="formData.position.x" required/>
<label for="addSignalY" class="input-group-text">Y</label>
<input class="form-control" type="number" id="addSignalY" v-model="formData.position.y" required/>
<label for="addSignalZ" class="input-group-text">Z</label>
<input class="form-control" type="number" id="addSignalZ" v-model="formData.position.z" required/>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="addSegmentBoundarySegmentA" class="form-label">Segment A</label>
<select id="addSegmentBoundarySegmentA" class="form-select" v-model="formData.segmentA">
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
{{segment.name}}
</option>
</select>
</div>
<div class="col-md-6">
<label for="addSegmentBoundarySegmentA" class="form-label">Segment B</label>
<select id="addSegmentBoundarySegmentA" class="form-select" v-model="formData.segmentB">
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
{{segment.name}}
</option>
</select>
</div>
</div>
</form>
<div v-if="warnings.length > 0">
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
{{msg}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import {Modal} from "bootstrap";
export default {
name: "AddSegmentBoundaryModal",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: "",
position: {
x: 0,
y: 0,
z: 0
},
segmentA: null,
segmentB: null,
segments: [],
type: "SEGMENT_BOUNDARY"
},
warnings: []
};
},
methods: {
formSubmitted() {
const modal = Modal.getInstance(document.getElementById("addSegmentBoundaryModal"));
this.formData.segments = [this.formData.segmentA, this.formData.segmentB];
this.rsStore.addComponent(this.formData)
.then(() => {
modal.hide();
})
.catch(error => {
this.warnings.length = 0;
this.warnings.push("Couldn't add the segment boundary: " + error.response.data.message)
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,62 +0,0 @@
<template>
<h4>Add Signal</h4>
<form @submit.prevent="rsStore.addComponent(formData)">
<div>
<label for="addSignalX">X</label>
<input type="number" id="addSignalX" v-model="formData.position.x" required/>
</div>
<div>
<label for="addSignalY">Y</label>
<input type="number" id="addSignalY" v-model="formData.position.y" required/>
</div>
<div>
<label for="addSignalZ">Z</label>
<input type="number" id="addSignalZ" v-model="formData.position.z" required/>
</div>
<div>
<label for="addSignalName">Name</label>
<input type="text" id="addSignalName" v-model="formData.name"/>
</div>
<div>
<label for="addSignalSegment">Segment</label>
<select id="addSignalSegment" v-model="formData.segment">
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
{{segment.id}} | {{segment.name}}
</option>
</select>
</div>
<button type="submit">Submit</button>
</form>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
export default {
name: "AddSignal",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: "",
position: {
x: 0,
y: 0,
z: 0
},
segment: null,
type: "SIGNAL"
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,98 @@
<template>
<div class="modal fade" tabindex="-1" id="addSignalModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Signal</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
A <em>signal</em> is a component that relays information about your
rail system to in-world devices. Classically, rail signals show a
lamp indicator to tell information about the segment of the network
they're attached to.
</p>
<form>
<div class="mb-3">
<label for="addSignalName" class="form-label">Name</label>
<input class="form-control" type="text" id="addSignalName" v-model="formData.name" required/>
</div>
<div class="input-group mb-3">
<label for="addSignalX" class="input-group-text">X</label>
<input class="form-control" type="number" id="addSignalX" v-model="formData.position.x" required/>
<label for="addSignalY" class="input-group-text">Y</label>
<input class="form-control" type="number" id="addSignalY" v-model="formData.position.y" required/>
<label for="addSignalZ" class="input-group-text">Z</label>
<input class="form-control" type="number" id="addSignalZ" v-model="formData.position.z" required/>
</div>
<div class="mb-3">
<label for="addSignalSegment" class="form-label">Segment</label>
<select id="addSignalSegment" class="form-select" v-model="formData.segment">
<option v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id" :value="segment">
{{segment.name}}
</option>
</select>
</div>
</form>
<div v-if="warnings.length > 0">
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
{{msg}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import {Modal} from "bootstrap";
export default {
name: "AddSignalModal",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: "",
position: {
x: 0,
y: 0,
z: 0
},
segment: null,
type: "SIGNAL"
},
warnings: []
};
},
methods: {
formSubmitted() {
const modal = Modal.getInstance(document.getElementById("addSignalModal"));
this.rsStore.addComponent(this.formData)
.then(() => {
modal.hide();
})
.catch(error => {
this.warnings.length = 0;
this.warnings.push("Couldn't add the signal: " + error.response.data.message)
});
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,122 @@
<template>
<div class="modal fade" tabindex="-1" id="addSwitchModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Switch</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="addSwitchName" class="form-label">Name</label>
<input class="form-control" type="text" id="addSwitchName" v-model="formData.name" required/>
</div>
<div class="input-group mb-3">
<label for="addSwitchX" class="input-group-text">X</label>
<input class="form-control" type="number" id="addSwitchX" v-model="formData.position.x" required/>
<label for="addSwitchY" class="input-group-text">Y</label>
<input class="form-control" type="number" id="addSwitchY" v-model="formData.position.y" required/>
<label for="addSwitchZ" class="input-group-text">Z</label>
<input class="form-control" type="number" id="addSwitchZ" v-model="formData.position.z" required/>
</div>
<div class="mb-3">
<ul class="list-group overflow-auto" style="height: 200px;">
<li class="list-group-item" v-for="(config, idx) in formData.possibleConfigurations" :key="idx">
{{getConfigString(config)}}
<button class="btn btn-sm btn-secondary" @click="formData.possibleConfigurations.splice(idx, 1)">Remove</button>
</li>
</ul>
</div>
<div class="mb-3">
<label for="addSwitchConfigs" class="form-label">Segment</label>
<select id="addSwitchConfigs" class="form-select" multiple v-model="formData.possibleConfigQueue">
<option v-for="node in getEligibleNodes()" :key="node.id" :value="node">
{{node.name}}
</option>
</select>
<button type="button" class="btn btn-sm btn-success" @click="addPossibleConfig()">Add</button>
</div>
</form>
<div v-if="warnings.length > 0">
<div v-for="msg in warnings" :key="msg" class="alert alert-danger mt-2">
{{msg}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="formSubmitted()">Add</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import {Modal} from "bootstrap";
export default {
name: "AddSwitchModal",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
data() {
return {
formData: {
name: "",
position: {
x: 0,
y: 0,
z: 0
},
possibleConfigurations: [],
possibleConfigQueue: [],
type: "SWITCH"
},
warnings: []
};
},
methods: {
formSubmitted() {
const modal = Modal.getInstance(document.getElementById("addSwitchModal"));
this.rsStore.addComponent(this.formData)
.then(() => {
modal.hide();
})
.catch(error => {
this.warnings.length = 0;
this.warnings.push("Couldn't add the signal: " + error.response.data.message)
});
},
getConfigString(config) {
return config.nodes.map(n => n.name).join(", ");
},
getEligibleNodes() {
return this.rsStore.selectedRailSystem.components.filter(c => {
if (c.connectedNodes === undefined || c.connectedNodes === null) return false;
for (let i = 0; i < this.formData.possibleConfigurations.length; i++) {
const config = this.formData.possibleConfigurations[i];
for (const node in config.nodes) {
if (node.id === c.id) return false;
}
}
return true;
});
},
addPossibleConfig() {
if (this.formData.possibleConfigQueue.length < 2) return;
this.formData.possibleConfigurations.push({nodes: this.formData.possibleConfigQueue});
this.formData.possibleConfigQueue = [];
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,23 +1,42 @@
<template> <template>
<div class="rs-component"> <div>
<h3>{{component.name}}</h3> <h3>{{component.name}}</h3>
<p> <small class="text-muted">{{component.type}}</small>
Id: {{component.id}} <table class="table">
</p> <tbody>
<p> <tr>
Position: (x = {{component.position.x}}, y = {{component.position.y}}, z = {{component.position.z}}) <th>Id</th><td>{{component.id}}</td>
</p> </tr>
<p> <tr>
Type: {{component.type}} <th>Position</th>
</p> <td>
<p> <table class="table table-borderless m-0 p-0">
Online: {{component.online}} <tbody>
</p> <tr>
<td class="p-0">X = {{component.position.x}}</td>
<td class="p-0">Y = {{component.position.y}}</td>
<td class="p-0">Z = {{component.position.z}}</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<th>Online</th><td>{{component.online}}</td>
</tr>
</tbody>
</table>
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" /> <SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" /> <SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" /> <PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
<button @click="rsStore.removeComponent(component.id)">Remove</button> <button @click="removeComponent()" class="btn btn-sm btn-danger">Remove</button>
</div> </div>
<ConfirmModal
ref="removeConfirm"
:id="'removeComponentModal'"
:title="'Remove Component'"
:message="'Are you sure you want to remove this component? It, and all associated data, will be permanently deleted.'"
/>
</template> </template>
<script> <script>
@ -25,9 +44,11 @@ import SignalComponentView from "./SignalComponentView.vue";
import PathNodeComponentView from "./PathNodeComponentView.vue"; import PathNodeComponentView from "./PathNodeComponentView.vue";
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue"; import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
import {useRailSystemsStore} from "../../../stores/railSystemsStore"; import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import ConfirmModal from "../../ConfirmModal.vue";
export default { export default {
components: { components: {
ConfirmModal,
SegmentBoundaryNodeComponentView, SegmentBoundaryNodeComponentView,
SignalComponentView, SignalComponentView,
PathNodeComponentView PathNodeComponentView
@ -47,13 +68,15 @@ export default {
type: Object, type: Object,
required: true required: true
} }
},
methods: {
removeComponent() {
this.$refs.removeConfirm.showConfirm()
.then(() => this.rsStore.removeComponent(this.component.id));
}
} }
} }
</script> </script>
<style> <style>
.rs-component {
width: 20%;
border: 1px solid black;
}
</style> </style>

View File

@ -1,22 +1,39 @@
<template> <template>
<h5>Connected Nodes</h5> <h5>Connected Nodes</h5>
<ul v-if="pathNode.connectedNodes.length > 0"> <table class="table" v-if="pathNode.connectedNodes.length > 0">
<li v-for="node in pathNode.connectedNodes" :key="node.id"> <thead>
{{node.id}} | {{node.name}} <tr>
<button @click="rsStore.removeConnection(pathNode, node)">Remove</button> <th>Name</th>
</li> </tr>
</ul> </thead>
<tbody>
<tr v-for="node in pathNode.connectedNodes" :key="node.id">
<td>{{node.name}}</td>
<td>
<button
@click="rsStore.removeConnection(pathNode, node)"
class="btn btn-sm btn-danger"
>
Remove
</button>
</td>
</tr>
</tbody>
</table>
<p v-if="pathNode.connectedNodes.length === 0"> <p v-if="pathNode.connectedNodes.length === 0">
There are no connected nodes. There are no connected nodes.
</p> </p>
<form @submit.prevent="rsStore.addConnection(pathNode, formData.nodeToAdd)"> <form
<label for="pathNodeAddConnection">Add Connection</label> @submit.prevent="rsStore.addConnection(pathNode, formData.nodeToAdd)"
<select id="pathNodeAddConnection" v-model="formData.nodeToAdd"> v-if="getEligibleConnections().length > 0"
class="input-group mb-3"
>
<select v-model="formData.nodeToAdd" class="form-select form-select-sm">
<option v-for="node in this.getEligibleConnections()" :key="node.id" :value="node"> <option v-for="node in this.getEligibleConnections()" :key="node.id" :value="node">
{{node.id}} | {{node.name}} | {{node.type}} {{node.name}}
</option> </option>
</select> </select>
<button type="submit">Add</button> <button type="submit" class="btn btn-sm btn-success">Add Connection</button>
</form> </form>
</template> </template>

View File

@ -1,10 +1,17 @@
<template> <template>
<h5>Segments</h5> <h5>Segments Connected</h5>
<ul> <table class="table">
<li v-for="segment in node.segments" :key="segment.id"> <thead>
{{segment.id}} | {{segment.name}} <tr>
</li> <th>Name</th>
</ul> </tr>
</thead>
<tbody>
<tr v-for="segment in node.segments" :key="segment.id">
<td>{{segment.name}}</td>
</tr>
</tbody>
</table>
</template> </template>
<script> <script>

View File

@ -1,7 +1,13 @@
<template> <template>
<p> <h5>Signal Properties</h5>
Connected segment: {{signal.segment.id}} {{signal.segment.name}} <table class="table">
</p> <tbody>
<tr>
<th>Connected to</th>
<td>{{signal.segment.name}}</td>
</tr>
</tbody>
</table>
</template> </template>
<script> <script>

View File

@ -11,22 +11,27 @@ export function drawComponent(ctx, worldTx, component) {
const s = getScaleFactor(); const s = getScaleFactor();
tx.scaleSelf(1/s, 1/s, 1/s); tx.scaleSelf(1/s, 1/s, 1/s);
tx.scaleSelf(20, 20, 20); tx.scaleSelf(20, 20, 20);
ctx.setTransform(tx.translate(0.75, -0.75));
drawOnlineIndicator(ctx, component);
ctx.setTransform(tx); ctx.setTransform(tx);
// Draw hovered status.
if (isComponentHovered(component)) {
ctx.fillStyle = `rgba(255, 255, 0, 32)`;
circle(ctx, 0, 0, 0.75);
ctx.fill();
}
if (component.type === "SIGNAL") { if (component.type === "SIGNAL") {
drawSignal(ctx, component); drawSignal(ctx, component);
} else if (component.type === "SEGMENT_BOUNDARY") { } else if (component.type === "SEGMENT_BOUNDARY") {
drawSegmentBoundary(ctx, component); drawSegmentBoundary(ctx, component);
} else if (component.type === "SWITCH") {
drawSwitch(ctx, component);
}
ctx.setTransform(tx.translate(0.75, -0.75));
if (component.online !== undefined && component.online !== null) {
drawOnlineIndicator(ctx, component);
}
ctx.setTransform(tx);
// Draw hovered status.
if (isComponentHovered(component)) {
ctx.fillStyle = `rgba(255, 255, 0, 0.5)`;
circle(ctx, 0, 0, 0.75);
ctx.fill();
} }
} }
@ -35,7 +40,7 @@ function drawSignal(ctx, signal) {
ctx.fillStyle = "black"; ctx.fillStyle = "black";
ctx.fill(); ctx.fill();
ctx.fillStyle = "rgb(0, 255, 0)"; ctx.fillStyle = "rgb(0, 255, 0)";
circle(ctx, 0, -0.2, 0.1); circle(ctx, 0, -0.2, 0.15);
ctx.fill(); ctx.fill();
} }
@ -50,6 +55,50 @@ function drawSegmentBoundary(ctx, segmentBoundary) {
ctx.fill(); 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();
}
}
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 drawOnlineIndicator(ctx, component) { function drawOnlineIndicator(ctx, component) {
ctx.lineWidth = 0.1; ctx.lineWidth = 0.1;
if (component.online) { if (component.online) {
@ -70,11 +119,7 @@ function drawOnlineIndicator(ctx, component) {
} }
export function drawConnectedNodes(ctx, worldTx, component) { export function drawConnectedNodes(ctx, worldTx, component) {
// const tx = DOMMatrix.fromMatrix(worldTx);
const s = getScaleFactor(); const s = getScaleFactor();
// tx.scaleSelf(1/s, 1/s, 1/s);
// tx.scaleSelf(20, 20, 20);
// ctx.setTransform(tx);
ctx.lineWidth = 5 / s; ctx.lineWidth = 5 / s;
ctx.strokeStyle = "black"; ctx.strokeStyle = "black";
for (let i = 0; i < component.connectedNodes.length; i++) { for (let i = 0; i < component.connectedNodes.length; i++) {

View File

@ -9,6 +9,7 @@ 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,
const SCALE_INDEX_NORMAL = 7; const SCALE_INDEX_NORMAL = 7;
const HOVER_RADIUS = 10; const HOVER_RADIUS = 10;
let mapContainerDiv = null;
let mapCanvas = null; let mapCanvas = null;
let railSystem = null; let railSystem = null;
@ -24,6 +25,7 @@ export function initMap(rs) {
console.log("Initializing map for rail system: " + rs.name); console.log("Initializing map for rail system: " + rs.name);
hoveredElements.length = 0; hoveredElements.length = 0;
mapCanvas = document.getElementById("railSystemMapCanvas"); mapCanvas = document.getElementById("railSystemMapCanvas");
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
mapCanvas.removeEventListener("wheel", onMouseWheel); mapCanvas.removeEventListener("wheel", onMouseWheel);
mapCanvas.addEventListener("wheel", onMouseWheel); mapCanvas.addEventListener("wheel", onMouseWheel);
mapCanvas.removeEventListener("mousedown", onMouseDown); mapCanvas.removeEventListener("mousedown", onMouseDown);
@ -43,6 +45,12 @@ export function draw() {
return; return;
} }
const ctx = mapCanvas.getContext("2d"); const ctx = mapCanvas.getContext("2d");
if (mapCanvas.width !== mapContainerDiv.clientWidth) {
mapCanvas.width = mapContainerDiv.clientWidth;
}
if (mapCanvas.height !== mapContainerDiv.clientHeight) {
mapCanvas.height = mapContainerDiv.clientHeight;
}
const width = mapCanvas.width; const width = mapCanvas.width;
const height = mapCanvas.height; const height = mapCanvas.height;
ctx.resetTransform(); ctx.resetTransform();
@ -51,6 +59,44 @@ export function draw() {
const worldTx = getWorldTransform(); const worldTx = getWorldTransform();
ctx.setTransform(worldTx); 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);
});
for (let i = 0; i < railSystem.components.length; i++) { for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i]; const c = railSystem.components[i];
if (c.connectedNodes !== undefined && c.connectedNodes !== null) { if (c.connectedNodes !== undefined && c.connectedNodes !== null) {

View File

@ -1,8 +1,10 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import App from './App.vue' import App from './App.vue'
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap";
const app = createApp(App) const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
app.use(pinia); app.use(pinia);

View File

@ -26,9 +26,10 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
createRailSystem(name) { createRailSystem(name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.post(this.apiUrl + "/rs", {name: name}) axios.post(this.apiUrl + "/rs", {name: name})
.then(() => { .then(response => {
const newId = response.data.id;
this.refreshRailSystems() this.refreshRailSystems()
.then(() => resolve()) .then(() => resolve(this.railSystems.find(rs => rs.id === newId)))
.catch(error => reject(error)); .catch(error => reject(error));
}) })
.catch(error => reject(error)); .catch(error => reject(error));
@ -71,9 +72,15 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
}, },
addSegment(name) { addSegment(name) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;
axios.post(`${this.apiUrl}/rs/${rs.id}/s`, {name: name}) return new Promise((resolve, reject) => {
.then(() => this.refreshSegments(rs)) axios.post(`${this.apiUrl}/rs/${rs.id}/s`, {name: name})
.catch(error => console.log(error)); .then(() => {
this.refreshSegments(rs)
.then(() => resolve())
.catch(error => reject(error));
})
.catch(error => reject(error));
});
}, },
removeSegment(id) { removeSegment(id) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;
@ -83,9 +90,15 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
}, },
addComponent(data) { addComponent(data) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;
axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data) return new Promise((resolve, reject) => {
.then(() => this.refreshAllComponents(rs)) axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data)
.catch(error => console.log(error)); .then(() => {
this.refreshAllComponents(rs)
.then(() => resolve())
.catch(error => reject(error));
})
.catch(error => reject(error));
});
}, },
removeComponent(id) { removeComponent(id) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;

View File

@ -0,0 +1,13 @@
package nl.andrewl.railsignalapi.dao;
import nl.andrewl.railsignalapi.model.Label;
import nl.andrewl.railsignalapi.model.RailSystem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface LabelRepository extends JpaRepository<Label, Long> {
List<Label> findAllByRailSystem(RailSystem rs);
}

View File

@ -0,0 +1,31 @@
package nl.andrewl.railsignalapi.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
/**
* A simple label element that allows text to be placed in the rail system
* model.
*/
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Label {
@Id
@GeneratedValue
private Long id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private RailSystem railSystem;
@Column(nullable = false, length = 63)
private String text;
public Label(RailSystem rs, String text) {
this.railSystem = rs;
this.text = text;
}
}

View File

@ -35,9 +35,10 @@ public abstract class Component {
private Position position; private Position position;
/** /**
* A human-readable name for the component. * A human-readable name for the component. This must be unique among all
* components in the rail system.
*/ */
@Column @Column(nullable = false)
private String name; private String name;
/** /**

View File

@ -65,8 +65,11 @@ public class ComponentService {
pos.setY(data.get("position").get("y").asDouble()); pos.setY(data.get("position").get("y").asDouble());
pos.setZ(data.get("position").get("z").asDouble()); pos.setZ(data.get("position").get("z").asDouble());
String name = data.get("name").asText(); String name = data.get("name").asText();
if (name == null || name.isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required name.");
}
if (name != null && componentRepository.existsByNameAndRailSystem(name, rs)) { if (componentRepository.existsByNameAndRailSystem(name, rs)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with that name already exists."); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with that name already exists.");
} }
@ -104,6 +107,9 @@ public class ComponentService {
} }
s.getPossibleConfigurations().add(new SwitchConfiguration(s, pathNodes)); s.getPossibleConfigurations().add(new SwitchConfiguration(s, pathNodes));
} }
if (s.getPossibleConfigurations().size() < 2) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least two switch configurations are needed.");
}
return s; return s;
} }