Merge branch 'develop'

This commit is contained in:
ziajka 2018-12-10 12:26:48 +01:00
commit 03478fdb60
149 changed files with 3565 additions and 712 deletions

View File

@ -1,4 +1,4 @@
# Editor configuration, see http://editorconfig.org # Editor configuration, see https://editorconfig.org
root = true root = true
[*] [*]

View File

@ -46,7 +46,7 @@ before_script:
- greenkeeper-lockfile-update - greenkeeper-lockfile-update
- npm install -g codecov - npm install -g codecov
script: yarn ng test --watch=false --code-coverage script: yarn coverage
after_success: after_success:
- codecov - codecov

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch localhost",
"type": "firefox",
"request": "launch",
"reAttach": true,
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -133,6 +133,9 @@
"assets": [ "assets": [
"src/assets", "src/assets",
"src/favicon.ico" "src/favicon.ico"
],
"codeCoverageExclude": [
"src/app/cartography/components/experimental-map/**/*"
] ]
} }
}, },

View File

@ -22,7 +22,8 @@
"distlinux": "yarn buildforelectron && electron-builder --linux --x64", "distlinux": "yarn buildforelectron && electron-builder --linux --x64",
"distwin": "yarn buildforelectron && electron-builder --win --x64", "distwin": "yarn buildforelectron && electron-builder --win --x64",
"distmac": "yarn buildforelectron && electron-builder --mac --x64", "distmac": "yarn buildforelectron && electron-builder --mac --x64",
"release": "build" "release": "build",
"coverage": "ng test --watch=false --code-coverage"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -91,8 +92,5 @@
"ignore": [ "ignore": [
"typescript" "typescript"
] ]
}, }
"comments": [
"Typescript should remain below 2.8.0, @todo: check later if packages were adjusted"
]
} }

View File

@ -72,6 +72,8 @@ import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu-
import { MATERIAL_IMPORTS } from './material.imports'; import { MATERIAL_IMPORTS } from './material.imports';
import { DrawingService } from './services/drawing.service'; import { DrawingService } from './services/drawing.service';
import { ProjectNameValidator } from './components/projects/models/projectNameValidator'; import { ProjectNameValidator } from './components/projects/models/projectNameValidator';
import { NodeSelectInterfaceComponent } from './components/project-map/node-select-interface/node-select-interface.component';
import { DrawLinkToolComponent } from './components/project-map/draw-link-tool/draw-link-tool.component';
if (environment.production) { if (environment.production) {
@ -113,6 +115,8 @@ if (environment.production) {
LocalServerComponent, LocalServerComponent,
ProgressComponent, ProgressComponent,
ServerDiscoveryComponent, ServerDiscoveryComponent,
NodeSelectInterfaceComponent,
DrawLinkToolComponent
], ],
imports: [ imports: [
NgbModule.forRoot(), NgbModule.forRoot(),

View File

@ -0,0 +1,28 @@
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';
import { DraggableComponent } from './components/experimental-map/draggable/draggable.component';
import { SelectionComponent } from './components/experimental-map/selection/selection.component';
export const ANGULAR_MAP_DECLARATIONS = [
NodeComponent,
LinkComponent,
StatusComponent,
DrawingComponent,
EllipseComponent,
ImageComponent,
LineComponent,
RectComponent,
TextComponent,
DraggableComponent,
SelectionComponent,
InterfaceLabelComponent
];

View File

@ -2,10 +2,6 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatMenuModule, MatIconModule } from '@angular/material'; 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 { CssFixer } from './helpers/css-fixer';
import { FontFixer } from './helpers/font-fixer'; import { FontFixer } from './helpers/font-fixer';
import { MultiLinkCalculatorHelper } from './helpers/multi-link-calculator-helper'; import { MultiLinkCalculatorHelper } from './helpers/multi-link-calculator-helper';
@ -14,10 +10,9 @@ import { QtDasharrayFixer } from './helpers/qt-dasharray-fixer';
import { LayersManager } from './managers/layers-manager'; import { LayersManager } from './managers/layers-manager';
import { MapChangeDetectorRef } from './services/map-change-detector-ref'; import { MapChangeDetectorRef } from './services/map-change-detector-ref';
import { Context } from './models/context'; import { Context } from './models/context';
import { ANGULAR_MAP_DECLARATIONS } from './angular-map.imports';
import { D3_MAP_IMPORTS } from './d3-map.imports'; import { D3_MAP_IMPORTS } from './d3-map.imports';
import { CanvasSizeDetector } from './helpers/canvas-size-detector'; 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 { DrawingsEventSource } from './events/drawings-event-source';
import { NodesEventSource } from './events/nodes-event-source'; import { NodesEventSource } from './events/nodes-event-source';
import { DrawingToMapDrawingConverter } from './converters/map/drawing-to-map-drawing-converter'; import { DrawingToMapDrawingConverter } from './converters/map/drawing-to-map-drawing-converter';
@ -35,10 +30,17 @@ import { PortToMapPortConverter } from './converters/map/port-to-map-port-conver
import { SymbolToMapSymbolConverter } from './converters/map/symbol-to-map-symbol-converter'; import { SymbolToMapSymbolConverter } from './converters/map/symbol-to-map-symbol-converter';
import { LinkNodeToMapLinkNodeConverter } from './converters/map/link-node-to-map-link-node-converter'; import { LinkNodeToMapLinkNodeConverter } from './converters/map/link-node-to-map-link-node-converter';
import { GraphDataManager } from './managers/graph-data-manager'; import { GraphDataManager } from './managers/graph-data-manager';
import { SelectionUpdateListener } from './listeners/selection-update-listener';
import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource } from './datasources/map-datasource'; import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource } from './datasources/map-datasource';
import { SelectionListener } from './listeners/selection-listener';
import { LinksEventSource } from './events/links-event-source'; 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';
import { MapSettingsManager } from './managers/map-settings-manager';
import { FontBBoxCalculator } from './helpers/font-bbox-calculator';
import { StylesToFontConverter } from './converters/styles-to-font-converter';
@NgModule({ @NgModule({
@ -48,9 +50,12 @@ import { LinksEventSource } from './events/links-event-source';
MatIconModule MatIconModule
], ],
declarations: [ declarations: [
MapComponent, D3MapComponent,
DrawLinkToolComponent, ExperimentalMapComponent,
NodeSelectInterfaceComponent ...ANGULAR_MAP_DECLARATIONS,
SelectionControlComponent,
SelectionSelectComponent,
DraggableSelectionComponent
], ],
providers: [ providers: [
CssFixer, CssFixer,
@ -62,10 +67,6 @@ import { LinksEventSource } from './events/links-event-source';
MapChangeDetectorRef, MapChangeDetectorRef,
CanvasSizeDetector, CanvasSizeDetector,
Context, Context,
SelectionUpdateListener,
MapListeners,
DraggableListener,
SelectionListener,
DrawingsEventSource, DrawingsEventSource,
NodesEventSource, NodesEventSource,
LinksEventSource, LinksEventSource,
@ -88,8 +89,12 @@ import { LinksEventSource } from './events/links-event-source';
MapLinksDataSource, MapLinksDataSource,
MapDrawingsDataSource, MapDrawingsDataSource,
MapSymbolsDataSource, MapSymbolsDataSource,
SelectionEventSource,
MapSettingsManager,
FontBBoxCalculator,
StylesToFontConverter,
...D3_MAP_IMPORTS ...D3_MAP_IMPORTS
], ],
exports: [ MapComponent ] exports: [ D3MapComponent, ExperimentalMapComponent ]
}) })
export class CartographyModule { } export class CartographyModule { }

View File

@ -0,0 +1,13 @@
<svg
#svg
class="map"
preserveAspectRatio="none"
>
<filter id="grayscale">
<feColorMatrix id="feGrayscale" type="saturate" values="0"/>
</filter>
</svg>
<app-selection-control></app-selection-control>
<app-selection-select></app-selection-select>
<app-draggable-selection [svg]="svg"></app-draggable-selection>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -1,14 +1,14 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MapComponent } from './map.component'; import { D3MapComponent } from './d3-map.component';
describe('MapComponent', () => { describe('D3MapComponent', () => {
let component: MapComponent; let component: D3MapComponent;
let fixture: ComponentFixture<MapComponent>; let fixture: ComponentFixture<D3MapComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ MapComponent ] declarations: [ D3MapComponent ]
}) })
.compileComponents(); .compileComponents();
})); }));

