diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 74e82e1a..023c08d9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -72,6 +72,7 @@ import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu- import { MATERIAL_IMPORTS } from './material.imports'; import { DrawingService } from './services/drawing.service'; import { ProjectNameValidator } from './components/projects/models/projectNameValidator'; +import { NodeSelectInterfaceComponent } from './components/project-map/node-select-interface/node-select-interface.component'; if (environment.production) { @@ -113,6 +114,7 @@ if (environment.production) { LocalServerComponent, ProgressComponent, ServerDiscoveryComponent, + NodeSelectInterfaceComponent ], imports: [ NgbModule.forRoot(), diff --git a/src/app/cartography/angular-map.imports.ts b/src/app/cartography/angular-map.imports.ts new file mode 100644 index 00000000..3a78c523 --- /dev/null +++ b/src/app/cartography/angular-map.imports.ts @@ -0,0 +1,28 @@ +import { DraggableComponent } from './components/draggable/draggable.component'; +import { SelectionComponent } from './components/selection/selection.component'; +import { NodeComponent } from './components/experimental-map/node/node.component'; +import { LinkComponent } from './components/experimental-map/link/link.component'; +import { StatusComponent } from './components/experimental-map/status/status.component'; +import { DrawingComponent } from './components/experimental-map/drawing/drawing.component'; +import { EllipseComponent } from './components/experimental-map/drawing/drawings/ellipse/ellipse.component'; +import { ImageComponent } from './components/experimental-map/drawing/drawings/image/image.component'; +import { LineComponent } from './components/experimental-map/drawing/drawings/line/line.component'; +import { RectComponent } from './components/experimental-map/drawing/drawings/rect/rect.component'; +import { TextComponent } from './components/experimental-map/drawing/drawings/text/text.component'; +import { InterfaceLabelComponent } from './components/experimental-map/interface-label/interface-label.component'; + + +export const ANGULAR_MAP_DECLARATIONS = [ + NodeComponent, + LinkComponent, + StatusComponent, + DrawingComponent, + EllipseComponent, + ImageComponent, + LineComponent, + RectComponent, + TextComponent, + DraggableComponent, + SelectionComponent, + InterfaceLabelComponent +]; diff --git a/src/app/cartography/cartography.module.ts b/src/app/cartography/cartography.module.ts index 8b9080fd..5898a32c 100644 --- a/src/app/cartography/cartography.module.ts +++ b/src/app/cartography/cartography.module.ts @@ -2,9 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatMenuModule, MatIconModule } from '@angular/material'; -import { MapComponent } from './components/map/map.component'; import { DrawLinkToolComponent } from './components/draw-link-tool/draw-link-tool.component'; -import { NodeSelectInterfaceComponent } from './components/node-select-interface/node-select-interface.component'; import { CssFixer } from './helpers/css-fixer'; import { FontFixer } from './helpers/font-fixer'; @@ -14,10 +12,9 @@ import { QtDasharrayFixer } from './helpers/qt-dasharray-fixer'; import { LayersManager } from './managers/layers-manager'; import { MapChangeDetectorRef } from './services/map-change-detector-ref'; import { Context } from './models/context'; +import { ANGULAR_MAP_DECLARATIONS } from './angular-map.imports'; import { D3_MAP_IMPORTS } from './d3-map.imports'; import { CanvasSizeDetector } from './helpers/canvas-size-detector'; -import { MapListeners } from './listeners/map-listeners'; -import { DraggableListener } from './listeners/draggable-listener'; import { DrawingsEventSource } from './events/drawings-event-source'; import { NodesEventSource } from './events/nodes-event-source'; import { DrawingToMapDrawingConverter } from './converters/map/drawing-to-map-drawing-converter'; @@ -35,10 +32,14 @@ import { PortToMapPortConverter } from './converters/map/port-to-map-port-conver import { SymbolToMapSymbolConverter } from './converters/map/symbol-to-map-symbol-converter'; import { LinkNodeToMapLinkNodeConverter } from './converters/map/link-node-to-map-link-node-converter'; import { GraphDataManager } from './managers/graph-data-manager'; -import { SelectionUpdateListener } from './listeners/selection-update-listener'; import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource } from './datasources/map-datasource'; -import { SelectionListener } from './listeners/selection-listener'; import { LinksEventSource } from './events/links-event-source'; +import { D3MapComponent } from './components/d3-map/d3-map.component'; +import { ExperimentalMapComponent } from './components/experimental-map/experimental-map.component'; +import { SelectionEventSource } from './events/selection-event-source'; +import { SelectionControlComponent } from './components/selection-control/selection-control.component'; +import { SelectionSelectComponent } from './components/selection-select/selection-select.component'; +import { DraggableSelectionComponent } from './components/draggable-selection/draggable-selection.component'; @NgModule({ @@ -48,9 +49,13 @@ import { LinksEventSource } from './events/links-event-source'; MatIconModule ], declarations: [ - MapComponent, + D3MapComponent, + ExperimentalMapComponent, DrawLinkToolComponent, - NodeSelectInterfaceComponent + ...ANGULAR_MAP_DECLARATIONS, + SelectionControlComponent, + SelectionSelectComponent, + DraggableSelectionComponent ], providers: [ CssFixer, @@ -62,10 +67,6 @@ import { LinksEventSource } from './events/links-event-source'; MapChangeDetectorRef, CanvasSizeDetector, Context, - SelectionUpdateListener, - MapListeners, - DraggableListener, - SelectionListener, DrawingsEventSource, NodesEventSource, LinksEventSource, @@ -88,8 +89,9 @@ import { LinksEventSource } from './events/links-event-source'; MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource, + SelectionEventSource, ...D3_MAP_IMPORTS ], - exports: [ MapComponent ] + exports: [ D3MapComponent, ExperimentalMapComponent ] }) export class CartographyModule { } diff --git a/src/app/cartography/components/d3-map/d3-map.component.html b/src/app/cartography/components/d3-map/d3-map.component.html new file mode 100644 index 00000000..bebf16a7 --- /dev/null +++ b/src/app/cartography/components/d3-map/d3-map.component.html @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/app/cartography/components/map/map.component.scss b/src/app/cartography/components/d3-map/d3-map.component.scss similarity index 100% rename from src/app/cartography/components/map/map.component.scss rename to src/app/cartography/components/d3-map/d3-map.component.scss diff --git a/src/app/cartography/components/map/map.component.spec.ts b/src/app/cartography/components/d3-map/d3-map.component.spec.ts similarity index 67% rename from src/app/cartography/components/map/map.component.spec.ts rename to src/app/cartography/components/d3-map/d3-map.component.spec.ts index 2aa33cd9..ed5697fc 100644 --- a/src/app/cartography/components/map/map.component.spec.ts +++ b/src/app/cartography/components/d3-map/d3-map.component.spec.ts @@ -1,14 +1,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MapComponent } from './map.component'; +import { D3MapComponent } from './d3-map.component'; -describe('MapComponent', () => { - let component: MapComponent; - let fixture: ComponentFixture; +describe('D3MapComponent', () => { + let component: D3MapComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ MapComponent ] + declarations: [ D3MapComponent ] }) .compileComponents(); })); diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/d3-map/d3-map.component.ts similarity index 92% rename from src/app/cartography/components/map/map.component.ts rename to src/app/cartography/components/d3-map/d3-map.component.ts index d3879d8b..416df23a 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/d3-map/d3-map.component.ts @@ -1,5 +1,5 @@ import { - Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChange, EventEmitter, Output + Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChange, EventEmitter, Output, ViewChild } from '@angular/core'; import { Selection, select } from 'd3-selection'; @@ -13,7 +13,6 @@ import { SelectionTool } from '../../tools/selection-tool'; import { MovingTool } from '../../tools/moving-tool'; import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; -import { MapListeners } from '../../listeners/map-listeners'; import { DrawingsWidget } from '../../widgets/drawings'; import { Node } from '../../models/node'; import { Link } from '../../../models/link'; @@ -23,11 +22,11 @@ import { GraphDataManager } from '../../managers/graph-data-manager'; @Component({ - selector: 'app-map', - templateUrl: './map.component.html', - styleUrls: ['./map.component.scss'] + selector: 'app-d3-map', + templateUrl: './d3-map.component.html', + styleUrls: ['./d3-map.component.scss'] }) -export class MapComponent implements OnInit, OnChanges, OnDestroy { +export class D3MapComponent implements OnInit, OnChanges, OnDestroy { @Input() nodes: Node[] = []; @Input() links: Link[] = []; @Input() drawings: Drawing[] = []; @@ -36,6 +35,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { @Input() width = 1500; @Input() height = 600; + @ViewChild('svg') svgRef: ElementRef; + private parentNativeElement: any; private svg: Selection; @@ -50,7 +51,6 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { private context: Context, private mapChangeDetectorRef: MapChangeDetectorRef, private canvasSizeDetector: CanvasSizeDetector, - private mapListeners: MapListeners, protected element: ElementRef, protected nodesWidget: NodesWidget, protected drawingsWidget: DrawingsWidget, @@ -117,14 +117,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { this.redraw(); } }); - - this.mapListeners.onInit(this.svg); } ngOnDestroy() { this.graphLayout.disconnect(this.svg); this.onChangesDetected.unsubscribe(); - this.mapListeners.onDestroy(); } public createGraph(domElement: HTMLElement) { diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.html b/src/app/cartography/components/draggable-selection/draggable-selection.component.html new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.scss b/src/app/cartography/components/draggable-selection/draggable-selection.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts b/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts new file mode 100644 index 00000000..12b1884b --- /dev/null +++ b/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DraggableSelectionComponent } from './draggable-selection.component'; + +describe('DraggableSelectionComponent', () => { + let component: DraggableSelectionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DraggableSelectionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DraggableSelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.ts b/src/app/cartography/components/draggable-selection/draggable-selection.component.ts new file mode 100644 index 00000000..bdca40e1 --- /dev/null +++ b/src/app/cartography/components/draggable-selection/draggable-selection.component.ts @@ -0,0 +1,147 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { Subscription, merge } from 'rxjs'; +import { NodesWidget } from '../../widgets/nodes'; +import { DrawingsWidget } from '../../widgets/drawings'; +import { LinksWidget } from '../../widgets/links'; +import { SelectionManager } from '../../managers/selection-manager'; +import { NodesEventSource } from '../../events/nodes-event-source'; +import { DrawingsEventSource } from '../../events/drawings-event-source'; +import { GraphDataManager } from '../../managers/graph-data-manager'; +import { DraggableStart, DraggableDrag, DraggableEnd } from '../../events/draggable'; +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', + templateUrl: './draggable-selection.component.html', + styleUrls: ['./draggable-selection.component.scss'] +}) +export class DraggableSelectionComponent implements OnInit, OnDestroy { + private start: Subscription; + private drag: Subscription; + private end: Subscription; + + @Input('svg') svg: SVGSVGElement; + + constructor( + private nodesWidget: NodesWidget, + private drawingsWidget: DrawingsWidget, + private linksWidget: LinksWidget, + private labelWidget: LabelWidget, + private selectionManager: SelectionManager, + private nodesEventSource: NodesEventSource, + private drawingsEventSource: DrawingsEventSource, + private graphDataManager: GraphDataManager + ) { } + + ngOnInit() { + const svg = select(this.svg); + + this.start = merge( + this.nodesWidget.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]); + } + } + + if (evt.datum instanceof MapDrawing) { + if (selected.filter((item) => item instanceof MapDrawing && item.id === evt.datum.id).length === 0) { + 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.labelWidget.draggable.drag + ).subscribe((evt: DraggableDrag) => { + const selected = this.selectionManager.getSelected(); + const selectedNodes = selected.filter((item) => item instanceof MapNode); + // update nodes + selectedNodes.forEach((node: MapNode) => { + node.x += evt.dx; + node.y += evt.dy; + + this.nodesWidget.redrawNode(svg, node); + + const links = this.graphDataManager.getLinks().filter( + (link) => link.target.id === node.id || link.source.id === node.id); + links.forEach((link) => { + this.linksWidget.redrawLink(svg, link); + }); + }); + + // update drawings + selected.filter((item) => item instanceof MapDrawing).forEach((drawing: MapDrawing) => { + drawing.x += evt.dx; + drawing.y += evt.dy; + 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.labelWidget.draggable.end, + ).subscribe((evt: DraggableEnd) => { + const selected = this.selectionManager.getSelected(); + const selectedNodes = selected.filter((item) => item instanceof 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)); + }); + + }); + } + + ngOnDestroy() { + this.start.unsubscribe(); + this.drag.unsubscribe(); + this.end.unsubscribe(); + } + +} diff --git a/src/app/cartography/components/draggable/draggable.component.scss b/src/app/cartography/components/draggable/draggable.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/draggable/draggable.component.spec.ts b/src/app/cartography/components/draggable/draggable.component.spec.ts new file mode 100644 index 00000000..84bfbb9c --- /dev/null +++ b/src/app/cartography/components/draggable/draggable.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DraggableComponent } from './draggable.component'; + +describe('DraggableComponent', () => { + let component: DraggableComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DraggableComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DraggableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/draggable/draggable.component.ts b/src/app/cartography/components/draggable/draggable.component.ts new file mode 100644 index 00000000..1cd1c20f --- /dev/null +++ b/src/app/cartography/components/draggable/draggable.component.ts @@ -0,0 +1,101 @@ +import { Component, OnInit, ElementRef, AfterViewInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; +import { Point } from '../../models/point'; + + +export class DraggableDraggedEvent { + constructor( + public x: number, + public y: number, + public dx: number, + public dy: number + ) {} +} + + +@Component({ + selector: '[app-draggable]', + template:``, + styleUrls: ['./draggable.component.scss'] +}) +export class DraggableComponent implements OnInit, AfterViewInit, OnDestroy { + @Input('app-draggable') item: Point; + @Output() dragging = new EventEmitter(); + @Output() dragged = new EventEmitter(); + + draggable: Subscription; + + private startX: number; + private startY: number; + + private posX: number; + private posY: number; + + constructor( + private elementRef: ElementRef + ) { } + + ngOnInit() { + } + + ngAfterViewInit() { + const down = Observable.fromEvent(this.elementRef.nativeElement, 'mousedown').do((e: MouseEvent) => e.preventDefault()) + + down.subscribe((e: MouseEvent) => { + this.posX = this.item.x; + this.posY = this.item.y; + + this.startX = e.clientX; + this.startY = e.clientY; + }); + + const up = Observable + .fromEvent(document, 'mouseup') + .do((e: MouseEvent) => { + e.preventDefault(); + }); + + const mouseMove = Observable + .fromEvent(document, 'mousemove') + .do((e: MouseEvent) => e.stopPropagation()); + + const scrollWindow = Observable + .fromEvent(document, 'scroll') + .startWith({}); + + const move = Observable.combineLatest(mouseMove, scrollWindow); + + const drag = down.mergeMap((md: MouseEvent) => { + return move + .map(([mm, s]) => mm) + .do((mm: MouseEvent) => { + const x = this.startX - mm.clientX; + const y = this.startY - mm.clientY; + + this.item.x = Math.round(this.posX - x); + this.item.y = Math.round(this.posY - y); + this.dragging.emit(new DraggableDraggedEvent(this.item.x, this.item.y, -x, -y)); + }) + .skipUntil(up + .take(1) + .do((e: MouseEvent) => { + const x = this.startX - e.clientX; + const y = this.startY - e.clientY; + + this.item.x = Math.round(this.posX - x); + this.item.y = Math.round(this.posY - y); + + this.dragged.emit(new DraggableDraggedEvent(this.item.x, this.item.y, -x, -y)); + })) + .take(1); + }); + + this.draggable = drag.subscribe((e: MouseEvent) => { + // this.cd.detectChanges(); + }); + } + + ngOnDestroy() { + this.draggable.unsubscribe(); + } +} diff --git a/src/app/cartography/components/draw-link-tool/draw-link-tool.component.html b/src/app/cartography/components/draw-link-tool/draw-link-tool.component.html index 16188157..89ab7db7 100644 --- a/src/app/cartography/components/draw-link-tool/draw-link-tool.component.html +++ b/src/app/cartography/components/draw-link-tool/draw-link-tool.component.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts b/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts index b4452b08..66845a31 100644 --- a/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts +++ b/src/app/cartography/components/draw-link-tool/draw-link-tool.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, Output, EventEmitter, OnDestroy, ViewChild } from '@angular/core'; import { DrawingLineWidget } from '../../widgets/drawing-line'; import { Subscription } from 'rxjs'; -import { NodeSelectInterfaceComponent } from '../node-select-interface/node-select-interface.component'; +// import { NodeSelectInterfaceComponent } from '../node-select-interface/node-select-interface.component'; import { MapLinkCreated } from '../../events/links'; import { NodeClicked } from '../../events/nodes'; import { NodeWidget } from '../../widgets/node'; @@ -16,7 +16,7 @@ import { LinksEventSource } from '../../events/links-event-source'; styleUrls: ['./draw-link-tool.component.scss'] }) export class DrawLinkToolComponent implements OnInit, OnDestroy { - @ViewChild(NodeSelectInterfaceComponent) nodeSelectInterfaceMenu: NodeSelectInterfaceComponent; + // @ViewChild(NodeSelectInterfaceComponent) nodeSelectInterfaceMenu: NodeSelectInterfaceComponent; // @Output('linkCreated') linkCreated = new EventEmitter(); @@ -29,13 +29,13 @@ export class DrawLinkToolComponent implements OnInit, OnDestroy { ) { } ngOnInit() { - this.onNodeClicked = this.nodeWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => { - this.nodeSelectInterfaceMenu.open( - eventNode.node, - eventNode.event.clientY, - eventNode.event.clientX - ); - }); + // this.onNodeClicked = this.nodeWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => { + // this.nodeSelectInterfaceMenu.open( + // eventNode.node, + // eventNode.event.clientY, + // eventNode.event.clientX + // ); + // }); } ngOnDestroy() { diff --git a/src/app/cartography/components/experimental-map/drawing/drawing.component.html b/src/app/cartography/components/experimental-map/drawing/drawing.component.html new file mode 100644 index 00000000..abfcf9e2 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawing.component.html @@ -0,0 +1,32 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/drawing/drawing.component.scss b/src/app/cartography/components/experimental-map/drawing/drawing.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/drawing/drawing.component.spec.ts b/src/app/cartography/components/experimental-map/drawing/drawing.component.spec.ts new file mode 100644 index 00000000..d0ab0a6f --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawing.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DrawingComponent } from './drawing.component'; + +describe('DrawingComponent', () => { + let component: DrawingComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DrawingComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DrawingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/drawing/drawing.component.ts b/src/app/cartography/components/experimental-map/drawing/drawing.component.ts new file mode 100644 index 00000000..e2bfc811 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawing.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit, Input, ChangeDetectorRef } from '@angular/core'; +import { EllipseElement } from '../../../models/drawings/ellipse-element'; +import { ImageElement } from '../../../models/drawings/image-element'; +import { LineElement } from '../../../models/drawings/line-element'; +import { RectElement } from '../../../models/drawings/rect-element'; +import { TextElement } from '../../../models/drawings/text-element'; +import { SvgToDrawingConverter } from '../../../helpers/svg-to-drawing-converter'; +import { DraggedDataEvent } from '../../../events/event-source'; +import { MapDrawing } from '../../../models/map/map-drawing'; +import { DrawingsEventSource } from '../../../events/drawings-event-source'; + +@Component({ + selector: '[app-drawing]', + templateUrl: './drawing.component.html', + styleUrls: ['./drawing.component.scss'] +}) +export class DrawingComponent implements OnInit { + @Input('app-drawing') drawing: MapDrawing; + + constructor( + private svgToDrawingConverter: SvgToDrawingConverter, + private drawingsEventSource: DrawingsEventSource, + private cd: ChangeDetectorRef, + ) { } + + ngOnInit() { + try { + this.drawing.element = this.svgToDrawingConverter.convert(this.drawing.svg); + } catch (error) { + console.log(`Cannot convert due to Error: '${error}'`); + } + } + + OnDragging(evt) { + this.drawing.x = evt.x; + this.drawing.y = evt.y; + this.cd.detectChanges(); + } + + OnDragged(evt) { + this.cd.detectChanges(); + this.drawingsEventSource.dragged.emit(new DraggedDataEvent(this.drawing, evt.dx, evt.dy)) + } + + is(element, type: string) { + if (!element) { + return false; + } + + if (type === "ellipse") { + return element instanceof EllipseElement; + } + if (type === "image") { + return element instanceof ImageElement; + } + if (type === "line") { + return element instanceof LineElement; + } + if (type === "rect") { + return element instanceof RectElement; + } + if (type === "text") { + return element instanceof TextElement; + } + return false; + } + + get transformation() { + return `translate(${this.drawing.x},${this.drawing.y}) rotate(${this.drawing.rotation})`; + } +} diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.html b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.html new file mode 100644 index 00000000..8c4a296b --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.html @@ -0,0 +1,12 @@ + diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.scss b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.spec.ts b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.spec.ts new file mode 100644 index 00000000..ac1e24ac --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EllipseComponent } from './ellipse.component'; + +describe('EllipseComponent', () => { + let component: EllipseComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EllipseComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EllipseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.ts b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.ts new file mode 100644 index 00000000..c2bb0475 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/ellipse/ellipse.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { EllipseElement } from '../../../../../models/drawings/ellipse-element'; +import { QtDasharrayFixer } from '../../../../../helpers/qt-dasharray-fixer'; + +@Component({ + selector: '[app-ellipse]', + templateUrl: './ellipse.component.html', + styleUrls: ['./ellipse.component.scss'] +}) +export class EllipseComponent implements OnInit { + @Input('app-ellipse') ellipse: EllipseElement; + + constructor( + private qtDasharrayFixer: QtDasharrayFixer + ) { } + + ngOnInit() { + } + + get fill_opacity() { + if(isFinite(this.ellipse.fill_opacity)) { + return this.ellipse.fill_opacity; + } + return null; + } + + get stroke_width() { + if(isFinite(this.ellipse.stroke_width)) { + return this.ellipse.stroke_width; + } + return null + } + + get stroke_dasharray() { + if(this.ellipse.stroke_dasharray) { + return this.qtDasharrayFixer.fix(this.ellipse.stroke_dasharray); + } + return null; + } +} diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.html b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.html new file mode 100644 index 00000000..6e36c391 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.scss b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.spec.ts b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.spec.ts new file mode 100644 index 00000000..6d8acaad --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ImageComponent } from './image.component'; + +describe('ImageComponent', () => { + let component: ImageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ImageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ImageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.ts b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.ts new file mode 100644 index 00000000..f151d5a2 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/image/image.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ImageElement } from '../../../../../models/drawings/image-element'; + +@Component({ + selector: '[app-image]', + templateUrl: './image.component.html', + styleUrls: ['./image.component.scss'] +}) +export class ImageComponent implements OnInit { + + @Input('app-image') image: ImageElement; + + constructor() { } + + ngOnInit() { + } +} diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.html b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.html new file mode 100644 index 00000000..d32f4207 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.scss b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.spec.ts b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.spec.ts new file mode 100644 index 00000000..64f2500f --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LineComponent } from './line.component'; + +describe('LineComponent', () => { + let component: LineComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LineComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LineComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.ts b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.ts new file mode 100644 index 00000000..a7f8fa06 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/line/line.component.ts @@ -0,0 +1,34 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { QtDasharrayFixer } from '../../../../../helpers/qt-dasharray-fixer'; +import { LineElement } from '../../../../../models/drawings/line-element'; + +@Component({ + selector: '[app-line]', + templateUrl: './line.component.html', + styleUrls: ['./line.component.scss'] +}) +export class LineComponent implements OnInit { + @Input('app-line') line: LineElement; + + constructor( + private qtDasharrayFixer: QtDasharrayFixer + ) { } + + ngOnInit() { + } + + get stroke_width() { + if(isFinite(this.line.stroke_width)) { + return this.line.stroke_width; + } + return null + } + + get stroke_dasharray() { + if(this.line.stroke_dasharray) { + return this.qtDasharrayFixer.fix(this.line.stroke_dasharray); + } + return null; + } + +} diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.html b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.html new file mode 100644 index 00000000..1a0944f4 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.scss b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.spec.ts b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.spec.ts new file mode 100644 index 00000000..d7b1ea95 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RectComponent } from './rect.component'; + +describe('RectComponent', () => { + let component: RectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ RectComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.ts b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.ts new file mode 100644 index 00000000..42878aab --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/rect/rect.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { RectElement } from '../../../../../models/drawings/rect-element'; +import { QtDasharrayFixer } from '../../../../../helpers/qt-dasharray-fixer'; + +@Component({ + selector: '[app-rect]', + templateUrl: './rect.component.html', + styleUrls: ['./rect.component.scss'] +}) +export class RectComponent implements OnInit { + @Input('app-rect') rect: RectElement; + + constructor( + private qtDasharrayFixer: QtDasharrayFixer + ) { } + + ngOnInit() { + } + + get fill_opacity() { + if(isFinite(this.rect.fill_opacity)) { + return this.rect.fill_opacity; + } + return null; + } + + get stroke_width() { + if(isFinite(this.rect.stroke_width)) { + return this.rect.stroke_width; + } + return null + } + + get stroke_dasharray() { + if(this.rect.stroke_dasharray) { + return this.qtDasharrayFixer.fix(this.rect.stroke_dasharray); + } + return null; + } + +} diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.html b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.html new file mode 100644 index 00000000..ad6d71c2 --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.html @@ -0,0 +1,14 @@ + + {{line}} + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.scss b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.spec.ts b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.spec.ts new file mode 100644 index 00000000..845e947d --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TextComponent } from './text.component'; + +describe('TextComponent', () => { + let component: TextComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TextComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TextComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.ts b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.ts new file mode 100644 index 00000000..be113d5b --- /dev/null +++ b/src/app/cartography/components/experimental-map/drawing/drawings/text/text.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit, Input, ViewChild, ElementRef, DoCheck } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { TextElement } from '../../../../../models/drawings/text-element'; +import { FontFixer } from '../../../../../helpers/font-fixer'; + +@Component({ + selector: '[app-text]', + templateUrl: './text.component.html', + styleUrls: ['./text.component.scss'] +}) +export class TextComponent implements OnInit, DoCheck { + static MARGIN = 4; + + @Input('app-text') text: TextElement; + + @ViewChild('text') textRef: ElementRef; + + lines: string[] = []; + + transformation = ""; + + constructor( + private fontFixer: FontFixer, + private sanitizer: DomSanitizer + ) { } + + ngOnInit() { + this.lines = this.getLines(this.text.text); + } + + ngDoCheck() { + this.transformation = this.calculateTransformation(); + } + + get style() { + const font = this.fontFixer.fix(this.text); + + const styles: string[] = []; + if (font.font_family) { + styles.push(`font-family: "${this.text.font_family}"`); + } + if (font.font_size) { + styles.push(`font-size: ${this.text.font_size}pt`); + } + if (font.font_weight) { + styles.push(`font-weight: ${this.text.font_weight}`); + } + return this.sanitizer.bypassSecurityTrustStyle(styles.join("; ")); + } + + get textDecoration() { + return this.text.text_decoration; + } + + calculateTransformation() { + const tspans = this.textRef.nativeElement.getElementsByTagName('tspan'); + if(tspans.length > 0) { + const height = this.textRef.nativeElement.getBBox().height / tspans.length; + return `translate(${TextComponent.MARGIN}, ${height - TextComponent.MARGIN})`; + } + return ''; + } + + getLines(text: string) { + return text.split(/\r?\n/) + } + +} diff --git a/src/app/cartography/components/experimental-map/experimental-map.component.html b/src/app/cartography/components/experimental-map/experimental-map.component.html new file mode 100644 index 00000000..2a918534 --- /dev/null +++ b/src/app/cartography/components/experimental-map/experimental-map.component.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/cartography/components/experimental-map/experimental-map.component.scss b/src/app/cartography/components/experimental-map/experimental-map.component.scss new file mode 100644 index 00000000..7fb7062f --- /dev/null +++ b/src/app/cartography/components/experimental-map/experimental-map.component.scss @@ -0,0 +1,5 @@ +svg { + display: block; +} + + diff --git a/src/app/cartography/components/experimental-map/experimental-map.component.spec.ts b/src/app/cartography/components/experimental-map/experimental-map.component.spec.ts new file mode 100644 index 00000000..0d5db4bb --- /dev/null +++ b/src/app/cartography/components/experimental-map/experimental-map.component.spec.ts @@ -0,0 +1,26 @@ +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +// import { MapComponent } from './map.component'; + +// describe('MapComponent', () => { +// let component: MapComponent; +// let fixture: ComponentFixture; + +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [ MapComponent ] +// }) +// .compileComponents(); +// })); + +// // beforeEach(() => { +// // fixture = TestBed.createComponent(MapComponent); +// // component = fixture.componentInstance; +// // fixture.detectChanges(); +// // }); +// // +// // it('should create', () => { +// // expect(component).toBeTruthy(); +// // }); +// }); +// // \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/experimental-map.component.ts b/src/app/cartography/components/experimental-map/experimental-map.component.ts new file mode 100644 index 00000000..21442474 --- /dev/null +++ b/src/app/cartography/components/experimental-map/experimental-map.component.ts @@ -0,0 +1,131 @@ +import { + Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, + SimpleChange, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild +} from '@angular/core'; + +import { GraphLayout } from "../../widgets/graph-layout"; +import { Context } from "../../models/context"; +import { Size } from "../../models/size"; +import { Subscription } from 'rxjs'; +import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; +import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; +import { Node } from '../../models/node'; +import { Link } from '../../../models/link'; +import { Drawing } from '../../models/drawing'; +import { Symbol } from '../../../models/symbol'; +import { GraphDataManager } from '../../managers/graph-data-manager'; +import { LayersManager } from '../../managers/layers-manager'; + + +@Component({ + selector: 'app-experimental-map', + templateUrl: './experimental-map.component.html', + styleUrls: ['./experimental-map.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ExperimentalMapComponent implements OnInit, OnChanges, OnDestroy { + @Input() nodes: Node[] = []; + @Input() links: Link[] = []; + @Input() drawings: Drawing[] = []; + @Input() symbols: Symbol[] = []; + // @Input() changed: EventEmitter; + + // @Input('node-updated') nodeUpdated: EventEmitter; + + @Input() width = 1500; + @Input() height = 600; + + @ViewChild('svg') svg: ElementRef; + + private changesDetected: Subscription; + + protected settings = { + 'show_interface_labels': true + }; + + constructor( + private graphDataManager: GraphDataManager, + private context: Context, + private mapChangeDetectorRef: MapChangeDetectorRef, + private canvasSizeDetector: CanvasSizeDetector, + private changeDetectorRef: ChangeDetectorRef, + private layersManger: LayersManager, + public graphLayout: GraphLayout, + ) { + } + + @Input('show-interface-labels') + set showInterfaceLabels(value) { + this.settings.show_interface_labels = value; + this.mapChangeDetectorRef.detectChanges(); + } + + @Input('moving-tool') + set movingTool(value) { + this.mapChangeDetectorRef.detectChanges(); + } + + @Input('selection-tool') + set selectionTool(value) { + this.mapChangeDetectorRef.detectChanges(); + } + + @Input('draw-link-tool') drawLinkTool: boolean; + + @Input('readonly') set readonly(value) { + } + + ngOnChanges(changes: { [propKey: string]: SimpleChange }) { + } + + ngOnInit() { + // this.changeDetectorRef.detach(); + + this.changesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => { + this.graphDataManager.setNodes(this.nodes); + this.graphDataManager.setLinks(this.links); + this.graphDataManager.setDrawings(this.drawings); + this.graphDataManager.setSymbols(this.symbols); + + this.changeDetectorRef.detectChanges(); + }); + + + // this.changedSubscription = this.changed.subscribe(() => { + // this.changeDetectorRef.detectChanges(); + // }); + + // this.nodeUpdated.subscribe((node: Node) => { + // this.nodeChanged.emit(node); + // }); + } + + ngOnDestroy() { + this.changesDetected.unsubscribe(); + // this.changedSubscription.unsubscribe(); + } + + public getSize(): Size { + return this.canvasSizeDetector.getOptimalSize(this.width, this.height); + } + + public get layers() { + return this.layersManger.getLayersList(); + } + + public get transform() { + const ctx = new Context(); + ctx.size = this.getSize(); + + const xTrans = ctx.getZeroZeroTransformationPoint().x + ctx.transformation.x; + const yTrans = ctx.getZeroZeroTransformationPoint().y + ctx.transformation.y; + const kTrans = ctx.transformation.k; + return `translate(${xTrans}, ${yTrans}) scale(${kTrans})`; + } + + + @HostListener('window:resize', ['$event']) + onResize(event) { + + } +} diff --git a/src/app/cartography/components/experimental-map/interface-label/interface-label.component.html b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.html new file mode 100644 index 00000000..2a2317ad --- /dev/null +++ b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.html @@ -0,0 +1,28 @@ + + + + + {{ text }} + + + diff --git a/src/app/cartography/components/experimental-map/interface-label/interface-label.component.scss b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/interface-label/interface-label.component.spec.ts b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.spec.ts new file mode 100644 index 00000000..1e121d81 --- /dev/null +++ b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InterfaceLabelComponent } from './interface-label.component'; + +describe('InterfaceLabelComponent', () => { + let component: InterfaceLabelComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ InterfaceLabelComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(InterfaceLabelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/interface-label/interface-label.component.ts b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.ts new file mode 100644 index 00000000..d596cc3a --- /dev/null +++ b/src/app/cartography/components/experimental-map/interface-label/interface-label.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit, Input, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { CssFixer } from '../../../helpers/css-fixer'; + +@Component({ + selector: '[app-interface-label]', + templateUrl: './interface-label.component.html', + styleUrls: ['./interface-label.component.scss'] +}) +export class InterfaceLabelComponent implements OnInit { + @Input('app-interface-label') ignore: any; + + @ViewChild('textSvg') textRef: ElementRef; + + private label = { + 'x': 0, + 'y': 0, + 'text': '', + 'style': '', + 'rotation': 0 + }; + + borderSize = 5; + + textWidth = 0; + textHeight = 0; + + constructor( + private elementRef: ElementRef, + private ref: ChangeDetectorRef, + private sanitizer: DomSanitizer, + private cssFixer: CssFixer + ) { } + + ngOnInit() { + } + + @Input('x') + set x(value) { + this.label['x'] = value; + this.ref.detectChanges(); + } + + @Input('y') + set y(value) { + this.label['y'] = value; + this.ref.detectChanges(); + } + + @Input('text') + set text(value) { + this.label['text'] = value; + this.ref.detectChanges(); + } + + @Input('style') + set style(value) { + this.label['style'] = this.cssFixer.fix(value); + this.ref.detectChanges(); + } + + @Input('rotation') + set rotation(value) { + this.label['rotation'] = value; + this.ref.detectChanges(); + } + + get text() { + return this.label.text; + } + + get sanitizedStyle() { + return this.sanitizer.bypassSecurityTrustStyle(this.label.style); + } + + get rectX() { + return 0; + } + + get rectY() { + return -this.textRef.nativeElement.getBBox().height - this.borderSize; + } + + get rectWidth() { + return this.textRef.nativeElement.getBBox().width + this.borderSize*2; + } + + get rectHeight() { + return this.textRef.nativeElement.getBBox().height + this.borderSize; + } + + get transform() { + const bbox = this.elementRef.nativeElement.getBBox(); + const x = this.label.x; + const y = this.label.y + bbox.height; + return `translate(${x}, ${y}) rotate(${this.label.rotation}, ${x}, ${y})`; + } +} diff --git a/src/app/cartography/components/experimental-map/link/link.component.html b/src/app/cartography/components/experimental-map/link/link.component.html new file mode 100644 index 00000000..c9201b75 --- /dev/null +++ b/src/app/cartography/components/experimental-map/link/link.component.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/link/link.component.scss b/src/app/cartography/components/experimental-map/link/link.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/link/link.component.spec.ts b/src/app/cartography/components/experimental-map/link/link.component.spec.ts new file mode 100644 index 00000000..3642d967 --- /dev/null +++ b/src/app/cartography/components/experimental-map/link/link.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LinkComponent } from './link.component'; + +describe('LinkComponent', () => { + let component: LinkComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LinkComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/link/link.component.ts b/src/app/cartography/components/experimental-map/link/link.component.ts new file mode 100644 index 00000000..8c0d8d60 --- /dev/null +++ b/src/app/cartography/components/experimental-map/link/link.component.ts @@ -0,0 +1,65 @@ +import { + Component, OnInit, Input, ViewChild, + ElementRef, EventEmitter, ChangeDetectorRef, + OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { LinkStrategy } from './strategies/link-strategy'; +import { EthernetLinkStrategy } from './strategies/ethernet-link-strategy'; +import { SerialLinkStrategy } from './strategies/serial-link-strategy'; +import { MultiLinkCalculatorHelper } from '../../../helpers/multi-link-calculator-helper'; +import { Node } from '../../../models/node'; +import { MapLink } from '../../../models/map/map-link'; + + +@Component({ + selector: '[app-link]', + templateUrl: './link.component.html', + styleUrls: ['./link.component.scss'], +}) +export class LinkComponent implements OnInit, OnDestroy { + @Input('app-link') link: MapLink; + @Input('node-changed') nodeChanged: EventEmitter; + @Input('show-interface-labels') showInterfaceLabels: boolean; + + @ViewChild('path') path: ElementRef; + + private ethernetLinkStrategy = new EthernetLinkStrategy(); + private serialLinkStrategy = new SerialLinkStrategy(); + + private nodeChangedSubscription: Subscription; + + constructor( + private multiLinkCalculatorHelper: MultiLinkCalculatorHelper, + private ref: ChangeDetectorRef + ) {} + + ngOnInit() { + this.ref.detectChanges(); + // this.nodeChangedSubscription = this.nodeChanged.subscribe((node: Node) => { + // if (this.link.source.node_id === node.node_id || this.link.target.node_id === node.node_id) { + // this.ref.detectChanges(); + // } + // }); + } + + ngOnDestroy() { + // this.nodeChangedSubscription.unsubscribe(); + } + + get strategy(): LinkStrategy { + if (this.link.linkType === 'serial') { + return this.serialLinkStrategy; + } + return this.ethernetLinkStrategy; + } + + get transform() { + const translation = this.multiLinkCalculatorHelper.linkTranslation(this.link.distance, this.link.source, this.link.target); + return `translate (${translation.dx}, ${translation.dy})`; + } + + get d() { + return this.strategy.d(this.link); + } + +} diff --git a/src/app/cartography/components/experimental-map/link/strategies/ethernet-link-strategy.ts b/src/app/cartography/components/experimental-map/link/strategies/ethernet-link-strategy.ts new file mode 100644 index 00000000..f4767e72 --- /dev/null +++ b/src/app/cartography/components/experimental-map/link/strategies/ethernet-link-strategy.ts @@ -0,0 +1,18 @@ +import { LinkStrategy } from "./link-strategy"; +import { path } from "d3-path"; +import { MapLink } from "../../../../models/map/map-link"; + + +export class EthernetLinkStrategy implements LinkStrategy { + public d(link: MapLink): string { + const points = [ + [link.source.x + link.source.width / 2., link.source.y + link.source.height / 2.], + [link.target.x + link.target.width / 2., link.target.y + link.target.height / 2.] + ]; + + const line_generator = path(); + line_generator.moveTo(points[0][0], points[0][1]); + line_generator.lineTo(points[1][0], points[1][1]); + return line_generator.toString(); + } +} \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/link/strategies/link-strategy.ts b/src/app/cartography/components/experimental-map/link/strategies/link-strategy.ts new file mode 100644 index 00000000..511c9e7f --- /dev/null +++ b/src/app/cartography/components/experimental-map/link/strategies/link-strategy.ts @@ -0,0 +1,5 @@ +import { MapLink } from "../../../../models/map/map-link"; + +export interface LinkStrategy { + d(link: MapLink): string; +} diff --git a/src/app/cartography/components/experimental-map/link/strategies/serial-link-strategy.ts b/src/app/cartography/components/experimental-map/link/strategies/serial-link-strategy.ts new file mode 100644 index 00000000..9dd64d2d --- /dev/null +++ b/src/app/cartography/components/experimental-map/link/strategies/serial-link-strategy.ts @@ -0,0 +1,55 @@ +import { path } from "d3-path"; +import { LinkStrategy } from "./link-strategy"; +import { MapLink } from "../../../../models/map/map-link"; + + +export class SerialLinkStrategy implements LinkStrategy { + private linkToPoints(link: MapLink) { + const source = { + 'x': link.source.x + link.source.width / 2, + 'y': link.source.y + link.source.height / 2 + }; + const target = { + 'x': link.target.x + link.target.width / 2, + 'y': link.target.y + link.target.height / 2 + }; + + const dx = target.x - source.x; + const dy = target.y - source.y; + + const vector_angle = Math.atan2(dy, dx); + const rot_angle = -Math.PI / 4.0; + const vect_rot = [ + Math.cos(vector_angle + rot_angle), + Math.sin(vector_angle + rot_angle) + ]; + + const angle_source: [number, number] = [ + source.x + dx / 2.0 + 15 * vect_rot[0], + source.y + dy / 2.0 + 15 * vect_rot[1] + ]; + + const angle_target: [number, number] = [ + target.x - dx / 2.0 - 15 * vect_rot[0], + target.y - dy / 2.0 - 15 * vect_rot[1] + ]; + + return [ + [source.x, source.y], + angle_source, + angle_target, + [target.x, target.y] + ]; + } + + d(link: MapLink): string { + const points = this.linkToPoints(link); + + const line_generator = path(); + line_generator.moveTo(points[0][0], points[0][1]); + line_generator.lineTo(points[1][0], points[1][1]); + line_generator.lineTo(points[2][0], points[2][1]); + line_generator.lineTo(points[3][0], points[3][1]); + return line_generator.toString(); + } +} diff --git a/src/app/cartography/components/experimental-map/node/node.component.html b/src/app/cartography/components/experimental-map/node/node.component.html new file mode 100644 index 00000000..eabce9c7 --- /dev/null +++ b/src/app/cartography/components/experimental-map/node/node.component.html @@ -0,0 +1,25 @@ + + + + {{ node.label.text }} + + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/node/node.component.scss b/src/app/cartography/components/experimental-map/node/node.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/node/node.component.spec.ts b/src/app/cartography/components/experimental-map/node/node.component.spec.ts new file mode 100644 index 00000000..4902d2ec --- /dev/null +++ b/src/app/cartography/components/experimental-map/node/node.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NodeComponent } from './node.component'; + +describe('NodeComponent', () => { + let component: NodeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NodeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NodeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/experimental-map/node/node.component.ts b/src/app/cartography/components/experimental-map/node/node.component.ts new file mode 100644 index 00000000..2abc52b2 --- /dev/null +++ b/src/app/cartography/components/experimental-map/node/node.component.ts @@ -0,0 +1,119 @@ +import { + Component, OnInit, Input, ElementRef, + ViewChild, ChangeDetectorRef, ChangeDetectionStrategy, Output, + EventEmitter, OnDestroy, OnChanges, AfterViewInit } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { Subscription } from 'rxjs'; +import { CssFixer } from '../../../helpers/css-fixer'; +import { FontFixer } from '../../../helpers/font-fixer'; +import { Symbol } from '../../../../models/symbol'; +import { MapNode } from '../../../models/map/map-node'; +import { NodesEventSource } from '../../../events/nodes-event-source'; +import { DraggedDataEvent } from '../../../events/event-source'; + +@Component({ + selector: '[app-node]', + templateUrl: './node.component.html', + styleUrls: ['./node.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NodeComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { + static NODE_LABEL_MARGIN = 3; + + @ViewChild('label') label: ElementRef; + @ViewChild('image') imageRef: ElementRef; + + @Input('app-node') node: MapNode; + @Input('symbols') symbols: Symbol[]; + @Input('node-changed') nodeChanged: EventEmitter; + + // @Output() valueChange = new EventEmitter(); + + nodeChangedSubscription: Subscription; + + private labelHeight = 0; + + constructor( + private cssFixer: CssFixer, + private fontFixer: FontFixer, + private sanitizer: DomSanitizer, + protected element: ElementRef, + private cd: ChangeDetectorRef, + private nodesEventSource: NodesEventSource + ) { } + + ngOnInit() { + // this.nodeChangedSubscription = this.nodeChanged.subscribe((node: Node) => { + // if (node.node_id === this.node.node_id) { + // this.cd.detectChanges(); + // } + // }); + } + + ngOnDestroy() { + // this.nodeChangedSubscription.unsubscribe(); + } + + ngOnChanges(changes) { + this.cd.detectChanges(); + } + + ngAfterViewInit() { + this.labelHeight = this.getLabelHeight(); + // reload BBox + this.cd.detectChanges(); + } + + OnDragging(evt) { + this.node.x = evt.x; + this.node.y = evt.y; + this.cd.detectChanges(); + } + + OnDragged(evt) { + this.cd.detectChanges(); + this.nodesEventSource.dragged.emit(new DraggedDataEvent(this.node, evt.dx, evt.dy)) + } + + + get symbol(): string { + const symbol = this.symbols.find((s: Symbol) => s.symbol_id === this.node.symbol); + if (symbol) { + return 'data:image/svg+xml;base64,' + btoa(symbol.raw); + } + // @todo; we need to have default image + return 'data:image/svg+xml;base64,none'; + } + + get label_style() { + + let styles = this.cssFixer.fix(this.node.label.style); + styles = this.fontFixer.fixStyles(styles); + return this.sanitizer.bypassSecurityTrustStyle(styles); + } + + get label_x(): number { + if (this.node.label.x === null) { + // center + const bbox = this.label.nativeElement.getBBox(); + + return -bbox.width / 2.; + } + return this.node.label.x + NodeComponent.NODE_LABEL_MARGIN; + } + + get label_y(): number { + this.labelHeight = this.getLabelHeight(); + + if (this.node.label.x === null) { + // center + return - this.node.height / 2. - this.labelHeight ; + } + return this.node.label.y + this.labelHeight - NodeComponent.NODE_LABEL_MARGIN; + } + + private getLabelHeight() { + const bbox = this.label.nativeElement.getBBox(); + return bbox.height; + } +} diff --git a/src/app/cartography/components/experimental-map/status/status.component.html b/src/app/cartography/components/experimental-map/status/status.component.html new file mode 100644 index 00000000..cfcef840 --- /dev/null +++ b/src/app/cartography/components/experimental-map/status/status.component.html @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/src/app/cartography/components/experimental-map/status/status.component.scss b/src/app/cartography/components/experimental-map/status/status.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/experimental-map/status/status.component.spec.ts b/src/app/cartography/components/experimental-map/status/status.component.spec.ts new file mode 100644 index 00000000..ddf4f677 --- /dev/null +++ b/src/app/cartography/components/experimental-map/status/status.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatusComponent } from './status.component'; + +describe('StatusComponent', () => { + let component: StatusComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ StatusComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/cartography/components/experimental-map/status/status.component.ts b/src/app/cartography/components/experimental-map/status/status.component.ts new file mode 100644 index 00000000..05d58efc --- /dev/null +++ b/src/app/cartography/components/experimental-map/status/status.component.ts @@ -0,0 +1,83 @@ +import { Component, ElementRef, Input, ChangeDetectorRef } from '@angular/core'; + + +@Component({ + selector: '[app-status]', + templateUrl: './status.component.html', + styleUrls: ['./status.component.scss'], +}) +export class StatusComponent { + static STOPPED_STATUS_RECT_WIDTH = 10; + + data = { + 'status': '', + 'path': null, + 'direction': null, + 'd': null + }; + + constructor( + protected element: ElementRef, + private ref: ChangeDetectorRef + ) {} + + @Input('app-status') + set status(value) { + this.data.status = value; + this.ref.markForCheck(); + } + + @Input('path') + set path(value) { + this.data.path = value; + this.ref.markForCheck(); + } + + @Input('direction') + set direction(value) { + this.data.direction = value; + this.ref.markForCheck(); + } + + @Input('d') + set d(value) { + if (this.data.d !== value) { + this.data.d = value; + this.ref.markForCheck(); + } + } + + get status() { + return this.data.status; + } + + get direction() { + return this.data.direction; + } + + get path() { + return this.data.path; + } + + get sourceStatusPoint() { + if (!this.path) { + return null; + } + return this.path.nativeElement.getPointAtLength(45); + } + + get targetStatusPoint() { + if (!this.path) { + return null; + } + return this.path.nativeElement.getPointAtLength(this.path.nativeElement.getTotalLength() - 45); + } + + get point() { + if (this.direction === 'source') { + return this.sourceStatusPoint; + } + return this.targetStatusPoint; + } + +} diff --git a/src/app/cartography/components/map/map.component.html b/src/app/cartography/components/map/map.component.html deleted file mode 100644 index dc9747ac..00000000 --- a/src/app/cartography/components/map/map.component.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/app/cartography/components/selection-control/selection-control.component.html b/src/app/cartography/components/selection-control/selection-control.component.html new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/selection-control/selection-control.component.scss b/src/app/cartography/components/selection-control/selection-control.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/selection-control/selection-control.component.spec.ts b/src/app/cartography/components/selection-control/selection-control.component.spec.ts new file mode 100644 index 00000000..af1fecd6 --- /dev/null +++ b/src/app/cartography/components/selection-control/selection-control.component.spec.ts @@ -0,0 +1,72 @@ +import { fakeAsync, tick } from '@angular/core/testing'; + +import { SelectionControlComponent } from './selection-control.component'; +import { SelectionManager } from '../../managers/selection-manager'; +import { SelectionEventSource } from '../../events/selection-event-source'; +import { mock, when, instance } from 'ts-mockito'; +import { GraphDataManager } from '../../managers/graph-data-manager'; +import { MapNode } from '../../models/map/map-node'; +import { MapLink } from '../../models/map/map-link'; +import { InRectangleHelper } from '../../helpers/in-rectangle-helper'; +import { Rectangle } from '../../models/rectangle'; + +describe('SelectionControlComponent', () => { + let component: SelectionControlComponent; + let manager: SelectionManager; + let selectionEventSource: SelectionEventSource; + + beforeEach(() => { + + const mockedGraphData = mock(GraphDataManager); + + const node_1 = new MapNode(); + node_1.id = "test1"; + node_1.name = "Node 1"; + node_1.x = 150; + node_1.y = 150; + + const node_2 = new MapNode(); + node_2.id = "test2"; + node_2.name = "Node 2"; + node_2.x = 300; + node_2.y = 300; + + const link_1 = new MapLink(); + link_1.id = "test1"; + + when(mockedGraphData.getNodes()).thenReturn([node_1, node_2]); + when(mockedGraphData.getLinks()).thenReturn([link_1]); + when(mockedGraphData.getDrawings()).thenReturn([]); + + const graphData = instance(mockedGraphData); + const inRectangleHelper = new InRectangleHelper(); + + selectionEventSource = new SelectionEventSource(); + manager = new SelectionManager(); + + component = new SelectionControlComponent(selectionEventSource, graphData, inRectangleHelper, manager); + component.ngOnInit(); + }); + + afterEach(() => { + component.ngOnDestroy(); + }) + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('node should be selected', fakeAsync(() => { + selectionEventSource.selected.next(new Rectangle(100, 100, 100, 100)); + tick(); + expect(manager.getSelected().length).toEqual(1); + })); + + it('node should be selected and deselected', fakeAsync(() => { + selectionEventSource.selected.next(new Rectangle(100, 100, 100, 100)); + tick(); + selectionEventSource.selected.next(new Rectangle(350, 350, 100, 100)); + tick(); + expect(manager.getSelected().length).toEqual(0); + })); +}); diff --git a/src/app/cartography/components/selection-control/selection-control.component.ts b/src/app/cartography/components/selection-control/selection-control.component.ts new file mode 100644 index 00000000..6d603cd7 --- /dev/null +++ b/src/app/cartography/components/selection-control/selection-control.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { SelectionEventSource } from '../../events/selection-event-source'; +import { GraphDataManager } from '../../managers/graph-data-manager'; +import { InRectangleHelper } from '../../helpers/in-rectangle-helper'; +import { SelectionManager } from '../../managers/selection-manager'; +import { Rectangle } from '../../models/rectangle'; + +@Component({ + selector: 'app-selection-control', + templateUrl: './selection-control.component.html', + styleUrls: ['./selection-control.component.scss'] +}) +export class SelectionControlComponent implements OnInit, OnDestroy { + private onSelection: Subscription; + + constructor( + private selectionEventSource: SelectionEventSource, + private graphDataManager: GraphDataManager, + private inRectangleHelper: InRectangleHelper, + private selectionManager: SelectionManager + ) { } + + ngOnInit() { + this.onSelection = this.selectionEventSource.selected.subscribe((rectangle: Rectangle) => { + const selectedNodes = this.graphDataManager.getNodes().filter((node) => { + return this.inRectangleHelper.inRectangle(rectangle, node.x, node.y) + }); + + const selectedLinks = this.graphDataManager.getLinks().filter((link) => { + return this.inRectangleHelper.inRectangle(rectangle, link.x, link.y) + }); + + const selectedDrawings = this.graphDataManager.getDrawings().filter((drawing) => { + return this.inRectangleHelper.inRectangle(rectangle, drawing.x, drawing.y) + }); + + const selectedLabels = this.graphDataManager.getNodes().filter((node) => { + if (node.label === undefined) { + return false; + } + const labelX = node.x + node.label.x; + const labelY = node.y + node.label.y; + return this.inRectangleHelper.inRectangle(rectangle, labelX, labelY); + }).map((node) => node.label); + + const selected = [ + ...selectedNodes, + ...selectedLinks, + ...selectedDrawings, + ...selectedLabels + ]; + + this.selectionManager.setSelected(selected); + }); + } + + ngOnDestroy() { + this.onSelection.unsubscribe(); + } + +} diff --git a/src/app/cartography/components/selection-select/selection-select.component.html b/src/app/cartography/components/selection-select/selection-select.component.html new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/selection-select/selection-select.component.scss b/src/app/cartography/components/selection-select/selection-select.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/selection-select/selection-select.component.spec.ts b/src/app/cartography/components/selection-select/selection-select.component.spec.ts new file mode 100644 index 00000000..3af1b594 --- /dev/null +++ b/src/app/cartography/components/selection-select/selection-select.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectionSelectComponent } from './selection-select.component'; + +describe('SelectionSelectComponent', () => { + let component: SelectionSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SelectionSelectComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectionSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/listeners/selection-update-listener.ts b/src/app/cartography/components/selection-select/selection-select.component.ts similarity index 50% rename from src/app/cartography/listeners/selection-update-listener.ts rename to src/app/cartography/components/selection-select/selection-select.component.ts index 8272c282..fffba5d7 100644 --- a/src/app/cartography/listeners/selection-update-listener.ts +++ b/src/app/cartography/components/selection-select/selection-select.component.ts @@ -1,21 +1,23 @@ -import { Injectable } from "@angular/core"; -import { Subscription } from "rxjs"; -import { MapChangeDetectorRef } from "../services/map-change-detector-ref"; -import { SelectionManager } from "../managers/selection-manager"; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { SelectionManager } from '../../managers/selection-manager'; +import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; - -@Injectable() -export class SelectionUpdateListener { +@Component({ + selector: 'app-selection-select', + templateUrl: './selection-select.component.html', + styleUrls: ['./selection-select.component.scss'] +}) +export class SelectionSelectComponent implements OnInit, OnDestroy { private onSelected: Subscription; private onUnselected: Subscription; - + constructor( private selectionManager: SelectionManager, private mapChangeDetectorRef: MapChangeDetectorRef - ) { - } + ) { } - public onInit(svg: any) { + ngOnInit() { this.onSelected = this.selectionManager.selected.subscribe(() => { this.mapChangeDetectorRef.detectChanges(); }); @@ -24,8 +26,9 @@ export class SelectionUpdateListener { }); } - public onDestroy() { + ngOnDestroy() { this.onSelected.unsubscribe(); this.onUnselected.unsubscribe(); } -} \ No newline at end of file + +} diff --git a/src/app/cartography/components/selection/selection.component.html b/src/app/cartography/components/selection/selection.component.html new file mode 100644 index 00000000..8dee5ced --- /dev/null +++ b/src/app/cartography/components/selection/selection.component.html @@ -0,0 +1,10 @@ + + + + diff --git a/src/app/cartography/components/selection/selection.component.scss b/src/app/cartography/components/selection/selection.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/selection/selection.component.spec.ts b/src/app/cartography/components/selection/selection.component.spec.ts new file mode 100644 index 00000000..111505c9 --- /dev/null +++ b/src/app/cartography/components/selection/selection.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectionComponent } from './selection.component'; + +describe('SelectionComponent', () => { + let component: SelectionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SelectionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/cartography/components/selection/selection.component.ts b/src/app/cartography/components/selection/selection.component.ts new file mode 100644 index 00000000..b9589b29 --- /dev/null +++ b/src/app/cartography/components/selection/selection.component.ts @@ -0,0 +1,122 @@ +import { Component, OnInit, Input, AfterViewInit, ChangeDetectorRef, Output, EventEmitter } from '@angular/core'; +import { Observable, Subscription, Subject } from 'rxjs'; +import { Rectangle } from '../../models/rectangle'; + +@Component({ + selector: '[app-selection]', + templateUrl: './selection.component.html', + styleUrls: ['./selection.component.scss'] +}) +export class SelectionComponent implements OnInit, AfterViewInit { + @Input('app-selection') svg: SVGSVGElement; + + private startX: number; + private startY: number; + + private width: number; + private height: number; + + started = false; + visible = false; + draggable: Subscription; + + + @Output('selected') rectangleSelected = new EventEmitter(); + + constructor( + private ref: ChangeDetectorRef + ) { } + + ngOnInit() { + } + + + ngAfterViewInit() { + const down = Observable.fromEvent(this.svg, 'mousedown').do((e: MouseEvent) => e.preventDefault()); + + down.subscribe((e: MouseEvent) => { + if(e.target !== this.svg) { + return; + } + + this.started = true; + this.startX = e.clientX + window.scrollX; + this.startY = e.clientY + window.scrollY; + this.width = 0; + this.height = 0; + this.visible = true; + this.ref.detectChanges(); + }); + + const up = Observable.fromEvent(document, 'mouseup') + .do((e: MouseEvent) => { + e.preventDefault(); + }); + + const mouseMove = Observable.fromEvent(document, 'mousemove') + .do((e: MouseEvent) => e.stopPropagation()); + + const scrollWindow = Observable.fromEvent(document, 'scroll') + .startWith({}); + + const move = Observable.combineLatest(mouseMove, scrollWindow); + + const drag = down.mergeMap((md: MouseEvent) => { + return move + .map(([mm, s]) => mm) + .do((mm: MouseEvent) => { + if(!this.started) { + return; + } + this.visible = true; + this.width = mm.clientX - this.startX + window.scrollX; + this.height = mm.clientY - this.startY + window.scrollY; + + this.ref.detectChanges(); + + this.selectedEvent([this.startX, this.startY], [this.width, this.height]); + }) + .skipUntil(up + .take(1) + .do((e: MouseEvent) => { + if(!this.started) { + return; + } + this.visible = false; + this.started = false; + + this.width = e.clientX - this.startX + window.scrollX; + this.height = e.clientY - this.startY + window.scrollY; + + this.ref.detectChanges(); + + this.selectedEvent([this.startX, this.startY], [this.width, this.height]); + })) + .take(1); + }); + + this.draggable = drag.subscribe((e: MouseEvent) => { + // this.cd.detectChanges(); + }); + } + + ngOnDestroy() { + this.draggable.unsubscribe(); + } + + get d() { + return this.rect(this.startX, this.startY, this.width, this.height); + } + + private rect(x: number, y: number, w: number, h: number) { + return "M" + [x, y] + " l" + [w, 0] + " l" + [0, h] + " l" + [-w, 0] + "z"; + } + + private selectedEvent(start, end) { + const x = Math.min(start[0], end[0]); + const y = Math.min(start[1], end[1]); + const width = Math.abs(start[0] - end[0]); + const height = Math.abs(start[1] - end[1]); + this.rectangleSelected.emit(new Rectangle(x, y, width, height)); + } +} diff --git a/src/app/cartography/converters/map/label-to-map-label-converter.ts b/src/app/cartography/converters/map/label-to-map-label-converter.ts index 0501978f..3f19485b 100644 --- a/src/app/cartography/converters/map/label-to-map-label-converter.ts +++ b/src/app/cartography/converters/map/label-to-map-label-converter.ts @@ -7,13 +7,17 @@ import { MapLabel } from "../../models/map/map-label"; @Injectable() export class LabelToMapLabelConverter implements Converter { - convert(label: Label) { + convert(label: Label, paramaters?: {[node_id: string]: string}) { const mapLabel = new MapLabel(); mapLabel.rotation = label.rotation; mapLabel.style = label.style; mapLabel.text = label.text; mapLabel.x = label.x; mapLabel.y = label.y; + if (paramaters !== undefined) { + mapLabel.id = paramaters.node_id; + mapLabel.nodeId = paramaters.node_id; + } return mapLabel; } } diff --git a/src/app/cartography/converters/map/map-label-to-label-converter.ts b/src/app/cartography/converters/map/map-label-to-label-converter.ts index 1a0ca88d..39b17e4f 100644 --- a/src/app/cartography/converters/map/map-label-to-label-converter.ts +++ b/src/app/cartography/converters/map/map-label-to-label-converter.ts @@ -7,7 +7,7 @@ import { MapLabel } from "../../models/map/map-label"; @Injectable() export class MapLabelToLabelConverter implements Converter { - convert(mapLabel: MapLabel) { + convert(mapLabel: MapLabel, paramters?: any) { const label = new Label(); label.rotation = mapLabel.rotation; label.style = mapLabel.style; diff --git a/src/app/cartography/converters/map/node-to-map-node-converter.ts b/src/app/cartography/converters/map/node-to-map-node-converter.ts index 2019d1a5..3ea82974 100644 --- a/src/app/cartography/converters/map/node-to-map-node-converter.ts +++ b/src/app/cartography/converters/map/node-to-map-node-converter.ts @@ -23,7 +23,7 @@ export class NodeToMapNodeConverter implements Converter { mapNode.consoleHost = node.console_host; mapNode.firstPortName = node.first_port_name; mapNode.height = node.height; - mapNode.label = this.labelToMapLabel.convert(node.label); + mapNode.label = this.labelToMapLabel.convert(node.label, { node_id: node.node_id }); mapNode.name = node.name; mapNode.nodeDirectory = node.node_directory; mapNode.nodeType = node.node_type; 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/events/selection-event-source.ts b/src/app/cartography/events/selection-event-source.ts new file mode 100644 index 00000000..c97aef44 --- /dev/null +++ b/src/app/cartography/events/selection-event-source.ts @@ -0,0 +1,9 @@ +import { Injectable } from "@angular/core"; +import { Subject } from "rxjs"; +import { Rectangle } from "../models/rectangle"; + + +@Injectable() +export class SelectionEventSource { + public selected = new Subject(); +} \ No newline at end of file diff --git a/src/app/cartography/listeners/draggable-listener.ts b/src/app/cartography/listeners/draggable-listener.ts deleted file mode 100644 index c2406491..00000000 --- a/src/app/cartography/listeners/draggable-listener.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Injectable } from "@angular/core"; -import { NodesWidget } from "../widgets/nodes"; -import { DraggableStart, DraggableDrag, DraggableEnd } from "../events/draggable"; -import { Subscription } from "rxjs"; -import { SelectionManager } from "../managers/selection-manager"; -import { LinksWidget } from "../widgets/links"; -import { NodesEventSource } from "../events/nodes-event-source"; -import { DraggedDataEvent } from "../events/event-source"; -import { MapNode } from "../models/map/map-node"; -import { GraphDataManager } from "../managers/graph-data-manager"; -import { DrawingsWidget } from "../widgets/drawings"; -import { merge } from "rxjs"; -import { MapDrawing } from "../models/map/map-drawing"; -import { DrawingsEventSource } from "../events/drawings-event-source"; - - -@Injectable() -export class DraggableListener { - private start: Subscription; - private drag: Subscription; - private end: Subscription; - - constructor( - private nodesWidget: NodesWidget, - private drawingsWidget: DrawingsWidget, - private linksWidget: LinksWidget, - private selectionManager: SelectionManager, - private nodesEventSource: NodesEventSource, - private drawingsEventSource: DrawingsEventSource, - private graphDataManager: GraphDataManager - ) { - } - - public onInit(svg: any) { - this.start = merge( - this.nodesWidget.draggable.start, - this.drawingsWidget.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]); - } - } - - if (evt.datum instanceof MapDrawing) { - if (selected.filter((item) => item instanceof MapDrawing && item.id === evt.datum.id).length === 0) { - this.selectionManager.setSelected([evt.datum]); - } - } - }); - - this.drag = merge( - this.nodesWidget.draggable.drag, - this.drawingsWidget.draggable.drag - ).subscribe((evt: DraggableDrag) => { - const selected = this.selectionManager.getSelected(); - - // update nodes - selected.filter((item) => item instanceof MapNode).forEach((node: MapNode) => { - node.x += evt.dx; - node.y += evt.dy; - - this.nodesWidget.redrawNode(svg, node); - - const links = this.graphDataManager.getLinks().filter( - (link) => link.target.id === node.id || link.source.id === node.id); - links.forEach((link) => { - this.linksWidget.redrawLink(svg, link); - }); - }); - - // update drawings - selected.filter((item) => item instanceof MapDrawing).forEach((drawing: MapDrawing) => { - drawing.x += evt.dx; - drawing.y += evt.dy; - this.drawingsWidget.redrawDrawing(svg, drawing); - }); - - }); - - this.end = merge( - this.nodesWidget.draggable.end, - this.drawingsWidget.draggable.end - ).subscribe((evt: DraggableEnd) => { - const selected = this.selectionManager.getSelected(); - - selected.filter((item) => item instanceof MapNode).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)); - }); - }); - - } - - public onDestroy() { - this.start.unsubscribe(); - this.drag.unsubscribe(); - this.end.unsubscribe(); - } -} \ No newline at end of file diff --git a/src/app/cartography/listeners/map-listener.ts b/src/app/cartography/listeners/map-listener.ts deleted file mode 100644 index 50c7a2bb..00000000 --- a/src/app/cartography/listeners/map-listener.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface MapListener { - onInit(svg: any); - onDestroy(); -} \ No newline at end of file diff --git a/src/app/cartography/listeners/map-listeners.ts b/src/app/cartography/listeners/map-listeners.ts deleted file mode 100644 index 40752896..00000000 --- a/src/app/cartography/listeners/map-listeners.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from "@angular/core"; -import { MapListener } from "./map-listener"; -import { DraggableListener } from "./draggable-listener"; -import { SelectionUpdateListener } from "./selection-update-listener"; -import { SelectionListener } from "./selection-listener"; - - -@Injectable() -export class MapListeners { - private listeners: MapListener[] = []; - constructor( - private nodesDraggableListener: DraggableListener, - private selectionUpdateListener: SelectionUpdateListener, - private selectionListener: SelectionListener - ) { - this.listeners.push(this.nodesDraggableListener); - this.listeners.push(this.selectionUpdateListener); - this.listeners.push(this.selectionListener); - } - - public onInit(svg: any) { - this.listeners.forEach((listener) => { - listener.onInit(svg); - }); - } - - public onDestroy() { - this.listeners.forEach((listener) => { - listener.onDestroy(); - }); - } -} \ No newline at end of file diff --git a/src/app/cartography/listeners/selection-listener.spec.ts b/src/app/cartography/listeners/selection-listener.spec.ts deleted file mode 100644 index 5c8436ef..00000000 --- a/src/app/cartography/listeners/selection-listener.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Rectangle } from "../models/rectangle"; -import { InRectangleHelper } from "../helpers/in-rectangle-helper"; -import { MapNode } from "../models/map/map-node"; -import { MapLink } from "../models/map/map-link"; -import { mock, instance, when } from "ts-mockito"; -import { fakeAsync, tick } from "@angular/core/testing"; -import { SelectionListener } from "./selection-listener"; -import { SelectionManager } from "../managers/selection-manager"; -import { GraphDataManager } from "../managers/graph-data-manager"; -import { SelectionTool } from "../tools/selection-tool"; -import { Context } from "../models/context"; - - -describe('SelectionListener', () => { - let selectionListener: SelectionListener; - let manager: SelectionManager; - let selectionTool: SelectionTool; - - beforeEach(() => { - const mockedGraphData = mock(GraphDataManager); - - const node_1 = new MapNode(); - node_1.id = "test1"; - node_1.name = "Node 1"; - node_1.x = 150; - node_1.y = 150; - - const node_2 = new MapNode(); - node_2.id = "test2"; - node_2.name = "Node 2"; - node_2.x = 300; - node_2.y = 300; - - const link_1 = new MapLink(); - link_1.id = "test1"; - - when(mockedGraphData.getNodes()).thenReturn([node_1, node_2]); - when(mockedGraphData.getLinks()).thenReturn([link_1]); - when(mockedGraphData.getDrawings()).thenReturn([]); - - const graphData = instance(mockedGraphData); - const inRectangleHelper = new InRectangleHelper(); - - manager = new SelectionManager(); - selectionTool = new SelectionTool(new Context()); - selectionListener = new SelectionListener(selectionTool, graphData, inRectangleHelper, manager); - selectionListener.onInit(null); - }); - - afterEach(() => { - selectionListener.onDestroy(); - }) - - it('node should be selected', fakeAsync(() => { - selectionTool.rectangleSelected.next(new Rectangle(100, 100, 100, 100)); - tick(); - expect(manager.getSelected().length).toEqual(1); - })); - - it('node should be selected and deselected', fakeAsync(() => { - selectionTool.rectangleSelected.next(new Rectangle(100, 100, 100, 100)); - tick(); - selectionTool.rectangleSelected.next(new Rectangle(350, 350, 100, 100)); - tick(); - expect(manager.getSelected().length).toEqual(0); - })); - -}); diff --git a/src/app/cartography/listeners/selection-listener.ts b/src/app/cartography/listeners/selection-listener.ts deleted file mode 100644 index b97e366c..00000000 --- a/src/app/cartography/listeners/selection-listener.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Injectable } from "@angular/core"; -import { GraphDataManager } from "../managers/graph-data-manager"; -import { InRectangleHelper } from "../helpers/in-rectangle-helper"; -import { SelectionTool } from "../tools/selection-tool"; -import { Subscription } from "rxjs"; -import { Rectangle } from "electron"; -import { SelectionManager } from "../managers/selection-manager"; - - -@Injectable() -export class SelectionListener { - private onSelection: Subscription; - - constructor( - private selectionTool: SelectionTool, - private graphDataManager: GraphDataManager, - private inRectangleHelper: InRectangleHelper, - private selectionManager: SelectionManager - ) { - } - - public onInit(svg: any) { - this.onSelection = this.selectionTool.rectangleSelected.subscribe((rectangle: Rectangle) => { - const selectedNodes = this.graphDataManager.getNodes().filter((node) => { - return this.inRectangleHelper.inRectangle(rectangle, node.x, node.y) - }); - - const selectedLinks = this.graphDataManager.getLinks().filter((link) => { - return this.inRectangleHelper.inRectangle(rectangle, link.x, link.y) - }); - - const selectedDrawings = this.graphDataManager.getDrawings().filter((drawing) => { - return this.inRectangleHelper.inRectangle(rectangle, drawing.x, drawing.y) - }); - - const selected = [...selectedNodes, ...selectedLinks, ...selectedDrawings]; - - this.selectionManager.setSelected(selected); - }); - } - - public onDestroy() { - this.onSelection.unsubscribe(); - } -} \ No newline at end of file diff --git a/src/app/cartography/managers/graph-data-manager.ts b/src/app/cartography/managers/graph-data-manager.ts index 7e6516d0..2b7e5621 100644 --- a/src/app/cartography/managers/graph-data-manager.ts +++ b/src/app/cartography/managers/graph-data-manager.ts @@ -11,6 +11,7 @@ import { Drawing } from "../models/drawing"; import { Symbol } from "../../models/symbol"; import { LayersManager } from "./layers-manager"; import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource } from "../datasources/map-datasource"; +import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper"; @Injectable() export class GraphDataManager { @@ -23,7 +24,8 @@ export class GraphDataManager { private linkToMapLink: LinkToMapLinkConverter, private drawingToMapDrawing: DrawingToMapDrawingConverter, private symbolToMapSymbol: SymbolToMapSymbolConverter, - private layersManager: LayersManager + private layersManager: LayersManager, + private multiLinkCalculator: MultiLinkCalculatorHelper ) {} public setNodes(nodes: Node[]) { @@ -98,6 +100,8 @@ export class GraphDataManager { link.y = link.source.y + (link.target.y - link.source.y) * 0.5; } }); + + this.multiLinkCalculator.assignDataToLinks(this.getLinks()); } } \ No newline at end of file diff --git a/src/app/cartography/managers/selection-manager.ts b/src/app/cartography/managers/selection-manager.ts index 2f7b0dd3..40817618 100644 --- a/src/app/cartography/managers/selection-manager.ts +++ b/src/app/cartography/managers/selection-manager.ts @@ -23,8 +23,13 @@ export class SelectionManager { this.selection = dictItems; - this.selected.emit(selected); - this.unselected.emit(unselected); + if (selected.length > 0) { + this.selected.emit(selected); + } + + if (unselected.length > 0) { + this.unselected.emit(unselected); + } } public getSelected(): Indexed[] { diff --git a/src/app/cartography/models/map/map-label.ts b/src/app/cartography/models/map/map-label.ts index fd1ffbd0..6dd1dd1d 100644 --- a/src/app/cartography/models/map/map-label.ts +++ b/src/app/cartography/models/map/map-label.ts @@ -1,8 +1,11 @@ -export class MapLabel { - rotation: number; - style: string; - text: string; - x: number; - y: number; - isSelected: boolean; +import { Indexed } from "../../datasources/map-datasource"; + +export class MapLabel implements Indexed { + id: string; + rotation: number; + style: string; + text: string; + x: number; + y: number; + nodeId: string; } diff --git a/src/app/cartography/tools/selection-tool.spec.ts b/src/app/cartography/tools/selection-tool.spec.ts index 914eada8..4e2efc59 100644 --- a/src/app/cartography/tools/selection-tool.spec.ts +++ b/src/app/cartography/tools/selection-tool.spec.ts @@ -1,10 +1,9 @@ -import { select } from "d3-selection"; - import { SelectionTool } from "./selection-tool"; import { Context } from "../models/context"; import { SVGSelection } from "../models/types"; import { Rectangle } from "../models/rectangle"; import { TestSVGCanvas } from "../testing"; +import { SelectionEventSource } from "../events/selection-event-source"; describe('SelectionTool', () => { @@ -14,11 +13,14 @@ describe('SelectionTool', () => { let selection_line_tool: SVGSelection; let path_selection: SVGSelection; let selected_rectangle: Rectangle; + let selection_event_source: SelectionEventSource; beforeEach(() => { context = new Context(); - tool = new SelectionTool(context); - tool.rectangleSelected.subscribe((rectangle: Rectangle) => { + selection_event_source = new SelectionEventSource(); + + tool = new SelectionTool(context, selection_event_source); + selection_event_source.selected.subscribe((rectangle: Rectangle) => { selected_rectangle = rectangle; }); diff --git a/src/app/cartography/tools/selection-tool.ts b/src/app/cartography/tools/selection-tool.ts index e5602dd4..228a1a53 100644 --- a/src/app/cartography/tools/selection-tool.ts +++ b/src/app/cartography/tools/selection-tool.ts @@ -5,6 +5,7 @@ import { Subject } from "rxjs"; import { SVGSelection } from "../models/types"; import { Context } from "../models/context"; import { Rectangle } from "../models/rectangle"; +import { SelectionEventSource } from "../events/selection-event-source"; @Injectable() @@ -15,11 +16,10 @@ export class SelectionTool { private path; private enabled = false; - private needsDeactivate = false; - private needsActivate = false; public constructor( - private context: Context + private context: Context, + private selectionEventSource: SelectionEventSource ) {} public setEnabled(enabled) { @@ -107,7 +107,7 @@ export class SelectionTool { const y = Math.min(start[1], end[1]); const width = Math.abs(start[0] - end[0]); const height = Math.abs(start[1] - end[1]); - this.rectangleSelected.next(new Rectangle(x, y, width, height)); + this.selectionEventSource.selected.next(new Rectangle(x, y, width, height)); } private rect(x: number, y: number, w: number, h: number) { diff --git a/src/app/cartography/widgets/interface-label.spec.ts b/src/app/cartography/widgets/interface-label.spec.ts index c007d4a5..cffd5428 100644 --- a/src/app/cartography/widgets/interface-label.spec.ts +++ b/src/app/cartography/widgets/interface-label.spec.ts @@ -8,6 +8,7 @@ import { MapNode } from "../models/map/map-node"; import { MapLink } from "../models/map/map-link"; import { MapLinkNode } from "../models/map/map-link-node"; import { MapLabel } from "../models/map/map-label"; +import { FontFixer } from "../helpers/font-fixer"; describe('InterfaceLabelsWidget', () => { @@ -67,7 +68,7 @@ describe('InterfaceLabelsWidget', () => { .exit() .remove(); - widget = new InterfaceLabelWidget(new CssFixer()); + widget = new InterfaceLabelWidget(new CssFixer(), new FontFixer()); }); afterEach(() => { 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..2a016963 --- /dev/null +++ b/src/app/cartography/widgets/label.ts @@ -0,0 +1,130 @@ +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 ; + // } + return l.y + bbox.height - LabelWidget.NODE_LABEL_MARGIN; + }) + .attr('transform', (l: MapLabel) => { + return `rotate(${l.rotation}, ${l.x}, ${l.y})`; + }) + + 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); + current.attr('transform', `rotate(${l.rotation}, ${bbox.x - border}, ${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 9413e62d..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,10 +35,6 @@ export class NodeWidget implements Widget { node_body_enter .append('image'); - // add label of node - node_body_enter - .append('text') - .attr('class', 'label'); const node_body_merge = node_body.merge(node_body_enter) .classed('selected', (n: MapNode) => this.selectionManager.isSelected(n)) @@ -80,33 +72,7 @@ export class NodeWidget implements Widget { .attr('transform', (n: MapNode) => { return `translate(${n.x},${n.y})`; }); - - node_body_merge - .select('text.label') - // .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; - }); + this.labelWidget.draw(node_body_merge); } } diff --git a/src/app/cartography/components/node-select-interface/node-select-interface.component.html b/src/app/components/project-map/node-select-interface/node-select-interface.component.html similarity index 83% rename from src/app/cartography/components/node-select-interface/node-select-interface.component.html rename to src/app/components/project-map/node-select-interface/node-select-interface.component.html index 8900bba8..d912c26b 100644 --- a/src/app/cartography/components/node-select-interface/node-select-interface.component.html +++ b/src/app/components/project-map/node-select-interface/node-select-interface.component.html @@ -1,6 +1,6 @@
- +