mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-04-08 02:54:16 +00:00
Dragging nodes labels and updating their positions
This commit is contained in:
parent
f9d8f0db29
commit
f802d8d952
@ -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));
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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>>();
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
135
src/app/cartography/widgets/label.ts
Normal file
135
src/app/cartography/widgets/label.ts
Normal 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}"]`);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}`, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user