View File

@ -1,33 +1,31 @@
import { 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'; } from '@angular/core';
import { Selection, select } from 'd3-selection'; import { Selection, select } from 'd3-selection';
import { GraphLayout } from "../../widgets/graph-layout"; import { GraphLayout } from "../../widgets/graph-layout";
import { Context } from "../../models/context"; import { Context } from "../../models/context";
import { Size } from "../../models/size"; import { Size } from "../../models/size";
import { NodesWidget } from '../../widgets/nodes';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { InterfaceLabelWidget } from '../../widgets/interface-label'; import { InterfaceLabelWidget } from '../../widgets/interface-label';
import { SelectionTool } from '../../tools/selection-tool'; import { SelectionTool } from '../../tools/selection-tool';
import { MovingTool } from '../../tools/moving-tool'; import { MovingTool } from '../../tools/moving-tool';
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; import { CanvasSizeDetector } from '../../helpers/canvas-size-detector';
import { MapListeners } from '../../listeners/map-listeners';
import { DrawingsWidget } from '../../widgets/drawings';
import { Node } from '../../models/node'; import { Node } from '../../models/node';
import { Link } from '../../../models/link'; import { Link } from '../../../models/link';
import { Drawing } from '../../models/drawing'; import { Drawing } from '../../models/drawing';
import { Symbol } from '../../../models/symbol'; import { Symbol } from '../../../models/symbol';
import { GraphDataManager } from '../../managers/graph-data-manager'; import { GraphDataManager } from '../../managers/graph-data-manager';
import { MapSettingsManager } from '../../managers/map-settings-manager';
@Component({ @Component({
selector: 'app-map', selector: 'app-d3-map',
templateUrl: './map.component.html', templateUrl: './d3-map.component.html',
styleUrls: ['./map.component.scss'] styleUrls: ['./d3-map.component.scss']
}) })
export class MapComponent implements OnInit, OnChanges, OnDestroy { export class D3MapComponent implements OnInit, OnChanges, OnDestroy {
@Input() nodes: Node[] = []; @Input() nodes: Node[] = [];
@Input() links: Link[] = []; @Input() links: Link[] = [];
@Input() drawings: Drawing[] = []; @Input() drawings: Drawing[] = [];
@ -36,6 +34,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
@Input() width = 1500; @Input() width = 1500;
@Input() height = 600; @Input() height = 600;
@ViewChild('svg') svgRef: ElementRef;
private parentNativeElement: any; private parentNativeElement: any;
private svg: Selection<SVGSVGElement, any, null, undefined>; private svg: Selection<SVGSVGElement, any, null, undefined>;
@ -50,10 +50,8 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
private context: Context, private context: Context,
private mapChangeDetectorRef: MapChangeDetectorRef, private mapChangeDetectorRef: MapChangeDetectorRef,
private canvasSizeDetector: CanvasSizeDetector, private canvasSizeDetector: CanvasSizeDetector,
private mapListeners: MapListeners, private mapSettings: MapSettingsManager,
protected element: ElementRef, protected element: ElementRef,
protected nodesWidget: NodesWidget,
protected drawingsWidget: DrawingsWidget,
protected interfaceLabelWidget: InterfaceLabelWidget, protected interfaceLabelWidget: InterfaceLabelWidget,
protected selectionToolWidget: SelectionTool, protected selectionToolWidget: SelectionTool,
protected movingToolWidget: MovingTool, protected movingToolWidget: MovingTool,
@ -84,8 +82,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
@Input('draw-link-tool') drawLinkTool: boolean; @Input('draw-link-tool') drawLinkTool: boolean;
@Input('readonly') set readonly(value) { @Input('readonly') set readonly(value) {
this.nodesWidget.draggingEnabled = !value; this.mapSettings.isReadOnly = value;
this.drawingsWidget.draggingEnabled = !value;
} }
ngOnChanges(changes: { [propKey: string]: SimpleChange }) { ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
@ -117,14 +114,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
this.redraw(); this.redraw();
} }
}); });
this.mapListeners.onInit(this.svg);
} }
ngOnDestroy() { ngOnDestroy() {
this.graphLayout.disconnect(this.svg); this.graphLayout.disconnect(this.svg);
this.onChangesDetected.unsubscribe(); this.onChangesDetected.unsubscribe();
this.mapListeners.onDestroy();
} }
public createGraph(domElement: HTMLElement) { public createGraph(domElement: HTMLElement) {

View File

@ -0,0 +1,549 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { DraggableSelectionComponent } from './draggable-selection.component';
import { NodesWidget } from '../../widgets/nodes';
import { DrawingsWidget } from '../../widgets/drawings';
import { LinksWidget } from '../../widgets/links';
import { LabelWidget } from '../../widgets/label';
import { InterfaceLabelWidget } from '../../widgets/interface-label';
import { SelectionManager } from '../../managers/selection-manager';
import { SelectionManagerMock } from '../../managers/selection-manager.spec';
import { NodesEventSource } from '../../events/nodes-event-source';
import { DrawingsEventSource } from '../../events/drawings-event-source';
import { GraphDataManager } from '../../managers/graph-data-manager';
import { MockedGraphDataManager } from '../../managers/graph-data-manager.spec';
import { LinksEventSource } from '../../events/links-event-source';
import { DraggableStart, DraggableDrag, DraggableEnd } from '../../events/draggable';
import { MapNode } from '../../models/map/map-node';
import { EventEmitter } from '@angular/core';
import { MapDrawing } from '../../models/map/map-drawing';
import { MapLabel } from '../../models/map/map-label';
import { MapLinkNode } from '../../models/map/map-link-node';
import { select } from 'd3-selection';
import { MapLink } from '../../models/map/map-link';
describe('DraggableSelectionComponent', () => {
let component: DraggableSelectionComponent;
let fixture: ComponentFixture<DraggableSelectionComponent>;
let mockedGraphDataManager: MockedGraphDataManager;
let nodesStartEventEmitter: EventEmitter<DraggableStart<MapNode>>;
let nodesDragEventEmitter: EventEmitter<DraggableDrag<MapNode>>;
let nodesEndEventEmitter: EventEmitter<DraggableEnd<MapNode>>;
let drawingsStartEventEmitter: EventEmitter<DraggableStart<MapDrawing>>;
let drawingsDragEventEmitter: EventEmitter<DraggableDrag<MapDrawing>>;
let drawingsEndEventEmitter: EventEmitter<DraggableEnd<MapDrawing>>;
let labelStartEventEmitter: EventEmitter<DraggableStart<MapLabel>>;
let labelDragEventEmitter: EventEmitter<DraggableDrag<MapLabel>>;
let labelEndEventEmitter: EventEmitter<DraggableEnd<MapLabel>>;
let interfaceLabelStartEventEmitter: EventEmitter<DraggableStart<MapLinkNode>>;
let interfaceLabelDragEventEmitter: EventEmitter<DraggableDrag<MapLinkNode>>;
let interfaceLabelEndEventEmitter: EventEmitter<DraggableEnd<MapLinkNode>>;
beforeEach(async(() => {
mockedGraphDataManager = new MockedGraphDataManager();
nodesStartEventEmitter = new EventEmitter<DraggableStart<MapNode>>();
nodesDragEventEmitter = new EventEmitter<DraggableDrag<MapNode>>();
nodesEndEventEmitter = new EventEmitter<DraggableEnd<MapNode>>();
drawingsStartEventEmitter = new EventEmitter<DraggableStart<MapDrawing>>();
drawingsDragEventEmitter = new EventEmitter<DraggableDrag<MapDrawing>>();
drawingsEndEventEmitter = new EventEmitter<DraggableEnd<MapDrawing>>();
labelStartEventEmitter = new EventEmitter<DraggableStart<MapLabel>>();
labelDragEventEmitter = new EventEmitter<DraggableDrag<MapLabel>>();
labelEndEventEmitter = new EventEmitter<DraggableEnd<MapLabel>>();
interfaceLabelStartEventEmitter = new EventEmitter<DraggableStart<MapLinkNode>>();
interfaceLabelDragEventEmitter = new EventEmitter<DraggableDrag<MapLinkNode>>();
interfaceLabelEndEventEmitter = new EventEmitter<DraggableEnd<MapLinkNode>>();
const nodesWidgetStub = {
redrawNode: () => {},
draggable: {
start: nodesStartEventEmitter,
drag: nodesDragEventEmitter,
end: nodesEndEventEmitter
}
};
const drawingsWidgetStub = {
redrawDrawing: () => {},
draggable: {
start: drawingsStartEventEmitter,
drag: drawingsDragEventEmitter,
end: drawingsEndEventEmitter
}
};
const linksWidgetStub = {
redrawLink: () => {},
};
const labelWidgetStub = {
redrawLabel: () => {},
draggable: {
start: labelStartEventEmitter,
drag: labelDragEventEmitter,
end: labelEndEventEmitter
}
};
const interfaceLabelWidgetStub = {
draggable: {
start: interfaceLabelStartEventEmitter,
drag: interfaceLabelDragEventEmitter,
end: interfaceLabelEndEventEmitter
}
};
const nodesEventSourceStub = {
dragged: { emit: () => {}},
labelDragged: { emit: () => {}}
};
const drawingsEventSourceStub = {
dragged: { emit: () => {}}
};
const linksEventSourceStub = {
interfaceDragged: { emit: () => {}}
};
TestBed.configureTestingModule({
providers: [
{ provide: NodesWidget, useValue: nodesWidgetStub },
{ provide: DrawingsWidget, useValue: drawingsWidgetStub },
{ provide: LinksWidget, useValue: linksWidgetStub },
{ provide: LabelWidget, useValue: labelWidgetStub },
{ provide: InterfaceLabelWidget, useValue: interfaceLabelWidgetStub },
{ provide: SelectionManager, useValue: new SelectionManagerMock() },
{ provide: NodesEventSource, useValue: nodesEventSourceStub },
{ provide: DrawingsEventSource, useValue: drawingsEventSourceStub },
{ provide: GraphDataManager, useValue: mockedGraphDataManager },
{ provide: LinksEventSource, useValue: linksEventSourceStub },
],
declarations: [ DraggableSelectionComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DraggableSelectionComponent);
component = fixture.componentInstance;
component.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('nodes dragging', () => {
let nodesWidgetStub: NodesWidget;
let linksWidgetStub: LinksWidget;
let selectionManagerStub: SelectionManager;
let node: MapNode;
beforeEach(() => {
nodesWidgetStub = fixture.debugElement.injector.get(NodesWidget);
linksWidgetStub = fixture.debugElement.injector.get(LinksWidget);
selectionManagerStub = fixture.debugElement.injector.get(SelectionManager);
node = new MapNode();
node.id = "nodeid";
node.x = 1;
node.y = 2;
});
it('should select node when started dragging', fakeAsync(() => {
nodesWidgetStub.draggable.start.emit(new DraggableStart<MapNode>(node));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should ignore node when started dragging and node is in selection', fakeAsync(() => {
selectionManagerStub.setSelected([node]);
nodesWidgetStub.draggable.start.emit(new DraggableStart<MapNode>(node));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should update node position when dragging', fakeAsync(() => {
spyOn(nodesWidgetStub, 'redrawNode');
selectionManagerStub.setSelected([node]);
const dragEvent = new DraggableDrag<MapNode>(node);
dragEvent.dx = 10;
dragEvent.dy = 20;
nodesWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(nodesWidgetStub.redrawNode).toHaveBeenCalledWith(select(fixture.componentInstance.svg), node);
expect(node.x).toEqual(11);
expect(node.y).toEqual(22);
}));
it('should redraw related links target when dragging node', fakeAsync(() => {
spyOn(nodesWidgetStub, 'redrawNode');
spyOn(linksWidgetStub, 'redrawLink');
const link = new MapLink();
link.target = node;
mockedGraphDataManager.setLinks([link]);
selectionManagerStub.setSelected([node]);
nodesWidgetStub.draggable.drag.emit(new DraggableDrag<MapNode>(node));
tick();
expect(linksWidgetStub.redrawLink).toHaveBeenCalledWith(select(fixture.componentInstance.svg), link);
}));
it('should redraw related links source when dragging node', fakeAsync(() => {
spyOn(nodesWidgetStub, 'redrawNode');
spyOn(linksWidgetStub, 'redrawLink');
const link = new MapLink();
link.source = node;
mockedGraphDataManager.setLinks([link]);
selectionManagerStub.setSelected([node]);
nodesWidgetStub.draggable.drag.emit(new DraggableDrag<MapNode>(node));
tick();
expect(linksWidgetStub.redrawLink).toHaveBeenCalledWith(select(fixture.componentInstance.svg), link);
}));
it('should emit event when node stopped dragging', fakeAsync(() => {
const nodesEventSourceStub = fixture.debugElement.injector.get(NodesEventSource);
const spyDragged = spyOn(nodesEventSourceStub.dragged, 'emit');
selectionManagerStub.setSelected([node]);
const dragEvent = new DraggableEnd<MapNode>(node);
dragEvent.dx = 10;
dragEvent.dy = 20;
nodesWidgetStub.draggable.end.emit(dragEvent);
tick();
expect(nodesEventSourceStub.dragged.emit).toHaveBeenCalled();
expect(spyDragged.calls.mostRecent().args[0].datum).toEqual(node);
expect(spyDragged.calls.mostRecent().args[0].dx).toEqual(10);
expect(spyDragged.calls.mostRecent().args[0].dy).toEqual(20);
}));
});
describe('drawings dragging', () => {
let drawingsWidgetStub: DrawingsWidget;
let selectionManagerStub: SelectionManager;
let drawing: MapDrawing;
beforeEach(() => {
drawingsWidgetStub = fixture.debugElement.injector.get(DrawingsWidget);
selectionManagerStub = fixture.debugElement.injector.get(SelectionManager);
drawing = new MapDrawing();
drawing.id = "drawingid";
drawing.x = 1;
drawing.y = 2;
});
it('should select drawing when started dragging', fakeAsync(() => {
drawingsWidgetStub.draggable.start.emit(new DraggableStart<MapDrawing>(drawing));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should ignore drawing when started dragging and node is in selection', fakeAsync(() => {
selectionManagerStub.setSelected([drawing]);
drawingsWidgetStub.draggable.start.emit(new DraggableStart<MapDrawing>(drawing));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should update drawing position when dragging', fakeAsync(() => {
spyOn(drawingsWidgetStub, 'redrawDrawing');
selectionManagerStub.setSelected([drawing]);
const dragEvent = new DraggableDrag<MapDrawing>(drawing);
dragEvent.dx = 10;
dragEvent.dy = 20;
drawingsWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(drawingsWidgetStub.redrawDrawing).toHaveBeenCalledWith(select(fixture.componentInstance.svg), drawing);
expect(drawing.x).toEqual(11);
expect(drawing.y).toEqual(22);
}));
it('should emit event when drawing stopped dragging', fakeAsync(() => {
const drawingsEventSourceStub = fixture.debugElement.injector.get(DrawingsEventSource);
const spyDragged = spyOn(drawingsEventSourceStub.dragged, 'emit');
selectionManagerStub.setSelected([drawing]);
const dragEvent = new DraggableEnd<MapDrawing>(drawing);
dragEvent.dx = 10;
dragEvent.dy = 20;
drawingsWidgetStub.draggable.end.emit(dragEvent);
tick();
expect(drawingsEventSourceStub.dragged.emit).toHaveBeenCalled();
expect(spyDragged.calls.mostRecent().args[0].datum).toEqual(drawing);
expect(spyDragged.calls.mostRecent().args[0].dx).toEqual(10);
expect(spyDragged.calls.mostRecent().args[0].dy).toEqual(20);
}));
});
describe('labels dragging', () => {
let labelWidgetStub: LabelWidget;
let selectionManagerStub: SelectionManager;
let label: MapLabel;
beforeEach(() => {
labelWidgetStub = fixture.debugElement.injector.get(LabelWidget);
selectionManagerStub = fixture.debugElement.injector.get(SelectionManager);
label = new MapLabel();
label.id = "labelid";
label.x = 1;
label.y = 2;
});
it('should select label when started dragging', fakeAsync(() => {
labelWidgetStub.draggable.start.emit(new DraggableStart<MapLabel>(label));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should ignore label when started dragging and node is in selection', fakeAsync(() => {
selectionManagerStub.setSelected([label]);
labelWidgetStub.draggable.start.emit(new DraggableStart<MapLabel>(label));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should update label position when dragging', fakeAsync(() => {
spyOn(labelWidgetStub, 'redrawLabel');
selectionManagerStub.setSelected([label]);
const node = new MapNode();
node.id = "nodeid";
node.label = label;
label.nodeId = node.id;
mockedGraphDataManager.setNodes([node]);
const dragEvent = new DraggableDrag<MapLabel>(label);
dragEvent.dx = 10;
dragEvent.dy = 20;
labelWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(labelWidgetStub.redrawLabel).toHaveBeenCalledWith(select(fixture.componentInstance.svg), label);
expect(label.x).toEqual(11);
expect(label.y).toEqual(22);
}));
it('should not update label position when dragging and parent is selected', fakeAsync(() => {
spyOn(labelWidgetStub, 'redrawLabel');
const node = new MapNode();
node.id = "nodeid";
node.label = label;
label.nodeId = node.id;
selectionManagerStub.setSelected([label, node]);
mockedGraphDataManager.setNodes([node]);
const dragEvent = new DraggableDrag<MapLabel>(label);
dragEvent.dx = 10;
dragEvent.dy = 20;
labelWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(labelWidgetStub.redrawLabel).not.toHaveBeenCalled();
expect(label.x).toEqual(1);
expect(label.y).toEqual(2);
}));
it('should emit event when label stopped dragging', fakeAsync(() => {
const nodesEventSourceStub = fixture.debugElement.injector.get(NodesEventSource);
const spyDragged = spyOn(nodesEventSourceStub.labelDragged, 'emit');
selectionManagerStub.setSelected([label]);
const dragEvent = new DraggableEnd<MapLabel>(label);
dragEvent.dx = 10;
dragEvent.dy = 20;
labelWidgetStub.draggable.end.emit(dragEvent);
tick();
expect(nodesEventSourceStub.labelDragged.emit).toHaveBeenCalled();
expect(spyDragged.calls.mostRecent().args[0].datum).toEqual(label);
expect(spyDragged.calls.mostRecent().args[0].dx).toEqual(10);
expect(spyDragged.calls.mostRecent().args[0].dy).toEqual(20);
}));
it('should not emit event when label stopped dragging and parent node is selected', fakeAsync(() => {
const nodesEventSourceStub = fixture.debugElement.injector.get(NodesEventSource);
spyOn(nodesEventSourceStub.labelDragged, 'emit');
const node = new MapNode();
node.id = "nodeid";
label.nodeId = node.id;
selectionManagerStub.setSelected([label, node]);
const dragEvent = new DraggableEnd<MapLabel>(label);
dragEvent.dx = 10;
dragEvent.dy = 20;
labelWidgetStub.draggable.end.emit(dragEvent);
tick();
expect(nodesEventSourceStub.labelDragged.emit).not.toHaveBeenCalled();
}));
});
describe('interfaces labels dragging', () => {
let linksWidgetStub: LinksWidget;
let interfaceLabelWidgetStub: InterfaceLabelWidget;
let selectionManagerStub: SelectionManager;
let linkNode: MapLinkNode;
beforeEach(() => {
interfaceLabelWidgetStub = fixture.debugElement.injector.get(InterfaceLabelWidget);
linksWidgetStub = fixture.debugElement.injector.get(LinksWidget);
selectionManagerStub = fixture.debugElement.injector.get(SelectionManager);
linkNode = new MapLinkNode();
linkNode.label = new MapLabel();
linkNode.label.x = 1;
linkNode.label.y = 2;
linkNode.id = "linknodeid";
});
it('should select interface label when started dragging', fakeAsync(() => {
interfaceLabelWidgetStub.draggable.start.emit(new DraggableStart<MapLinkNode>(linkNode));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should ignore interface label when started dragging and node is in selection', fakeAsync(() => {
selectionManagerStub.setSelected([linkNode]);
interfaceLabelWidgetStub.draggable.start.emit(new DraggableStart<MapLinkNode>(linkNode));
tick();
expect(selectionManagerStub.getSelected().length).toEqual(1);
}));
it('should update interface label position when dragging first node', fakeAsync(() => {
spyOn(linksWidgetStub, 'redrawLink');
selectionManagerStub.setSelected([linkNode]);
const node = new MapNode();
node.id = "nodeid";
linkNode.nodeId = node.id;
const secondLinkNode = new MapLinkNode();
secondLinkNode.label = new MapLabel();
secondLinkNode.label.x = 1;
secondLinkNode.label.y = 2;
secondLinkNode.id = "secondlinknodeid";
const link = new MapLink();
link.nodes = [linkNode, secondLinkNode];
mockedGraphDataManager.setLinks([link]);
const dragEvent = new DraggableDrag<MapLinkNode>(linkNode);
dragEvent.dx = 10;
dragEvent.dy = 20;
interfaceLabelWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(linksWidgetStub.redrawLink).toHaveBeenCalledWith(select(fixture.componentInstance.svg), link);
expect(linkNode.label.x).toEqual(11);
expect(linkNode.label.y).toEqual(22);
}));
it('should update interface label position when dragging second node', fakeAsync(() => {
spyOn(linksWidgetStub, 'redrawLink');
selectionManagerStub.setSelected([linkNode]);
const node = new MapNode();
node.id = "nodeid";
linkNode.nodeId = node.id;
const secondLinkNode = new MapLinkNode();
secondLinkNode.label = new MapLabel();
secondLinkNode.label.x = 1;
secondLinkNode.label.y = 2;
secondLinkNode.id = "secondlinknodeid";
const link = new MapLink();
link.nodes = [secondLinkNode, linkNode];
mockedGraphDataManager.setLinks([link]);
const dragEvent = new DraggableDrag<MapLinkNode>(linkNode);
dragEvent.dx = 10;
dragEvent.dy = 20;
interfaceLabelWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(linksWidgetStub.redrawLink).toHaveBeenCalledWith(select(fixture.componentInstance.svg), link);
expect(linkNode.label.x).toEqual(11);
expect(linkNode.label.y).toEqual(22);
}));
it('should not update interface label position when dragging and parent node is selected', fakeAsync(() => {
spyOn(linksWidgetStub, 'redrawLink');
const node = new MapNode();
node.id = "nodeid";
linkNode.nodeId = node.id;
selectionManagerStub.setSelected([linkNode, node]);
const secondLinkNode = new MapLinkNode();
secondLinkNode.label = new MapLabel();
secondLinkNode.label.x = 1;
secondLinkNode.label.y = 2;
secondLinkNode.id = "secondlinknodeid";
const link = new MapLink();
link.nodes = [linkNode, secondLinkNode];
mockedGraphDataManager.setLinks([link]);
const dragEvent = new DraggableDrag<MapLinkNode>(linkNode);
dragEvent.dx = 10;
dragEvent.dy = 20;
interfaceLabelWidgetStub.draggable.drag.emit(dragEvent);
tick();
expect(linksWidgetStub.redrawLink).not.toHaveBeenCalled();
expect(linkNode.label.x).toEqual(1);
expect(linkNode.label.y).toEqual(2);
}));
it('should emit event when interface label stopped dragging', fakeAsync(() => {
const linksEventSourceStub = fixture.debugElement.injector.get(LinksEventSource);
const spyDragged = spyOn(linksEventSourceStub.interfaceDragged, 'emit');
selectionManagerStub.setSelected([linkNode]);
const dragEvent = new DraggableEnd<MapLinkNode>(linkNode);
dragEvent.dx = 10;
dragEvent.dy = 20;
interfaceLabelWidgetStub.draggable.end.emit(dragEvent);
tick();
expect(linksEventSourceStub.interfaceDragged.emit).toHaveBeenCalled();
expect(spyDragged.calls.mostRecent().args[0].datum).toEqual(linkNode);
expect(spyDragged.calls.mostRecent().args[0].dx).toEqual(10);
expect(spyDragged.calls.mostRecent().args[0].dy).toEqual(20);
}));
it('should not emit event when interface label stopped dragging and parent node is selected', fakeAsync(() => {
const linksEventSourceStub = fixture.debugElement.injector.get(LinksEventSource);
spyOn(linksEventSourceStub.interfaceDragged, 'emit');
const node = new MapNode();
node.id = "nodeid";
linkNode.nodeId = node.id;
selectionManagerStub.setSelected([linkNode, node]);
const dragEvent = new DraggableEnd<MapLinkNode>(linkNode);
dragEvent.dx = 10;
dragEvent.dy = 20;
interfaceLabelWidgetStub.draggable.end.emit(dragEvent);
tick();
expect(linksEventSourceStub.interfaceDragged.emit).not.toHaveBeenCalled();
}));
});
});

View File

@ -0,0 +1,191 @@
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 { MapLabel } from '../../models/map/map-label';
import { LabelWidget } from '../../widgets/label';
import { InterfaceLabelWidget } from '../../widgets/interface-label';
import { MapLinkNode } from '../../models/map/map-link-node';
import { LinksEventSource } from '../../events/links-event-source';
@Component({
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 interfaceWidget: InterfaceLabelWidget,
private selectionManager: SelectionManager,
private nodesEventSource: NodesEventSource,
private drawingsEventSource: DrawingsEventSource,
private graphDataManager: GraphDataManager,
private linksEventSource: LinksEventSource
) { }
ngOnInit() {
const svg = select(this.svg);
this.start = merge(
this.nodesWidget.draggable.start,
this.drawingsWidget.draggable.start,
this.labelWidget.draggable.start,
this.interfaceWidget.draggable.start
).subscribe((evt: DraggableStart<any>) => {
const selected = this.selectionManager.getSelected();
if (evt.datum instanceof MapNode) {
if (selected.filter((item) => item instanceof MapNode && item.id === evt.datum.id).length === 0) {
this.selectionManager.setSelected([evt.datum]);
}
}
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]);
}
}
if (evt.datum instanceof MapLinkNode) {
if (selected.filter((item) => item instanceof MapLinkNode && item.id === evt.datum.id).length === 0) {
this.selectionManager.setSelected([evt.datum]);
}
}
});
this.drag = merge(
this.nodesWidget.draggable.drag,
this.drawingsWidget.draggable.drag,
this.labelWidget.draggable.drag,
this.interfaceWidget.draggable.drag
).subscribe((evt: DraggableDrag<any>) => {
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 !== undefined && link.target.id === node.id) || (link.source !== undefined && 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);
});
// update interface labels
selected.filter((item) => item instanceof MapLinkNode).forEach((interfaceLabel: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === interfaceLabel.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
const link = this.graphDataManager.getLinks().filter((link) => link.nodes[0].id === interfaceLabel.id || link.nodes[1].id === interfaceLabel.id)[0];
if(link.nodes[0].id === interfaceLabel.id) {
link.nodes[0].label.x += evt.dx;
link.nodes[0].label.y += evt.dy;
}
if(link.nodes[1].id === interfaceLabel.id) {
link.nodes[1].label.x += evt.dx;
link.nodes[1].label.y += evt.dy;
}
this.linksWidget.redrawLink(svg, link);
});
});
this.end = merge(
this.nodesWidget.draggable.end,
this.drawingsWidget.draggable.end,
this.labelWidget.draggable.end,
this.interfaceWidget.draggable.end
).subscribe((evt: DraggableEnd<any>) => {
const selected = this.selectionManager.getSelected();
const selectedNodes = selected.filter((item) => item instanceof MapNode);
selectedNodes.forEach((item: MapNode) => {
this.nodesEventSource.dragged.emit(new DraggedDataEvent<MapNode>(item, evt.dx, evt.dy));
})
selected.filter((item) => item instanceof MapDrawing).forEach((item: MapDrawing) => {
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<MapDrawing>(item, evt.dx, evt.dy));
});
selected.filter((item) => item instanceof MapLabel).forEach((label: MapLabel) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
this.nodesEventSource.labelDragged.emit(new DraggedDataEvent<MapLabel>(label, evt.dx, evt.dy));
});
selected.filter((item) => item instanceof MapLinkNode).forEach((label: MapLinkNode) => {
const isParentNodeSelected = selectedNodes.filter((node) => node.id === label.nodeId).length > 0;
if (isParentNodeSelected) {
return;
}
this.linksEventSource.interfaceDragged.emit(new DraggedDataEvent<MapLinkNode>(label, evt.dx, evt.dy));
});
});
}
ngOnDestroy() {
this.start.unsubscribe();
this.drag.unsubscribe();
this.end.unsubscribe();
}
}

