Added proper connection management.

This commit is contained in:
Andrew Lalis 2022-05-08 01:37:25 +02:00
parent 3ac886feeb
commit a02758ecd4
10 changed files with 248 additions and 75 deletions

View File

@ -2,7 +2,7 @@
<h2>{{railSystem.name}}</h2>
<div>
<MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" />
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent"/>
<ComponentView v-if="railSystem.selectedComponent" :component="railSystem.selectedComponent" :railSystem="railSystem"/>
</div>
<SegmentsView />
<AddSignal v-if="railSystem.segments && railSystem.segments.length > 0" />

View File

@ -0,0 +1,16 @@
export 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();
}
export function circle(ctx, x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
}

View File

@ -15,16 +15,16 @@
</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" />
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
<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";
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
export default {
components: {
@ -42,6 +42,10 @@ export default {
component: {
type: Object,
required: true
},
railSystem: {
type: Object,
required: true
}
}
}

View File

@ -3,20 +3,68 @@
<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>
<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">
<option v-for="node in this.getEligibleConnections()" :key="node.id" :value="node">
{{node.id}} | {{node.name}} | {{node.type}}
</option>
</select>
<button type="submit">Add</button>
</form>
</template>
<script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
export default {
name: "PathNodeComponentView",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
props: {
pathNode: {
type: Object,
required: true
},
railSystem: {
type: Object,
required: true
}
},
data() {
return {
formData: {
nodeToAdd: null
}
}
},
methods: {
getEligibleConnections() {
const nodes = [];
for (let i = 0; i < this.railSystem.components.length; i++) {
const c = this.railSystem.components[i];
if (c.id !== this.pathNode.id && c.connectedNodes !== undefined && c.connectedNodes !== null) {
let exists = false;
for (let j = 0; j < this.pathNode.connectedNodes.length; j++) {
if (this.pathNode.connectedNodes[j].id === c.id) {
exists = true;
break;
}
}
if (!exists) nodes.push(c);
}
}
return nodes;
}
}
}

View File

@ -0,0 +1,87 @@
/*
Helper functions to actually perform rendering of different components.
*/
import {getScaleFactor, isComponentHovered} from "./mapRenderer";
import {roundedRect, circle} from "./canvasUtils";
export 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(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);
}
}
function drawSignal(ctx, signal) {
roundedRect(ctx, -0.3, -0.5, 0.6, 1, 0.25);
ctx.fillStyle = "black";
ctx.fill();
ctx.fillStyle = "rgb(0, 255, 0)";
circle(ctx, 0, -0.2, 0.1);
ctx.fill();
}
function drawSegmentBoundary(ctx, segmentBoundary) {
ctx.fillStyle = `rgb(150, 58, 224)`;
ctx.beginPath();
ctx.moveTo(0, -0.5);
ctx.lineTo(-0.5, 0);
ctx.lineTo(0, 0.5);
ctx.lineTo(0.5, 0);
ctx.lineTo(0, -0.5);
ctx.fill();
}
function drawOnlineIndicator(ctx, component) {
ctx.lineWidth = 0.1;
if (component.online) {
ctx.fillStyle = `rgba(52, 174, 235, 128)`;
ctx.strokeStyle = `rgba(52, 174, 235, 128)`;
} else {
ctx.fillStyle = `rgba(153, 153, 153, 128)`;
ctx.strokeStyle = `rgba(153, 153, 153, 128)`;
}
ctx.beginPath();
ctx.arc(0, 0.2, 0.125, 0, Math.PI * 2);
ctx.fill();
for (let r = 0; r < 3; r++) {
ctx.beginPath();
ctx.arc(0, 0, 0.1 + 0.2 * r, 7 * Math.PI / 6, 11 * Math.PI / 6);
ctx.stroke();
}
}
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++) {
const node = component.connectedNodes[i];
ctx.beginPath();
ctx.moveTo(component.position.x, component.position.z);
ctx.lineTo(node.position.x, node.position.z);
ctx.stroke();
}
}

View File

