blob: 17bcf702651261b818cfa539b715d8678db51d7e [file] [log] [blame]
Matteo Scandolo8cf33a32017-11-14 15:52:29 -08001/*
2 * Copyright 2017-present Open Networking Foundation
3
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7
8 * http://www.apache.org/licenses/LICENSE-2.0
9
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import * as d3 from 'd3';
18import * as _ from 'lodash';
19import {IXosSgNode} from '../../interfaces';
20import {XosServiceGraphConfig as config} from '../../graph.config';
21import {IXosServiceGraphIcons} from '../d3-helpers/graph-icons.service';
22import {IXosGraphHelpers} from '../d3-helpers/graph-elements.helpers';
23
24export interface IXosNodeRenderer {
25 renderNodes(forceLayout: d3.forceLayout, nodeContainer: d3.Selection, nodes: IXosSgNode[]): void;
26}
27
28export class XosNodeRenderer {
29
30 static $inject = [
31 'XosServiceGraphIcons',
32 'XosGraphHelpers'
33 ];
34
35 private drag;
36
37 constructor (
38 private XosServiceGraphIcons: IXosServiceGraphIcons,
39 private XosGraphHelpers: IXosGraphHelpers
40 ) {}
41
42 public renderNodes(forceLayout: any, nodeContainer: any, nodes: IXosSgNode[]): void {
43
44 this.drag = forceLayout.drag()
45 .on('dragstart', (n: IXosSgNode) => {
46 n.fixed = true;
47 });
48
49 const node = nodeContainer
50 .selectAll('g.node')
51 .data(nodes, n => n.id);
52
53 node
54 .call(this.drag);
55
56 const entering = node.enter()
57 .append('g')
58 .attr({
59 id: n => n.id,
60 class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
61 });
62
63 this.renderServiceNodes(entering.filter('.service'));
64 this.renderServiceInstanceNodes(entering.filter('.serviceinstance'));
65
66 node.exit().remove();
67 }
68
69 private renderServiceNodes(nodes: d3.selection) {
70
71 nodes
72 .append('rect')
73 .attr({
74 rx: config.node.radius,
75 ry: config.node.radius
76 });
77
78 nodes
79 .append('path')
80 .attr({
81 d: this.XosServiceGraphIcons.get('service').path,
82 transform: this.XosServiceGraphIcons.get('service').transform,
83 class: 'icon'
84 });
85
86 this.positionServiceNodeGroup(nodes);
87 this.handleLabels(nodes);
88 }
89
90 private renderServiceInstanceNodes(nodes: d3.selection) {
91 nodes.append('rect')
92 .attr({
93 width: 40,
94 height: 40,
95 x: -20,
96 y: -20,
97 transform: `rotate(45)`
98 });
99
100 nodes
101 .append('path')
102 .attr({
103 d: this.XosServiceGraphIcons.get('serviceinstance').path,
104 class: 'icon'
105 });
106
107 this.positionServiceInstanceNodeGroup(nodes);
108 this.handleLabels(nodes); // eventually improve, padding top is wrong
109 }
110
111 private positionServiceNodeGroup(nodes: d3.selection) {
112 const self = this;
113 nodes.each(function (d: IXosSgNode) {
114 const node = d3.select(this);
115 const rect = node.select('rect');
116 const icon = node.select('path');
117 const bbox = self.XosGraphHelpers.getSiblingIconBBox(rect.node());
118
119 rect
120 .attr({
121 width: bbox.width + config.node.padding,
122 height: bbox.height + config.node.padding,
123 x: - (config.node.padding / 2),
124 y: - (config.node.padding / 2),
125 transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
126 });
127
128 icon
129 .attr({
130 transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
131 });
132 });
133 }
134
135 private positionServiceInstanceNodeGroup(nodes: d3.selection) {
136 const self = this;
137 nodes.each(function (d: IXosSgNode) {
138 const node = d3.select(this);
139 const rect = node.select('rect');
140 const icon = node.select('path');
141 const bbox = self.XosGraphHelpers.getSiblingIconBBox(rect.node());
142 const size = _.max([bbox.width, bbox.height]); // NOTE we need it to be a square
143 rect
144 .attr({
145 width: size + config.node.padding,
146 height: size + config.node.padding,
147 x: - (config.node.padding / 2),
148 y: - (config.node.padding / 2),
149 transform: `rotate(45), translate(${-bbox.width / 2}, ${-bbox.height / 2})`
150 });
151
152 icon
153 .attr({
154 transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
155 });
156 });
157 }
158
159 private handleLabels(nodes: d3.selection) {
160 const self = this;
161 // if (this.userConfig.labels) {
162
163 // group to contain label text and wrapper
164 const label = nodes.append('g')
165 .attr({
166 class: 'label'
167 });
168
169 // setting up the wrapper
170 label
171 .append('rect')
172 .attr({
173 class: 'label-wrapper',
174 rx: config.node.radius,
175 ry: config.node.radius
176 });
177
178 // adding text
179 label
180 .append('text')
181 .text(n => this.getNodeLabel(n))
182 .attr({
183 'opacity': 0,
184 'text-anchor': 'left',
185 'alignment-baseline': 'bottom',
186 'font-size': config.node.text,
187 y: config.node.text * 0.78
188 })
189 .transition()
190 .duration(config.duration)
191 .attr({
192 opacity: 1
193 });
194
195 // resize and position label
196 label.each(function() {
197 const text = d3.select(this).select('text').node();
198 const rect = d3.select(this).select('rect');
199 const iconRect = d3.select(this.parentNode).select('rect').node();
200 const icon = self.XosGraphHelpers.getBBox(iconRect);
201 const bbox = self.XosGraphHelpers.getBBox(text);
202
203 // scale the rectangle around the label to fit the text
204 rect
205 .attr({
206 width: bbox.width + config.node.padding,
207 height: config.node.text - 2 + config.node.padding,
208 x: -(config.node.padding / 2),
209 y: -(config.node.padding / 2),
210 });
211
212 // translate the lable group to the correct position
213 d3.select(this)
214 .attr({
215 transform: function() {
216 const label = self.XosGraphHelpers.getBBox(this);
217 const x = - (label.width - config.node.padding) / 2;
218 const y = (icon.height / 2) + config.node.padding;
219 return `translate(${x}, ${y})`;
220 }
221 });
222 });
223 // }
224 // else {
225 // node.selectAll('text')
226 // .transition()
227 // .duration(this.duration)
228 // .attr({
229 // opacity: 0
230 // })
231 // .remove();
232 // }
233 }
234
235 private getNodeLabel(n: any): string {
236 return n.data.name ? n.data.name.toUpperCase() : n.data.id;
237 // return n.data.name ? n.data.name.toUpperCase() + ` - ${n.data.id}` : n.data.id;
238 }
239}