View File

@ -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<DraggableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DraggableComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DraggableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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:`<ng-content></ng-content>`,
styleUrls: ['./draggable.component.scss']
})
export class DraggableComponent implements OnInit, AfterViewInit, OnDestroy {
@Input('app-draggable') item: Point;
@Output() dragging = new EventEmitter<DraggableDraggedEvent>();
@Output() dragged = new EventEmitter<DraggableDraggedEvent>();
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();
}
}

View File

@ -0,0 +1,32 @@
<svg:g
class="drawing"
[attr.transform]="transformation"
[app-draggable]="drawing"
(dragging)="OnDragging($event)"
(dragged)="OnDragged($event)"
>
<svg:g
*ngIf="is(drawing.element, 'ellipse')"
[app-ellipse]="drawing.element"
/>
<svg:g
*ngIf="is(drawing.element, 'image')"
[app-image]="drawing.element"
/>
<svg:g
*ngIf="is(drawing.element, 'line')"
[app-line]="drawing.element"
/>
<svg:g
*ngIf="is(drawing.element, 'rect')"
[app-rect]="drawing.element"
/>
<svg:g
*ngIf="is(drawing.element, 'text')"
[app-text]="drawing.element"
/>
</svg:g>

After

Width:  |  Height:  |  Size: 618 B

