Added proper connection management.
This commit is contained in:
parent
3ac886feeb
commit
a02758ecd4
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue