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" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RailSignal</title>
<title>Rail Signal</title>
</head>
<body>
<div id="app"></div>

View File

@ -8,8 +8,9 @@
"name": "railsignal-app",
"version": "0.0.0",
"dependencies": {
"@popperjs/core": "^2.11.5",
"axios": "^0.27.2",
"bootstrap": "^4.6.1",
"bootstrap": "^5.1.3",
"pinia": "^2.0.14",
"three": "^0.140.0",
"vue": "^3.2.33",
@ -73,6 +74,15 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"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": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.2.tgz",
@ -287,16 +297,15 @@
"dev": true
},
"node_modules/bootstrap": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.1"
"@popperjs/core": "^2.10.2"
}
},
"node_modules/brace-expansion": {
@ -1262,12 +1271,6 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"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": {
"version": "4.1.0",
"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": {
"version": "8.4.13",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz",
@ -1978,6 +1970,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"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": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.3.2.tgz",
@ -2162,9 +2159,9 @@
"dev": true
},
"bootstrap": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz",
"integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
"requires": {}
},
"brace-expansion": {
@ -2781,12 +2778,6 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"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": {
"version": "4.1.0",
"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": {
"version": "8.4.13",
"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"
},
"dependencies": {
"@popperjs/core": "^2.11.5",
"axios": "^0.27.2",
"bootstrap": "^5.1.3",
"pinia": "^2.0.14",
"three": "^0.140.0",
"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>
<header>
<h1>RailSignal</h1>
</header>
<RailSystemsManager />
<AppNavbar />
<RailSystem v-if="rsStore.selectedRailSystem !== null" :railSystem="rsStore.selectedRailSystem"/>
</template>
<script>
import RailSystem from "./components/RailSystem.vue";
import RailSystemsManager from "./components/RailSystemsManager.vue";
import {useRailSystemsStore} from "./stores/railSystemsStore";
import AppNavbar from "./components/AppNavbar.vue";
import RailSystem from "./components/RailSystem.vue";
export default {
components: {
RailSystem,
RailSystemsManager
AppNavbar,
RailSystem
},
setup() {
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>
<h2>{{railSystem.name}}</h2>
<div>
<MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" />
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent" :railSystem="railSystem"/>
<div class="container-fluid">
<div class="row">
<div class="col-md-8 p-0">
<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>
<SegmentsView />
<AddSignal v-if="railSystem.segments && railSystem.segments.length > 0" />
<AddSegmentBoundary v-if="railSystem.segments && railSystem.segments.length > 0" />
</template>
<script>
import MapView from './railsystem/MapView.vue'
import ComponentView from './railsystem/component/ComponentView.vue'
import SegmentsView from "./railsystem/SegmentsView.vue";
import AddSignal from "./railsystem/component/AddSignal.vue";
import AddSegmentBoundary from "./railsystem/component/AddSegmentBoundary.vue";
import RailSystemPropertiesView from "./railsystem/RailSystemPropertiesView.vue";
export default {
components: {
AddSignal,
AddSegmentBoundary,
SegmentsView,
RailSystemPropertiesView,
MapView,
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>
import {initMap} from "./mapRenderer.js";
import {initMap, draw} from "./mapRenderer.js";
export default {
props: {
@ -15,18 +15,33 @@ export default {
updated() {
// Also, re-initialize any time this view is updated.
initMap(this.railSystem);
},
watch: {
railSystem: {
handler() {
draw();
},
deep: true
}
}
}
</script>
<template>
<canvas id="railSystemMapCanvas" width="1000" height="600">
<div class="canvas-container" id="railSystemMapCanvasContainer">
<canvas id="railSystemMapCanvas">
Your browser doesn't support canvas!
</canvas>
</canvas>
</div>
</template>
<style>
.canvas-container {
width: 100%;
height: 800px;
}
canvas {
border: 1px solid black;
width: 100%;
height: 100%;
}
</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>
<h3>Segments</h3>
<ul>
<li v-for="segment in rsStore.selectedRailSystem.segments" :key="segment.id">
{{segment.name}} <button @click.prevent="rsStore.removeSegment(segment.id)">Remove</button>
<h5>Segments</h5>
<ul class="list-group overflow-auto mb-2" style="max-height: 200px;">
<li
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>
</ul>
<AddSegment />
</template>
<script>
import AddSegment from "./AddSegment.vue";
import {useRailSystemsStore} from "../../stores/railSystemsStore";
export default {
name: "SegmentsView.vue",
components: {
AddSegment
},
setup() {
const rsStore = useRailSystemsStore();
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>
<div class="rs-component">
<div>
<h3>{{component.name}}</h3>
<p>
Id: {{component.id}}
</p>
<p>
Position: (x = {{component.position.x}}, y = {{component.position.y}}, z = {{component.position.z}})
</p>
<p>
Type: {{component.type}}
</p>
<p>
Online: {{component.online}}
</p>
<small class="text-muted">{{component.type}}</small>
<table class="table">
<tbody>
<tr>
<th>Id</th><td>{{component.id}}</td>
</tr>
<tr>
<th>Position</th>
<td>
<table class="table table-borderless m-0 p-0">
<tbody>
<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" />
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
<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>
<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>
<script>
@ -25,9 +44,11 @@ import SignalComponentView from "./SignalComponentView.vue";
import PathNodeComponentView from "./PathNodeComponentView.vue";
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import ConfirmModal from "../../ConfirmModal.vue";
export default {
components: {
ConfirmModal,
SegmentBoundaryNodeComponentView,
SignalComponentView,
PathNodeComponentView
@ -47,13 +68,15 @@ export default {
type: Object,
required: true
}
},
methods: {
removeComponent() {
this.$refs.removeConfirm.showConfirm()
.then(() => this.rsStore.removeComponent(this.component.id));
}
}
}
</script>
<style>
.rs-component {
width: 20%;
border: 1px solid black;
}
</style>

View File

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

View File

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

View File

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

View File

@ -11,22 +11,27 @@ export function drawComponent(ctx, worldTx, component) {
const s = getScaleFactor();
tx.scaleSelf(1/s, 1/s, 1/s);
tx.scaleSelf(20, 20, 20);
ctx.setTransform(tx.translate(0.75, -0.75));
drawOnlineIndicator(ctx, component);
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") {
drawSignal(ctx, component);
} else if (component.type === "SEGMENT_BOUNDARY") {
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.fill();
ctx.fillStyle = "rgb(0, 255, 0)";
circle(ctx, 0, -0.2, 0.1);
circle(ctx, 0, -0.2, 0.15);
ctx.fill();
}
@ -50,6 +55,50 @@ function drawSegmentBoundary(ctx, segmentBoundary) {
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) {
ctx.lineWidth = 0.1;
if (component.online) {
@ -70,11 +119,7 @@ function drawOnlineIndicator(ctx, component) {
}
export function drawConnectedNodes(ctx, worldTx, component) {
// const tx = DOMMatrix.fromMatrix(worldTx);
const s = getScaleFactor();
// tx.scaleSelf(1/s, 1/s, 1/s);
// tx.scaleSelf(20, 20, 20);
// ctx.setTransform(tx);
ctx.lineWidth = 5 / s;
ctx.strokeStyle = "black";
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 HOVER_RADIUS = 10;
let mapContainerDiv = null;
let mapCanvas = null;
let railSystem = null;
@ -24,6 +25,7 @@ export function initMap(rs) {
console.log("Initializing map for rail system: " + rs.name);
hoveredElements.length = 0;
mapCanvas = document.getElementById("railSystemMapCanvas");
mapContainerDiv = document.getElementById("railSystemMapCanvasContainer");
mapCanvas.removeEventListener("wheel", onMouseWheel);
mapCanvas.addEventListener("wheel", onMouseWheel);
mapCanvas.removeEventListener("mousedown", onMouseDown);
@ -43,6 +45,12 @@ export function draw() {
return;
}
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 height = mapCanvas.height;
ctx.resetTransform();
@ -51,6 +59,44 @@ export function draw() {
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);
});
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {

View File

@ -1,8 +1,10 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
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();
app.use(pinia);

View File

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

View File

@ -65,8 +65,11 @@ public class ComponentService {
pos.setY(data.get("position").get("y").asDouble());
pos.setZ(data.get("position").get("z").asDouble());
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.");
}
@ -104,6 +107,9 @@ public class ComponentService {
}
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;
}