View File

@ -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<DrawingComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DrawingComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DrawingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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<MapDrawing>(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})`;
}
}

View File

@ -0,0 +1,12 @@
<svg:ellipse
class="ellipse_element noselect"
[attr.fill]="ellipse.fill"
[attr.fill-opacity]="fill_opacity"
[attr.stroke]="ellipse.stroke"
[attr.stroke-width]="stroke_width"
[attr.stroke-dasharray]="stroke_dasharray"
[attr.cx]="ellipse.cx"
[attr.cy]="ellipse.cy"
[attr.rx]="ellipse.rx"
[attr.ry]="ellipse.ry"
/>

After

Width:  |  Height:  |  Size: 333 B

View File

@ -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<EllipseComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EllipseComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EllipseComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
<svg:image
class="image_element noselect"
[attr.xlink:href]="image.data"
[attr.width]="image.width"
[attr.height]="image.height"
/>

After

Width:  |  Height:  |  Size: 140 B

View File

@ -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<ImageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ImageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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() {
}
}

View File

@ -0,0 +1,10 @@
<svg:line
class="line_element noselect"
[attr.stroke]="line.stroke"
[attr.stroke-width]="stroke_width"
[attr.stroke-dasharray]="stroke_dasharray"
[attr.x1]="line.x1"
[attr.x2]="line.x2"
[attr.y1]="line.y1"
[attr.y2]="line.y2"
/>

After

Width:  |  Height:  |  Size: 245 B

View File

@ -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<LineComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LineComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LineComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
<svg:rect
class="rect_element noselect"
[attr.fill]="rect.fill"
[attr.fill-opacity]="fill_opacity"
[attr.stroke]="rect.stroke"
[attr.stroke-width]="stroke_width"
[attr.stroke-dasharray]="stroke_dasharray"
[attr.width]="rect.width"
[attr.height]="rect.height"
/>

After

Width:  |  Height:  |  Size: 278 B

View File

@ -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<RectComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RectComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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;
}
}

View File

@ -0,0 +1,14 @@
<svg:text #text
class="text_element noselect"
[attr.style]="style"
[attr.text-decoration]="textDecoration"
[attr.fill]="text.fill"
[attr.transform]="transformation"
>
<svg:tspan
*ngFor="let line of lines; index as i"
xml:space="preserve"
x="0"
[attr.dy]="i == 0 ? '0em' : '1.4em'"
>{{line}}</svg:tspan>
</svg:text>

After

Width:  |  Height:  |  Size: 344 B

View File

@ -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<TextComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TextComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TextComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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/)
}
}

View File

@ -0,0 +1,41 @@
<svg #svg
class="map"
preserveAspectRatio="none"
[attr.width]="width"
[attr.height]="height"
>
<g [attr.transform]="transform">
<g *ngFor="let layer of layers">
<g class="links">
<g
*ngFor="let link of layer.links"
[app-link]="link"
[show-interface-labels]="settings.show_interface_labels"
></g>
<!-- [node-changed]="nodeChanged" -->
</g>
<g class="nodes">
<g
*ngFor="let node of layer.nodes"
[app-node]="node"
[symbols]="symbols"
></g>
<!-- [node-changed]="nodeChanged"
(valueChange)="onNodeChanged($event)" -->
</g>
<g class="drawings">
<g
*ngFor="let drawing of layer.drawings"
[app-drawing]="drawing" >
</g>
</g>
</g>
</g>
<g [app-selection]="svg"></g>
<!-- (selected)="onSelection($event)" -->
<filter id="grayscale">
<feColorMatrix id="feGrayscale" type="saturate" values="0"/>
</filter>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
svg {
display: block;
}

View File

@ -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<MapComponent>;
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// declarations: [ MapComponent ]
// })
// .compileComponents();
// }));
// // beforeEach(() => {
// // fixture = TestBed.createComponent(MapComponent);
// // component = fixture.componentInstance;
// // fixture.detectChanges();
// // });
// //
// // it('should create', () => {
// // expect(component).toBeTruthy();
// // });
// });
// //

View File

@ -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<any>;
// @Input('node-updated') nodeUpdated: EventEmitter<any>;
@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) {
}
}

View File

@ -0,0 +1,28 @@
<svg:g
class="text_container"
[attr.transform]="transform"
width="100"
height="100"
>
<svg:rect
stroke-dasharray="3,3"
stroke-width="0.5"
fill="none"
stroke="black"
[attr.x]="rectX"
[attr.y]="rectY"
[attr.width]="rectWidth"
[attr.height]="rectHeight"
/>
<svg:text
#textSvg
class="interface_label"
[attr.style]="sanitizedStyle"
[attr.x]="borderSize"
[attr.y]="-borderSize"
>
{{ text }}
</svg:text>
</svg:g>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -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<InterfaceLabelComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ InterfaceLabelComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(InterfaceLabelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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})`;
}
}

