added stuff

This commit is contained in:
Andrew Lalis 2022-05-07 20:31:15 +02:00
parent e608e2ba8c
commit 3ac886feeb
32 changed files with 784 additions and 86 deletions

View File

@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "^4.6.1",
"pinia": "^2.0.14", "pinia": "^2.0.14",
"three": "^0.140.0", "three": "^0.140.0",
"vue": "^3.2.33", "vue": "^3.2.33",
@ -285,6 +286,19 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true "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==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.1"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -1248,6 +1262,12 @@
"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",
@ -1500,6 +1520,17 @@
} }
} }
}, },
"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",
@ -2130,6 +2161,12 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true "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==",
"requires": {}
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -2744,6 +2781,12 @@
"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",
@ -2924,6 +2967,12 @@
} }
} }
}, },
"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

@ -4,10 +4,7 @@
</header> </header>
<RailSystemsManager /> <RailSystemsManager />
<RailSystem <RailSystem v-if="rsStore.selectedRailSystem !== null" :railSystem="rsStore.selectedRailSystem"/>
v-if="rsStore.selectedRailSystem !== null"
:railSystem="rsStore.selectedRailSystem"
/>
</template> </template>
<script> <script>

View File

@ -1,29 +1,35 @@
<template> <template>
<h2>{{railSystem.name}}</h2> <h2>{{railSystem.name}}</h2>
<RsMap :railSystem="railSystem" /> <div>
<RsComponent v-if="selectedComponent !== null" :component="selectedComponent" /> <MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" />
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent"/>
</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 RsMap from './railsystem/MapView.vue' import MapView from './railsystem/MapView.vue'
import RsComponent from './railsystem/Component.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";
export default { export default {
components: { components: {
RsMap, AddSignal,
RsComponent AddSegmentBoundary,
}, SegmentsView,
props: { MapView,
railSystem: { ComponentView
type: Object, },
required: true props: {
} railSystem: {
}, type: Object,
data() { required: true
return {
selectedComponent: null
}
} }
}
} }
</script> </script>

View File

