diff --git a/src/app/cartography/components/map/map.component.ts b/src/app/cartography/components/map/map.component.ts index a6cba58c..bf9397cd 100644 --- a/src/app/cartography/components/map/map.component.ts +++ b/src/app/cartography/components/map/map.component.ts @@ -90,11 +90,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((event: any, n: Node) => { 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 selection = select(this); - linksWidget.revise(selection); - } + + const links = this.links.filter((link) => link.target.node_id === n.node_id || link.source.node_id === n.node_id) + + links.forEach((link) => { + linksWidget.redrawLink(this.svg, link); }); }); diff --git a/src/app/cartography/widgets/drawings/ellipse-drawing.ts b/src/app/cartography/widgets/drawings/ellipse-drawing.ts index c46809b3..cd32afdf 100644 --- a/src/app/cartography/widgets/drawings/ellipse-drawing.ts +++ b/src/app/cartography/widgets/drawings/ellipse-drawing.ts @@ -22,7 +22,7 @@ export class EllipseDrawingWidget implements DrawingWidget { const drawing_enter = drawing .enter() .append('ellipse') - .attr('class', 'ellipse_element noselect'); + .attr('class', 'ellipse_element noselect'); const merge = drawing.merge(drawing_enter); diff --git a/src/app/cartography/widgets/interface-status.ts b/src/app/cartography/widgets/interface-status.ts new file mode 100644 index 00000000..f4fab995 --- /dev/null +++ b/src/app/cartography/widgets/interface-status.ts @@ -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(this); + const link_path = link_group.select('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('circle.status_started') + .data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started')); + + const status_started_enter = status_started + .enter() + .append('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('rect.status_stopped') + .data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped')); + + const status_stopped_enter = status_stopped + .enter() + .append('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(); + }); + } +} diff --git a/src/app/cartography/widgets/link.ts b/src/app/cartography/widgets/link.ts new file mode 100644 index 00000000..bec6f7f0 --- /dev/null +++ b/src/app/cartography/widgets/link.ts @@ -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("g.link_body") + .data((l) => [l]); + + const link_body_enter = link_body.enter() + .append('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('path') + .classed('selected', (l: Link) => l.is_selected); + + this.getInterfaceLabelWidget().draw(link_body_merge); + this.getInterfaceStatusWidget().draw(link_body_merge); + + } +} diff --git a/src/app/cartography/widgets/links.spec.ts b/src/app/cartography/widgets/links.spec.ts index 6ed90ed6..221ed356 100644 --- a/src/app/cartography/widgets/links.spec.ts +++ b/src/app/cartography/widgets/links.spec.ts @@ -1,4 +1,4 @@ -import { anything, instance, mock, verify } from "ts-mockito"; +import { instance, mock } from "ts-mockito"; import { Selection } from "d3-selection"; @@ -7,7 +7,7 @@ import { Layer } from "../models/layer"; import { LinksWidget } from "./links"; import { Node } from "../models/node"; import { Link } from "../../models/link"; -import { InterfaceLabelWidget } from "./interface-label"; +import { LinkWidget } from "./link"; describe('LinksWidget', () => { @@ -62,9 +62,9 @@ describe('LinksWidget', () => { }); it('should draw links', () => { - const interfaceLabelWidgetMock = mock(InterfaceLabelWidget); - const interfaceLabelWidget = instance(interfaceLabelWidgetMock); - spyOn(widget, 'getInterfaceLabelWidget').and.returnValue(interfaceLabelWidget); + const linkWidgetMock = mock(LinkWidget); + const linkWidget = instance(linkWidgetMock); + spyOn(widget, 'getLinkWidget').and.returnValue(linkWidget); widget.draw(layersEnter); @@ -73,9 +73,6 @@ describe('LinksWidget', () => { expect(linkNode.getAttribute('link_id')).toEqual('link1'); expect(linkNode.getAttribute('map-source')).toEqual('1'); expect(linkNode.getAttribute('map-target')).toEqual('2'); - expect(linkNode.getAttribute('transform')).toEqual('translate (0, 0)'); - - verify(interfaceLabelWidgetMock.draw(anything())).called(); }); }); diff --git a/src/app/cartography/widgets/links.ts b/src/app/cartography/widgets/links.ts index c30e60fd..662d59fb 100644 --- a/src/app/cartography/widgets/links.ts +++ b/src/app/cartography/widgets/links.ts @@ -1,125 +1,27 @@ -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"; 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 { InterfaceLabelWidget } from "./interface-label"; -import { CssFixer } from "../helpers/css-fixer"; +import { LinkWidget } from "./link"; export class LinksWidget implements Widget { private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper(); - - private interfaceLabelWidget: InterfaceLabelWidget; + private linkWidget = new LinkWidget(); constructor() { - this.interfaceLabelWidget = new InterfaceLabelWidget(new CssFixer()); } - public getInterfaceLabelWidget() { - return this.interfaceLabelWidget; + public getLinkWidget() { + return this.linkWidget } - public setInterfaceLabelWidget(interfaceLabelWidget: InterfaceLabelWidget) { - this.interfaceLabelWidget = interfaceLabelWidget; + public redrawLink(view: SVGSelection, link: Link) { + this.getLinkWidget().draw(this.selectLink(view, link)); } - public getLinkWidget(link: Link) { - if (link.link_type === 'serial') { - return new SerialLinkWidget(); - } - return new EthernetLinkWidget(); - } - - public select(view: SVGSelection) { - return view.selectAll("g.link"); - } - - public revise(selection: SVGSelection) { - const self = this; - - selection - .each(function (this: SVGGElement, l: Link) { - const link_group = select(this); - const link_widget = self.getLinkWidget(l); - - link_widget.draw(link_group, l); - - const link_path = link_group.select('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('circle.status_started') - .data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started')); - - const status_started_enter = status_started - .enter() - .append('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('rect.status_stopped') - .data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped')); - - const status_stopped_enter = status_stopped - .enter() - .append('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[]) { + public draw(view: SVGSelection) { const link = view .selectAll("g.link") .data((layer: Layer) => { @@ -144,12 +46,14 @@ export class LinksWidget implements Widget { const merge = link.merge(link_enter); - this.revise(merge); - + this.getLinkWidget().draw(merge); link .exit() .remove(); } + private selectLink(view: SVGSelection, link: Link) { + return view.selectAll(`g.link[link_id="${link.link_id}"]`); + } } diff --git a/src/app/cartography/widgets/links/ethernet-link.ts b/src/app/cartography/widgets/links/ethernet-link.ts index 4cb5e012..162c3a52 100644 --- a/src/app/cartography/widgets/links/ethernet-link.ts +++ b/src/app/cartography/widgets/links/ethernet-link.ts @@ -1,35 +1,52 @@ -import { line } from "d3-shape"; +import { path } from "d3-path"; import { Widget } from "../widget"; import { SVGSelection } from "../../models/types"; import { Link } from "../../../models/link"; +class EthernetLinkPath { + constructor( + public source: [number, number], + public target: [number, number] + ) { + } +} export class EthernetLinkWidget implements Widget { - - public draw(view: SVGSelection, link: Link) { - - const link_data = [[ - [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('path'); - link_path.classed('selected', (l: Link) => l.is_selected); - - if (!link_path.node()) { - link_path = view.append('path'); - } - - const link_path_data = link_path.data(link_data); - - link_path_data - .attr('d', value_line) - .attr('stroke', '#000') - .attr('stroke-width', '2'); - + private linktoEthernetLink(link: Link) { + return new EthernetLinkPath( + [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.] + ); } + public draw(view: SVGSelection) { + + const link = view + .selectAll('path.ethernet_link') + .data((link) => { + if(link.link_type === 'ethernet') { + return [this.linktoEthernetLink(link)]; + } + return []; + }); + + const link_enter = link.enter() + .append('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(); + }); + } } diff --git a/src/app/cartography/widgets/links/serial-link.ts b/src/app/cartography/widgets/links/serial-link.ts index 561d684e..7f108043 100644 --- a/src/app/cartography/widgets/links/serial-link.ts +++ b/src/app/cartography/widgets/links/serial-link.ts @@ -5,63 +5,88 @@ import { SVGSelection } from "../../models/types"; 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 { - public draw(view: SVGSelection, link: Link) { - 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 - }; + private linkToSerialLink(link: Link) { + 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 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 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 = [ - source.x + dx / 2.0 + 15 * vect_rot[0], - source.y + dy / 2.0 + 15 * vect_rot[1] - ]; + 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 = [ - target.x - dx / 2.0 - 15 * vect_rot[0], - target.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] + ]; - const line_data = [ - [source.x, source.y], - angle_source, - angle_target, - [target.x, target.y] - ]; + return new SerialLinkPath( + [source.x, source.y], + angle_source, + angle_target, + [target.x, target.y] + ); + } - let link_path = view.select('path'); + public draw(view: SVGSelection) { - if (!link_path.node()) { - link_path = view.append('path'); - } + const link = view + .selectAll('path.serial_link') + .data((link) => { + if(link.link_type === 'serial') { + return [this.linkToSerialLink(link)]; + } + return []; + }); - const line_generator = path(); - line_generator.moveTo(line_data[0][0], line_data[0][1]); - line_generator.lineTo(line_data[1][0], line_data[1][1]); - line_generator.lineTo(line_data[2][0], line_data[2][1]); - line_generator.lineTo(line_data[3][0], line_data[3][1]); + const link_enter = link.enter() + .append('path') + .attr('class', 'serial_link'); - link_path - .attr('d', line_generator.toString()) - .attr('stroke', '#B22222') - .attr('fill', 'none') - .attr('stroke-width', '2'); + link_enter + .attr('stroke', '#B22222') + .attr('fill', 'none') + .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(); + }); } } diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index d21cfa00..fecd6cad 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -232,7 +232,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy { 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(); } @@ -328,7 +330,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { public toggleShowInterfaceLabels(enabled: boolean) { this.project.show_interface_labels = enabled; - this.mapChild.graphLayout.getLinksWidget().getInterfaceLabelWidget() + this.mapChild.graphLayout.getLinksWidget().getLinkWidget().getInterfaceLabelWidget() .setEnabled(this.project.show_interface_labels); this.mapChild.reload(); }