Change the way of drawing interfaces

This commit is contained in:
ziajka
2018-04-17 13:47:18 +02:00
parent 03b61b371a
commit 384a51ccea
11 changed files with 145 additions and 55 deletions

View File

@ -1,29 +1,22 @@
import { InRectangleHelper } from "./in-rectangle-helper"; import { InRectangleHelper } from "./in-rectangle-helper";
import { Selectable } from "../../shared/managers/selection-manager";
import { Rectangle } from "../../shared/models/rectangle"; import { Rectangle } from "../../shared/models/rectangle";
class ExampleNode implements Selectable {
constructor(public x: number, public y: number, public is_selected: boolean) {}
}
describe('InRectangleHelper', () => { describe('InRectangleHelper', () => {
let inRectangleHelper: InRectangleHelper; let inRectangleHelper: InRectangleHelper;
let node: Selectable;
beforeEach(() => { beforeEach(() => {
inRectangleHelper = new InRectangleHelper(); inRectangleHelper = new InRectangleHelper();
}); });
it('should be in rectangle', () => { it('should be in rectangle', () => {
node = new ExampleNode(100, 100, false); const isIn = inRectangleHelper.inRectangle(new Rectangle(10, 10, 150, 150), 100, 100);
const isIn = inRectangleHelper.inRectangle(node, new Rectangle(10, 10, 150, 150));
expect(isIn).toBeTruthy(); expect(isIn).toBeTruthy();
}); });
it('should be outside rectangle', () => { it('should be outside rectangle', () => {
node = new ExampleNode(100, 100, false); const isIn = inRectangleHelper.inRectangle(new Rectangle(10, 10, 50, 50), 100, 100);
const isIn = inRectangleHelper.inRectangle(node, new Rectangle(10, 10, 50, 50));
expect(isIn).toBeFalsy(); expect(isIn).toBeFalsy();
}); });
}); });

View File

@ -6,8 +6,8 @@ import { Rectangle } from "../../shared/models/rectangle";
@Injectable() @Injectable()
export class InRectangleHelper { export class InRectangleHelper {
public inRectangle(item: Selectable, rectangle: Rectangle): boolean { public inRectangle(rectangle: Rectangle, x: number, y: number): boolean {
return (rectangle.x <= item.x && item.x < (rectangle.x + rectangle.width) return (rectangle.x <= x && x < (rectangle.x + rectangle.width)
&& rectangle.y <= item.y && item.y < (rectangle.y + rectangle.height)); && rectangle.y <= y && y < (rectangle.y + rectangle.height));
} }
} }

View File

