Drag interface labels

This commit is contained in:
ziajka
2018-11-26 13:36:33 +01:00
parent 01cefc5c79
commit 8b91549984
11 changed files with 220 additions and 88 deletions

View File

@ -12,9 +12,11 @@ import { MapNode } from '../../models/map/map-node';
import { MapDrawing } from '../../models/map/map-drawing'; import { MapDrawing } from '../../models/map/map-drawing';
import { DraggedDataEvent } from '../../events/event-source'; import { DraggedDataEvent } from '../../events/event-source';
import { select } from 'd3-selection'; import { select } from 'd3-selection';
import { NodeWidget } from '../../widgets/node';
import { MapLabel } from '../../models/map/map-label'; import { MapLabel } from '../../models/map/map-label';
import { LabelWidget } from '../../widgets/label'; import { LabelWidget } from '../../widgets/label';
import { InterfaceLabelWidget } from '../../widgets/interface-label';
import { MapLinkNode } from '../../models/map/map-link-node';
import { LinksEventSource } from '../../events/links-event-source';
@Component({ @Component({
selector: 'app-draggable-selection', selector: 'app-draggable-selection',
@ -33,10 +35,12 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
private drawingsWidget: DrawingsWidget, private drawingsWidget: DrawingsWidget,
private linksWidget: LinksWidget, private linksWidget: LinksWidget,
private labelWidget: LabelWidget, private labelWidget: LabelWidget,
private interfaceWidget: InterfaceLabelWidget,
private selectionManager: SelectionManager, private selectionManager: SelectionManager,
private nodesEventSource: NodesEventSource, private nodesEventSource: NodesEventSource,
private drawingsEventSource: DrawingsEventSource, private drawingsEventSource: DrawingsEventSource,
private graphDataManager: GraphDataManager private graphDataManager: GraphDataManager,
private linksEventSource: LinksEventSource
) { } ) { }
ngOnInit() { ngOnInit() {
@ -45,7 +49,8 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.start = merge( this.start = merge(
this.nodesWidget.draggable.start, this.nodesWidget.draggable.start,
this.drawingsWidget.draggable.start, this.drawingsWidget.draggable.start,
this.labelWidget.draggable.start this.labelWidget.draggable.start,
this.interfaceWidget.draggable.start
).subscribe((evt: DraggableStart<any>) => { ).subscribe((evt: DraggableStart<any>) => {
const selected = this.selectionManager.getSelected(); const selected = this.selectionManager.getSelected();
if (evt.datum instanceof MapNode) { if (evt.datum instanceof MapNode) {
@ -65,12 +70,19 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.selectionManager.setSelected([evt.datum]); this.selectionManager.setSelected([evt.datum]);
} }
} }
if (evt.datum instanceof MapLinkNode) {
if (selected.filter((item) => item instanceof MapLinkNode && item.id === evt.datum.id).length === 0) {
this.selectionManager.setSelected([evt.datum]);
}
}
}); });
this.drag = merge( this.drag = merge(
this.nodesWidget.draggable.drag, this.nodesWidget.draggable.drag,
this.drawingsWidget.draggable.drag, this.drawingsWidget.draggable.drag,
this.labelWidget.draggable.drag this.labelWidget.draggable.drag,
this.interfaceWidget.draggable.drag
).subscribe((evt: DraggableDrag<any>) => { ).subscribe((evt: DraggableDrag<any>) => {
const selected = this.selectionManager.getSelected(); const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter((item) => item instanceof MapNode); const selectedNodes = selected.filter((item) => item instanceof MapNode);
@ -108,12 +120,33 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.labelWidget.redrawLabel(svg, label); this.labelWidget.redrawLabel(svg, label);
}); });
// update interface labels
selected.filter((item) => item instanceof MapLinkNode).forEach((interfaceLabel: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === interfaceLabel.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const link = this.graphDataManager.getLinks().filter((link) => link.nodes[0].id === interfaceLabel.id || link.nodes[1].id === interfaceLabel.id)[0];
if(link.nodes[0].id === interfaceLabel.id) {
link.nodes[0].label.x += evt.dx;
link.nodes[0].label.y += evt.dy;
}
if(link.nodes[1].id === interfaceLabel.id) {
link.nodes[1].label.x += evt.dx;
link.nodes[1].label.y += evt.dy;
}
this.linksWidget.redrawLink(svg, link);
});
}); });
this.end = merge( this.end = merge(
this.nodesWidget.draggable.end, this.nodesWidget.draggable.end,
this.drawingsWidget.draggable.end, this.drawingsWidget.draggable.end,
this.labelWidget.draggable.end, this.labelWidget.draggable.end,
this.interfaceWidget.draggable.end
).subscribe((evt: DraggableEnd<any>) => { ).subscribe((evt: DraggableEnd<any>) => {
const selected = this.selectionManager.getSelected(); const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter((item) => item instanceof MapNode); const selectedNodes = selected.filter((item) => item instanceof MapNode);
@ -135,7 +168,17 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy {
this.nodesEventSource.labelDragged.emit(new DraggedDataEvent<MapLabel>(label, evt.dx, evt.dy)); this.nodesEventSource.labelDragged.emit(new DraggedDataEvent<MapLabel>(label, evt.dx, evt.dy));
}); });
selected.filter((item) => item instanceof MapLinkNode).forEach((label: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
this.linksEventSource.interfaceDragged.emit(new DraggedDataEvent<MapLinkNode>(label, evt.dx, evt.dy));
}); });
});
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -44,11 +44,35 @@ export class SelectionControlComponent implements OnInit, OnDestroy {
return this.inRectangleHelper.inRectangle(rectangle, labelX, labelY); return this.inRectangleHelper.inRectangle(rectangle, labelX, labelY);
}).map((node) => node.label); }).map((node) => node.label);
const selectedInterfacesLabelsSources = this.graphDataManager.getLinks().filter((link) => {
if (link.source === undefined || link.nodes.length != 2 || link.nodes[0].label === undefined) {
return false;
}
const interfaceLabelX = link.source.x + link.nodes[0].label.x;
const interfaceLabelY = link.source.y + link.nodes[0].label.y;
return this.inRectangleHelper.inRectangle(rectangle, interfaceLabelX, interfaceLabelY);
}).map((link) => link.nodes[0]);
const selectedInterfacesLabelsTargets = this.graphDataManager.getLinks().filter((link) => {
if (link.target === undefined || link.nodes.length != 2 || link.nodes[1].label === undefined) {
return false;
}
const interfaceLabelX = link.target.x + link.nodes[1].label.x;
const interfaceLabelY = link.target.y + link.nodes[1].label.y;
return this.inRectangleHelper.inRectangle(rectangle, interfaceLabelX, interfaceLabelY);
}).map((link) => link.nodes[1]);
const selectedInterfaces = [
...selectedInterfacesLabelsSources,
...selectedInterfacesLabelsTargets,
]
const selected = [ const selected = [
...selectedNodes, ...selectedNodes,
...selectedLinks, ...selectedLinks,
...selectedDrawings, ...selectedDrawings,
...selectedLabels ...selectedLabels,
...selectedInterfaces,
]; ];
this.selectionManager.setSelected(selected); this.selectionManager.setSelected(selected);

