Matteo Scandolo | 8cf33a3 | 2017-11-14 15:52:29 -0800 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | import * as d3 from 'd3'; |
| 18 | import * as _ from 'lodash'; |
| 19 | import {IXosSgNode} from '../../interfaces'; |
| 20 | import {XosServiceGraphConfig as config} from '../../graph.config'; |
| 21 | import {IXosServiceGraphIcons} from '../d3-helpers/graph-icons.service'; |
| 22 | import {IXosGraphHelpers} from '../d3-helpers/graph-elements.helpers'; |
| 23 | |
| 24 | export interface IXosNodeRenderer { |
| 25 | renderNodes(forceLayout: d3.forceLayout, nodeContainer: d3.Selection, nodes: IXosSgNode[]): void; |
| 26 | } |
| 27 | |
| 28 | export 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 Scandolo | 8cf33a3 | 2017-11-14 15:52:29 -0800 | [diff] [blame] | 53 | 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 Scandolo | 35fdf24 | 2017-11-30 12:29:45 -0800 | [diff] [blame] | 58 | }) |
| 59 | .call(this.drag); |
Matteo Scandolo | 8cf33a3 | 2017-11-14 15:52:29 -0800 | [diff] [blame] | 60 | |
| 61 | this.renderServiceNodes(entering.filter('.service')); |
| 62 | this.renderServiceInstanceNodes(entering.filter('.serviceinstance')); |
Matteo Scandolo | 1888b2a | 2018-01-08 16:49:06 -0800 | [diff] [blame] | 63 | this.renderInstanceNodes(entering.filter('.instance')); |
| 64 | this.renderNetworkNodes(entering.filter('.network')); |
Matteo Scandolo | 8cf33a3 | 2017-11-14 15:52:29 -0800 | [diff] [blame] | 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 | |
Matteo Scandolo | 1888b2a | 2018-01-08 16:49:06 -0800 | [diff] [blame] | 111 | 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 Scandolo | 8cf33a3 | 2017-11-14 15:52:29 -0800 | [diff] [blame] | 151 | 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 Scandolo | 1888b2a | 2018-01-08 16:49:06 -0800 | [diff] [blame] | 276 | // NOTE for 'instances' display instance_name instead of name? |
Matteo Scandolo | 8cf33a3 | 2017-11-14 15:52:29 -0800 | [diff] [blame] | 277 | 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 | } |