View File

@ -0,0 +1,59 @@
<svg:g
class="link"
[attr.link_id]="link.id"
[attr.map-source]="link.source.id"
[attr.map-target]="link.target.id"
[attr.transform]="transform"
>
<svg:path #path
*ngIf="link.linkType == 'ethernet'"
class="ethernet_link"
stroke="#000"
stroke-width="2"
[attr.d]="d"
/>
<svg:path #path
*ngIf="link.linkType == 'serial'"
class="serial_link"
stroke="#B22222"
fill="none"
stroke-width="2"
[attr.d]="d"
/>
<svg:g
[app-status]="link.source.status"
[direction]="'source'"
[path]="path"
[d]="d"
/>
<svg:g
[app-status]="link.target.status"
[direction]="'target'"
[path]="path"
[d]="d"
/>
<svg:g
*ngIf="showInterfaceLabels"
[app-interface-label]
[x]="link.source.x+link.nodes[0].label.x"
[y]="link.source.y+link.nodes[0].label.y"
[text]="link.nodes[0].label.text"
[style]="link.nodes[0].label.style"
[rotation]="link.nodes[0].label.rotation"
/>
<svg:g
*ngIf="showInterfaceLabels"
[app-interface-label]
[x]="link.target.x+link.nodes[1].label.x"
[y]="link.target.y+link.nodes[1].label.y"
[text]="link.nodes[1].label.text"
[style]="link.nodes[1].label.style"
[rotation]="link.nodes[1].label.rotation"
/>
</svg:g>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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<LinkComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LinkComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LinkComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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<Node>;
@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);
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,5 @@
import { MapLink } from "../../../../models/map/map-link";
export interface LinkStrategy {
d(link: MapLink): string;
}

