Improved separation between Widgets

This commit is contained in:
ziajka 2018-10-24 16:31:33 +02:00
parent dcbefa996c
commit 055a161b17
9 changed files with 265 additions and 193 deletions

View File

@ -90,11 +90,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((event: any, n: Node) => { this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((event: any, n: Node) => {
const linksWidget = this.graphLayout.getLinksWidget(); const linksWidget = this.graphLayout.getLinksWidget();
linksWidget.select(this.svg).each(function(this: SVGGElement, link: Link) {
if (link.target.node_id === n.node_id || link.source.node_id === n.node_id) { const links = this.links.filter((link) => link.target.node_id === n.node_id || link.source.node_id === n.node_id)
const selection = select<SVGElement, Link>(this);
linksWidget.revise(selection); links.forEach((link) => {
} linksWidget.redrawLink(this.svg, link);
}); });
}); });

View File

@ -22,7 +22,7 @@ export class EllipseDrawingWidget implements DrawingWidget {
const drawing_enter = drawing const drawing_enter = drawing
.enter() .enter()
.append<SVGEllipseElement>('ellipse') .append<SVGEllipseElement>('ellipse')
.attr('class', 'ellipse_element noselect'); .attr('class', 'ellipse_element noselect');
const merge = drawing.merge(drawing_enter); const merge = drawing.merge(drawing_enter);

View File

@ -0,0 +1,74 @@
import { select } from "d3-selection";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../../models/link";
import { LinkStatus } from "../models/link-status";
export class InterfaceStatusWidget implements Widget {
constructor() {}
public draw(view: SVGSelection) {
view.each(function (this: SVGGElement, l: Link) {
const link_group = select<SVGGElement, Link>(this);
const link_path = link_group.select<SVGPathElement>('path');
const start_point: SVGPoint = link_path.node().getPointAtLength(45);
const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 45);
let statuses = [];
if (link_path.node().getTotalLength() > 2 * 45 + 10) {
statuses = [
new LinkStatus(start_point.x, start_point.y, l.source.status),
new LinkStatus(end_point.x, end_point.y, l.target.status)
];
}
const status_started = link_group
.selectAll<SVGCircleElement, LinkStatus>('circle.status_started')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started'));
const status_started_enter = status_started
.enter()
.append<SVGCircleElement>('circle');
status_started
.merge(status_started_enter)
.attr('class', 'status_started')
.attr('cx', (ls: LinkStatus) => ls.x)
.attr('cy', (ls: LinkStatus) => ls.y)
.attr('r', 6)
.attr('fill', '#2ecc71');
status_started
.exit()
.remove();
const status_stopped = link_group
.selectAll<SVGRectElement, LinkStatus>('rect.status_stopped')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped'));
const status_stopped_enter = status_stopped
.enter()
.append<SVGRectElement>('rect');
const STOPPED_STATUS_RECT_WIDTH = 10;
status_stopped
.merge(status_stopped_enter)
.attr('class', 'status_stopped')
.attr('x', (ls: LinkStatus) => ls.x - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('y', (ls: LinkStatus) => ls.y - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('width', STOPPED_STATUS_RECT_WIDTH)
.attr('height', STOPPED_STATUS_RECT_WIDTH)
.attr('fill', 'red');
status_stopped
.exit()
.remove();
});
}
}

View File

@ -0,0 +1,53 @@
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../../models/link";
import { SerialLinkWidget } from "./links/serial-link";
import { EthernetLinkWidget } from "./links/ethernet-link";
import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper";
import { InterfaceLabelWidget } from "./interface-label";
import { CssFixer } from "../helpers/css-fixer";
import { InterfaceStatusWidget } from "./interface-status";
export class LinkWidget implements Widget {
private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper();
constructor() {}
public getInterfaceLabelWidget() {
return new InterfaceLabelWidget(new CssFixer());
}
public getInterfaceStatusWidget() {
return new InterfaceStatusWidget();
}
public draw(view: SVGSelection) {
const link_body = view.selectAll<SVGGElement, Link>("g.link_body")
.data((l) => [l]);
const link_body_enter = link_body.enter()
.append<SVGGElement>('g')
.attr("class", "link_body");
const link_body_merge = link_body.merge(link_body_enter)
.attr('transform', (link) => {
const translation = this.multiLinkCalculatorHelper.linkTranslation(link.distance, link.source, link.target);
return `translate (${translation.dx}, ${translation.dy})`;
});
const serial_link_widget = new SerialLinkWidget();
serial_link_widget.draw(link_body_merge);
const ethernet_link_widget = new EthernetLinkWidget();
ethernet_link_widget.draw(link_body_merge);
link_body_merge
.select<SVGPathElement>('path')
.classed('selected', (l: Link) => l.is_selected);
this.getInterfaceLabelWidget().draw(link_body_merge);
this.getInterfaceStatusWidget().draw(link_body_merge);
}
}

View File

@ -1,4 +1,4 @@
import { anything, instance, mock, verify } from "ts-mockito"; import { instance, mock } from "ts-mockito";
import { Selection } from "d3-selection"; import { Selection } from "d3-selection";
@ -7,7 +7,7 @@ import { Layer } from "../models/layer";
import { LinksWidget } from "./links"; import { LinksWidget } from "./links";
import { Node } from "../models/node"; import { Node } from "../models/node";
import { Link } from "../../models/link"; import { Link } from "../../models/link";
import { InterfaceLabelWidget } from "./interface-label"; import { LinkWidget } from "./link";
describe('LinksWidget', () => { describe('LinksWidget', () => {
@ -62,9 +62,9 @@ describe('LinksWidget', () => {
}); });
it('should draw links', () => { it('should draw links', () => {
const interfaceLabelWidgetMock = mock(InterfaceLabelWidget); const linkWidgetMock = mock(LinkWidget);
const interfaceLabelWidget = instance(interfaceLabelWidgetMock); const linkWidget = instance(linkWidgetMock);
spyOn(widget, 'getInterfaceLabelWidget').and.returnValue(interfaceLabelWidget); spyOn(widget, 'getLinkWidget').and.returnValue(linkWidget);
widget.draw(layersEnter); widget.draw(layersEnter);
@ -73,9 +73,6 @@ describe('LinksWidget', () => {
expect(linkNode.getAttribute('link_id')).toEqual('link1'); expect(linkNode.getAttribute('link_id')).toEqual('link1');
expect(linkNode.getAttribute('map-source')).toEqual('1'); expect(linkNode.getAttribute('map-source')).toEqual('1');
expect(linkNode.getAttribute('map-target')).toEqual('2'); expect(linkNode.getAttribute('map-target')).toEqual('2');
expect(linkNode.getAttribute('transform')).toEqual('translate (0, 0)');
verify(interfaceLabelWidgetMock.draw(anything())).called();
}); });
}); });

View File

@ -1,125 +1,27 @@
import { select } from "d3-selection";
import { Widget } from "./widget"; import { Widget } from "./widget";
import { SVGSelection } from "../models/types"; import { SVGSelection } from "../models/types";
import { Link } from "../../models/link"; import { Link } from "../../models/link";
import { LinkStatus } from "../models/link-status";
import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper"; import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper";
import { SerialLinkWidget } from "./links/serial-link";
import { EthernetLinkWidget } from "./links/ethernet-link";
import { Layer } from "../models/layer"; import { Layer } from "../models/layer";
import { InterfaceLabelWidget } from "./interface-label"; import { LinkWidget } from "./link";
import { CssFixer } from "../helpers/css-fixer";
export class LinksWidget implements Widget { export class LinksWidget implements Widget {
private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper(); private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper();
private linkWidget = new LinkWidget();
private interfaceLabelWidget: InterfaceLabelWidget;
constructor() { constructor() {
this.interfaceLabelWidget = new InterfaceLabelWidget(new CssFixer());
} }
public getInterfaceLabelWidget() { public getLinkWidget() {
return this.interfaceLabelWidget; return this.linkWidget
} }
public setInterfaceLabelWidget(interfaceLabelWidget: InterfaceLabelWidget) { public redrawLink(view: SVGSelection, link: Link) {
this.interfaceLabelWidget = interfaceLabelWidget; this.getLinkWidget().draw(this.selectLink(view, link));
} }
public getLinkWidget(link: Link) { public draw(view: SVGSelection) {
if (link.link_type === 'serial') {
return new SerialLinkWidget();
}
return new EthernetLinkWidget();
}
public select(view: SVGSelection) {
return view.selectAll<SVGGElement, Link>("g.link");
}
public revise(selection: SVGSelection) {
const self = this;
selection
.each(function (this: SVGGElement, l: Link) {
const link_group = select<SVGGElement, Link>(this);
const link_widget = self.getLinkWidget(l);
link_widget.draw(link_group, l);
const link_path = link_group.select<SVGPathElement>('path');
const start_point: SVGPoint = link_path.node().getPointAtLength(45);
const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 45);
let statuses = [];
if (link_path.node().getTotalLength() > 2 * 45 + 10) {
statuses = [
new LinkStatus(start_point.x, start_point.y, l.source.status),
new LinkStatus(end_point.x, end_point.y, l.target.status)
];
}
const status_started = link_group
.selectAll<SVGCircleElement, LinkStatus>('circle.status_started')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started'));
const status_started_enter = status_started
.enter()
.append<SVGCircleElement>('circle');
status_started
.merge(status_started_enter)
.attr('class', 'status_started')
.attr('cx', (ls: LinkStatus) => ls.x)
.attr('cy', (ls: LinkStatus) => ls.y)
.attr('r', 6)
.attr('fill', '#2ecc71');
status_started
.exit()
.remove();
const status_stopped = link_group
.selectAll<SVGRectElement, LinkStatus>('rect.status_stopped')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped'));
const status_stopped_enter = status_stopped
.enter()
.append<SVGRectElement>('rect');
const STOPPED_STATUS_RECT_WIDTH = 10;
status_stopped
.merge(status_stopped_enter)
.attr('class', 'status_stopped')
.attr('x', (ls: LinkStatus) => ls.x - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('y', (ls: LinkStatus) => ls.y - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('width', STOPPED_STATUS_RECT_WIDTH)
.attr('height', STOPPED_STATUS_RECT_WIDTH)
.attr('fill', 'red');
status_stopped
.exit()
.remove();
})
.attr('transform', function(l) {
if (l.source && l.target) {
const translation = self.multiLinkCalculatorHelper.linkTranslation(l.distance, l.source, l.target);
return `translate (${translation.dx}, ${translation.dy})`;
}
return null;
});
this.getInterfaceLabelWidget().draw(selection);
}
public draw(view: SVGSelection, links?: Link[]) {
const link = view const link = view
.selectAll<SVGGElement, Link>("g.link") .selectAll<SVGGElement, Link>("g.link")
.data((layer: Layer) => { .data((layer: Layer) => {
@ -144,12 +46,14 @@ export class LinksWidget implements Widget {
const merge = link.merge(link_enter); const merge = link.merge(link_enter);
this.revise(merge); this.getLinkWidget().draw(merge);
link link
.exit() .exit()
.remove(); .remove();
} }
private selectLink(view: SVGSelection, link: Link) {
return view.selectAll<SVGGElement, Link>(`g.link[link_id="${link.link_id}"]`);
}
} }

View File

@ -1,35 +1,52 @@
import { line } from "d3-shape"; import { path } from "d3-path";
import { Widget } from "../widget"; import { Widget } from "../widget";
import { SVGSelection } from "../../models/types"; import { SVGSelection } from "../../models/types";
import { Link } from "../../../models/link"; import { Link } from "../../../models/link";
class EthernetLinkPath {
constructor(
public source: [number, number],
public target: [number, number]
) {
}
}
export class EthernetLinkWidget implements Widget { export class EthernetLinkWidget implements Widget {
private linktoEthernetLink(link: Link) {
public draw(view: SVGSelection, link: Link) { return new EthernetLinkPath(
[link.source.x + link.source.width / 2., link.source.y + link.source.height / 2.],
const link_data = [[ [link.target.x + link.target.width / 2., link.target.y + link.target.height / 2.]
[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 value_line = line();
let link_path = view.select<SVGPathElement>('path');
link_path.classed('selected', (l: Link) => l.is_selected);
if (!link_path.node()) {
link_path = view.append<SVGPathElement>('path');
}
const link_path_data = link_path.data(link_data);
link_path_data
.attr('d', value_line)
.attr('stroke', '#000')
.attr('stroke-width', '2');
} }
public draw(view: SVGSelection) {
const link = view
.selectAll<SVGPathElement, EthernetLinkPath>('path.ethernet_link')
.data((link) => {
if(link.link_type === 'ethernet') {
return [this.linktoEthernetLink(link)];
}
return [];
});
const link_enter = link.enter()
.append<SVGPathElement>('path')
.attr('class', 'ethernet_link');
link_enter
.attr('stroke', '#000')
.attr('stroke-width', '2');
const link_merge = link.merge(link_enter);
link_merge
.attr('d', (ethernet) => {
const line_generator = path();
line_generator.moveTo(ethernet.source[0], ethernet.source[1]);
line_generator.lineTo(ethernet.target[0], ethernet.target[1]);
return line_generator.toString();
});
}
} }

View File

@ -5,63 +5,88 @@ import { SVGSelection } from "../../models/types";
import { Link } from "../../../models/link"; import { Link } from "../../../models/link";
class SerialLinkPath {
constructor(
public source: [number, number],
public source_angle: [number, number],
public target_angle: [number, number],
public target: [number, number]
) {
}
}
export class SerialLinkWidget implements Widget { export class SerialLinkWidget implements Widget {
public draw(view: SVGSelection, link: Link) { private linkToSerialLink(link: Link) {
const source = { const source = {
'x': link.source.x + link.source.width / 2, 'x': link.source.x + link.source.width / 2,
'y': link.source.y + link.source.height / 2 'y': link.source.y + link.source.height / 2
}; };
const target = { const target = {
'x': link.target.x + link.target.width / 2, 'x': link.target.x + link.target.width / 2,
'y': link.target.y + link.target.height / 2 'y': link.target.y + link.target.height / 2
}; };
const dx = target.x - source.x; const dx = target.x - source.x;
const dy = target.y - source.y; const dy = target.y - source.y;
const vector_angle = Math.atan2(dy, dx); const vector_angle = Math.atan2(dy, dx);
const rot_angle = -Math.PI / 4.0; const rot_angle = -Math.PI / 4.0;
const vect_rot = [ const vect_rot = [
Math.cos(vector_angle + rot_angle), Math.cos(vector_angle + rot_angle),
Math.sin(vector_angle + rot_angle) Math.sin(vector_angle + rot_angle)
]; ];
const angle_source = [ const angle_source: [number, number] = [
source.x + dx / 2.0 + 15 * vect_rot[0], source.x + dx / 2.0 + 15 * vect_rot[0],
source.y + dy / 2.0 + 15 * vect_rot[1] source.y + dy / 2.0 + 15 * vect_rot[1]
]; ];
const angle_target = [ const angle_target: [number, number] = [
target.x - dx / 2.0 - 15 * vect_rot[0], target.x - dx / 2.0 - 15 * vect_rot[0],
target.y - dy / 2.0 - 15 * vect_rot[1] target.y - dy / 2.0 - 15 * vect_rot[1]
]; ];
const line_data = [ return new SerialLinkPath(
[source.x, source.y], [source.x, source.y],
angle_source, angle_source,
angle_target, angle_target,
[target.x, target.y] [target.x, target.y]
]; );
}
let link_path = view.select<SVGPathElement>('path'); public draw(view: SVGSelection) {
if (!link_path.node()) { const link = view
link_path = view.append<SVGPathElement>('path'); .selectAll<SVGPathElement, SerialLinkPath>('path.serial_link')
} .data((link) => {
if(link.link_type === 'serial') {
return [this.linkToSerialLink(link)];
}
return [];
});
const line_generator = path(); const link_enter = link.enter()
line_generator.moveTo(line_data[0][0], line_data[0][1]); .append<SVGPathElement>('path')
line_generator.lineTo(line_data[1][0], line_data[1][1]); .attr('class', 'serial_link');
line_generator.lineTo(line_data[2][0], line_data[2][1]);
line_generator.lineTo(line_data[3][0], line_data[3][1]);
link_path link_enter
.attr('d', line_generator.toString()) .attr('stroke', '#B22222')
.attr('stroke', '#B22222') .attr('fill', 'none')
.attr('fill', 'none') .attr('stroke-width', '2');
.attr('stroke-width', '2');
const link_merge = link.merge(link_enter);
link_merge
.attr('d', (serial) => {
const line_generator = path();
line_generator.moveTo(serial.source[0], serial.source[1]);
line_generator.lineTo(serial.source_angle[0], serial.source_angle[1]);
line_generator.lineTo(serial.target_angle[0], serial.target_angle[1]);
line_generator.lineTo(serial.target[0], serial.target[1]);
return line_generator.toString();
});
} }
} }

View File

@ -232,7 +232,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.mapChild.graphLayout.getSelectionTool().rectangleSelected) this.mapChild.graphLayout.getSelectionTool().rectangleSelected)
); );
this.mapChild.graphLayout.getLinksWidget().getInterfaceLabelWidget().setEnabled(this.project.show_interface_labels); this.mapChild.graphLayout
.getLinksWidget().getLinkWidget().getInterfaceLabelWidget().setEnabled(this.project.show_interface_labels);
this.mapChild.reload(); this.mapChild.reload();
} }
@ -328,7 +330,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
public toggleShowInterfaceLabels(enabled: boolean) { public toggleShowInterfaceLabels(enabled: boolean) {
this.project.show_interface_labels = enabled; this.project.show_interface_labels = enabled;
this.mapChild.graphLayout.getLinksWidget().getInterfaceLabelWidget() this.mapChild.graphLayout.getLinksWidget().getLinkWidget().getInterfaceLabelWidget()
.setEnabled(this.project.show_interface_labels); .setEnabled(this.project.show_interface_labels);
this.mapChild.reload(); this.mapChild.reload();
} }