@ -26,6 +26,12 @@ export default {
name: "RailSystemsManager.vue", name: "RailSystemsManager.vue",
setup() { setup() {
const rsStore = useRailSystemsStore(); const rsStore = useRailSystemsStore();
rsStore.$subscribe(mutation => {
const evt = mutation.events;
if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
rsStore.fetchSelectedRailSystemData();
}
});
return { return {
rsStore rsStore
}; };

View File

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

View File

@ -1,23 +0,0 @@
<script>
export default {
props: {
component: {
type: Object,
required: true
}
}
}
</script>
<template>
<div class="rs-component">
</div>
</template>
<style>
.rs-component {
width: 20%;
border: 1px solid black;
}
</style>

View File

@ -1,26 +1,32 @@
<script> <script>
import {initMap} from "./mapRenderer.js";
export default { export default {
props: { props: {
railSystem: { railSystem: {
type: Object, type: Object,
required: true required: true
}
},
data() {
return {}
} }
},
mounted() {
// The first time this map is mounted, initialize the map.
initMap(this.railSystem);
},
updated() {
// Also, re-initialize any time this view is updated.
initMap(this.railSystem);
}
} }
</script> </script>
<template> <template>
<canvas> <canvas id="railSystemMapCanvas" width="1000" height="600">
Your browser doesn't support canvas! Your browser doesn't support canvas!
</canvas> </canvas>
</template> </template>
<style> <style>
canvas { canvas {
border: 1px solid black; border: 1px solid black;
width: 70%;
} }
</style> </style>

View File

@ -0,0 +1,31 @@
<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>
</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 {
rsStore
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,76 @@
<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,62 @@
<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,55 @@
<template>
<div class="rs-component">
<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>
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" />
<button @click="rsStore.removeComponent(component.id)">Remove</button>
</div>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import SignalComponentView from "./SignalComponentView.vue";
import PathNodeComponentView from "./PathNodeComponentView.vue";
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
export default {
components: {
SegmentBoundaryNodeComponentView,
SignalComponentView,
PathNodeComponentView
},
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
props: {
component: {
type: Object,
required: true
}
}
}
</script>
<style>
.rs-component {
width: 20%;
border: 1px solid black;
}
</style>

View File

@ -0,0 +1,27 @@
<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}}
</li>
</ul>
<p v-if="pathNode.connectedNodes.length === 0">
There are no connected nodes.
</p>
</template>
<script>
export default {
name: "PathNodeComponentView",
props: {
pathNode: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,24 @@
<template>
<h5>Segments</h5>
<ul>
<li v-for="segment in node.segments" :key="segment.id">
{{segment.id}} | {{segment.name}}
</li>
</ul>
</template>
<script>
export default {
name: "SegmentBoundaryNodeComponentView",
props: {
node: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,21 @@
<template>
<p>
Connected segment: {{signal.segment.id}} {{signal.segment.name}}
</p>
</template>
<script>
export default {
name: "SignalComponentView",
props: {
signal: {
type: Object,
required: true
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,248 @@
/*
This component is responsible for the rendering of a RailSystem in a 2d map
view.
*/
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
const SCALE_INDEX_NORMAL = 7;
const HOVER_RADIUS = 10;
let mapCanvas = null;
let railSystem = null;
let mapScaleIndex = SCALE_INDEX_NORMAL;
let mapTranslation = {x: 0, y: 0};
let mapDragOrigin = null;
let mapDragTranslation = null;
let lastMousePoint = new DOMPoint(0, 0, 0, 0);
const hoveredElements = [];
export function initMap(rs) {
railSystem = rs;
console.log("Initializing map for rail system: " + rs.name);
hoveredElements.length = 0;
mapCanvas = document.getElementById("railSystemMapCanvas");
mapCanvas.removeEventListener("wheel", onMouseWheel);
mapCanvas.addEventListener("wheel", onMouseWheel);
mapCanvas.removeEventListener("mousedown", onMouseDown);
mapCanvas.addEventListener("mousedown", onMouseDown);
mapCanvas.removeEventListener("mouseup", onMouseUp);
mapCanvas.addEventListener("mouseup", onMouseUp);
mapCanvas.removeEventListener("mousemove", onMouseMove);
mapCanvas.addEventListener("mousemove", onMouseMove);
// Do an initial draw.
draw();
}
export function draw() {
if (!(mapCanvas && railSystem && railSystem.components)) {
console.warn("Attempted to draw map without canvas or railSystem.");
return;
}
const ctx = mapCanvas.getContext("2d");
const width = mapCanvas.width;
const height = mapCanvas.height;
ctx.resetTransform();
ctx.fillStyle = `rgb(240, 240, 240)`;
ctx.fillRect(0, 0, width, height);
const worldTx = getWorldTransform();
ctx.setTransform(worldTx);
for (let i = 0; i < railSystem.components.length; i++) {
drawComponent(ctx, worldTx, railSystem.components[i]);
}
// Draw debug info.
ctx.resetTransform();
ctx.fillStyle = "black";
ctx.strokeStyle = "black";
ctx.font = "10px Sans-Serif";
const lastWorldPoint = mapPointToWorld(lastMousePoint);
const lines = [
"Scale factor: " + getScaleFactor(),
`(x = ${lastWorldPoint.x.toFixed(2)}, y = ${lastWorldPoint.y.toFixed(2)}, z = ${lastWorldPoint.z.toFixed(2)})`,
`Components: ${railSystem.components.length}`,
`Hovered elements: ${hoveredElements.length}`
]
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 10, 20 + (i * 15));
}
}
function drawComponent(ctx, worldTx, component) {
const tx = DOMMatrix.fromMatrix(worldTx);
tx.translateSelf(component.position.x, component.position.z, 0);
const s = getScaleFactor();
tx.scaleSelf(1/s, 1/s, 1/s);
tx.scaleSelf(5, 5, 5);
ctx.setTransform(tx);
if (isComponentHovered(component)) {
ctx.fillStyle = `rgba(255, 255, 0, 32)`;
ctx.beginPath();
ctx.ellipse(0, 0, 1.8, 1.8, 0, 0, Math.PI * 2);
ctx.fill();
}
if (component.type === "SIGNAL") {
drawSignal(ctx, component);
} else if (component.type === "SEGMENT_BOUNDARY") {
drawSegmentBoundary(ctx, component);
}
}
function drawSignal(ctx, signal) {
roundedRect(ctx, -0.7, -1, 1.4, 2, 0.25);
ctx.fillStyle = "black";
ctx.fill();
// ctx.fillStyle = "green";
// ctx.beginPath();
// ctx.ellipse(0, 0, 0.8, 0.8, 0, 0, Math.PI * 2);
// ctx.fill();
//
// ctx.strokeStyle = "black";
// ctx.lineWidth = 0.5;
// ctx.beginPath();
// ctx.ellipse(0, 0, 1, 1, 0, 0, Math.PI * 2);
// ctx.stroke();
}
function drawSegmentBoundary(ctx, segmentBoundary) {
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.ellipse(0, 0, 1, 1, 0, 0, Math.PI * 2);
ctx.fill();
}
export function getScaleFactor() {
return SCALE_VALUES[mapScaleIndex];
}
function getWorldTransform() {
const canvasRect = mapCanvas.getBoundingClientRect();
const scale = getScaleFactor();
const tx = new DOMMatrix();
tx.translateSelf(canvasRect.width / 2, canvasRect.height / 2, 0);
tx.scaleSelf(scale, scale, scale);
tx.translateSelf(mapTranslation.x, mapTranslation.y, 0);
if (mapDragOrigin !== null && mapDragTranslation !== null) {
tx.translateSelf(mapDragTranslation.x, mapDragTranslation.y, 0);
}
return tx;
}
function isComponentHovered(component) {
for (let i = 0; i < hoveredElements.length; i++) {
if (hoveredElements[i].id === component.id) return true;
}
return false;
}
/**
* Maps a point on the map coordinates to world coordinates.
* @param {DOMPoint} p
* @returns {DOMPoint}
*/
function mapPointToWorld(p) {
return getWorldTransform().invertSelf().transformPoint(p);
}
/**
* Maps a point in the world to map coordinates.
* @param {DOMPoint} p
* @returns {DOMPoint}
*/
function worldPointToMap(p) {
return getWorldTransform().transformPoint(p);
}
function roundedRect(ctx, x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.arcTo(x+w, y, x+w, y+h, r);
ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r);
ctx.arcTo(x, y, x+w, y, r);
ctx.closePath();
}
/*
EVENT HANDLING
*/
/**
* @param {WheelEvent} event
*/
function onMouseWheel(event) {
const s = event.deltaY;
if (s > 0) {
mapScaleIndex = Math.max(0, mapScaleIndex - 1);
} else if (s < 0) {
mapScaleIndex = Math.min(SCALE_VALUES.length - 1, mapScaleIndex + 1);
}
draw();
event.stopPropagation();
}
/**
* @param {MouseEvent} event
*/
function onMouseDown(event) {
const p = getMousePoint(event);
mapDragOrigin = {x: p.x, y: p.y};
}
function onMouseUp() {
if (mapDragTranslation !== null) {
mapTranslation.x += mapDragTranslation.x;
mapTranslation.y += mapDragTranslation.y;
}
if (hoveredElements.length === 1) {
railSystem.selectedComponent = hoveredElements[0];
} else {
railSystem.selectedComponent = null;
}
mapDragOrigin = null;
mapDragTranslation = null;
}
/**
* @param {MouseEvent} event
*/
function onMouseMove(event) {
const p = getMousePoint(event);
lastMousePoint = p;
if (mapDragOrigin !== null) {
const scale = getScaleFactor();
const dx = p.x - mapDragOrigin.x;
const dy = p.y - mapDragOrigin.y;
mapDragTranslation = {x: dx / scale, y: dy / scale};
} else {
hoveredElements.length = 0;
// Populate with list of hovered elements.
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
const componentPoint = new DOMPoint(c.position.x, c.position.z, 0, 1);
const mapComponentPoint = worldPointToMap(componentPoint);
const dist2 = (p.x - mapComponentPoint.x) * (p.x - mapComponentPoint.x) + (p.y - mapComponentPoint.y) * (p.y - mapComponentPoint.y);
if (dist2 < HOVER_RADIUS * HOVER_RADIUS) {
hoveredElements.push(c);
}
}
}
draw();
}
/**
* Gets the point at which the user clicked on the map.
* @param {MouseEvent} event
* @returns {DOMPoint}
*/
function getMousePoint(event) {
const rect = mapCanvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return new DOMPoint(x, y, 0, 1);
}

View File

@ -4,13 +4,16 @@ import axios from "axios";
export const useRailSystemsStore = defineStore('RailSystemsStore', { export const useRailSystemsStore = defineStore('RailSystemsStore', {
state: () => ({ state: () => ({
railSystems: [], railSystems: [],
/**
* @type {{segments: [Object], components: [Object], selectedComponent: Object} | null}
*/
selectedRailSystem: null, selectedRailSystem: null,
selectedComponent: null apiUrl: import.meta.env.VITE_API_URL
}), }),
actions: { actions: {
refreshRailSystems() { refreshRailSystems() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.get(import.meta.env.VITE_API_URL + "/rs") axios.get(this.apiUrl + "/rs")
.then(response => { .then(response => {
this.railSystems = response.data; this.railSystems = response.data;
resolve(); resolve();
@ -22,10 +25,7 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
}, },
createRailSystem(name) { createRailSystem(name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.post( axios.post(this.apiUrl + "/rs", {name: name})
import.meta.env.VITE_API_URL + "/rs",
{name: name}
)
.then(() => { .then(() => {
this.refreshRailSystems() this.refreshRailSystems()
.then(() => resolve()) .then(() => resolve())
@ -36,13 +36,69 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
}, },
removeRailSystem(rs) { removeRailSystem(rs) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
axios.delete(import.meta.env.VITE_API_URL + "/rs/" + rs.id) axios.delete(this.apiUrl + "/rs/" + rs.id)
.then(() => { .then(() => {
this.selectedRailSystem = null;
this.refreshRailSystems() this.refreshRailSystems()
.then(() => resolve) .then(() => resolve)
.catch(error => reject(error)); .catch(error => reject(error));
}) })
}) })
},
refreshSegments(rs) {
return new Promise(resolve => {
axios.get(`${this.apiUrl}/rs/${rs.id}/s`)
.then(response => {
rs.segments = response.data;
resolve();
});
});
},
refreshComponents(rs) {
return new Promise(resolve => {
axios.get(`${this.apiUrl}/rs/${rs.id}/c`)
.then(response => {
rs.components = response.data;
resolve();
});
});
},
fetchSelectedRailSystemData() {
if (!this.selectedRailSystem) return;
this.refreshSegments(this.selectedRailSystem);
this.refreshComponents(this.selectedRailSystem);
},
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));
},
removeSegment(id) {
const rs = this.selectedRailSystem;
axios.delete(`${this.apiUrl}/rs/${rs.id}/s/${id}`)
.then(() => this.refreshSegments(rs))
.catch(error => console.log(error));
},
addComponent(data) {
const rs = this.selectedRailSystem;
axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data)
.then(() => this.refreshComponents(rs))
.catch(error => console.log(error));
},
removeComponent(id) {
const rs = this.selectedRailSystem;
axios.delete(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
.then(() => this.refreshComponents(rs))
.catch(error => console.log(error));
},
fetchComponentData(component) {
return new Promise(resolve => {
const rs = this.selectedRailSystem;
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${component.id}`)
.then(response => resolve(response.data))
.catch(error => console.log(error));
});
} }
} }
}); });

View File

@ -2,8 +2,8 @@ package nl.andrewl.railsignalapi.rest;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.rest.dto.component.ComponentResponse; import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
import nl.andrewl.railsignalapi.rest.dto.component.SimpleComponentResponse; import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
import nl.andrewl.railsignalapi.service.ComponentService; import nl.andrewl.railsignalapi.service.ComponentService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -17,7 +17,7 @@ public class ComponentsApiController {
private final ComponentService componentService; private final ComponentService componentService;
@GetMapping @GetMapping
public List<SimpleComponentResponse> getAllComponents(@PathVariable long rsId) { public List<ComponentResponse> getAllComponents(@PathVariable long rsId) {
return componentService.getComponents(rsId); return componentService.getComponents(rsId);
} }
@ -36,4 +36,9 @@ public class ComponentsApiController {
componentService.removeComponent(rsId, cId); componentService.removeComponent(rsId, cId);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@PatchMapping(path = "/{cId}/connectedNodes")
public ComponentResponse updateConnectedNodes(@PathVariable long rsId, @PathVariable long cId, @RequestBody PathNodeUpdatePayload payload) {
return componentService.updatePath(rsId, cId, payload);
}
} }

View File

@ -2,8 +2,10 @@ package nl.andrewl.railsignalapi.rest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse; import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse;
import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
import nl.andrewl.railsignalapi.service.SegmentService; import nl.andrewl.railsignalapi.service.SegmentService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@ -28,4 +30,10 @@ public class SegmentsApiController {
public FullSegmentResponse createSegment(@PathVariable long rsId, @RequestBody SegmentPayload payload) { public FullSegmentResponse createSegment(@PathVariable long rsId, @RequestBody SegmentPayload payload) {
return segmentService.create(rsId, payload); return segmentService.create(rsId, payload);
} }
@DeleteMapping(path = "/{sId}")
public ResponseEntity<Void> removeSegment(@PathVariable long rsId, @PathVariable long sId) {
segmentService.remove(rsId, sId);
return ResponseEntity.noContent().build();
}
} }

View File

@ -1,8 +1,8 @@
package nl.andrewl.railsignalapi.rest.dto; package nl.andrewl.railsignalapi.rest.dto;
import nl.andrewl.railsignalapi.model.Segment; import nl.andrewl.railsignalapi.model.Segment;
import nl.andrewl.railsignalapi.rest.dto.component.SegmentBoundaryNodeResponse; import nl.andrewl.railsignalapi.rest.dto.component.out.SegmentBoundaryNodeResponse;
import nl.andrewl.railsignalapi.rest.dto.component.SignalResponse; import nl.andrewl.railsignalapi.rest.dto.component.out.SignalResponse;
import java.util.List; import java.util.List;

View File

@ -0,0 +1,5 @@
package nl.andrewl.railsignalapi.rest.dto;
public record PathNodeUpdatePayload (
long[] connectedNodeIds
) {}

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest; package nl.andrewl.railsignalapi.rest.dto;
public record SegmentPayload(String name) { public record SegmentPayload(String name) {
} }

View File

@ -0,0 +1,9 @@
package nl.andrewl.railsignalapi.rest.dto.component.in;
import nl.andrewl.railsignalapi.model.component.Position;
public abstract class ComponentPayload {
public String name;
public String type;
public Position position;
}

View File

@ -0,0 +1,5 @@
package nl.andrewl.railsignalapi.rest.dto.component.in;
public class SignalPayload extends ComponentPayload {
public long segmentId;
}

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.*; import nl.andrewl.railsignalapi.model.component.*;

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.PathNode; import nl.andrewl.railsignalapi.model.component.PathNode;

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode; import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.Signal; import nl.andrewl.railsignalapi.model.component.Signal;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.Component; import nl.andrewl.railsignalapi.model.component.Component;
import nl.andrewl.railsignalapi.model.component.Position; import nl.andrewl.railsignalapi.model.component.Position;

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration; import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;

View File

@ -1,4 +1,4 @@
package nl.andrewl.railsignalapi.rest.dto.component; package nl.andrewl.railsignalapi.rest.dto.component.out;
import nl.andrewl.railsignalapi.model.component.Switch; import nl.andrewl.railsignalapi.model.component.Switch;

View File

@ -11,8 +11,8 @@ import nl.andrewl.railsignalapi.dao.SwitchConfigurationRepository;
import nl.andrewl.railsignalapi.model.RailSystem; import nl.andrewl.railsignalapi.model.RailSystem;
import nl.andrewl.railsignalapi.model.Segment; import nl.andrewl.railsignalapi.model.Segment;
import nl.andrewl.railsignalapi.model.component.*; import nl.andrewl.railsignalapi.model.component.*;
import nl.andrewl.railsignalapi.rest.dto.component.ComponentResponse; import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
import nl.andrewl.railsignalapi.rest.dto.component.SimpleComponentResponse; import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -31,10 +31,10 @@ public class ComponentService {
private final SwitchConfigurationRepository switchConfigurationRepository; private final SwitchConfigurationRepository switchConfigurationRepository;
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<SimpleComponentResponse> getComponents(long rsId) { public List<ComponentResponse> getComponents(long rsId) {
var rs = railSystemRepository.findById(rsId) var rs = railSystemRepository.findById(rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return componentRepository.findAllByRailSystem(rs).stream().map(SimpleComponentResponse::new).toList(); return componentRepository.findAllByRailSystem(rs).stream().map(ComponentResponse::of).toList();
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -123,7 +123,22 @@ public class ComponentService {
} }
@Transactional @Transactional
public void remove(long rsId, long componentId) { public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) {
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!(c instanceof PathNode p)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component is not a PathNode.");
Set<PathNode> newNodes = new HashSet<>();
for (var id : payload.connectedNodeIds()) {
var c1 = componentRepository.findByIdAndRailSystemId(id, rsId);
if (c1.isPresent() && c1.get() instanceof PathNode pn) {
newNodes.add(pn);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with id " + id + " is not a PathNode in the same rail system.");
}
}
p.getConnectedNodes().retainAll(newNodes);
p.getConnectedNodes().addAll(newNodes);
p = componentRepository.save(p);
return ComponentResponse.of(p);
} }
} }

View File

@ -6,7 +6,7 @@ import nl.andrewl.railsignalapi.dao.RailSystemRepository;
import nl.andrewl.railsignalapi.dao.SegmentRepository; import nl.andrewl.railsignalapi.dao.SegmentRepository;
import nl.andrewl.railsignalapi.model.Segment; import nl.andrewl.railsignalapi.model.Segment;
import nl.andrewl.railsignalapi.model.component.Component; import nl.andrewl.railsignalapi.model.component.Component;
import nl.andrewl.railsignalapi.rest.SegmentPayload; import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse; import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse; import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -53,5 +53,6 @@ public class SegmentService {
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
componentRepository.deleteAll(segment.getSignals()); componentRepository.deleteAll(segment.getSignals());
componentRepository.deleteAll(segment.getBoundaryNodes()); componentRepository.deleteAll(segment.getBoundaryNodes());
segmentRepository.delete(segment);
} }
} }