View File

@ -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();
}
}

View File

@ -0,0 +1,25 @@
<svg:g
class="node"
[attr.transform]="'translate(' + node.x + ',' + node.y + ')'"
>
<svg:image
#image
[attr.width]="node.width"
[attr.height]="node.height"
[attr.x]="0"
[attr.y]="0"
[attr.xlink:href]="symbol"
[app-draggable]="node"
(dragging)="OnDragging($event)"
(dragged)="OnDragged($event)"
/>
<svg:text
#label
class="label"
[attr.style]="label_style"
[attr.x]="label_x"
[attr.y]="label_y"
>
{{ node.label.text }}
</svg:text>
</svg:g>

After

Width:  |  Height:  |  Size: 513 B

View File

@ -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<NodeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NodeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NodeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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<Node>;
// @Output() valueChange = new EventEmitter<Node>();
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<MapNode>(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;
}
}

View File

@ -0,0 +1,10 @@
<svg:g
class="selection-line-tool"
>
<svg:path
class="selection"
*ngIf="visible"
[attr.d]="d"
>
</svg:path>
</svg:g>

After

Width:  |  Height:  |  Size: 137 B

View File

@ -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<SelectionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SelectionComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SelectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -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<Rectangle>();
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));
}
}

View File

@ -0,0 +1,21 @@
<svg:g *ngIf="status && point && direction">
<svg:circle
*ngIf="status == 'started'"
class="status_started"
[attr.cx]="point.x"
[attr.cy]="point.y"
r="6"
fill="#2ecc71"
/>
<svg:rect
*ngIf="status == 'stopped'"
class="status_stopped"
[attr.x]="point.x - 10/2"
[attr.y]="point.y - 10/2"
width="10"
height="10"
r="6"
fill="red"
/>
</svg:g>

After

Width:  |  Height:  |  Size: 401 B

View File

@ -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<StatusComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StatusComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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;
}
}

View File

@ -1,10 +0,0 @@
<svg
class="map"
preserveAspectRatio="none"
>
<filter id="grayscale">
<feColorMatrix id="feGrayscale" type="saturate" values="0"/>
</filter>
</svg>
<app-draw-link-tool *ngIf="drawLinkTool"></app-draw-link-tool>

Before

Width:  |  Height:  |  Size: 235 B

View File

@ -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);
}));
});

View File

@ -0,0 +1,86 @@
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 selectedInterfacesLabelsSources = this.graphDataManager.getLinks().filter((link) => {
if (link.source === undefined || link.nodes.length != 2 || link.nodes[0].label === undefined) {
return false;
}
const interfaceLabelX = link.source.x + link.nodes[0].label.x;
const interfaceLabelY = link.source.y + link.nodes[0].label.y;
return this.inRectangleHelper.inRectangle(rectangle, interfaceLabelX, interfaceLabelY);
}).map((link) => link.nodes[0]);
const selectedInterfacesLabelsTargets = this.graphDataManager.getLinks().filter((link) => {
if (link.target === undefined || link.nodes.length != 2 || link.nodes[1].label === undefined) {
return false;
}
const interfaceLabelX = link.target.x + link.nodes[1].label.x;
const interfaceLabelY = link.target.y + link.nodes[1].label.y;
return this.inRectangleHelper.inRectangle(rectangle, interfaceLabelX, interfaceLabelY);
}).map((link) => link.nodes[1]);
const selectedInterfaces = [
...selectedInterfacesLabelsSources,
...selectedInterfacesLabelsTargets,
]
const selected = [
...selectedNodes,
...selectedLinks,
...selectedDrawings,
...selectedLabels,
...selectedInterfaces,
];
this.selectionManager.setSelected(selected);
});
}
ngOnDestroy() {
this.onSelection.unsubscribe();
}
}

View File