View File

@ -9,15 +9,21 @@ import { MapLinkNode } from "../../models/map/map-link-node";
@Injectable() @Injectable()
export class LinkNodeToMapLinkNodeConverter implements Converter<LinkNode, MapLinkNode> { export class LinkNodeToMapLinkNodeConverter implements Converter<LinkNode, MapLinkNode> {
constructor( constructor(
private labelToMapLabel: LabelToMapLabelConverter private labelToMapLabel: LabelToMapLabelConverter,
) {} ) {}
convert(linkNode: LinkNode) { convert(linkNode: LinkNode, paramaters?: {[link_id: string]: string}) {
const mapLinkNode = new MapLinkNode(); const mapLinkNode = new MapLinkNode();
mapLinkNode.nodeId = linkNode.node_id; mapLinkNode.nodeId = linkNode.node_id;
mapLinkNode.adapterNumber = linkNode.adapter_number; mapLinkNode.adapterNumber = linkNode.adapter_number;
mapLinkNode.portNumber = linkNode.port_number; mapLinkNode.portNumber = linkNode.port_number;
mapLinkNode.label = this.labelToMapLabel.convert(linkNode.label); mapLinkNode.label = this.labelToMapLabel.convert(linkNode.label);
if (paramaters !== undefined) {
mapLinkNode.linkId = paramaters.link_id;
mapLinkNode.id = `${mapLinkNode.nodeId}-${mapLinkNode.linkId}`;
}
return mapLinkNode; return mapLinkNode;
} }
} }

View File

