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> <h2>{{railSystem.name}}</h2>
<div> <div>
<MapView :railSystem="railSystem" v-if="railSystem.segments && railSystem.components" /> <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> </div>
<SegmentsView /> <SegmentsView />
<AddSignal v-if="railSystem.segments && railSystem.segments.length > 0" /> <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> </p>
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" /> <SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" /> <SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" /> <PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
<button @click="rsStore.removeComponent(component.id)">Remove</button> <button @click="rsStore.removeComponent(component.id)">Remove</button>
</div> </div>
</template> </template>
<script> <script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import SignalComponentView from "./SignalComponentView.vue"; import SignalComponentView from "./SignalComponentView.vue";
import PathNodeComponentView from "./PathNodeComponentView.vue"; import PathNodeComponentView from "./PathNodeComponentView.vue";
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue"; import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
export default { export default {
components: { components: {
@ -42,6 +42,10 @@ export default {
component: { component: {
type: Object, type: Object,
required: true required: true
},
railSystem: {
type: Object,
required: true
} }
} }
} }

View File

@ -3,20 +3,68 @@
<ul v-if="pathNode.connectedNodes.length > 0"> <ul v-if="pathNode.connectedNodes.length > 0">
<li v-for="node in pathNode.connectedNodes" :key="node.id"> <li v-for="node in pathNode.connectedNodes" :key="node.id">
{{node.id}} | {{node.name}} {{node.id}} | {{node.name}}
<button @click="rsStore.removeConnection(pathNode, node)">Remove</button>
</li> </li>
</ul> </ul>
<p v-if="pathNode.connectedNodes.length === 0"> <p v-if="pathNode.connectedNodes.length === 0">
There are no connected nodes. There are no connected nodes.
</p> </p>
<form @submit.prevent="rsStore.addConnection(pathNode, formData.nodeToAdd)">
<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> </template>
<script> <script>
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
export default { export default {
name: "PathNodeComponentView", name: "PathNodeComponentView",
setup() {
const rsStore = useRailSystemsStore();
return {
rsStore
};
},
props: { props: {
pathNode: { pathNode: {
type: Object, type: Object,
required: true 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. 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_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 SCALE_INDEX_NORMAL = 7;
const HOVER_RADIUS = 10; const HOVER_RADIUS = 10;
@ -49,6 +51,13 @@ export function draw() {
const worldTx = getWorldTransform(); const worldTx = getWorldTransform();
ctx.setTransform(worldTx); 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++) { for (let i = 0; i < railSystem.components.length; i++) {
drawComponent(ctx, worldTx, railSystem.components[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() { export function getScaleFactor() {
return SCALE_VALUES[mapScaleIndex]; return SCALE_VALUES[mapScaleIndex];
} }
@ -131,7 +96,7 @@ function getWorldTransform() {
return tx; return tx;
} }
function isComponentHovered(component) { export function isComponentHovered(component) {
for (let i = 0; i < hoveredElements.length; i++) { for (let i = 0; i < hoveredElements.length; i++) {
if (hoveredElements[i].id === component.id) return true; if (hoveredElements[i].id === component.id) return true;
} }
@ -156,18 +121,6 @@ function worldPointToMap(p) {
return getWorldTransform().transformPoint(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 EVENT HANDLING
*/ */
@ -184,6 +137,7 @@ function onMouseWheel(event) {
} }
draw(); draw();
event.stopPropagation(); event.stopPropagation();
return false;
} }
/** /**

View File

@ -54,10 +54,11 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
}); });
}); });
}, },
refreshComponents(rs) { refreshAllComponents(rs) {
return new Promise(resolve => { return new Promise(resolve => {
axios.get(`${this.apiUrl}/rs/${rs.id}/c`) axios.get(`${this.apiUrl}/rs/${rs.id}/c`)
.then(response => { .then(response => {
rs.selectedComponent = null;
rs.components = response.data; rs.components = response.data;
resolve(); resolve();
}); });
@ -66,7 +67,7 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
fetchSelectedRailSystemData() { fetchSelectedRailSystemData() {
if (!this.selectedRailSystem) return; if (!this.selectedRailSystem) return;
this.refreshSegments(this.selectedRailSystem); this.refreshSegments(this.selectedRailSystem);
this.refreshComponents(this.selectedRailSystem); this.refreshAllComponents(this.selectedRailSystem);
}, },
addSegment(name) { addSegment(name) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;
@ -83,13 +84,13 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
addComponent(data) { addComponent(data) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;
axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data) axios.post(`${this.apiUrl}/rs/${rs.id}/c`, data)
.then(() => this.refreshComponents(rs)) .then(() => this.refreshAllComponents(rs))
.catch(error => console.log(error)); .catch(error => console.log(error));
}, },
removeComponent(id) { removeComponent(id) {
const rs = this.selectedRailSystem; const rs = this.selectedRailSystem;
axios.delete(`${this.apiUrl}/rs/${rs.id}/c/${id}`) axios.delete(`${this.apiUrl}/rs/${rs.id}/c/${id}`)
.then(() => this.refreshComponents(rs)) .then(() => this.refreshAllComponents(rs))
.catch(error => console.log(error)); .catch(error => console.log(error));
}, },
fetchComponentData(component) { fetchComponentData(component) {
@ -99,6 +100,51 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
.then(response => resolve(response.data)) .then(response => resolve(response.data))
.catch(error => console.log(error)); .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 lombok.NoArgsConstructor;
import nl.andrewl.railsignalapi.model.RailSystem; import nl.andrewl.railsignalapi.model.RailSystem;
import javax.persistence.Entity; import javax.persistence.*;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.ManyToMany;
import java.util.Set; import java.util.Set;
/** /**
@ -23,7 +20,7 @@ public abstract class PathNode extends Component {
/** /**
* The set of nodes that this one is connected to. * The set of nodes that this one is connected to.
*/ */
@ManyToMany @ManyToMany(cascade = CascadeType.DETACH)
private Set<PathNode> connectedNodes; private Set<PathNode> connectedNodes;
public PathNode(RailSystem railSystem, Position position, String name, ComponentType type, 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; package nl.andrewl.railsignalapi.rest.dto;
public record PathNodeUpdatePayload ( import java.util.List;
long[] connectedNodeIds
) {} 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)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!(c instanceof PathNode p)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component is not a PathNode."); if (!(c instanceof PathNode p)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component is not a PathNode.");
Set<PathNode> newNodes = new HashSet<>(); 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); var c1 = componentRepository.findByIdAndRailSystemId(id, rsId);
if (c1.isPresent() && c1.get() instanceof PathNode pn) { if (c1.isPresent() && c1.get() instanceof PathNode pn) {
newNodes.add(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."); 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); p = componentRepository.save(p);
return ComponentResponse.of(p); return ComponentResponse.of(p);
} }