@ -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<SelectionSelectComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SelectionSelectComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SelectionSelectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -1,21 +1,23 @@
import { Injectable } from "@angular/core"; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from "rxjs"; import { Subscription } from 'rxjs';
import { MapChangeDetectorRef } from "../services/map-change-detector-ref"; import { SelectionManager } from '../../managers/selection-manager';
import { SelectionManager } from "../managers/selection-manager"; import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
@Component({
@Injectable() selector: 'app-selection-select',
export class SelectionUpdateListener { templateUrl: './selection-select.component.html',
styleUrls: ['./selection-select.component.scss']
})
export class SelectionSelectComponent implements OnInit, OnDestroy {
private onSelected: Subscription; private onSelected: Subscription;
private onUnselected: Subscription; private onUnselected: Subscription;
constructor( constructor(
private selectionManager: SelectionManager, private selectionManager: SelectionManager,
private mapChangeDetectorRef: MapChangeDetectorRef private mapChangeDetectorRef: MapChangeDetectorRef
) { ) { }
}
public onInit(svg: any) { ngOnInit() {
this.onSelected = this.selectionManager.selected.subscribe(() => { this.onSelected = this.selectionManager.selected.subscribe(() => {
this.mapChangeDetectorRef.detectChanges(); this.mapChangeDetectorRef.detectChanges();
}); });
@ -24,8 +26,9 @@ export class SelectionUpdateListener {
}); });
} }
public onDestroy() { ngOnDestroy() {
this.onSelected.unsubscribe(); this.onSelected.unsubscribe();
this.onUnselected.unsubscribe(); this.onUnselected.unsubscribe();
} }
}
}

View File

@ -7,18 +7,18 @@ import { MapDrawing } from "../../models/map/map-drawing";
@Injectable() @Injectable()
export class DrawingToMapDrawingConverter implements Converter<Drawing, MapDrawing> { export class DrawingToMapDrawingConverter implements Converter<Drawing, MapDrawing> {
constructor( constructor(
) {} ) {}
convert(drawing: Drawing) { convert(drawing: Drawing) {
const mapDrawing = new MapDrawing(); const mapDrawing = new MapDrawing();
mapDrawing.id = drawing.drawing_id; mapDrawing.id = drawing.drawing_id;
mapDrawing.projectId = drawing.project_id; mapDrawing.projectId = drawing.project_id;
mapDrawing.rotation = drawing.rotation; mapDrawing.rotation = drawing.rotation;
mapDrawing.svg = drawing.svg; mapDrawing.svg = drawing.svg;
mapDrawing.x = drawing.x; mapDrawing.x = drawing.x;
mapDrawing.y = drawing.y; mapDrawing.y = drawing.y;
mapDrawing.z = drawing.z; mapDrawing.z = drawing.z;
return mapDrawing; return mapDrawing;
} }
} }

View File

@ -3,17 +3,45 @@ import { Injectable } from "@angular/core";
import { Converter } from "../converter"; import { Converter } from "../converter";
import { Label } from "../../models/label"; import { Label } from "../../models/label";
import { MapLabel } from "../../models/map/map-label"; import { MapLabel } from "../../models/map/map-label";
import { FontBBoxCalculator } from '../../helpers/font-bbox-calculator';
import { CssFixer } from '../../helpers/css-fixer';
import { FontFixer } from '../../helpers/font-fixer';
@Injectable() @Injectable()
export class LabelToMapLabelConverter implements Converter<Label, MapLabel> { export class LabelToMapLabelConverter implements Converter<Label, MapLabel> {
convert(label: Label) { constructor(
const mapLabel = new MapLabel(); private fontBBoxCalculator: FontBBoxCalculator,
mapLabel.rotation = label.rotation; private cssFixer: CssFixer,
mapLabel.style = label.style; private fontFixer: FontFixer
mapLabel.text = label.text; ) {}
mapLabel.x = label.x; convert(label: Label, paramaters?: {[node_id: string]: string}) {
mapLabel.y = label.y; const mapLabel = new MapLabel();
return mapLabel; mapLabel.rotation = label.rotation;
mapLabel.style = label.style;
mapLabel.text = label.text;
mapLabel.x = label.x;
mapLabel.y = label.y;
mapLabel.originalX = label.x;
mapLabel.originalY = label.y;
if (paramaters !== undefined) {
mapLabel.id = paramaters.node_id;
mapLabel.nodeId = paramaters.node_id;
} }
const fixedCss = this.cssFixer.fix(mapLabel.style);
const fixedFont = this.fontFixer.fixStyles(fixedCss);
const box = this.fontBBoxCalculator.calculate(mapLabel.text, fixedFont);
if (mapLabel.x !== null) {
mapLabel.x += 3;
}
if (mapLabel.y !== null) {
mapLabel.y += box.height;
}
return mapLabel;
}
} }

View File

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

View File

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

View File

@ -3,17 +3,33 @@ import { Injectable } from "@angular/core";
import { Converter } from "../converter"; import { Converter } from "../converter";
import { Label } from "../../models/label"; import { Label } from "../../models/label";
import { MapLabel } from "../../models/map/map-label"; import { MapLabel } from "../../models/map/map-label";
import { FontBBoxCalculator } from '../../helpers/font-bbox-calculator';
@Injectable() @Injectable()
export class MapLabelToLabelConverter implements Converter<MapLabel, Label> { export class MapLabelToLabelConverter implements Converter<MapLabel, Label> {
convert(mapLabel: MapLabel) { constructor(
const label = new Label(); private fontBBoxCalculator: FontBBoxCalculator
label.rotation = mapLabel.rotation; ) {}
label.style = mapLabel.style;
label.text = mapLabel.text; convert(mapLabel: MapLabel) {
label.x = mapLabel.x; const box = this.fontBBoxCalculator.calculate(mapLabel.text, mapLabel.style);
label.y = mapLabel.y;
return label; const label = new Label();
label.rotation = mapLabel.rotation;
label.style = mapLabel.style;
label.text = mapLabel.text;
label.x = mapLabel.x;
label.y = mapLabel.y;
if (label.x !== null) {
label.x += 3;
} }
if (label.y !== null) {
label.y -= box.height;
}
return label;
}
} }

View File

@ -5,38 +5,41 @@ import { MapNode } from "../../models/map/map-node";
import { MapLabelToLabelConverter } from "./map-label-to-label-converter"; import { MapLabelToLabelConverter } from "./map-label-to-label-converter";
import { MapPortToPortConverter } from "./map-port-to-port-converter"; import { MapPortToPortConverter } from "./map-port-to-port-converter";
import { Node } from "../../models/node"; import { Node } from "../../models/node";
import { FontBBoxCalculator } from '../../helpers/font-bbox-calculator';
import { CssFixer } from '../../helpers/css-fixer';
import { FontFixer } from '../../helpers/font-fixer';
@Injectable() @Injectable()
export class MapNodeToNodeConverter implements Converter<MapNode, Node> { export class MapNodeToNodeConverter implements Converter<MapNode, Node> {
constructor( constructor(
private mapLabelToLabel: MapLabelToLabelConverter, private mapLabelToLabel: MapLabelToLabelConverter,
private mapPortToPort: MapPortToPortConverter private mapPortToPort: MapPortToPortConverter
) {} ) {}
convert(mapNode: MapNode) { convert(mapNode: MapNode) {
const node = new Node(); const node = new Node();
node.node_id = mapNode.id; node.node_id = mapNode.id;
node.command_line = mapNode.commandLine; node.command_line = mapNode.commandLine;
node.compute_id = mapNode.computeId; node.compute_id = mapNode.computeId;
node.console = mapNode.console; node.console = mapNode.console;
node.console_host = mapNode.consoleHost; node.console_host = mapNode.consoleHost;
node.first_port_name = mapNode.firstPortName; node.first_port_name = mapNode.firstPortName;
node.height = mapNode.height; node.height = mapNode.height;
node.label = mapNode.label ? this.mapLabelToLabel.convert(mapNode.label) : undefined; node.label = mapNode.label ? this.mapLabelToLabel.convert(mapNode.label) : undefined;
node.name = mapNode.name; node.name = mapNode.name;
node.node_directory = mapNode.nodeDirectory; node.node_directory = mapNode.nodeDirectory;
node.node_type = mapNode.nodeType; node.node_type = mapNode.nodeType;
node.port_name_format = mapNode.portNameFormat; node.port_name_format = mapNode.portNameFormat;
node.port_segment_size = mapNode.portSegmentSize; node.port_segment_size = mapNode.portSegmentSize;
node.ports = mapNode.ports ? mapNode.ports.map((mapPort) => this.mapPortToPort.convert(mapPort)) : []; node.ports = mapNode.ports ? mapNode.ports.map((mapPort) => this.mapPortToPort.convert(mapPort)) : [];
node.project_id = mapNode.projectId; node.project_id = mapNode.projectId;
node.status = mapNode.status; node.status = mapNode.status;
node.symbol = mapNode.symbol; node.symbol = mapNode.symbol;
node.width = mapNode.width; node.width = mapNode.width;
node.x = mapNode.x; node.x = mapNode.x;
node.y = mapNode.y; node.y = mapNode.y;
node.z = mapNode.z; node.z = mapNode.z;
return node; return node;
} }
} }

