Drag nodes and links

This commit is contained in:
ziajka 2017-12-01 11:42:40 +01:00
parent abcf6f72de
commit d050e3024d
7 changed files with 158 additions and 59 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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) {

View File

@ -0,0 +1,9 @@
import {Node} from "../../cartography/shared/models/node.model";
export class Database<T> {
}
export class NodeDatabase extends Database<Node> {
}

View File

@ -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);
}
}