@ -7,6 +7,7 @@ import { SelectionManager } from "./selection-manager";
import { NodesDataSource } from "../datasources/nodes-datasource"; import { NodesDataSource } from "../datasources/nodes-datasource";
import { LinksDataSource } from "../datasources/links-datasource"; import { LinksDataSource } from "../datasources/links-datasource";
import { InRectangleHelper } from "../../map/helpers/in-rectangle-helper"; import { InRectangleHelper } from "../../map/helpers/in-rectangle-helper";
import { DrawingsDataSource } from "../datasources/drawings-datasource";
describe('SelectionManager', () => { describe('SelectionManager', () => {
@ -16,13 +17,14 @@ describe('SelectionManager', () => {
beforeEach(() => { beforeEach(() => {
const linksDataSource = new LinksDataSource(); const linksDataSource = new LinksDataSource();
const drawingsDataSource = new DrawingsDataSource();
const inRectangleHelper = new InRectangleHelper(); const inRectangleHelper = new InRectangleHelper();
selectedRectangleSubject = new Subject<Rectangle>(); selectedRectangleSubject = new Subject<Rectangle>();
nodesDataSource = new NodesDataSource(); nodesDataSource = new NodesDataSource();
manager = new SelectionManager(nodesDataSource, linksDataSource, inRectangleHelper); manager = new SelectionManager(nodesDataSource, linksDataSource, drawingsDataSource, inRectangleHelper);
manager.subscribe(selectedRectangleSubject); manager.subscribe(selectedRectangleSubject);
const node_1 = new Node(); const node_1 = new Node();

View File

@ -10,6 +10,9 @@ import { InRectangleHelper } from "../../map/helpers/in-rectangle-helper";
import { Rectangle } from "../models/rectangle"; import { Rectangle } from "../models/rectangle";
import { Link} from "../models/link"; import { Link} from "../models/link";
import { DataSource } from "../datasources/datasource"; import { DataSource } from "../datasources/datasource";
import { Drawing } from "../models/drawing";
import { InterfaceLabel } from "../models/interface-label";
import { DrawingsDataSource } from "../datasources/drawings-datasource";
export interface Selectable { export interface Selectable {
@ -22,10 +25,14 @@ export interface Selectable {
export class SelectionManager { export class SelectionManager {
private selectedNodes: Node[] = []; private selectedNodes: Node[] = [];
private selectedLinks: Link[] = []; private selectedLinks: Link[] = [];
private selectedDrawings: Drawing[] = [];
private selectedInterfaceLabels: InterfaceLabel[] = [];
private subscription: Subscription; private subscription: Subscription;
constructor(private nodesDataSource: NodesDataSource, constructor(private nodesDataSource: NodesDataSource,
private linksDataSource: LinksDataSource, private linksDataSource: LinksDataSource,
private drawingsDataSource: DrawingsDataSource,
private inRectangleHelper: InRectangleHelper) {} private inRectangleHelper: InRectangleHelper) {}
@ -33,6 +40,8 @@ export class SelectionManager {
this.subscription = subject.subscribe((rectangle: Rectangle) => { this.subscription = subject.subscribe((rectangle: Rectangle) => {
this.selectedNodes = this.getSelectedItemsInRectangle<Node>(this.nodesDataSource, rectangle); this.selectedNodes = this.getSelectedItemsInRectangle<Node>(this.nodesDataSource, rectangle);
this.selectedLinks = this.getSelectedItemsInRectangle<Link>(this.linksDataSource, rectangle); this.selectedLinks = this.getSelectedItemsInRectangle<Link>(this.linksDataSource, rectangle);
this.selectedDrawings = this.getSelectedItemsInRectangle<Drawing>(this.drawingsDataSource, rectangle);
this.selectedInterfaceLabels = this.getSelectedInterfaceLabelsInRectangle(rectangle);
}); });
return this.subscription; return this.subscription;
} }
@ -45,6 +54,10 @@ export class SelectionManager {
return this.selectedLinks; return this.selectedLinks;
} }
public getSelectedDrawings() {
return this.selectedDrawings;
}
public setSelectedNodes(nodes: Node[]) { public setSelectedNodes(nodes: Node[]) {
this.selectedNodes = this.setSelectedItems<Node>(this.nodesDataSource, (node: Node) => { this.selectedNodes = this.setSelectedItems<Node>(this.nodesDataSource, (node: Node) => {
return !!nodes.find((n: Node) => node.node_id === n.node_id); return !!nodes.find((n: Node) => node.node_id === n.node_id);
@ -57,12 +70,50 @@ export class SelectionManager {
}); });
} }
public setSelectedDrawings(drawings: Drawing[]) {
this.selectedDrawings = this.setSelectedItems<Drawing>(this.drawingsDataSource, (drawing: Drawing) => {
return !!drawings.find((d: Drawing) => drawing.drawing_id === d.drawing_id);
});
}
private getSelectedItemsInRectangle<T extends Selectable>(dataSource: DataSource<T>, rectangle: Rectangle) { private getSelectedItemsInRectangle<T extends Selectable>(dataSource: DataSource<T>, rectangle: Rectangle) {
return this.setSelectedItems<T>(dataSource, (item: T) => { return this.setSelectedItems<T>(dataSource, (item: T) => {
return this.inRectangleHelper.inRectangle(item, rectangle); return this.inRectangleHelper.inRectangle(rectangle, item.x, item.y);
}); });
} }
private getSelectedInterfaceLabelsInRectangle(rectangle: Rectangle) {
this.linksDataSource.getItems().forEach((link: Link) => {
if (!(link.source && link.target && link.nodes.length > 1)) {
return;
}
let updated = false;
let x = link.source.x + link.nodes[0].label.x;
let y = link.source.y + link.nodes[0].label.y;
if (this.inRectangleHelper.inRectangle(rectangle, x, y)) {
link.nodes[0].label.is_selected = true;
updated = true;
}
x = link.target.x + link.nodes[1].label.x;
y = link.target.y + link.nodes[1].label.y;
if (this.inRectangleHelper.inRectangle(rectangle, x, y)) {
link.nodes[1].label.is_selected = true;
updated = true;
}
if (updated) {
this.linksDataSource.update(link);
}
});
return [];
}
private setSelected<T extends Selectable>(item: T, isSelected: boolean, dataSource: DataSource<T>): boolean { private setSelected<T extends Selectable>(item: T, isSelected: boolean, dataSource: DataSource<T>): boolean {
if (item.is_selected !== isSelected) { if (item.is_selected !== isSelected) {
item.is_selected = isSelected; item.is_selected = isSelected;

View File

@ -1,4 +1,6 @@
export class Drawing { import { Selectable } from "../managers/selection-manager";
export class Drawing implements Selectable {
drawing_id: string; drawing_id: string;
project_id: string; project_id: string;
rotation: number; rotation: number;
@ -6,4 +8,5 @@ export class Drawing {
x: number; x: number;
y: number; y: number;
z: number; z: number;
is_selected = false;
} }

View File

@ -1,10 +1,14 @@
export class InterfaceLabel { import { Selectable } from "../managers/selection-manager";
export class InterfaceLabel implements Selectable {
constructor( constructor(
public link_id: string,
public direction: string,
public x: number, public x: number,
public y: number, public y: number,
public text: string, public text: string,
public style: string, public style: string,
public rotation = 0, public rotation = 0,
public type: string public is_selected = false
) {} ) {}
} }

View File

@ -1,7 +1,10 @@
export class Label { import { Selectable } from "../managers/selection-manager";
export class Label implements Selectable {
rotation: number; rotation: number;
style: string; style: string;
text: string; text: string;
x: number; x: number;
y: number; y: number;
is_selected: boolean;
} }

View File

@ -83,7 +83,7 @@ describe('InterfaceLabelsWidget', () => {
const sourceInterface = drew.nodes()[0]; const sourceInterface = drew.nodes()[0];
expect(sourceInterface.innerHTML).toEqual('Interface 1'); expect(sourceInterface.innerHTML).toEqual('Interface 1');
expect(sourceInterface.getAttribute('x')).toEqual('110'); expect(sourceInterface.getAttribute('x')).toEqual('110');
expect(sourceInterface.getAttribute('y')).toEqual('220'); expect(sourceInterface.getAttribute('y')).toEqual('237');
expect(sourceInterface.getAttribute('transform')).toEqual('rotate(5, 110, 220)'); expect(sourceInterface.getAttribute('transform')).toEqual('rotate(5, 110, 220)');
expect(sourceInterface.getAttribute('style')).toEqual('font-size:12px'); expect(sourceInterface.getAttribute('style')).toEqual('font-size:12px');
expect(sourceInterface.getAttribute('class')).toContain('noselect'); expect(sourceInterface.getAttribute('class')).toContain('noselect');
@ -92,10 +92,21 @@ describe('InterfaceLabelsWidget', () => {
const targetInterface = drew.nodes()[1]; const targetInterface = drew.nodes()[1];
expect(targetInterface.innerHTML).toEqual('Interface 2'); expect(targetInterface.innerHTML).toEqual('Interface 2');
expect(targetInterface.getAttribute('x')).toEqual('270'); expect(targetInterface.getAttribute('x')).toEqual('270');
expect(targetInterface.getAttribute('y')).toEqual('360'); expect(targetInterface.getAttribute('y')).toEqual('377');
expect(targetInterface.getAttribute('transform')).toEqual('rotate(0, 270, 360)'); expect(targetInterface.getAttribute('transform')).toEqual('rotate(0, 270, 360)');
expect(targetInterface.getAttribute('style')).toEqual(''); expect(targetInterface.getAttribute('style')).toEqual('');
expect(targetInterface.getAttribute('class')).toContain('noselect'); expect(targetInterface.getAttribute('class')).toContain('noselect');
}); });
it('should draw interface label with class `selected` when selected', () => {
links[0].nodes[0].label.is_selected = true;
widget.draw(linksEnter);
const drew = svg.canvas.selectAll<SVGGElement, InterfaceLabel>('text.interface_label');
const sourceInterface = drew.nodes()[0];
expect(sourceInterface.getAttribute('class')).toContain('selected');
});
}); });

View File

@ -6,6 +6,8 @@ import { select } from "d3-selection";
export class InterfaceLabelWidget { export class InterfaceLabelWidget {
static SURROUNDING_TEXT_BORDER = 10;
private cssFixer: CssFixer; private cssFixer: CssFixer;
constructor() { constructor() {
@ -15,24 +17,28 @@ export class InterfaceLabelWidget {
draw(selection: SVGSelection) { draw(selection: SVGSelection) {
const labels = selection const labels = selection
.selectAll<SVGTextElement, InterfaceLabel>('text.interface_label') .selectAll<SVGGElement, InterfaceLabel>('g.interface_label_container')
.data((l: Link) => { .data((l: Link) => {
const sourceInterface = new InterfaceLabel( const sourceInterface = new InterfaceLabel(
l.link_id,
'source',
Math.round( l.source.x + l.nodes[0].label.x), Math.round( l.source.x + l.nodes[0].label.x),
Math.round(l.source.y + l.nodes[0].label.y), Math.round(l.source.y + l.nodes[0].label.y),
l.nodes[0].label.text, l.nodes[0].label.text,
l.nodes[0].label.style, l.nodes[0].label.style,
l.nodes[0].label.rotation, l.nodes[0].label.rotation,
'source' l.nodes[0].label.is_selected
); );
const targetInterface = new InterfaceLabel( const targetInterface = new InterfaceLabel(
l.link_id,
'target',
Math.round( l.target.x + l.nodes[1].label.x), Math.round( l.target.x + l.nodes[1].label.x),
Math.round( l.target.y + l.nodes[1].label.y), Math.round( l.target.y + l.nodes[1].label.y),
l.nodes[1].label.text, l.nodes[1].label.text,
l.nodes[1].label.style, l.nodes[1].label.style,
l.nodes[1].label.rotation, l.nodes[1].label.rotation,
'target' l.nodes[1].label.is_selected
); );
return [sourceInterface, targetInterface]; return [sourceInterface, targetInterface];
@ -40,44 +46,56 @@ export class InterfaceLabelWidget {
const enter = labels const enter = labels
.enter() .enter()
.append<SVGTextElement>('text') .append<SVGGElement>('g')
.attr('class', 'interface_label noselect'); .classed('interface_label_container', true);
// create surrounding rect
enter
.append<SVGRectElement>('rect')
.attr('class', 'interface_label_border');
// create label
enter
.append<SVGTextElement>('text')
.attr('class', 'interface_label noselect');
const merge = labels const merge = labels
.merge(enter); .merge(enter);
merge merge
.text((l: InterfaceLabel) => l.text) .attr('width', 100)
.attr('x', function(this: SVGTextElement, l: InterfaceLabel) { .attr('height', 100)
/* @todo: in GUI probably it should be calculated based on the line; for now we keep it the same */ .attr('transform', function(this: SVGGElement, l: InterfaceLabel) {
// const link = select(this.parentElement);
// const path = link.select<SVGPathElement>('path');
// let point;
// if (l.type === 'source') {
// point = path.node().getPointAtLength(40);
// } else {
// point = path.node().getPointAtLength(path.node().getTotalLength() - 40);
// }
// return point.x + l.x;
const bbox = this.getBBox(); const bbox = this.getBBox();
return l.x; const x = l.x;
const y = l.y + bbox.height;
return `translate(${x}, ${y}) rotate(${l.rotation}, ${x}, ${y})`;
}) })
.attr('y', function(this: SVGTextElement, l: InterfaceLabel) { .classed('selected', (l: InterfaceLabel) => l.is_selected);
/* @todo: in GUI probably it should be calculated based on the line; for now we keep it the same */
// const link = select(this.parentElement); // update label
// const path = link.select<SVGPathElement>('path'); merge
// let point; .select<SVGTextElement>('text.interface_label')
// if (l.type === 'source') { .text((l: InterfaceLabel) => l.text)
// point = path.node().getPointAtLength(40); .attr('style', (l: InterfaceLabel) => this.cssFixer.fix(l.style));
// } else {
// point = path.node().getPointAtLength(path.node().getTotalLength() - 40); // update surrounding rect
// } merge
// return point.y + l.y; .select<SVGRectElement>('rect.interface_label_border')
const bbox = this.getBBox(); .attr('visibility', (l: InterfaceLabel) => l.is_selected ? 'visible' : 'hidden')
return l.y + bbox.height; .attr('stroke-dasharray', '3,3')
}) .attr('stroke-width', '0.5')
.attr('style', (l: InterfaceLabel) => this.cssFixer.fix(l.style)) .each(function (this: SVGRectElement, l: InterfaceLabel) {
.attr('transform', (l: InterfaceLabel) => `rotate(${l.rotation}, ${l.x}, ${l.y})`); const current = select(this);
const parent = select(this.parentElement);
const text = parent.select<SVGTextElement>('text');
const bbox = text.node().getBBox();
current.attr('width', bbox.width + InterfaceLabelWidget.SURROUNDING_TEXT_BORDER);
current.attr('height', bbox.height + InterfaceLabelWidget.SURROUNDING_TEXT_BORDER);
current.attr('x', bbox.x - InterfaceLabelWidget.SURROUNDING_TEXT_BORDER);
current.attr('y', bbox.y - InterfaceLabelWidget.SURROUNDING_TEXT_BORDER);
});
labels labels
.exit() .exit()

View File

@ -44,6 +44,11 @@ path.selected {
stroke: darkred; stroke: darkred;
} }
.selected > .interface_label_border {
stroke: black;
fill: none;
}
.selection-line-tool .selection { .selection-line-tool .selection {
fill: #7ccbe1; fill: #7ccbe1;
stroke: #66aec2 ; stroke: #66aec2 ;

View File

@ -92,7 +92,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
protected hotkeysService: HotkeysService protected hotkeysService: HotkeysService
) { ) {
this.selectionManager = new SelectionManager( this.selectionManager = new SelectionManager(
this.nodesDataSource, this.linksDataSource, new InRectangleHelper()); this.nodesDataSource, this.linksDataSource, this.drawingsDataSource, new InRectangleHelper());
this.subscriptions = []; this.subscriptions = [];
} }