View File

@ -5,38 +5,56 @@ import { MapNode } from "../../models/map/map-node";
import { Node } from "../../models/node"; import { Node } from "../../models/node";
import { LabelToMapLabelConverter } from "./label-to-map-label-converter"; import { LabelToMapLabelConverter } from "./label-to-map-label-converter";
import { PortToMapPortConverter } from "./port-to-map-port-converter"; import { PortToMapPortConverter } from "./port-to-map-port-converter";
import { FontBBoxCalculator } from '../../helpers/font-bbox-calculator';
import { CssFixer } from '../../helpers/css-fixer';
import { FontFixer } from '../../helpers/font-fixer';
@Injectable() @Injectable()
export class NodeToMapNodeConverter implements Converter<Node, MapNode> { export class NodeToMapNodeConverter implements Converter<Node, MapNode> {
constructor( constructor(
private labelToMapLabel: LabelToMapLabelConverter, private labelToMapLabel: LabelToMapLabelConverter,
private portToMapPort: PortToMapPortConverter private portToMapPort: PortToMapPortConverter,
) {} private fontBBoxCalculator: FontBBoxCalculator,
private cssFixer: CssFixer,
private fontFixer: FontFixer
) {}
convert(node: Node) { convert(node: Node) {
const mapNode = new MapNode(); const mapNode = new MapNode();
mapNode.id = node.node_id; mapNode.id = node.node_id;
mapNode.commandLine = node.command_line; mapNode.commandLine = node.command_line;
mapNode.computeId = node.compute_id; mapNode.computeId = node.compute_id;
mapNode.console = node.console; mapNode.console = node.console;
mapNode.consoleHost = node.console_host; mapNode.consoleHost = node.console_host;
mapNode.firstPortName = node.first_port_name; mapNode.firstPortName = node.first_port_name;
mapNode.height = node.height; 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.name = node.name;
mapNode.nodeDirectory = node.node_directory; mapNode.nodeDirectory = node.node_directory;
mapNode.nodeType = node.node_type; mapNode.nodeType = node.node_type;
mapNode.portNameFormat = node.port_name_format; mapNode.portNameFormat = node.port_name_format;
mapNode.portSegmentSize = node.port_segment_size; mapNode.portSegmentSize = node.port_segment_size;
mapNode.ports = node.ports.map((port) => this.portToMapPort.convert(port)); mapNode.ports = node.ports.map((port) => this.portToMapPort.convert(port));
mapNode.projectId = node.project_id; mapNode.projectId = node.project_id;
mapNode.status = node.status; mapNode.status = node.status;
mapNode.symbol = node.symbol; mapNode.symbol = node.symbol;
mapNode.width = node.width; mapNode.width = node.width;
mapNode.x = node.x; mapNode.x = node.x;
mapNode.y = node.y; mapNode.y = node.y;
mapNode.z = node.z; mapNode.z = node.z;
return mapNode;
if (mapNode.label !== undefined) {
const fixedCss = this.cssFixer.fix(mapNode.label.style);
const fixedFont = this.fontFixer.fixStyles(fixedCss);
const box = this.fontBBoxCalculator.calculate(mapNode.label.text, fixedFont);
if (node.label.x === null || node.label.y === null) {
mapNode.label.x = node.width / 2. - box.width / 2. + 3;
mapNode.label.y = -8;
}
} }
return mapNode;
}
} }

View File

@ -0,0 +1,24 @@
import { Font } from "../models/font";
import { StylesToFontConverter } from './styles-to-font-converter';
describe('StylesToFontConverter', () => {
let converter: StylesToFontConverter;
beforeEach(() => {
converter = new StylesToFontConverter();
});
it('should parse fonts from styles', () => {
const styles = "font-family: TypeWriter; font-size: 10px; font-weight: bold";
const expectedFont: Font = {
'font_family': 'TypeWriter',
'font_size': 10,
'font_weight': 'bold'
};
expect(converter.convert(styles)).toEqual(expectedFont);
});
});

View File

@ -0,0 +1,47 @@
import * as csstree from 'css-tree';
import { Injectable } from "@angular/core";
import { Converter } from './converter';
import { Font } from '../models/font';
@Injectable()
export class StylesToFontConverter implements Converter<string, Font> {
convert(styles: string) {
const font: Font = {
'font_family': undefined,
'font_size': undefined,
'font_weight': undefined
};
const ast = csstree.parse(styles, {
'context': 'declarationList'
});
ast.children.forEach((child) => {
if (child.property === 'font-size') {
child.value.children.forEach((value) => {
if (value.type === 'Dimension') {
font.font_size = parseInt(value.value);
}
});
}
if (child.property === 'font-family') {
child.value.children.forEach((value) => {
if (value.type === "Identifier") {
font.font_family = value.name;
}
});
}
if (child.property === 'font-weight') {
child.value.children.forEach((value) => {
if (value.type === "Identifier") {
font.font_weight = value.name;
}
});
}
});
return font;
}
}

View File

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

View File

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

View File

@ -7,3 +7,11 @@ export class DataEventSource<T> {
} }
export class DraggedDataEvent<T> extends DataEventSource<T> {} export class DraggedDataEvent<T> extends DataEventSource<T> {}
export class ClickedDataEvent<T> {
constructor(
public datum: T,
public x: number,
public y: number
) {}
}

View File

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

View File

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

View File

@ -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<Rectangle>();
}

View File

@ -6,7 +6,7 @@ import { Injectable } from "@angular/core";
@Injectable() @Injectable()
export class CssFixer { export class CssFixer {
public fix(styles: string) { public fix(styles: string): string {
const ast = csstree.parse(styles, { const ast = csstree.parse(styles, {
'context': 'declarationList' 'context': 'declarationList'
}); });

View File

@ -0,0 +1,24 @@
import { FontBBoxCalculator } from "./font-bbox-calculator";
describe('FontBBoxCalculator', () => {
let calculator: FontBBoxCalculator;
beforeEach(() => {
calculator = new FontBBoxCalculator();
});
it('should calculate font width and height', () => {
const box = calculator.calculate("My text", "font-family:Arial; font-size: 12px; font-weight:bold");
expect(box.height).toEqual(14);
expect(box.width).toEqual(41.34375);
});
it('should calculate font width and height for different font', () => {
const box = calculator.calculate("My text", "font-family:Tahoma; font-size: 14px; font-weight:bold");
expect(box.height).toEqual(15);
expect(box.width).toEqual(46.25);
});
});

View File

@ -0,0 +1,20 @@
import { Injectable } from "@angular/core";
@Injectable()
export class FontBBoxCalculator {
calculate(text: string, styles: string) {
const element = document.createElement("text");
element.innerText = text;
element.setAttribute("fill", "#00000");
element.setAttribute("fill-opacity", "0");
element.setAttribute("style", styles);
document.documentElement.appendChild(element);
const bbox = element.getBoundingClientRect();
document.documentElement.removeChild(element);
return {
width: bbox.width,
height: bbox.height
}
}
}

View File

@ -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<any>) => {
const selected = this.selectionManager.getSelected();
if (evt.datum instanceof MapNode) {
if (selected.filter((item) => item instanceof MapNode && item.id === evt.datum.id).length === 0) {
this.selectionManager.setSelected([evt.datum]);
}
}
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<any>) => {
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<any>) => {
const selected = this.selectionManager.getSelected();
selected.filter((item) => item instanceof MapNode).forEach((item: MapNode) => {
this.nodesEventSource.dragged.emit(new DraggedDataEvent<MapNode>(item, evt.dx, evt.dy));
})
selected.filter((item) => item instanceof MapDrawing).forEach((item: MapDrawing) => {
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<MapDrawing>(item, evt.dx, evt.dy));
});
});
}
public onDestroy() {
this.start.unsubscribe();
this.drag.unsubscribe();
this.end.unsubscribe();
}
}

View File

@ -1,4 +0,0 @@
export interface MapListener {
onInit(svg: any);
onDestroy();
}

View File

@ -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();
});
}
}

Some files were not shown because too many files have changed in this diff Show More