Dragging nodes labels and updating their positions

This commit is contained in:
ziajka 2018-11-21 10:42:36 +01:00
parent f9d8f0db29
commit f802d8d952
9 changed files with 236 additions and 81 deletions

View File

@ -12,6 +12,9 @@ import { MapNode } from '../../models/map/map-node';
import { MapDrawing } from '../../models/map/map-drawing';
import { DraggedDataEvent } from '../../events/event-source';
import { select } from 'd3-selection';
import { NodeWidget } from '../../widgets/node';
import { MapLabel } from '../../models/map/map-label';
import { LabelWidget } from '../../widgets/label';
@Component({
selector: 'app-draggable-selection',
@ -29,6 +32,7 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
private nodesWidget: NodesWidget,
private drawingsWidget: DrawingsWidget,
private linksWidget: LinksWidget,
private labelWidget: LabelWidget,
private selectionManager: SelectionManager,
private nodesEventSource: NodesEventSource,
private drawingsEventSource: DrawingsEventSource,
@ -40,10 +44,10 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.start = merge(
this.nodesWidget.draggable.start,
this.drawingsWidget.draggable.start
this.drawingsWidget.draggable.start,
this.labelWidget.draggable.start
).subscribe((evt: DraggableStart<any>) => {
const selected = this.selectionManager.getSelected();
if (evt.datum instanceof MapNode) {
if (selected.filter((item) => item instanceof MapNode && item.id === evt.datum.id).length === 0) {
this.selectionManager.setSelected([evt.datum]);
@ -55,16 +59,23 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.selectionManager.setSelected([evt.datum]);
}
}
if (evt.datum instanceof MapLabel) {
if (selected.filter((item) => item instanceof MapLabel && item.id === evt.datum.id).length === 0) {
this.selectionManager.setSelected([evt.datum]);
}
}
});
this.drag = merge(
this.nodesWidget.draggable.drag,
this.drawingsWidget.draggable.drag
this.drawingsWidget.draggable.drag,
this.labelWidget.draggable.drag
).subscribe((evt: DraggableDrag<any>) => {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter((item) => item instanceof MapNode);
// update nodes
selected.filter((item) => item instanceof MapNode).forEach((node: MapNode) => {
selectedNodes.forEach((node: MapNode) => {
node.x += evt.dx;
node.y += evt.dy;
@ -84,21 +95,46 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.drawingsWidget.redrawDrawing(svg, drawing);
});
// update labels
selected.filter((item) => item instanceof MapLabel).forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const node = this.graphDataManager.getNodes().filter((node) => node.id === label.nodeId)[0];
node.label.x += evt.dx;
node.label.y += evt.dy;
this.labelWidget.redrawLabel(svg, label);
});
});
this.end = merge(
this.nodesWidget.draggable.end,
this.drawingsWidget.draggable.end
this.drawingsWidget.draggable.end,
this.labelWidget.draggable.end,
).subscribe((evt: DraggableEnd<any>) => {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter((item) => item instanceof MapNode);
selected.filter((item) => item instanceof MapNode).forEach((item: MapNode) => {
selectedNodes.forEach((item: MapNode) => {
this.nodesEventSource.dragged.emit(new DraggedDataEvent<MapNode>(item, evt.dx, evt.dy));
})
selected.filter((item) => item instanceof MapDrawing).forEach((item: MapDrawing) => {
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<MapDrawing>(item, evt.dx, evt.dy));
});
selected.filter((item) => item instanceof MapLabel).forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
this.nodesEventSource.labelDragged.emit(new DraggedDataEvent<MapLabel>(label, evt.dx, evt.dy));
});
});
}

View File

