mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-04-20 08:10:49 +00:00
Drag nodes and links
This commit is contained in:
parent
abcf6f72de
commit
d050e3024d
@ -2,7 +2,7 @@ import {
|
||||
Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange
|
||||
} from '@angular/core';
|
||||
import { D3, D3Service } from 'd3-ng2-service';
|
||||
import { Selection } from 'd3-selection';
|
||||
import {select, Selection} from 'd3-selection';
|
||||
|
||||
import { Node } from "../shared/models/node.model";
|
||||
import { Link } from "../shared/models/link.model";
|
||||
@ -10,6 +10,7 @@ import { GraphLayout } from "../shared/widgets/graph.widget";
|
||||
import { Context } from "../../map/models/context";
|
||||
import { Size } from "../shared/models/size.model";
|
||||
import { Drawing } from "../shared/models/drawing.model";
|
||||
import {SVGSelection} from "../../map/models/types";
|
||||
|
||||
|
||||
@Component({
|
||||
@ -87,6 +88,17 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
this.graphLayout = new GraphLayout();
|
||||
|
||||
this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((n: Node) => {
|
||||
const linksWidget = this.graphLayout.getLinksWidget();
|
||||
linksWidget.select(this.svg).each(function(this: SVGGElement, link: Link) {
|
||||
if (link.target.node_id === n.node_id || link.source.node_id === n.node_id) {
|
||||
const selection = select<SVGElement, Link>(this);
|
||||
linksWidget.revise(selection);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.graphLayout.draw(this.svg, this.graphContext);
|
||||
|
||||
}
|
||||
|
@ -16,12 +16,24 @@ export class GraphLayout implements Widget {
|
||||
private links: Link[] = [];
|
||||
private drawings: Drawing[] = [];
|
||||
|
||||
private nodesWidget = new NodesWidget();
|
||||
private linksWidget = new LinksWidget();
|
||||
private drawingsWidget = new DrawingsWidget();
|
||||
private linksWidget: LinksWidget;
|
||||
private nodesWidget: NodesWidget;
|
||||
private drawingsWidget: DrawingsWidget;
|
||||
|
||||
private centerZeroZeroPoint = true;
|
||||
|
||||
constructor() {
|
||||
this.linksWidget = new LinksWidget();
|
||||
this.nodesWidget = new NodesWidget();
|
||||
|
||||
// this.nodesWidget.addOnNodeDraggingCallback((n: Node) => {
|
||||
// this.linksWidget.
|
||||
// // this.linksWidget.draw();
|
||||
// });
|
||||
|
||||
this.drawingsWidget = new DrawingsWidget();
|
||||
}
|
||||
|
||||
public setNodes(nodes: Node[]) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
@ -38,6 +50,14 @@ export class GraphLayout implements Widget {
|
||||
return this.nodesWidget;
|
||||
}
|
||||
|
||||
public getLinksWidget() {
|
||||
return this.linksWidget;
|
||||
}
|
||||
|
||||
public getCanvas(view: SVGSelection) {
|
||||
|
||||
}
|
||||
|
||||
draw(view: SVGSelection, context: Context) {
|
||||
const self = this;
|
||||
|
||||
@ -55,12 +75,6 @@ export class GraphLayout implements Widget {
|
||||
(ctx: Context) => `translate(${ctx.getSize().width / 2}, ${ctx.getSize().height / 2})`);
|
||||
}
|
||||
|
||||
// const links = canvasEnter.append<SVGGElement>('g')
|
||||
// .attr('class', 'links');
|
||||
//
|
||||
// const nodes = canvasEnter.append<SVGGElement>('g')
|
||||
// .attr('class', 'nodes');
|
||||
|
||||
this.linksWidget.draw(canvas, this.links);
|
||||
this.nodesWidget.draw(canvas, this.nodes);
|
||||
this.drawingsWidget.draw(canvas, this.drawings);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { select } from "d3-selection";
|
||||
import {BaseType, select, Selection} from "d3-selection";
|
||||
import { line } from "d3-shape";
|
||||
|
||||
import { Widget } from "./widget";
|
||||
@ -13,33 +13,21 @@ import {EthernetLinkWidget} from "./ethernet-link.widget";
|
||||
export class LinksWidget implements Widget {
|
||||
private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper();
|
||||
|
||||
private getLinkWidget(link: Link) {
|
||||
public getLinkWidget(link: Link) {
|
||||
if (link.link_type === 'serial') {
|
||||
return new SerialLinkWidget();
|
||||
}
|
||||
return new EthernetLinkWidget();
|
||||
}
|
||||
|
||||
public draw(view: SVGSelection, links: Link[]) {
|
||||
public select(view: SVGSelection) {
|
||||
return view.selectAll<SVGGElement, Link>("g.link");
|
||||
}
|
||||
|
||||
public revise(selection: Selection<BaseType, Link, SVGElement, any>) {
|
||||
const self = this;
|
||||
|
||||
this.multiLinkCalculatorHelper.assignDataToLinks(links);
|
||||
|
||||
const link = view
|
||||
.selectAll("g.link")
|
||||
.data(links.filter((l: Link) => {
|
||||
return l.target && l.source;
|
||||
}));
|
||||
|
||||
const link_enter = link.enter()
|
||||
.append<SVGGElement>('g')
|
||||
.attr('class', 'link')
|
||||
.attr('link_id', (l: Link) => l.link_id)
|
||||
.attr('map-source', (l: Link) => l.source.node_id)
|
||||
.attr('map-target', (l: Link) => l.target.node_id);
|
||||
|
||||
link.merge(link_enter)
|
||||
.each(function (this: SVGGElement, l: Link) {
|
||||
selection.each(function (this: SVGGElement, l: Link) {
|
||||
const link_group = select<SVGGElement, Link>(this);
|
||||
const link_widget = self.getLinkWidget(l);
|
||||
|
||||
@ -93,7 +81,28 @@ export class LinksWidget implements Widget {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
link.exit().remove();
|
||||
public draw(view: SVGSelection, links: Link[]) {
|
||||
const self = this;
|
||||
|
||||
this.multiLinkCalculatorHelper.assignDataToLinks(links);
|
||||
|
||||
const link = view
|
||||
.selectAll("g.link")
|
||||
.data(links.filter((l: Link) => {
|
||||
return l.target && l.source;
|
||||
}));
|
||||
|
||||
const link_enter = link.enter()
|
||||
.append<SVGGElement>('g')
|
||||
.attr('class', 'link')
|
||||
.attr('link_id', (l: Link) => l.link_id)
|
||||
.attr('map-source', (l: Link) => l.source.node_id)
|
||||
.attr('map-target', (l: Link) => l.target.node_id);
|
||||
|
||||
this.revise(link.merge(link_enter));
|
||||
|
||||
link.exit().remove();
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,66 @@
|
||||
import { Widget } from "./widget";
|
||||
import { Node } from "../models/node.model";
|
||||
import { SVGSelection } from "../../../map/models/types";
|
||||
import { event } from "d3-selection";
|
||||
import {event, select} from "d3-selection";
|
||||
import {D3DragEvent, drag} from "d3-drag";
|
||||
|
||||
export interface NodeOnContextMenuListener {
|
||||
onContextMenu(): void;
|
||||
};
|
||||
|
||||
export class NodesWidget implements Widget {
|
||||
private onContextMenuListener: NodeOnContextMenuListener;
|
||||
private onContextMenuCallback: (event: any, node: Node) => void;
|
||||
private onNodeDraggedCallback: (event: any, node: Node) => void;
|
||||
private onNodeDraggingCallbacks: ((event: any, node: Node) => void)[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
public setOnContextMenuListener(onContextMenuListener: NodeOnContextMenuListener) {
|
||||
this.onContextMenuListener = onContextMenuListener;
|
||||
}
|
||||
|
||||
public setOnContextMenuCallback(onContextMenuCallback: (event: any, node: Node) => void) {
|
||||
this.onContextMenuCallback = onContextMenuCallback;
|
||||
}
|
||||
|
||||
public setOnNodeDraggedCallback(onNodeDraggedCallback: (event: any, node: Node) => void) {
|
||||
this.onNodeDraggedCallback = onNodeDraggedCallback;
|
||||
}
|
||||
|
||||
public addOnNodeDraggingCallback(onNodeDraggingCallback: (n: Node) => void) {
|
||||
this.onNodeDraggingCallbacks.push(onNodeDraggingCallback);
|
||||
}
|
||||
|
||||
private executeOnNodeDraggingCallback(n: Node) {
|
||||
this.onNodeDraggingCallbacks.forEach((callback: (n: Node) => void) => {
|
||||
callback(n);
|
||||
});
|
||||
}
|
||||
|
||||
public revise(selection: SVGSelection) {
|
||||
selection
|
||||
.attr('transform', (n: Node) => {
|
||||
return `translate(${n.x},${n.y})`;
|
||||
});
|
||||
|
||||
selection
|
||||
.select<SVGTextElement>('text.label')
|
||||
.attr('x', (n: Node) => n.label.x)
|
||||
.attr('y', (n: Node) => n.label.y)
|
||||
.attr('style', (n: Node) => n.label.style)
|
||||
.text((n: Node) => n.label.text);
|
||||
|
||||
selection
|
||||
.select<SVGTextElement>('text.node_point_label')
|
||||
.text((n: Node) => `(${n.x}, ${n.y})`);
|
||||
|
||||
}
|
||||
|
||||
public draw(view: SVGSelection, nodes: Node[]) {
|
||||
const self = this;
|
||||
|
||||
// function dragged(this: SVGElement, node: Node) {
|
||||
// const element = this;
|
||||
// const e: D3DragEvent<SVGGElement, Node, Node> = d3.event;
|
||||
//
|
||||
// d3.select(this)
|
||||
// .attr('transform', `translate(${e.x},${e.y})`);
|
||||
//
|
||||
// node.x = e.x;
|
||||
// node.y = e.y;
|
||||
// }
|
||||
|
||||
const node = view.selectAll<SVGGElement, any>('g.node')
|
||||
.data(nodes);
|
||||
|
||||
const node_enter = node.enter()
|
||||
.append<SVGGElement>('g')
|
||||
.attr('class', 'node');
|
||||
// .call(d3.drag<SVGGElement, Node>().on('drag', dragged))
|
||||
|
||||
const node_image = node_enter.append<SVGImageElement>('image')
|
||||
.attr('xlink:href', (n: Node) => 'data:image/svg+xml;base64,' + btoa(n.icon.raw))
|
||||
@ -67,19 +86,32 @@ export class NodesWidget implements Widget {
|
||||
if (self.onContextMenuCallback !== null) {
|
||||
self.onContextMenuCallback(event, n);
|
||||
}
|
||||
})
|
||||
.attr('transform', (n: Node) => {
|
||||
return `translate(${n.x},${n.y})`;
|
||||
});
|
||||
|
||||
node_merge.select<SVGTextElement>('text.label')
|
||||
.attr('x', (n: Node) => n.label.x)
|
||||
.attr('y', (n: Node) => n.label.y)
|
||||
.attr('style', (n: Node) => n.label.style)
|
||||
.text((n: Node) => n.label.text);
|
||||
this.revise(node_merge);
|
||||
|
||||
node_merge.select<SVGTextElement>('text.node_point_label')
|
||||
.text((n: Node) => `(${n.x}, ${n.y})`);
|
||||
const callback = function (this: SVGGElement, n: Node) {
|
||||
const e: D3DragEvent<SVGGElement, Node, Node> = event;
|
||||
|
||||
n.x = e.x;
|
||||
n.y = e.y;
|
||||
|
||||
self.revise(select(this));
|
||||
self.executeOnNodeDraggingCallback(n);
|
||||
};
|
||||
|
||||
const dragging = () => {
|
||||
return drag<SVGGElement, Node>()
|
||||
.on('drag', callback)
|
||||
.on('end', (n: Node) => {
|
||||
if (self.onNodeDraggedCallback) {
|
||||
const e: D3DragEvent<SVGGElement, Node, Node> = event;
|
||||
self.onNodeDraggedCallback(e, n);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
node_merge.call(dragging());
|
||||
|
||||
node.exit().remove();
|
||||
}
|
||||
|
@ -177,6 +177,21 @@ export class ProjectMapComponent implements OnInit {
|
||||
this.mapChild.graphLayout.getNodesWidget().setOnContextMenuCallback((event: any, node: Node) => {
|
||||
this.nodeContextMenu.open(node, event.clientY, event.clientX);
|
||||
});
|
||||
|
||||
this.mapChild.graphLayout.getNodesWidget().setOnNodeDraggedCallback((event: any, node: Node) => {
|
||||
const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id);
|
||||
if (index >= 0) {
|
||||
this.nodes[index] = node;
|
||||
this.mapChild.reload(); // temporary invocation
|
||||
|
||||
this.nodeService
|
||||
.updatePosition(this.server, node, node.x, node.y)
|
||||
.subscribe((n: Node) => {
|
||||
this.nodes[index] = node;
|
||||
this.mapChild.reload(); // temporary invocation
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNodeCreation(appliance: Appliance) {
|
||||
|
9
src/app/shared/databases/node-database.ts
Normal file
9
src/app/shared/databases/node-database.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {Node} from "../../cartography/shared/models/node.model";
|
||||
|
||||
export class Database<T> {
|
||||
|
||||
}
|
||||
|
||||
export class NodeDatabase extends Database<Node> {
|
||||
|
||||
}
|
@ -37,4 +37,12 @@ export class NodeService {
|
||||
{'x': x, 'y': y, 'compute_id': compute_id});
|
||||
}
|
||||
|
||||
updatePosition(server: Server, node: Node, x: number, y: number): Observable<Node> {
|
||||
return this.httpServer
|
||||
.put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, {
|
||||
'x': x,
|
||||
'y': y
|
||||
})
|
||||
.map(response => response.json() as Node);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user