diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.ts b/src/app/cartography/components/draggable-selection/draggable-selection.component.ts index 6fbdf59c..bdca40e1 100644 --- a/src/app/cartography/components/draggable-selection/draggable-selection.component.ts +++ b/src/app/cartography/components/draggable-selection/draggable-selection.component.ts @@ -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) => { 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) => { 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) => { 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(item, evt.dx, evt.dy)); }) selected.filter((item) => item instanceof MapDrawing).forEach((item: MapDrawing) => { this.drawingsEventSource.dragged.emit(new DraggedDataEvent(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(label, evt.dx, evt.dy)); + }); + }); } diff --git a/src/app/cartography/d3-map.imports.ts b/src/app/cartography/d3-map.imports.ts index 4fbe4f32..25aa4e27 100644 --- a/src/app/cartography/d3-map.imports.ts +++ b/src/app/cartography/d3-map.imports.ts @@ -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, diff --git a/src/app/cartography/events/draggable.ts b/src/app/cartography/events/draggable.ts index 1b616ab5..2d5f057a 100644 --- a/src/app/cartography/events/draggable.ts +++ b/src/app/cartography/events/draggable.ts @@ -44,9 +44,13 @@ export class Draggable { private behaviour() { let startEvt; - + let lastX: number; + let lastY: number; return drag() .on('start', (datum: Datum) => { + lastX = event.sourceEvent.clientX; + lastY = event.sourceEvent.clientY; + startEvt = new DraggableStart(datum); startEvt.dx = event.dx; startEvt.dy = event.dy; @@ -56,18 +60,18 @@ export class Draggable { }) .on('drag', (datum: Datum) => { const evt = new DraggableDrag(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); evt.dx = event.x - startEvt.x; evt.dy = event.y - startEvt.y; - evt.x = event.x; - evt.y = event.y; this.end.emit(evt); }); } diff --git a/src/app/cartography/events/nodes-event-source.ts b/src/app/cartography/events/nodes-event-source.ts index 1930828b..564de247 100644 --- a/src/app/cartography/events/nodes-event-source.ts +++ b/src/app/cartography/events/nodes-event-source.ts @@ -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>(); + public labelDragged = new EventEmitter>(); } diff --git a/src/app/cartography/widgets/interface-label.ts b/src/app/cartography/widgets/interface-label.ts index 4a81ce30..d719583d 100644 --- a/src/app/cartography/widgets/interface-label.ts +++ b/src/app/cartography/widgets/interface-label.ts @@ -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('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); diff --git a/src/app/cartography/widgets/label.ts b/src/app/cartography/widgets/label.ts new file mode 100644 index 00000000..b88f9e35 --- /dev/null +++ b/src/app/cartography/widgets/label.ts @@ -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(); + + 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("g.label_container") + .data((node: MapNode) => { + return [node.label]; + }); + + const label_enter = label_view.enter() + .append('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("g.label_body") + .data((label) => [label]); + + const label_body_enter = label_body.enter() + .append('g') + .attr("class", "label_body"); + + // add label of node + label_body_enter + .append('text') + .attr('class', 'label'); + + label_body_enter + .append('rect') + .attr('class', 'label_selection'); + + const label_body_merge = label_body.merge(label_body_enter) + + label_body_merge + .select('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('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(`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(`g.label_container[label_id="${label.id}"]`); + } + +} diff --git a/src/app/cartography/widgets/node.ts b/src/app/cartography/widgets/node.ts index 77b81f28..df38fe48 100644 --- a/src/app/cartography/widgets/node.ts +++ b/src/app/cartography/widgets/node.ts @@ -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(); public onNodeClicked = new EventEmitter(); 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('image'); - // add label of node - node_body_enter - .append('text') - .attr('class', 'label'); - - node_body_enter - .append('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('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('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(`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); } } diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index df5e1029..dd516fe7 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -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) { + 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) { const drawing = this.drawingsDataSource.get(draggedEvent.datum.id); drawing.x += draggedEvent.dx; diff --git a/src/app/services/node.service.ts b/src/app/services/node.service.ts index a2c1b80b..28c5a4b7 100644 --- a/src/app/services/node.service.ts +++ b/src/app/services/node.service.ts @@ -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 { + return this.httpServer + .put(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 { return this.httpServer .put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, {