@ -16,12 +16,14 @@ import { TextDrawingWidget } from './widgets/drawings/text-drawing';
import { LineDrawingWidget } from './widgets/drawings/line-drawing';
import { NodeWidget } from './widgets/node';
import { DrawingWidget } from './widgets/drawing';
import { LabelWidget } from './widgets/label';
export const D3_MAP_IMPORTS = [
GraphLayout,
LinksWidget,
NodesWidget,
NodeWidget,
LabelWidget,
DrawingsWidget,
DrawingLineWidget,
SelectionTool,

View File

@ -44,9 +44,13 @@ export class Draggable<GElement extends DraggedElementBaseType, Datum> {
private behaviour() {
let startEvt;
let lastX: number;
let lastY: number;
return drag<GElement, Datum>()
.on('start', (datum: Datum) => {
lastX = event.sourceEvent.clientX;
lastY = event.sourceEvent.clientY;
startEvt = new DraggableStart<Datum>(datum);
startEvt.dx = event.dx;
startEvt.dy = event.dy;
@ -56,18 +60,18 @@ export class Draggable<GElement extends DraggedElementBaseType, Datum> {
})
.on('drag', (datum: Datum) => {
const evt = new DraggableDrag<Datum>(datum);
evt.dx = event.dx;
evt.dy = event.dy;
evt.x = event.x;
evt.y = event.y;
evt.dx = event.sourceEvent.clientX - lastX;
evt.dy = event.sourceEvent.clientY - lastY;
lastX = event.sourceEvent.clientX;
lastY = event.sourceEvent.clientY;
this.drag.emit(evt);
})
.on('end', (datum: Datum) => {
const evt = new DraggableEnd<Datum>(datum);
evt.dx = event.x - startEvt.x;
evt.dy = event.y - startEvt.y;
evt.x = event.x;
evt.y = event.y;
this.end.emit(evt);
});
}

View File

@ -1,9 +1,11 @@
import { Injectable, EventEmitter } from "@angular/core";
import { DraggedDataEvent } from "./event-source";
import { MapNode } from "../models/map/map-node";
import { MapLabel } from "../models/map/map-label";
@Injectable()
export class NodesEventSource {
public dragged = new EventEmitter<DraggedDataEvent<MapNode>>();
public labelDragged = new EventEmitter<DraggedDataEvent<MapLabel>>();
}

View File

@ -5,6 +5,7 @@ import { InterfaceLabel } from "../models/interface-label";
import { CssFixer } from "../helpers/css-fixer";
import { select } from "d3-selection";
import { MapLink } from "../models/map/map-link";
import { FontFixer } from "../helpers/font-fixer";
@Injectable()
export class InterfaceLabelWidget {
@ -13,7 +14,8 @@ export class InterfaceLabelWidget {
private enabled = true;
constructor(
private cssFixer: CssFixer
private cssFixer: CssFixer,
private fontFixer: FontFixer
) {
}
@ -85,7 +87,11 @@ export class InterfaceLabelWidget {
merge
.select<SVGTextElement>('text.interface_label')
.text((l: InterfaceLabel) => l.text)
.attr('style', (l: InterfaceLabel) => this.cssFixer.fix(l.style))
.attr('style', (l: InterfaceLabel) => {
let styles = this.cssFixer.fix(l.style);
styles = this.fontFixer.fixStyles(styles);
return styles;
})
.attr('x', -InterfaceLabelWidget.SURROUNDING_TEXT_BORDER)
.attr('y', -InterfaceLabelWidget.SURROUNDING_TEXT_BORDER);

View File

@ -0,0 +1,135 @@
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { select } from "d3-selection";
import { MapNode } from "../models/map/map-node";
import { SelectionManager } from "../managers/selection-manager";
import { Draggable } from "../events/draggable";
import { MapLabel } from "../models/map/map-label";
@Injectable()
export class LabelWidget implements Widget {
public draggable = new Draggable<SVGGElement, MapLabel>();
static NODE_LABEL_MARGIN = 3;
constructor(
private cssFixer: CssFixer,
private fontFixer: FontFixer,
private selectionManager: SelectionManager
) {}
public redrawLabel(view: SVGSelection, label: MapLabel) {
this.drawLabel(this.selectLabel(view, label));
}
public draw(view: SVGSelection) {
const label_view = view
.selectAll<SVGGElement, MapLabel>("g.label_container")
.data((node: MapNode) => {
return [node.label];
});
const label_enter = label_view.enter()
.append<SVGGElement>('g')
.attr('class', 'label_container')
.attr('label_id', (l: MapLabel) => l.id)
const merge = label_view.merge(label_enter);
this.drawLabel(merge);
label_view
.exit()
.remove();
this.draggable.call(label_view);
}
private drawLabel(view: SVGSelection) {
const label_body = view.selectAll<SVGGElement, MapLabel>("g.label_body")
.data((label) => [label]);
const label_body_enter = label_body.enter()
.append<SVGGElement>('g')
.attr("class", "label_body");
// add label of node
label_body_enter
.append<SVGTextElement>('text')
.attr('class', 'label');
label_body_enter
.append<SVGRectElement>('rect')
.attr('class', 'label_selection');
const label_body_merge = label_body.merge(label_body_enter)
label_body_merge
.select<SVGTextElement>('text.label')
.attr('label_id', (l: MapLabel) => l.id)
// .attr('y', (n: Node) => n.label.y - n.height / 2. + 10) // @todo: server computes y in auto way
.attr('style', (l: MapLabel) => {
let styles = this.cssFixer.fix(l.style);
styles = this.fontFixer.fixStyles(styles);
return styles;
})
.text((l: MapLabel) => l.text)
.attr('x', function (this: SVGTextElement, l: MapLabel) {
// if (l.x === null) {
// // center
// const bbox = this.getBBox();
// return -bbox.width / 2.;
// }
return l.x + LabelWidget.NODE_LABEL_MARGIN;
})
.attr('y', function (this: SVGTextElement, l: MapLabel) {
let bbox = this.getBBox();
// if (n.label.x === null) {
// // center
// bbox = this.getBBox();
// return - n.height / 2. - bbox.height ;
// selected.filter((item) => item instanceof MapLabel).forEach((label: MapLabel) => {
// label.x += evt.dx;
// label.y += evt.dy;
// console.log("test");
// // this.drawingsWidget.redrawDrawing(svg, label);
// });
// }
return l.y + bbox.height - LabelWidget.NODE_LABEL_MARGIN;
})
.attr('transform', (l: MapLabel) => {
return `rotate(${l.rotation}, 0, 0)`;
})
label_body_merge
.select<SVGRectElement>('rect.label_selection')
.attr('visibility', (l: MapLabel) => this.selectionManager.isSelected(l) ? 'visible' : 'hidden')
.attr('stroke', 'black')
.attr('stroke-dasharray', '3,3')
.attr('stroke-width', '0.5')
.attr('fill', 'none')
.each(function (this: SVGRectElement, l: MapLabel) {
const current = select(this);
const textLabel = label_body_merge.select<SVGTextElement>(`text[label_id="${l.id}"]`);
const bbox = textLabel.node().getBBox();
const border = 2;
current.attr('width', bbox.width + border * 2);
current.attr('height', bbox.height + border * 2);
current.attr('x', bbox.x - border);
current.attr('y', bbox.y - border);
});
}
private selectLabel(view: SVGSelection, label: MapLabel) {
return view.selectAll<SVGGElement, MapLabel>(`g.label_container[label_id="${label.id}"]`);
}
}

View File

@ -3,27 +3,23 @@ import { Injectable, EventEmitter } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { NodeContextMenu, NodeClicked } from "../events/nodes";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { select, event } from "d3-selection";
import { MapSymbol } from "../models/map/map-symbol";
import { MapNode } from "../models/map/map-node";
import { GraphDataManager } from "../managers/graph-data-manager";
import { SelectionManager } from "../managers/selection-manager";
import { LabelWidget } from "./label";
@Injectable()
export class NodeWidget implements Widget {
static NODE_LABEL_MARGIN = 3;
public onContextMenu = new EventEmitter<NodeContextMenu>();
public onNodeClicked = new EventEmitter<NodeClicked>();
constructor(
private cssFixer: CssFixer,
private fontFixer: FontFixer,
private graphDataManager: GraphDataManager,
private selectionManager: SelectionManager
private selectionManager: SelectionManager,
private labelWidget: LabelWidget
) {}
public draw(view: SVGSelection) {
@ -39,14 +35,6 @@ export class NodeWidget implements Widget {
node_body_enter
.append<SVGImageElement>('image');
// add label of node
node_body_enter
.append<SVGTextElement>('text')
.attr('class', 'label');
node_body_enter
.append<SVGRectElement>('rect')
.attr('class', 'label_selection');
const node_body_merge = node_body.merge(node_body_enter)
.classed('selected', (n: MapNode) => this.selectionManager.isSelected(n))
@ -84,56 +72,7 @@ export class NodeWidget implements Widget {
.attr('transform', (n: MapNode) => {
return `translate(${n.x},${n.y})`;
});
node_body_merge
.select<SVGTextElement>('text.label')
.attr('label_id', (n: MapNode) => n.label.id)
// .attr('y', (n: Node) => n.label.y - n.height / 2. + 10) // @todo: server computes y in auto way
.attr('style', (n: MapNode) => {
let styles = this.cssFixer.fix(n.label.style);
styles = this.fontFixer.fixStyles(styles);
return styles;
})
.text((n: MapNode) => n.label.text)
.attr('x', function (this: SVGTextElement, n: MapNode) {
if (n.label.x === null) {
// center
const bbox = this.getBBox();
return -bbox.width / 2.;
}
return n.label.x + NodeWidget.NODE_LABEL_MARGIN;
})
.attr('y', function (this: SVGTextElement, n: MapNode) {
let bbox = this.getBBox();
if (n.label.x === null) {
// center
bbox = this.getBBox();
return - n.height / 2. - bbox.height ;
}
return n.label.y + bbox.height - NodeWidget.NODE_LABEL_MARGIN;
})
.attr('transform', (node) => {
return `rotate(${node.label.rotation}, 0, 0)`;
})
node_body_merge
.select<SVGRectElement>('rect.label_selection')
.attr('visibility', (n: MapNode) => this.selectionManager.isSelected(n.label) ? 'visible' : 'hidden')
.attr('stroke', 'black')
.attr('stroke-dasharray', '3,3')
.attr('stroke-width', '0.5')
.attr('fill', 'none')
.each(function (this: SVGRectElement, node: MapNode) {
const current = select(this);
const textLabel = node_body_merge.select<SVGTextElement>(`text[label_id="${node.label.id}"]`);
const bbox = textLabel.node().getBBox();
const border = 2;
current.attr('width', bbox.width + border * 2);
current.attr('height', bbox.height + border * 2);
current.attr('x', bbox.x - border);
current.attr('y', bbox.y - border);
});
this.labelWidget.draw(node_body_merge);
}
}

View File

@ -37,6 +37,7 @@ import { LinksEventSource } from '../../cartography/events/links-event-source';
import { MapDrawing } from '../../cartography/models/map/map-drawing';
import { MapPortToPortConverter } from '../../cartography/converters/map/map-port-to-port-converter';
import { SettingsService, Settings } from '../../services/settings.service';
import { MapLabel } from '../../cartography/models/map/map-label';
@Component({
@ -160,6 +161,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.nodesEventSource.dragged.subscribe((evt) => this.onNodeDragged(evt))
);
this.subscriptions.push(
this.nodesEventSource.labelDragged.subscribe((evt) => this.onNodeLabelDragged(evt))
);
this.subscriptions.push(
this.drawingsEventSource.dragged.subscribe((evt) => this.onDrawingDragged(evt))
);
@ -245,6 +250,18 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
});
}
private onNodeLabelDragged(draggedEvent: DraggedDataEvent<MapLabel>) {
const node = this.nodesDataSource.get(draggedEvent.datum.nodeId);
node.label.x += draggedEvent.dx;
node.label.y += draggedEvent.dy;
this.nodeService
.updateLabel(this.server, node, node.label)
.subscribe((serverNode: Node) => {
this.nodesDataSource.update(serverNode);
});
}
private onDrawingDragged(draggedEvent: DraggedDataEvent<MapDrawing>) {
const drawing = this.drawingsDataSource.get(draggedEvent.datum.id);
drawing.x += draggedEvent.dx;

View File

@ -7,6 +7,7 @@ import 'rxjs/add/operator/map';
import { Server } from "../models/server";
import { HttpServer } from "./http-server.service";
import {Appliance} from "../models/appliance";
import { Label } from '../cartography/models/label';
@Injectable()
@ -42,6 +43,19 @@ export class NodeService {
});
}
updateLabel(server: Server, node: Node, label: Label): Observable<Node> {
return this.httpServer
.put<Node>(server, `/projects/${node.project_id}/nodes/${node.node_id}`, {
'label': {
'rotation': label.rotation,
'style': label.style,
'text': label.text,
'x': label.x,
'y': label.y
}
});
}
update(server: Server, node: Node): Observable<Node> {
return this.httpServer
.put<Node>(server, `/projects/${node.project_id}/nodes/${node.node_id}`, {