1 |
|
/**
|
2 |
|
* @constructor
|
3 |
|
*/
|
4 |
|
function Viewport() {
|
5 |
|
/** @prop {VertexContextMenuList} contextMenuComponent */
|
6 |
|
this.contextMenuComponent = null;
|
7 |
|
/** @prop {VertexPopoverComponent} vertexPopoverComponent */
|
8 |
|
this.vertexPopoverComponent = null;
|
9 |
|
/** @prop {EdgePopoverComponent} edgePopoverComponent */
|
10 |
|
this.edgePopoverComponent = null;
|
11 |
|
|
12 |
|
/** @prop {ForceDirected} forceDirected */
|
13 |
|
this.forceDirected = new ForceDirected;
|
14 |
|
|
15 |
|
var rootElement;
|
16 |
|
var innerSvgElement;
|
17 |
|
var edgesContainer;
|
18 |
|
var verticesContainer;
|
19 |
|
var groupsContainer;
|
20 |
|
var definitions;
|
21 |
|
|
22 |
|
var pan = false;
|
23 |
|
|
24 |
|
var edgeList = [];
|
25 |
|
var nodeList = [];
|
26 |
|
var vertexList = [];
|
27 |
|
var groupList = [];
|
28 |
|
|
29 |
|
this.addEdge = function(edge) {
|
|
1 |
class Viewport {
|
|
2 |
/**
|
|
3 |
* @constructor
|
|
4 |
*/
|
|
5 |
constructor() {
|
|
6 |
/** @prop {VertexContextMenuList} contextMenuComponent */
|
|
7 |
this.contextMenuComponent = null;
|
|
8 |
/** @prop {VertexPopoverComponent} vertexPopoverComponent */
|
|
9 |
this.vertexPopoverComponent = null;
|
|
10 |
/** @prop {EdgePopoverComponent} edgePopoverComponent */
|
|
11 |
this.edgePopoverComponent = null;
|
|
12 |
|
|
13 |
/** @prop {ForceDirected} forceDirected */
|
|
14 |
this.forceDirected = new ForceDirected;
|
|
15 |
|
|
16 |
this._edgeList = [];
|
|
17 |
this._nodeList = [];
|
|
18 |
this._vertexList = [];
|
|
19 |
this._groupList = [];
|
|
20 |
}
|
|
21 |
|
|
22 |
addEdge(edge) {
|
30 |
23 |
if (!(edge instanceof Edge)) {
|
31 |
24 |
throw new TypeError(edge.toString() + ' is not an instance of Edge');
|
32 |
25 |
}
|
33 |
26 |
|
34 |
|
edgeList.push(edge);
|
|
27 |
this._edgeList.push(edge);
|
35 |
28 |
|
36 |
29 |
if (edge.from === null || edge.from.isExcluded) return;
|
37 |
30 |
if (edge.to === null || edge.to.isExcluded) return;
|
38 |
31 |
|
39 |
|
edgesContainer.appendChild(edge.render());
|
40 |
|
};
|
|
32 |
this._edgesContainer.appendChild(edge.render());
|
|
33 |
}
|
41 |
34 |
|
42 |
|
this.addNode = function(node) {
|
|
35 |
addNode(node) {
|
43 |
36 |
if (!(node instanceof Node)) {
|
44 |
37 |
throw new TypeError(node.toString() + ' is not an instance of Node');
|
45 |
38 |
}
|
46 |
39 |
|
47 |
|
nodeList.push(node);
|
|
40 |
this._nodeList.push(node);
|
48 |
41 |
|
49 |
42 |
if (node instanceof Vertex) {
|
50 |
|
vertexList.push(node);
|
51 |
|
verticesContainer.appendChild(node.render());
|
|
43 |
this._vertexList.push(node);
|
|
44 |
this._verticesContainer.appendChild(node.render());
|
52 |
45 |
} else if (node instanceof Group) {
|
53 |
|
groupList.push(node);
|
54 |
|
groupsContainer.appendChild(node.render());
|
|
46 |
this._groupList.push(node);
|
|
47 |
this._groupsContainer.appendChild(node.render());
|
55 |
48 |
}
|
56 |
|
};
|
|
49 |
}
|
57 |
50 |
|
58 |
|
this.removeNode = function(node) {
|
|
51 |
removeNode(node) {
|
59 |
52 |
if (!(node instanceof Node)) {
|
60 |
53 |
throw new TypeError(node.toString() + ' is not an instance of Node');
|
61 |
54 |
}
|
62 |
55 |
|
63 |
|
nodeList.splice(nodeList.indexOf(node), 1);
|
|
56 |
this._nodeList.splice(this._nodeList.indexOf(node), 1);
|
64 |
57 |
|
65 |
58 |
if (node instanceof Vertex) {
|
66 |
|
vertexList.splice(vertexList.indexOf(node), 1);
|
|
59 |
this._vertexList.splice(this._vertexList.indexOf(node), 1);
|
67 |
60 |
} else if (node instanceof Group) {
|
68 |
|
groupList.splice(groupList.indexOf(node), 1);
|
|
61 |
this._groupList.splice(this._groupList.indexOf(node), 1);
|
69 |
62 |
}
|
70 |
|
};
|
71 |
|
|
72 |
|
this.getEdgeList = function() {
|
73 |
|
return edgeList;
|
74 |
|
};
|
75 |
|
|
76 |
|
this.getNodeList = function() {
|
77 |
|
return nodeList;
|
78 |
|
};
|
79 |
|
|
80 |
|
this.getVertexList = function() {
|
81 |
|
return vertexList;
|
82 |
|
};
|
|
63 |
}
|
|
64 |
|
|
65 |
get edgeList() {
|
|
66 |
return this._edgeList;
|
|
67 |
}
|
83 |
68 |
|
84 |
|
this.getGroupList = function() {
|
85 |
|
return groupList;
|
86 |
|
};
|
|
69 |
get nodeList() {
|
|
70 |
return this._nodeList;
|
|
71 |
}
|
|
72 |
|
|
73 |
get vertexList() {
|
|
74 |
return this._vertexList;
|
|
75 |
}
|
87 |
76 |
|
88 |
|
this.addSvgDefinition = function(id, svgString) {
|
89 |
|
definitions.appendChild(DOM.s('g', {
|
|
77 |
get groupList() {
|
|
78 |
return this._groupList;
|
|
79 |
}
|
|
80 |
|
|
81 |
addSvgDefinition(id, svgString) {
|
|
82 |
this._definitions.appendChild(DOM.s('g', {
|
90 |
83 |
id,
|
91 |
84 |
innerHTML: svgString,
|
92 |
85 |
}));
|
93 |
|
};
|
|
86 |
}
|
94 |
87 |
|
95 |
|
this.getSize = function() {
|
|
88 |
get size() {
|
96 |
89 |
return new Dimensions(
|
97 |
|
rootElement.offsetWidth,
|
98 |
|
rootElement.offsetHeight,
|
|
90 |
this._rootElement.offsetWidth,
|
|
91 |
this._rootElement.offsetHeight,
|
99 |
92 |
);
|
100 |
|
};
|
|
93 |
}
|
101 |
94 |
|
102 |
|
this.getPosition = function() {
|
|
95 |
get position() {
|
103 |
96 |
return new Coordinates(
|
104 |
|
+innerSvgElement.getAttribute('x'),
|
105 |
|
+innerSvgElement.getAttribute('y'),
|
|
97 |
+this._innerSvgElement.getAttribute('x'),
|
|
98 |
+this._innerSvgElement.getAttribute('y'),
|
106 |
99 |
);
|
107 |
|
};
|
|
100 |
}
|
108 |
101 |
|
109 |
|
this.setPosition = function(coords) {
|
110 |
|
innerSvgElement.setAttribute('x', coords.x);
|
111 |
|
innerSvgElement.setAttribute('y', coords.y);
|
112 |
|
};
|
|
102 |
set position(coords) {
|
|
103 |
this._innerSvgElement.setAttribute('x', coords.x);
|
|
104 |
this._innerSvgElement.setAttribute('y', coords.y);
|
|
105 |
}
|
113 |
106 |
|
114 |
|
this.center = function() {
|
115 |
|
var sumOfCenters = new Coordinates(0, 0);
|
116 |
|
var bbox = rootElement.getBoundingClientRect();
|
|
107 |
center() {
|
|
108 |
let sumOfCenters = new Coordinates(0, 0);
|
|
109 |
let bbox = this._rootElement.getBoundingClientRect();
|
117 |
110 |
|
118 |
|
var nodeList = app.viewportComponent.getNodeList();
|
|
111 |
let nodeList = this.nodeList;
|
119 |
112 |
nodeList.forEach(node => {
|
120 |
|
var center = node.center;
|
|
113 |
let center = node.center;
|
121 |
114 |
|
122 |
115 |
sumOfCenters.x += center.x;
|
123 |
116 |
sumOfCenters.y += center.y;
|
124 |
117 |
});
|
125 |
118 |
|
126 |
|
var center = new Coordinates(-1 * sumOfCenters.x / nodeList.length + bbox.width / 2, -1 * sumOfCenters.y / nodeList.length + bbox.height / 2);
|
|
119 |
let center = new Coordinates(
|
|
120 |
-1 * sumOfCenters.x / nodeList.length + (bbox.width / 2),
|
|
121 |
-1 * sumOfCenters.y / nodeList.length + bbox.height / 2,
|
|
122 |
);
|
127 |
123 |
|
128 |
|
innerSvgElement.setAttribute('x', center.x);
|
129 |
|
innerSvgElement.setAttribute('y', center.y);
|
|
124 |
this._innerSvgElement.setAttribute('x', center.x);
|
|
125 |
this._innerSvgElement.setAttribute('y', center.y);
|
130 |
126 |
|
131 |
127 |
app.sidebarComponent.minimapComponent.viewportPosition = center;
|
132 |
|
};
|
133 |
|
|
134 |
|
this.render = function() {
|
135 |
|
rootElement = DOM.createHtmlElement('div', {
|
136 |
|
'class': 'viewport',
|
137 |
|
'id': 'viewport',
|
138 |
|
});
|
139 |
|
rootElement.addEventListener('wheel', onMouseWheel.bind(this));
|
140 |
|
rootElement.addEventListener('mousedown', onMouseDown.bind(this));
|
141 |
|
rootElement.addEventListener('dblclick', onDoubleClick.bind(this));
|
142 |
|
|
143 |
|
var mainSvg = DOM.createSvgElement('svg', {
|
144 |
|
'xmlns': 'http://www.w3.org/2000/svg',
|
145 |
|
'width': '100%',
|
146 |
|
'height': '100%',
|
147 |
|
'style': 'margin-bottom: -4px;',
|
148 |
|
})
|
149 |
|
rootElement.appendChild(mainSvg);
|
150 |
|
|
151 |
|
innerSvgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
152 |
|
innerSvgElement.setAttribute('x', 0);
|
153 |
|
innerSvgElement.setAttribute('y', 0);
|
154 |
|
innerSvgElement.setAttribute('id', 'svg1');
|
155 |
|
innerSvgElement.setAttribute('style', 'overflow: visible;');
|
156 |
|
mainSvg.appendChild(innerSvgElement);
|
157 |
|
|
158 |
|
var graph = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
159 |
|
graph.setAttribute('id', 'graph');
|
160 |
|
innerSvgElement.appendChild(graph);
|
161 |
|
|
162 |
|
edgesContainer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
163 |
|
edgesContainer.setAttribute('data-id', 'edges');
|
164 |
|
graph.appendChild(edgesContainer);
|
165 |
|
|
166 |
|
verticesContainer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
167 |
|
verticesContainer.setAttribute('data-id', 'vertices');
|
168 |
|
graph.appendChild(verticesContainer);
|
169 |
|
|
170 |
|
groupsContainer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
171 |
|
groupsContainer.setAttribute('data-id', 'groups');
|
172 |
|
graph.appendChild(groupsContainer);
|
|
128 |
}
|
173 |
129 |
|
174 |
|
definitions = DOM.createSvgElement('defs');
|
175 |
|
mainSvg.appendChild(definitions);
|
|
130 |
render() {
|
|
131 |
this.contextMenuComponent = new VertexContextMenuList;
|
|
132 |
this.vertexPopoverComponent = new VertexPopover;
|
|
133 |
this.edgePopoverComponent = new EdgePopover;
|
176 |
134 |
|
177 |
|
var linearGradient = DOM.createSvgElement('linearGradient', {
|
178 |
|
'id': 'node--highlighted-as-required-provided',
|
|
135 |
// graph canvas
|
|
136 |
const mainSvg = DOM.s('svg', {
|
|
137 |
width: '100%',
|
|
138 |
height: '100%',
|
|
139 |
style: 'margin-bottom: -4px;',
|
179 |
140 |
});
|
180 |
|
linearGradient.appendChild(DOM.createSvgElement('stop', {
|
181 |
|
'offset': '0%',
|
182 |
|
'stop-color': 'red',
|
183 |
|
}));
|
184 |
|
linearGradient.appendChild(DOM.createSvgElement('stop', {
|
185 |
|
'offset': '100%',
|
186 |
|
'stop-color': '#5896ff',
|
187 |
|
}));
|
188 |
|
definitions.appendChild(linearGradient);
|
|
141 |
|
|
142 |
this._innerSvgElement = DOM.s('svg', {
|
|
143 |
x: 0,
|
|
144 |
y: 0,
|
|
145 |
id: 'svg1',
|
|
146 |
style: 'overflow: visible;',
|
|
147 |
});
|
|
148 |
mainSvg.appendChild(this._innerSvgElement);
|
189 |
149 |
|
190 |
|
this.contextMenuComponent = new VertexContextMenuList;
|
191 |
|
rootElement.appendChild(this.contextMenuComponent.render());
|
|
150 |
const graph = DOM.s('g', {
|
|
151 |
id: 'graph',
|
|
152 |
});
|
|
153 |
this._innerSvgElement.appendChild(graph);
|
192 |
154 |
|
193 |
|
this.vertexPopoverComponent = new VertexPopover;
|
194 |
|
rootElement.appendChild(this.vertexPopoverComponent.render());
|
|
155 |
// graph feature containers
|
|
156 |
this._edgesContainer = DOM.s('g', {
|
|
157 |
'data-id': 'edges',
|
|
158 |
});
|
|
159 |
graph.appendChild(this._edgesContainer);
|
195 |
160 |
|
196 |
|
this.edgePopoverComponent = new EdgePopover;
|
197 |
|
rootElement.appendChild(this.edgePopoverComponent.render());
|
|
161 |
this._verticesContainer = DOM.s('g', {
|
|
162 |
'data-id': 'vertices',
|
|
163 |
});
|
|
164 |
graph.appendChild(this._verticesContainer);
|
198 |
165 |
|
199 |
|
return rootElement;
|
200 |
|
};
|
|
166 |
this._groupsContainer = DOM.s('g', {
|
|
167 |
'data-id': 'groups',
|
|
168 |
});
|
|
169 |
graph.appendChild(this._groupsContainer);
|
|
170 |
|
|
171 |
// reusable definitions
|
|
172 |
this._definitions = DOM.s('defs', {}, [
|
|
173 |
// linear gradient
|
|
174 |
DOM.s('linearGradient', {
|
|
175 |
id: 'node--highlighted-as-required-provided',
|
|
176 |
}, [
|
|
177 |
DOM.s('stop', {
|
|
178 |
offset: '0%',
|
|
179 |
'stop-color': 'red',
|
|
180 |
}),
|
|
181 |
DOM.s('stop', {
|
|
182 |
offset: '100%',
|
|
183 |
'stop-color': '#5896ff',
|
|
184 |
}),
|
|
185 |
]),
|
|
186 |
]);
|
|
187 |
mainSvg.appendChild(this._definitions);
|
|
188 |
|
|
189 |
// root
|
|
190 |
this._rootElement = DOM.h('div', {
|
|
191 |
class: 'viewport',
|
|
192 |
id: 'viewport',
|
|
193 |
onWheel: this._onRootWheel.bind(this),
|
|
194 |
onMouseDown: this._onRootMouseDown.bind(this),
|
|
195 |
onDblClick: this._onRootDoubleClick.bind(this),
|
|
196 |
}, [
|
|
197 |
mainSvg,
|
|
198 |
this.contextMenuComponent.render(),
|
|
199 |
this.vertexPopoverComponent.render(),
|
|
200 |
this.edgePopoverComponent.render(),
|
|
201 |
]);
|
|
202 |
|
|
203 |
return this._rootElement;
|
|
204 |
}
|
201 |
205 |
|
202 |
|
this.reset = function() {
|
203 |
|
edgeList = [];
|
204 |
|
nodeList = [];
|
205 |
|
vertexList = [];
|
206 |
|
groupList = [];
|
|
206 |
reset() {
|
|
207 |
this._edgeList = [];
|
|
208 |
this._nodeList = [];
|
|
209 |
this._vertexList = [];
|
|
210 |
this._groupList = [];
|
207 |
211 |
|
208 |
|
edgesContainer.innerHTML = '';
|
209 |
|
verticesContainer.innerHTML = '';
|
210 |
|
groupsContainer.innerHTML = '';
|
211 |
|
};
|
|
212 |
this._edgesContainer.innerHTML = '';
|
|
213 |
this._verticesContainer.innerHTML = '';
|
|
214 |
this._groupsContainer.innerHTML = '';
|
|
215 |
}
|
212 |
216 |
|
213 |
|
function onMouseWheel(e) {
|
|
217 |
_onRootWheel(e) {
|
214 |
218 |
app.closeFloatingComponents();
|
215 |
219 |
|
216 |
220 |
// prevent pinch-to-zoom gesture from zooming the whole page
|
... | ... | |
225 |
229 |
}
|
226 |
230 |
}
|
227 |
231 |
|
228 |
|
function onMouseDown(e) {
|
229 |
|
pan = true;
|
|
232 |
_onRootMouseDown(e) {
|
|
233 |
let start = new Coordinates(e.clientX, e.clientY);
|
|
234 |
let viewportPosition = this.position;
|
230 |
235 |
|
231 |
|
var start = new Coordinates(e.clientX, e.clientY);
|
232 |
|
var position = this.getPosition();
|
|
236 |
let that = this;
|
233 |
237 |
|
234 |
238 |
document.body.addEventListener('mousemove', mouseMove);
|
235 |
239 |
document.body.addEventListener('mouseup', mouseUp);
|
236 |
|
|
237 |
|
function mouseMove(e) {
|
238 |
|
if (!pan) return;
|
239 |
|
|
240 |
|
e.preventDefault();
|
241 |
240 |
|
242 |
|
var coords = new Coordinates(position.x - (start.x - e.clientX), position.y - (start.y - e.clientY));
|
|
241 |
function mouseMove(e) {
|
|
242 |
let offset = new Coordinates(start.x - e.clientX, start.y - e.clientY);
|
243 |
243 |
|
244 |
|
innerSvgElement.setAttribute('x', coords.x);
|
245 |
|
innerSvgElement.setAttribute('y', coords.y);
|
|
244 |
that._innerSvgElement.setAttribute('x', viewportPosition.x - offset.x);
|
|
245 |
that._innerSvgElement.setAttribute('y', viewportPosition.y - offset.y);
|
246 |
246 |
|
247 |
|
app.sidebarComponent.minimapComponent.viewportPosition = coords;
|
|
247 |
app.sidebarComponent.minimapComponent.viewportPosition = new Coordinates(viewportPosition.x - offset.x, viewportPosition.y - offset.y);
|
248 |
248 |
}
|
249 |
249 |
|
250 |
|
function mouseUp(e) {
|
251 |
|
pan = false;
|
252 |
|
|
253 |
|
start = null;
|
254 |
|
position = null;
|
255 |
|
|
|
250 |
function mouseUp() {
|
256 |
251 |
document.body.removeEventListener('mousemove', mouseMove);
|
257 |
252 |
document.body.removeEventListener('mouseup', mouseUp);
|
258 |
|
|
|
253 |
|
259 |
254 |
app.redrawEdges();
|
260 |
255 |
}
|
261 |
256 |
}
|
262 |
257 |
|
263 |
|
function onDoubleClick(e) {
|
|
258 |
_onRootDoubleClick(e) {
|
264 |
259 |
app.closeFloatingComponents();
|
265 |
260 |
|
266 |
261 |
app.zoom.zoomIn(new Coordinates(e.offsetX, e.offsetY));
|
reworked Viewport to ES6 class