blob: 18719f1c057762ad6a65bd5c086267e898d78e4a [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
Matteo Scandolo8cf33a32017-11-14 15:52:29 -080053 const entering = node.enter()
54 .append('g')
55 .attr({
56 id: n => n.id,
57 class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo35fdf242017-11-30 12:29:45 -080058 })
59 .call(this.drag);
Matteo Scandolo8cf33a32017-11-14 15:52:29 -080060
61 this.renderServiceNodes(entering.filter('.service'));
62 this.renderServiceInstanceNodes(entering.filter('.serviceinstance'));
Matteo Scandolo1888b2a2018-01-08 16:49:06 -080063 this.renderInstanceNodes(entering.filter('.instance'));
64 this.renderNetworkNodes(entering.filter('.network'));
Matteo Scandolo8cf33a32017-11-14 15:52:29 -080065
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
Matteo Scandolo1888b2a2018-01-08 16:49:06 -0800111 private renderInstanceNodes(nodes: d3.selection) {
112 nodes
113 .append('rect')
114 .attr({
115 rx: config.node.radius,
116 ry: config.node.radius
117 });
118
119 nodes
120 .append('path')
121 .attr({
122 d: this.XosServiceGraphIcons.get('instance').path,
123 transform: this.XosServiceGraphIcons.get('instance').transform,
124 class: 'icon'
125 });
126
127 this.positionServiceNodeGroup(nodes);
128 this.handleLabels(nodes);
129 }
130
131 private renderNetworkNodes(nodes: d3.selection) {
132 nodes
133 .append('rect')
134 .attr({
135 rx: config.node.radius,
136 ry: config.node.radius
137 });
138
139 nodes
140 .append('path')
141 .attr({
142 d: this.XosServiceGraphIcons.get('network').path,
143 transform: this.XosServiceGraphIcons.get('network').transform,
144 class: 'icon'
145 });
146
147 this.positionServiceNodeGroup(nodes);
148 this.handleLabels(nodes);
149 }
150
Matteo Scandolo8cf33a32017-11-14 15:52:29 -0800151 private positionServiceNodeGroup(nodes: d3.selection) {
152 const self = this;
153 nodes.each(function (d: IXosSgNode) {
154 const node = d3.select(this);
155 const rect = node.select('rect');
156 const icon = node.select('path');
157 const bbox = self.XosGraphHelpers.getSiblingIconBBox(rect.node());
158
159 rect
160 .attr({
161 width: bbox.width + config.node.padding,
162 height: bbox.height + config.node.padding,
163 x: - (config.node.padding / 2),
164 y: - (config.node.padding / 2),
165 transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
166 });
167
168 icon
169 .attr({
170 transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
171 });
172 });
173 }
174
175 private positionServiceInstanceNodeGroup(nodes: d3.selection) {
176 const self = this;
177 nodes.each(function (d: IXosSgNode) {
178 const node = d3.select(this);
179 const rect = node.select('rect');
180 const icon = node.select('path');
181 const bbox = self.XosGraphHelpers.getSiblingIconBBox(rect.node());
182 const size = _.max([bbox.width, bbox.height]); // NOTE we need it to be a square
183 rect
184 .attr({
185 width: size + config.node.padding,
186 height: size + config.node.padding,
187 x: - (config.node.padding / 2),
188 y: - (config.node.padding / 2),
189 transform: `rotate(45), translate(${-bbox.width / 2}, ${-bbox.height / 2})`
190 });
191
192 icon
193 .attr({
194 transform: `translate(${-bbox.width / 2}, ${-bbox.height / 2})`
195 });
196 });
197 }
198
199 private handleLabels(nodes: d3.selection) {
200 const self = this;
201 // if (this.userConfig.labels) {
202
203 // group to contain label text and wrapper
204 const label = nodes.append('g')
205 .attr({
206 class: 'label'
207 });
208
209 // setting up the wrapper
210 label
211 .append('rect')
212 .attr({
213 class: 'label-wrapper',
214 rx: config.node.radius,
215 ry: config.node.radius
216 });
217
218 // adding text
219 label
220 .append('text')
221 .text(n => this.getNodeLabel(n))
222 .attr({
223 'opacity': 0,
224 'text-anchor': 'left',
225 'alignment-baseline': 'bottom',
226 'font-size': config.node.text,
227 y: config.node.text * 0.78
228 })
229 .transition()
230 .duration(config.duration)
231 .attr({
232 opacity: 1
233 });
234
235 // resize and position label
236 label.each(function() {
237 const text = d3.select(this).select('text').node();
238 const rect = d3.select(this).select('rect');
239 const iconRect = d3.select(this.parentNode).select('rect').node();
240 const icon = self.XosGraphHelpers.getBBox(iconRect);
241 const bbox = self.XosGraphHelpers.getBBox(text);
242
243 // scale the rectangle around the label to fit the text
244 rect
245 .attr({
246 width: bbox.width + config.node.padding,
247 height: config.node.text - 2 + config.node.padding,
248 x: -(config.node.padding / 2),
249 y: -(config.node.padding / 2),
250 });
251
252 // translate the lable group to the correct position
253 d3.select(this)
254 .attr({
255 transform: function() {
256 const label = self.XosGraphHelpers.getBBox(this);
257 const x = - (label.width - config.node.padding) / 2;
258 const y = (icon.height / 2) + config.node.padding;
259 return `translate(${x}, ${y})`;
260 }
261 });
262 });
263 // }
264 // else {
265 // node.selectAll('text')
266 // .transition()
267 // .duration(this.duration)
268 // .attr({
269 // opacity: 0
270 // })
271 // .remove();
272 // }
273 }
274
275 private getNodeLabel(n: any): string {
Matteo Scandolo1888b2a2018-01-08 16:49:06 -0800276 // NOTE for 'instances' display instance_name instead of name?
Matteo Scandolo8cf33a32017-11-14 15:52:29 -0800277 return n.data.name ? n.data.name.toUpperCase() : n.data.id;
278 // return n.data.name ? n.data.name.toUpperCase() + ` - ${n.data.id}` : n.data.id;
279 }
280}