@ -19,7 +19,7 @@ export class LinkToMapLinkConverter implements Converter<Link, MapLink> {
mapLink.captureFilePath = link.capture_file_path; mapLink.captureFilePath = link.capture_file_path;
mapLink.capturing = link.capturing; mapLink.capturing = link.capturing;
mapLink.linkType = link.link_type; mapLink.linkType = link.link_type;
mapLink.nodes = link.nodes.map((linkNode) => this.linkNodeToMapLinkNode.convert(linkNode)); mapLink.nodes = link.nodes.map((linkNode) => this.linkNodeToMapLinkNode.convert(linkNode,{ link_id: link.link_id }));
mapLink.projectId = link.project_id; mapLink.projectId = link.project_id;
return mapLink; return mapLink;
} }

View File

@ -7,7 +7,7 @@ import { MapLabel } from "../../models/map/map-label";
@Injectable() @Injectable()
export class MapLabelToLabelConverter implements Converter<MapLabel, Label> { export class MapLabelToLabelConverter implements Converter<MapLabel, Label> {
convert(mapLabel: MapLabel, paramters?: any) { convert(mapLabel: MapLabel) {
const label = new Label(); const label = new Label();
label.rotation = mapLabel.rotation; label.rotation = mapLabel.rotation;
label.style = mapLabel.style; label.style = mapLabel.style;

View File

@ -1,8 +1,11 @@
import { Injectable, EventEmitter } from "@angular/core"; import { Injectable, EventEmitter } from "@angular/core";
import { MapLinkCreated } from "./links"; import { MapLinkCreated } from "./links";
import { MapLinkNode } from "../models/map/map-link-node";
import { DraggedDataEvent } from "./event-source";
@Injectable() @Injectable()
export class LinksEventSource { export class LinksEventSource {
public created = new EventEmitter<MapLinkCreated>(); public created = new EventEmitter<MapLinkCreated>();
public interfaceDragged = new EventEmitter<DraggedDataEvent<MapLinkNode>>();
} }

View File

@ -1,5 +1,8 @@
export class InterfaceLabel { import { Indexed } from "../datasources/map-datasource";
export class InterfaceLabel implements Indexed {
constructor( constructor(
public id: string,
public link_id: string, public link_id: string,
public direction: string, public direction: string,
public x: number, public x: number,

View File

@ -1,7 +1,10 @@
import { MapLabel } from "./map-label"; import { MapLabel } from "./map-label";
import { Indexed } from "../../datasources/map-datasource";
export class MapLinkNode { export class MapLinkNode implements Indexed {
id: string;
nodeId: string; nodeId: string;
linkId: string;
adapterNumber: number; adapterNumber: number;
portNumber: number; portNumber: number;
label: MapLabel; label: MapLabel;

View File

@ -6,50 +6,53 @@ import { CssFixer } from "../helpers/css-fixer";
import { select } from "d3-selection"; import { select } from "d3-selection";
import { MapLink } from "../models/map/map-link"; import { MapLink } from "../models/map/map-link";
import { FontFixer } from "../helpers/font-fixer"; import { FontFixer } from "../helpers/font-fixer";
import { SelectionManager } from "../managers/selection-manager";
import { MapLinkNode } from "../models/map/map-link-node";
import { MapNode } from "../models/map/map-node";
import { Draggable } from "../events/draggable";
@Injectable() @Injectable()
export class InterfaceLabelWidget { export class InterfaceLabelWidget {
static SURROUNDING_TEXT_BORDER = 5; public draggable = new Draggable<SVGGElement, MapLinkNode>();
static SURROUNDING_TEXT_BORDER = 5;
private enabled = true; private enabled = true;
constructor( constructor(
private cssFixer: CssFixer, private cssFixer: CssFixer,
private fontFixer: FontFixer private fontFixer: FontFixer,
private selectionManager: SelectionManager
) { ) {
} }
public setEnabled(enabled: boolean) { public setEnabled(enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
draw(selection: SVGSelection) { draw(selection: SVGSelection) {
const link_node_position = selection
.selectAll<SVGGElement, MapLinkNode>('g.link_node_position')
.data((link: MapLink) => [
[link.source, link.nodes[0]],
[link.target, link.nodes[1]]
]);
const labels = selection const enter_link_node_position = link_node_position
.enter()
.append<SVGGElement>('g')
.classed('link_node_position', true);
const merge_link_node_position = link_node_position.merge(enter_link_node_position);
merge_link_node_position.attr('transform', (nodeAndMapLinkNode: [MapNode, MapLinkNode]) => {
return `translate(${nodeAndMapLinkNode[0].x}, ${nodeAndMapLinkNode[0].y})`;
});
const labels = merge_link_node_position
.selectAll<SVGGElement, InterfaceLabel>('g.interface_label_container') .selectAll<SVGGElement, InterfaceLabel>('g.interface_label_container')
.data((l: MapLink) => { .data((nodeAndMapLinkNode: [MapNode, MapLinkNode]) => {
const sourceInterface = new InterfaceLabel(
l.id,
'source',
Math.round(l.source.x + l.nodes[0].label.x),
Math.round(l.source.y + l.nodes[0].label.y),
l.nodes[0].label.text,
l.nodes[0].label.style,
l.nodes[0].label.rotation
);
const targetInterface = new InterfaceLabel(
l.id,
'target',
Math.round( l.target.x + l.nodes[1].label.x),
Math.round( l.target.y + l.nodes[1].label.y),
l.nodes[1].label.text,
l.nodes[1].label.style,
l.nodes[1].label.rotation
);
if (this.enabled) { if (this.enabled) {
return [sourceInterface, targetInterface]; return [nodeAndMapLinkNode[1]];
} }
return []; return [];
}); });
@ -60,67 +63,66 @@ export class InterfaceLabelWidget {
.classed('interface_label_container', true); .classed('interface_label_container', true);
// create surrounding rect // create surrounding rect
// enter enter
// .append<SVGRectElement>('rect') .append<SVGRectElement>('rect')
// .attr('class', 'interface_label_border'); .attr('class', 'interface_label_selection');
// create label // create label
enter enter
.append<SVGTextElement>('text') .append<SVGTextElement>('text')
.attr('class', 'interface_label noselect') .attr('class', 'interface_label noselect')
.attr('interface_label_id', (i: InterfaceLabel) => `${i.direction}-${i.link_id}`) .attr('interface_label_id', (i: MapLinkNode) => `${i.id}`)
const merge = labels const merge = labels
.merge(enter); .merge(enter);
merge
.attr('width', 100)
.attr('height', 100)
.attr('transform', function(this: SVGGElement, l: InterfaceLabel) {
const textLabel = merge.select<SVGTextElement>(`text[interface_label_id="${l.direction}-${l.link_id}"]`);
const bbox = textLabel.node().getBBox();
const x = l.x ;
const y = l.y + bbox.height;
return `translate(${x}, ${y}) rotate(${l.rotation}, ${x}, ${y})`;
})
.classed('selected', (l: InterfaceLabel) => false);
// update label // update label
merge merge
.select<SVGTextElement>('text.interface_label') .select<SVGTextElement>('text.interface_label')
.text((l: InterfaceLabel) => l.text) .text((l: MapLinkNode) => l.label.text)
.attr('style', (l: InterfaceLabel) => { .attr('style', (l: MapLinkNode) => {
let styles = this.cssFixer.fix(l.style); let styles = this.cssFixer.fix(l.label.style);
styles = this.fontFixer.fixStyles(styles); styles = this.fontFixer.fixStyles(styles);
return styles; return styles;
}); })
// .attr('x', -InterfaceLabelWidget.SURROUNDING_TEXT_BORDER) .attr('x', function (this: SVGTextElement, l: MapLinkNode) {
// .attr('y', -InterfaceLabelWidget.SURROUNDING_TEXT_BORDER); return l.label.x;
})
.attr('y', function (this: SVGTextElement, l: MapLinkNode) {
let bbox = this.getBBox();
return l.label.y + bbox.height;
})
.attr('transform', (l: MapLinkNode) => {
return `rotate(${l.label.rotation}, ${l.label.x}, ${l.label.y})`;
})
// update surrounding rect // update surrounding rect
// merge merge
// .select<SVGRectElement>('rect.interface_label_border') .select<SVGRectElement>('rect.interface_label_selection')
// .attr('visibility', (l: InterfaceLabel) => false ? 'visible' : 'hidden') .attr('visibility', (l: MapLinkNode) => this.selectionManager.isSelected(l) ? 'visible' : 'hidden')
// .attr('stroke-dasharray', '3,3') .attr('stroke', 'black')
// .attr('stroke-width', '0.5') .attr('stroke-dasharray', '3,3')
// .each(function (this: SVGRectElement, l: InterfaceLabel) { .attr('stroke-width', '0.5')
// const current = select(this); .attr('fill', 'none')
// const parent = select(this.parentElement); .each(function (this: SVGRectElement, l: MapLinkNode) {
// const text = parent.select<SVGTextElement>('text'); const current = select(this);
// const bbox = text.node().getBBox(); const textLabel = merge.select<SVGTextElement>(`text[interface_label_id="${l.id}"]`);
const bbox = textLabel.node().getBBox();
const border = 2;
// const border = InterfaceLabelWidget.SURROUNDING_TEXT_BORDER; current.attr('width', bbox.width + border * 2);
current.attr('height', bbox.height + border * 2);
// current.attr('width', bbox.width + border * 2); current.attr('x', bbox.x - border);
// current.attr('height', bbox.height + border); current.attr('y', bbox.y - border);
// current.attr('x', - border); current.attr('transform', `rotate(${l.label.rotation}, ${bbox.x - border}, ${bbox.y - border})`);
// current.attr('y', - bbox.height); });
// });
labels labels
.exit() .exit()
.remove(); .remove();
this.draggable.call(merge);
} }
} }

View File

@ -38,6 +38,7 @@ import { MapDrawing } from '../../cartography/models/map/map-drawing';
import { MapPortToPortConverter } from '../../cartography/converters/map/map-port-to-port-converter'; import { MapPortToPortConverter } from '../../cartography/converters/map/map-port-to-port-converter';
import { SettingsService, Settings } from '../../services/settings.service'; import { SettingsService, Settings } from '../../services/settings.service';
import { MapLabel } from '../../cartography/models/map/map-label'; import { MapLabel } from '../../cartography/models/map/map-label';
import { MapLinkNode } from '../../cartography/models/map/map-link-node';
@Component({ @Component({
@ -172,6 +173,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.subscriptions.push( this.subscriptions.push(
this.linksEventSource.created.subscribe((evt) => this.onLinkCreated(evt)) this.linksEventSource.created.subscribe((evt) => this.onLinkCreated(evt))
); );
this.subscriptions.push(
this.linksEventSource.interfaceDragged.subscribe((evt) => this.onInterfaceLabelDragged(evt))
);
} }
onProjectLoad(project: Project) { onProjectLoad(project: Project) {
@ -274,6 +279,24 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}); });
} }
private onInterfaceLabelDragged(draggedEvent: DraggedDataEvent<MapLinkNode>) {
const link = this.linksDataSource.get(draggedEvent.datum.linkId);
if (link.nodes[0].node_id === draggedEvent.datum.nodeId) {
link.nodes[0].label.x += draggedEvent.dx;
link.nodes[0].label.y += draggedEvent.dy;
}
if (link.nodes[1].node_id === draggedEvent.datum.nodeId) {
link.nodes[1].label.x += draggedEvent.dx;
link.nodes[1].label.y += draggedEvent.dy;
}
this.linkService
.updateNodes(this.server, link, link.nodes)
.subscribe((serverLink: Link) => {
this.linksDataSource.update(serverLink);
});
}
private onLinkCreated(linkCreated: MapLinkCreated) { private onLinkCreated(linkCreated: MapLinkCreated) {
const sourceNode = this.mapNodeToNode.convert(linkCreated.sourceNode); const sourceNode = this.mapNodeToNode.convert(linkCreated.sourceNode);
const sourcePort = this.mapPortToPort.convert(linkCreated.sourcePort); const sourcePort = this.mapPortToPort.convert(linkCreated.sourcePort);

View File

@ -5,6 +5,8 @@ import 'rxjs/add/operator/map';
import { Server } from "../models/server"; import { Server } from "../models/server";
import { HttpServer } from "./http-server.service"; import { HttpServer } from "./http-server.service";
import {Port} from "../models/port"; import {Port} from "../models/port";
import { Link } from '../models/link';
import { LinkNode } from '../models/link-node';
@Injectable() @Injectable()
export class LinkService { export class LinkService {
@ -33,4 +35,27 @@ export class LinkService {
]}); ]});
} }
updateNodes(
server: Server, link: Link, nodes: LinkNode[]) {
const requestNodes = nodes.map((linkNode) => {
return {
node_id: linkNode.node_id,
port_number: linkNode.port_number,
adapter_number: linkNode.adapter_number,
label: {
rotation: linkNode.label.rotation,
style: linkNode.label.style,
text: linkNode.label.text,
x: linkNode.label.x,
y: linkNode.label.y
}
}
});
return this.httpServer
.put(
server,
`/projects/${link.project_id}/links/${link.link_id}`,
{"nodes": requestNodes});
}
} }