@ -3,6 +3,8 @@ This component is responsible for the rendering of a RailSystem in a 2d map
view.
*/
import {drawComponent, drawConnectedNodes} from "./drawing";
const SCALE_VALUES = [0.01, 0.1, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 16.0, 20.0, 30.0, 45.0, 60.0, 80.0, 100.0];
const SCALE_INDEX_NORMAL = 7;
const HOVER_RADIUS = 10;
@ -49,6 +51,13 @@ export function draw() {
const worldTx = getWorldTransform();
ctx.setTransform(worldTx);
for (let i = 0; i < railSystem.components.length; i++) {
const c = railSystem.components[i];
if (c.connectedNodes !== undefined && c.connectedNodes !== null) {
drawConnectedNodes(ctx, worldTx, c);
}
}
for (let i = 0; i < railSystem.components.length; i++) {
drawComponent(ctx, worldTx, railSystem.components[i]);
}
@ -70,50 +79,6 @@ export function draw() {
}
}
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];
}
@ -131,7 +96,7 @@ function getWorldTransform() {
return tx;
}
function isComponentHovered(component) {
export function isComponentHovered(component) {
for (let i = 0; i < hoveredElements.length; i++) {
if (hoveredElements[i].id === component.id) return true;
}
@ -156,18 +121,6 @@ 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
*/
@ -184,6 +137,7 @@ function onMouseWheel(event) {
}
draw();
event.stopPropagation();
return false;
}
/**

View File

@ -54,10 +54,11 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
});
});
},
refreshComponents(rs) {
refreshAllComponents(rs) {
return new Promise(resolve => {
axios.get(`${this.apiUrl}/rs/${rs.id}/c`)
.then(response => {
rs.selectedComponent = null;
rs.components = response.data;
resolve();
});
@ -66,7 +67,7 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
fetchSelectedRailSystemData() {
if (!this.selectedRailSystem) return;
this.refreshSegments(this.selectedRailSystem);
this.refreshComponents(this.selectedRailSystem);
this.refreshAllComponents(this.selectedRailSystem);
},
addSegment(name) {
const rs = this.selectedRailSystem;
@ -83,13 +84,13 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
addComponent(data) {
const rs = this.selectedRailSystem;
axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data)
.then(() => this.refreshComponents(rs))
.then(() => this.refreshAllComponents(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))
.then(() => this.refreshAllComponents(rs))
.catch(error => console.log(error));
},
fetchComponentData(component) {
@ -99,6 +100,51 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
.then(response => resolve(response.data))
.catch(error => console.log(error));
});
},
refreshComponents(components) {
const rs = this.selectedRailSystem;
for (let i = 0; i < components.length; i++) {
axios.get(`${this.apiUrl}/rs/${rs.id}/c/${components[i].id}`)
.then(resp => {
const idx = this.selectedRailSystem.components.findIndex(c => c.id === resp.data.id);
if (idx > -1) this.selectedRailSystem.components[idx] = resp.data;
})
.catch(error => console.log(error));
}
},
updateConnections(pathNode) {
const rs = this.selectedRailSystem;
return new Promise(resolve => {
axios.patch(
`${this.apiUrl}/rs/${rs.id}/c/${pathNode.id}/connectedNodes`,
pathNode
)
.then(response => {
pathNode.connectedNodes = response.data.connectedNodes;
resolve();
})
.catch(error => console.log(error));
});
},
addConnection(pathNode, other) {
pathNode.connectedNodes.push(other);
this.updateConnections(pathNode)
.then(() => {
this.refreshComponents(pathNode.connectedNodes);
});
},
removeConnection(pathNode, other) {
const idx = pathNode.connectedNodes.findIndex(n => n.id === other.id);
if (idx > -1) {
pathNode.connectedNodes.splice(idx, 1);
this.updateConnections(pathNode)
.then(() => {
const nodes = [];
nodes.push(pathNode.connectedNodes);
nodes.push(other);
this.refreshComponents(nodes);
})
}
}
}
});

View File

@ -5,10 +5,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import nl.andrewl.railsignalapi.model.RailSystem;
import javax.persistence.Entity;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.ManyToMany;
import javax.persistence.*;
import java.util.Set;
/**
@ -23,7 +20,7 @@ public abstract class PathNode extends Component {
/**
* The set of nodes that this one is connected to.
*/
@ManyToMany
@ManyToMany(cascade = CascadeType.DETACH)
private Set<PathNode> connectedNodes;
public PathNode(RailSystem railSystem, Position position, String name, ComponentType type, Set<PathNode> connectedNodes) {

View File

@ -1,5 +1,10 @@
package nl.andrewl.railsignalapi.rest.dto;
public record PathNodeUpdatePayload (
long[] connectedNodeIds
) {}
import java.util.List;
public class PathNodeUpdatePayload {
public List<NodeIdObj> connectedNodes;
public static class NodeIdObj {
public long id;
}
}

View File

@ -128,7 +128,8 @@ public class ComponentService {
.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()) {
for (var nodeObj : payload.connectedNodes) {
long id = nodeObj.id;
var c1 = componentRepository.findByIdAndRailSystemId(id, rsId);
if (c1.isPresent() && c1.get() instanceof PathNode pn) {
newNodes.add(pn);
@ -136,8 +137,23 @@ public class ComponentService {
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);
Set<PathNode> nodesToRemove = new HashSet<>(p.getConnectedNodes());
nodesToRemove.removeAll(newNodes);
Set<PathNode> nodesToAdd = new HashSet<>(newNodes);
nodesToAdd.removeAll(p.getConnectedNodes());
p.getConnectedNodes().removeAll(nodesToRemove);
p.getConnectedNodes().addAll(nodesToAdd);
for (var node : nodesToRemove) {
node.getConnectedNodes().remove(p);
}
for (var node : nodesToAdd) {
node.getConnectedNodes().add(p);
}
componentRepository.saveAll(nodesToRemove);
componentRepository.saveAll(nodesToAdd);
p = componentRepository.save(p);
return ComponentResponse.of(p);
}