0
|
1 /**
|
|
2 * Popoto.js is a JavaScript library built with D3.js providing a graph based search interface generated in HTML and SVG usable on any modern browser.
|
|
3 * This library generates an interactive graph query builder into any website or web based application to create dynamic queries on Neo4j databases and display the results.
|
|
4 *
|
|
5 * Copyright (C) 2014-2015 Frederic Ciminera
|
|
6 *
|
|
7 * This program is free software: you can redistribute it and/or modify
|
|
8 * it under the terms of the GNU General Public License as published by
|
|
9 * the Free Software Foundation, either version 3 of the License, or
|
|
10 * (at your option) any later version.
|
|
11 *
|
|
12 * This program is distributed in the hope that it will be useful,
|
|
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15 * GNU General Public License for more details.
|
|
16 *
|
|
17 * You should have received a copy of the GNU General Public License
|
|
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19 *
|
|
20 * contact@popotojs.com
|
|
21 */
|
|
22 popoto = function () {
|
|
23 var popoto = {
|
|
24 version: "0.0-a6"
|
|
25 };
|
|
26
|
|
27 /**
|
|
28 * Main function to call to use Popoto.js.
|
|
29 * This function will create all the HTML content based on available IDs in the page.
|
|
30 * popoto.graph.containerId for the graph query builder.
|
|
31 * popoto.queryviewer.containerId for the query viewer.
|
|
32 *
|
|
33 * @param label Root label to use in the graph query builder.
|
|
34 */
|
|
35 popoto.start = function (label) {
|
|
36 popoto.logger.info("Popoto " + popoto.version + " start.");
|
|
37
|
|
38 if (typeof popoto.rest.CYPHER_URL == 'undefined') {
|
|
39 popoto.logger.error("popoto.rest.CYPHER_URL is not set but this property is required.");
|
|
40 } else {
|
|
41 // TODO introduce component generator mechanism instead for future plugin extensions
|
|
42 popoto.checkHtmlComponents();
|
|
43
|
|
44 if (popoto.taxonomy.isActive) {
|
|
45 popoto.taxonomy.createTaxonomyPanel();
|
|
46 }
|
|
47
|
|
48 if (popoto.graph.isActive) {
|
|
49 popoto.graph.createGraphArea();
|
|
50 popoto.graph.createForceLayout();
|
|
51 popoto.graph.addRootNode(label);
|
|
52 }
|
|
53
|
|
54 if (popoto.queryviewer.isActive) {
|
|
55 popoto.queryviewer.createQueryArea();
|
|
56 }
|
|
57
|
|
58 popoto.update();
|
|
59 }
|
|
60 };
|
|
61
|
|
62 /**
|
|
63 * Check in the HTML page the components to generate.
|
|
64 */
|
|
65 popoto.checkHtmlComponents = function () {
|
|
66 var graphHTMLContainer = d3.select("#" + popoto.graph.containerId);
|
|
67 var taxonomyHTMLContainer = d3.select("#" + popoto.taxonomy.containerId);
|
|
68 var queryHTMLContainer = d3.select("#" + popoto.queryviewer.containerId);
|
|
69 var cypherHTMLContainer = d3.select("#" + popoto.cypherviewer.containerId);
|
|
70 var resultsHTMLContainer = d3.select("#" + popoto.result.containerId);
|
|
71
|
|
72 if (graphHTMLContainer.empty()) {
|
|
73 popoto.logger.debug("The page doesn't contain a container with ID = \"" + popoto.graph.containerId + "\" no graph area will be generated. This ID is defined in popoto.graph.containerId property.");
|
|
74 popoto.graph.isActive = false;
|
|
75 } else {
|
|
76 popoto.graph.isActive = true;
|
|
77 }
|
|
78
|
|
79 if (taxonomyHTMLContainer.empty()) {
|
|
80 popoto.logger.debug("The page doesn't contain a container with ID = \"" + popoto.taxonomy.containerId + "\" no taxonomy filter will be generated. This ID is defined in popoto.taxonomy.containerId property.");
|
|
81 popoto.taxonomy.isActive = false;
|
|
82 } else {
|
|
83 popoto.taxonomy.isActive = true;
|
|
84 }
|
|
85
|
|
86 if (queryHTMLContainer.empty()) {
|
|
87 popoto.logger.debug("The page doesn't contain a container with ID = \"" + popoto.queryviewer.containerId + "\" no query viewer will be generated. This ID is defined in popoto.queryviewer.containerId property.");
|
|
88 popoto.queryviewer.isActive = false;
|
|
89 } else {
|
|
90 popoto.queryviewer.isActive = true;
|
|
91 }
|
|
92
|
|
93 if (cypherHTMLContainer.empty()) {
|
|
94 popoto.logger.debug("The page doesn't contain a container with ID = \"" + popoto.cypherviewer.containerId + "\" no cypher query viewer will be generated. This ID is defined in popoto.cypherviewer.containerId property.");
|
|
95 popoto.cypherviewer.isActive = false;
|
|
96 } else {
|
|
97 popoto.cypherviewer.isActive = true;
|
|
98 }
|
|
99
|
|
100 if (resultsHTMLContainer.empty()) {
|
|
101 popoto.logger.debug("The page doesn't contain a container with ID = \"" + popoto.result.containerId + "\" no result area will be generated. This ID is defined in popoto.result.containerId property.");
|
|
102 popoto.result.isActive = false;
|
|
103 } else {
|
|
104 popoto.result.isActive = true;
|
|
105 }
|
|
106 };
|
|
107
|
|
108 /**
|
|
109 * Function to call to update all the generated elements including svg graph, query viewer and generated results.
|
|
110 */
|
|
111 popoto.update = function () {
|
|
112 popoto.updateGraph();
|
|
113
|
|
114 if (popoto.queryviewer.isActive) {
|
|
115 popoto.queryviewer.updateQuery();
|
|
116 }
|
|
117 // Results are updated only if needed.
|
|
118 // If id found in html page or if result listeners have been added.
|
|
119 // In this case the query must be executed.
|
|
120 if (popoto.result.isActive || popoto.result.resultListeners.length > 0 || popoto.result.resultCountListeners.length > 0) {
|
|
121 popoto.result.updateResults();
|
|
122 }
|
|
123 };
|
|
124
|
|
125 /**
|
|
126 * Function to call to update the graph only.
|
|
127 */
|
|
128 popoto.updateGraph = function () {
|
|
129 if (popoto.graph.isActive) {
|
|
130 // Starts the D3.js force simulation.
|
|
131 // This method must be called when the layout is first created, after assigning the nodes and links.
|
|
132 // In addition, it should be called again whenever the nodes or links change.
|
|
133 popoto.graph.force.start();
|
|
134 popoto.graph.link.updateLinks();
|
|
135 popoto.graph.node.updateNodes();
|
|
136 }
|
|
137 };
|
|
138
|
|
139 // REST ------------------------------------------------------------------------------------------------------------
|
|
140 popoto.rest = {};
|
|
141
|
|
142 /**
|
|
143 * Default REST URL used to call Neo4j server with cypher queries to execute.
|
|
144 * This property should be updated to access to your own server.
|
|
145 * @type {string}
|
|
146 */
|
|
147 popoto.rest.CYPHER_URL = "http://localhost:7474/db/data/transaction/commit";
|
|
148
|
|
149 /**
|
|
150 * Create JQuery ajax POST request to access Neo4j REST API.
|
|
151 *
|
|
152 * @param data data object containing Cypher query
|
|
153 * @returns {*} the JQuery ajax request object.
|
|
154 */
|
|
155 popoto.rest.post = function (data) {
|
|
156 var strData = JSON.stringify(data);
|
|
157 popoto.logger.info("REST POST:" + strData);
|
|
158
|
|
159 return $.ajax({
|
|
160 type: "POST",
|
|
161 beforeSend: function (request) {
|
|
162 if (popoto.rest.AUTHORIZATION) {
|
|
163 request.setRequestHeader("Authorization", popoto.rest.AUTHORIZATION);
|
|
164 }
|
|
165 },
|
|
166 url: popoto.rest.CYPHER_URL,
|
|
167 contentType: "application/json",
|
|
168 data: strData
|
|
169 });
|
|
170 };
|
|
171
|
|
172 // LOGGER -----------------------------------------------------------------------------------------------------------
|
|
173 popoto.logger = {};
|
|
174 popoto.logger.LogLevels = Object.freeze({DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, NONE: 4});
|
|
175 popoto.logger.LEVEL = popoto.logger.LogLevels.NONE;
|
|
176 popoto.logger.TRACE = false;
|
|
177
|
|
178 /**
|
|
179 * Log a message on console depending on configured log levels.
|
|
180 * Level is define in popoto.logger.LEVEL property.
|
|
181 * If popoto.logger.TRACE is set to true, the stack trace is also added in log.
|
|
182 * @param logLevel Level of the message from popoto.logger.LogLevels.
|
|
183 * @param message Message to log.
|
|
184 */
|
|
185 popoto.logger.log = function (logLevel, message) {
|
|
186 if (console && logLevel >= popoto.logger.LEVEL) {
|
|
187 if (popoto.logger.TRACE) {
|
|
188 message = message + "\n" + new Error().stack
|
|
189 }
|
|
190 switch (logLevel) {
|
|
191 case popoto.logger.LogLevels.DEBUG:
|
|
192 console.log(message);
|
|
193 break;
|
|
194 case popoto.logger.LogLevels.INFO:
|
|
195 console.log(message);
|
|
196 break;
|
|
197 case popoto.logger.LogLevels.WARN:
|
|
198 console.warn(message);
|
|
199 break;
|
|
200 case popoto.logger.LogLevels.ERROR:
|
|
201 console.error(message);
|
|
202 break;
|
|
203 }
|
|
204 }
|
|
205 };
|
|
206
|
|
207 /**
|
|
208 * Log a message in DEBUG level.
|
|
209 * @param message to log.
|
|
210 */
|
|
211 popoto.logger.debug = function (message) {
|
|
212 popoto.logger.log(popoto.logger.LogLevels.DEBUG, message);
|
|
213 };
|
|
214
|
|
215 /**
|
|
216 * Log a message in INFO level.
|
|
217 * @param message to log.
|
|
218 */
|
|
219 popoto.logger.info = function (message) {
|
|
220 popoto.logger.log(popoto.logger.LogLevels.INFO, message);
|
|
221 };
|
|
222
|
|
223 /**
|
|
224 * Log a message in WARN level.
|
|
225 * @param message to log.
|
|
226 */
|
|
227 popoto.logger.warn = function (message) {
|
|
228 popoto.logger.log(popoto.logger.LogLevels.WARN, message);
|
|
229 };
|
|
230
|
|
231 /**
|
|
232 * Log a message in ERROR level.
|
|
233 * @param message to log.
|
|
234 */
|
|
235 popoto.logger.error = function (message) {
|
|
236 popoto.logger.log(popoto.logger.LogLevels.ERROR, message);
|
|
237 };
|
|
238
|
|
239 // TAXONOMIES -----------------------------------------------------------------------------------------------------
|
|
240
|
|
241 popoto.taxonomy = {};
|
|
242 popoto.taxonomy.containerId = "popoto-taxonomy";
|
|
243
|
|
244 /**
|
|
245 * Create the taxonomy panel HTML elements.
|
|
246 */
|
|
247 popoto.taxonomy.createTaxonomyPanel = function () {
|
|
248 var htmlContainer = d3.select("#" + popoto.taxonomy.containerId);
|
|
249
|
|
250 var taxoUL = htmlContainer.append("ul");
|
|
251
|
|
252 var data = popoto.taxonomy.generateTaxonomiesData();
|
|
253
|
|
254 var taxos = taxoUL.selectAll(".taxo").data(data);
|
|
255
|
|
256 var taxoli = taxos.enter().append("li")
|
|
257 .attr("id", function (d) {
|
|
258 return d.id
|
|
259 })
|
|
260 .attr("value", function (d) {
|
|
261 return d.label;
|
|
262 });
|
|
263
|
|
264 taxoli.append("img")
|
|
265 .attr("src", "css/image/category.png")
|
|
266 .attr("width", "24")
|
|
267 .attr("height", "24");
|
|
268
|
|
269 taxoli.append("span")
|
|
270 .attr("class", "ppt-label")
|
|
271 .text(function (d) {
|
|
272 return popoto.provider.getTaxonomyTextValue(d.label);
|
|
273 });
|
|
274
|
|
275 taxoli.append("span")
|
|
276 .attr("class", "ppt-count");
|
|
277
|
|
278 // Add an on click event on the taxonomy to clear the graph and set this label as root
|
|
279 taxoli.on("click", popoto.taxonomy.onClick);
|
|
280
|
|
281 popoto.taxonomy.addTaxonomyChildren(taxoli);
|
|
282
|
|
283 // The count is updated for each labels.
|
|
284 var flattenData = [];
|
|
285 data.forEach(function (d) {
|
|
286 flattenData.push(d);
|
|
287 if (d.children) {
|
|
288 popoto.taxonomy.flattenChildren(d, flattenData);
|
|
289 }
|
|
290 });
|
|
291
|
|
292 popoto.taxonomy.updateCount(flattenData);
|
|
293 };
|
|
294
|
|
295 /**
|
|
296 * Recursive function to flatten data content.
|
|
297 *
|
|
298 */
|
|
299 popoto.taxonomy.flattenChildren = function (d, vals) {
|
|
300 d.children.forEach(function (c) {
|
|
301 vals.push(c);
|
|
302 if (c.children) {
|
|
303 vals.concat(popoto.taxonomy.flattenChildren(c, vals));
|
|
304 }
|
|
305 });
|
|
306 };
|
|
307
|
|
308 /**
|
|
309 * Updates the count number on a taxonomy.
|
|
310 *
|
|
311 * @param taxonomyData
|
|
312 */
|
|
313 popoto.taxonomy.updateCount = function (taxonomyData) {
|
|
314 var statements = [];
|
|
315
|
|
316 taxonomyData.forEach(function (taxo) {
|
|
317 statements.push(
|
|
318 {
|
|
319 "statement": popoto.query.generateTaxonomyCountQuery(taxo.label)
|
|
320 }
|
|
321 );
|
|
322 });
|
|
323
|
|
324 (function (taxonomies) {
|
|
325 popoto.logger.info("Count taxonomies ==> ");
|
|
326 popoto.rest.post(
|
|
327 {
|
|
328 "statements": statements
|
|
329 })
|
|
330 .done(function (returnedData) {
|
|
331 for (var i = 0; i < taxonomies.length; i++) {
|
|
332 var count = returnedData.results[i].data[0].row[0];
|
|
333 d3.select("#" + taxonomies[i].id)
|
|
334 .select(".ppt-count")
|
|
335 .text(" (" + count + ")");
|
|
336 }
|
|
337 })
|
|
338 .fail(function (xhr, textStatus, errorThrown) {
|
|
339 popoto.logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + popoto.rest.CYPHER_URL + "\" defined in \"popoto.rest.CYPHER_URL\" property: " + errorThrown);
|
|
340 d3.select("#popoto-taxonomy")
|
|
341 .selectAll(".ppt-count")
|
|
342 .text(" (0)");
|
|
343 });
|
|
344 })(taxonomyData);
|
|
345 };
|
|
346
|
|
347 /**
|
|
348 * Recursively generate the taxonomy children elements.
|
|
349 *
|
|
350 * @param selection
|
|
351 */
|
|
352 popoto.taxonomy.addTaxonomyChildren = function (selection) {
|
|
353 selection.each(function (d) {
|
|
354 var li = d3.select(this);
|
|
355
|
|
356 var children = d.children;
|
|
357 if (d.children) {
|
|
358 var childLi = li.append("ul")
|
|
359 .selectAll("li")
|
|
360 .data(children)
|
|
361 .enter()
|
|
362 .append("li")
|
|
363 .attr("id", function (d) {
|
|
364 return d.id
|
|
365 })
|
|
366 .attr("value", function (d) {
|
|
367 return d.label;
|
|
368 });
|
|
369
|
|
370 childLi.append("img")
|
|
371 .attr("src", "css/image/category.png")
|
|
372 .attr("width", "24")
|
|
373 .attr("height", "24");
|
|
374
|
|
375 childLi.append("span")
|
|
376 .attr("class", "ppt-label")
|
|
377 .text(function (d) {
|
|
378 return popoto.provider.getTaxonomyTextValue(d.label);
|
|
379 });
|
|
380
|
|
381 childLi.append("span")
|
|
382 .attr("class", "ppt-count");
|
|
383
|
|
384 childLi.on("click", popoto.taxonomy.onClick);
|
|
385
|
|
386 popoto.taxonomy.addTaxonomyChildren(childLi);
|
|
387 }
|
|
388
|
|
389 });
|
|
390 };
|
|
391
|
|
392 popoto.taxonomy.onClick = function () {
|
|
393 d3.event.stopPropagation();
|
|
394
|
|
395 // Workaround to avoid click on taxonomies if root node has not yet been initialized
|
|
396 // If it contains a count it mean all the initialization has been done
|
|
397 var root = popoto.graph.getRootNode();
|
|
398 if (root.count === undefined) {
|
|
399 return;
|
|
400 }
|
|
401
|
|
402 var label = this.attributes.value.value;
|
|
403
|
|
404 while (popoto.graph.force.nodes().length > 0) {
|
|
405 popoto.graph.force.nodes().pop();
|
|
406 }
|
|
407
|
|
408 while (popoto.graph.force.links().length > 0) {
|
|
409 popoto.graph.force.links().pop();
|
|
410 }
|
|
411
|
|
412 // Reinitialize internal label generator
|
|
413 popoto.graph.node.internalLabels = {};
|
|
414
|
|
415 popoto.update();
|
|
416 popoto.graph.addRootNode(label);
|
|
417 popoto.graph.hasGraphChanged = true;
|
|
418 popoto.result.hasChanged = true;
|
|
419 popoto.update();
|
|
420 popoto.tools.center();
|
|
421 };
|
|
422
|
|
423 /**
|
|
424 * Parse the list of label providers and return a list of data object containing only searchable labels.
|
|
425 * @returns {Array}
|
|
426 */
|
|
427 popoto.taxonomy.generateTaxonomiesData = function () {
|
|
428 var id = 0;
|
|
429 var data = [];
|
|
430
|
|
431 // Retrieve root providers (searchable and without parent)
|
|
432 for (var label in popoto.provider.nodeProviders) {
|
|
433 if (popoto.provider.nodeProviders.hasOwnProperty(label)) {
|
|
434 if (popoto.provider.getProperty(label, "isSearchable") && !popoto.provider.nodeProviders[label].parent) {
|
|
435 data.push({
|
|
436 "label": label,
|
|
437 "id": "popoto-lbl-" + id++
|
|
438 });
|
|
439 }
|
|
440 }
|
|
441 }
|
|
442
|
|
443 // Add children data for each provider with children.
|
|
444 data.forEach(function (d) {
|
|
445 if (popoto.provider.getProvider(d.label).hasOwnProperty("children")) {
|
|
446 id = popoto.taxonomy.addChildrenData(d, id);
|
|
447 }
|
|
448 });
|
|
449
|
|
450 return data;
|
|
451 };
|
|
452
|
|
453 /**
|
|
454 * Add children providers data.
|
|
455 * @param parentData
|
|
456 * @param id
|
|
457 */
|
|
458 popoto.taxonomy.addChildrenData = function (parentData, id) {
|
|
459 parentData.children = [];
|
|
460
|
|
461 popoto.provider.getProvider(parentData.label).children.forEach(function (d) {
|
|
462 var childProvider = popoto.provider.getProvider(d);
|
|
463 var childData = {
|
|
464 "label": d,
|
|
465 "id": "popoto-lbl-" + id++
|
|
466 };
|
|
467 if (childProvider.hasOwnProperty("children")) {
|
|
468 id = popoto.taxonomy.addChildrenData(childData, id);
|
|
469 }
|
|
470 if (popoto.provider.getProperty(d, "isSearchable")) {
|
|
471 parentData.children.push(childData);
|
|
472 }
|
|
473 });
|
|
474
|
|
475 return id;
|
|
476 };
|
|
477
|
|
478 // TOOLS -----------------------------------------------------------------------------------------------------------
|
|
479
|
|
480 popoto.tools = {};
|
|
481 // TODO introduce plugin mechanism to add tools
|
|
482 popoto.tools.CENTER_GRAPH = true;
|
|
483 popoto.tools.RESET_GRAPH = true;
|
|
484 popoto.tools.TOGGLE_TAXONOMY = true;
|
|
485 popoto.tools.TOGGLE_FULL_SCREEN = true;
|
|
486
|
|
487 /**
|
|
488 * Reset all the graph to display the root node only.
|
|
489 */
|
|
490 popoto.tools.reset = function () {
|
|
491 var label = popoto.graph.getRootNode().label;
|
|
492
|
|
493 while (popoto.graph.force.nodes().length > 0) {
|
|
494 popoto.graph.force.nodes().pop();
|
|
495 }
|
|
496
|
|
497 while (popoto.graph.force.links().length > 0) {
|
|
498 popoto.graph.force.links().pop();
|
|
499 }
|
|
500
|
|
501 // Reinitialize internal label generator
|
|
502 popoto.graph.node.internalLabels = {};
|
|
503
|
|
504 popoto.update();
|
|
505 popoto.graph.addRootNode(label);
|
|
506 popoto.graph.hasGraphChanged = true;
|
|
507 popoto.result.hasChanged = true;
|
|
508 popoto.update();
|
|
509 popoto.tools.center();
|
|
510 };
|
|
511
|
|
512 /**
|
|
513 * Reset zoom and center the view on svg center.
|
|
514 */
|
|
515 popoto.tools.center = function () {
|
|
516 popoto.graph.zoom.translate([0, 0]).scale(1);
|
|
517 popoto.graph.svg.transition().attr("transform", "translate(" + popoto.graph.zoom.translate() + ")" + " scale(" + popoto.graph.zoom.scale() + ")");
|
|
518 };
|
|
519
|
|
520 /**
|
|
521 * Show, hide taxonomy panel.
|
|
522 */
|
|
523 popoto.tools.toggleTaxonomy = function () {
|
|
524 var taxo = d3.select("#" + popoto.taxonomy.containerId);
|
|
525 if (taxo.filter(".disabled").empty()) {
|
|
526 taxo.classed("disabled", true);
|
|
527 } else {
|
|
528 taxo.classed("disabled", false);
|
|
529 }
|
|
530 };
|
|
531
|
|
532 popoto.tools.toggleFullScreen = function () {
|
|
533
|
|
534 var elem = document.getElementById(popoto.graph.containerId);
|
|
535
|
|
536 if (!document.fullscreenElement && // alternative standard method
|
|
537 !document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { // current working methods
|
|
538 if (elem.requestFullscreen) {
|
|
539 elem.requestFullscreen();
|
|
540 } else if (elem.msRequestFullscreen) {
|
|
541 elem.msRequestFullscreen();
|
|
542 } else if (elem.mozRequestFullScreen) {
|
|
543 elem.mozRequestFullScreen();
|
|
544 } else if (elem.webkitRequestFullscreen) {
|
|
545 elem.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
|
|
546 }
|
|
547 } else {
|
|
548 if (document.exitFullscreen) {
|
|
549 document.exitFullscreen();
|
|
550 } else if (document.msExitFullscreen) {
|
|
551 document.msExitFullscreen();
|
|
552 } else if (document.mozCancelFullScreen) {
|
|
553 document.mozCancelFullScreen();
|
|
554 } else if (document.webkitExitFullscreen) {
|
|
555 document.webkitExitFullscreen();
|
|
556 }
|
|
557 }
|
|
558 };
|
|
559 // GRAPH -----------------------------------------------------------------------------------------------------------
|
|
560
|
|
561 popoto.graph = {};
|
|
562
|
|
563 /**
|
|
564 * ID of the HTML component where the graph query builder elements will be generated in.
|
|
565 * @type {string}
|
|
566 */
|
|
567 popoto.graph.containerId = "popoto-graph";
|
|
568 popoto.graph.hasGraphChanged = true;
|
|
569 // Defines the min and max level of zoom available in graph query builder.
|
|
570 popoto.graph.zoom = d3.behavior.zoom().scaleExtent([0.1, 10]);
|
|
571 popoto.graph.WHEEL_ZOOM_ENABLED = true;
|
|
572 popoto.graph.TOOL_TAXONOMY = "Show/hide taxonomy panel";
|
|
573 popoto.graph.TOOL_CENTER = "Center view";
|
|
574 popoto.graph.TOOL_FULL_SCREEN = "Full screen";
|
|
575 popoto.graph.TOOL_RESET = "Reset graph";
|
|
576
|
|
577 /**
|
|
578 * Define the list of listenable events on graph.
|
|
579 */
|
|
580 popoto.graph.Events = Object.freeze({NODE_ROOT_ADD: "root.node.add", NODE_EXPAND_RELATIONSHIP: "node.expandRelationship"});
|
|
581
|
|
582 /**
|
|
583 * Generates all the HTML and SVG element needed to display the graph query builder.
|
|
584 * Everything will be generated in the container with id defined by popoto.graph.containerId.
|
|
585 */
|
|
586 popoto.graph.createGraphArea = function () {
|
|
587
|
|
588 var htmlContainer = d3.select("#" + popoto.graph.containerId);
|
|
589
|
|
590 var toolbar = htmlContainer
|
|
591 .append("div")
|
|
592 .attr("class", "ppt-toolbar");
|
|
593
|
|
594 if (popoto.tools.RESET_GRAPH) {
|
|
595 toolbar.append("span")
|
|
596 .attr("id", "popoto-reset-menu")
|
|
597 .attr("class", "ppt-menu reset")
|
|
598 .attr("title", popoto.graph.TOOL_RESET)
|
|
599 .on("click", popoto.tools.reset);
|
|
600 }
|
|
601
|
|
602 if (popoto.taxonomy.isActive && popoto.tools.TOGGLE_TAXONOMY) {
|
|
603 toolbar.append("span")
|
|
604 .attr("id", "popoto-taxonomy-menu")
|
|
605 .attr("class", "ppt-menu taxonomy")
|
|
606 .attr("title", popoto.graph.TOOL_TAXONOMY)
|
|
607 .on("click", popoto.tools.toggleTaxonomy);
|
|
608 }
|
|
609
|
|
610 if (popoto.tools.CENTER_GRAPH) {
|
|
611 toolbar.append("span")
|
|
612 .attr("id", "popoto-center-menu")
|
|
613 .attr("class", "ppt-menu center")
|
|
614 .attr("title", popoto.graph.TOOL_CENTER)
|
|
615 .on("click", popoto.tools.center);
|
|
616 }
|
|
617
|
|
618 if (popoto.tools.TOGGLE_FULL_SCREEN) {
|
|
619 toolbar.append("span")
|
|
620 .attr("id", "popoto-fullscreen-menu")
|
|
621 .attr("class", "ppt-menu fullscreen")
|
|
622 .attr("title", popoto.graph.TOOL_FULL_SCREEN)
|
|
623 .on("click", popoto.tools.toggleFullScreen);
|
|
624 }
|
|
625
|
|
626 var svgTag = htmlContainer.append("svg").call(popoto.graph.zoom.on("zoom", popoto.graph.rescale));
|
|
627
|
|
628 svgTag.on("dblclick.zoom", null)
|
|
629 .attr("class", "ppt-svg-graph");
|
|
630
|
|
631 if (!popoto.graph.WHEEL_ZOOM_ENABLED) {
|
|
632 // Disable mouse wheel events.
|
|
633 svgTag.on("wheel.zoom", null)
|
|
634 .on("mousewheel.zoom", null);
|
|
635 }
|
|
636
|
|
637 popoto.graph.svg = svgTag.append('svg:g');
|
|
638
|
|
639 // Create two separated area for links and nodes
|
|
640 // Links and nodes are separated in a dedicated "g" element
|
|
641 // and nodes are generated after links to ensure that nodes are always on foreground.
|
|
642 popoto.graph.svg.append("g").attr("id", popoto.graph.link.gID);
|
|
643 popoto.graph.svg.append("g").attr("id", popoto.graph.node.gID);
|
|
644
|
|
645 // This listener is used to center the root node in graph during a window resize.
|
|
646 // TODO can the listener be limited on the parent container only?
|
|
647 window.addEventListener('resize', popoto.graph.centerRootNode);
|
|
648 };
|
|
649
|
|
650 popoto.graph.centerRootNode = function () {
|
|
651 popoto.graph.getRootNode().px = popoto.graph.getSVGWidth() / 2;
|
|
652 popoto.graph.getRootNode().py = popoto.graph.getSVGHeight() / 2;
|
|
653 popoto.update();
|
|
654 };
|
|
655
|
|
656 /**
|
|
657 * Get the actual width of the SVG element containing the graph query builder.
|
|
658 * @returns {number}
|
|
659 */
|
|
660 popoto.graph.getSVGWidth = function () {
|
|
661 if (typeof popoto.graph.svg == 'undefined' || popoto.graph.svg.empty()) {
|
|
662 popoto.logger.debug("popoto.graph.svg is undefined or empty.");
|
|
663 return 0;
|
|
664 } else {
|
|
665 return document.getElementById(popoto.graph.containerId).clientWidth;
|
|
666 }
|
|
667 };
|
|
668
|
|
669 /**
|
|
670 * Get the actual height of the SVG element containing the graph query builder.
|
|
671 * @returns {number}
|
|
672 */
|
|
673 popoto.graph.getSVGHeight = function () {
|
|
674 if (typeof popoto.graph.svg == 'undefined' || popoto.graph.svg.empty()) {
|
|
675 popoto.logger.debug("popoto.graph.svg is undefined or empty.");
|
|
676 return 0;
|
|
677 } else {
|
|
678 return document.getElementById(popoto.graph.containerId).clientHeight;
|
|
679 }
|
|
680 };
|
|
681
|
|
682 /**
|
|
683 * Function to call on SVG zoom event to update the svg transform attribute.
|
|
684 */
|
|
685 popoto.graph.rescale = function () {
|
|
686 var trans = d3.event.translate,
|
|
687 scale = d3.event.scale;
|
|
688
|
|
689 popoto.graph.svg.attr("transform",
|
|
690 "translate(" + trans + ")"
|
|
691 + " scale(" + scale + ")");
|
|
692 };
|
|
693
|
|
694 /******************************
|
|
695 * Default parameters used to configure D3.js force layout.
|
|
696 * These parameter can be modified to change graph behavior.
|
|
697 ******************************/
|
|
698 popoto.graph.LINK_DISTANCE = 150;
|
|
699 popoto.graph.LINK_STRENGTH = 1;
|
|
700 popoto.graph.FRICTION = 0.8;
|
|
701 popoto.graph.CHARGE = -1400;
|
|
702 popoto.graph.THETA = 0.8;
|
|
703 popoto.graph.GRAVITY = 0.0;
|
|
704
|
|
705 /**
|
|
706 * Contains the list off root node add listeners.
|
|
707 */
|
|
708 popoto.graph.rootNodeAddListeners = [];
|
|
709 popoto.graph.nodeExpandRelationsipListeners = [];
|
|
710
|
|
711 /**
|
|
712 * Create the D3.js force layout for the graph query builder.
|
|
713 */
|
|
714 popoto.graph.createForceLayout = function () {
|
|
715
|
|
716 popoto.graph.force = d3.layout.force()
|
|
717 .size([popoto.graph.getSVGWidth(), popoto.graph.getSVGHeight()])
|
|
718 .linkDistance(function (d) {
|
|
719 if (d.type === popoto.graph.link.LinkTypes.RELATION) {
|
|
720 return ((3 * popoto.graph.LINK_DISTANCE) / 2);
|
|
721 } else {
|
|
722 return popoto.graph.LINK_DISTANCE;
|
|
723 }
|
|
724 })
|
|
725 .linkStrength(function (d) {
|
|
726 if (d.linkStrength) {
|
|
727 return d.linkStrength;
|
|
728 } else {
|
|
729 return popoto.graph.LINK_STRENGTH;
|
|
730 }
|
|
731 })
|
|
732 .friction(popoto.graph.FRICTION)
|
|
733 .charge(function (d) {
|
|
734 if (d.charge) {
|
|
735 return d.charge;
|
|
736 } else {
|
|
737 return popoto.graph.CHARGE;
|
|
738 }
|
|
739 })
|
|
740 .theta(popoto.graph.THETA)
|
|
741 .gravity(popoto.graph.GRAVITY)
|
|
742 .on("tick", popoto.graph.tick); // Function called on every position update done by D3.js
|
|
743
|
|
744 // Disable event propagation on drag to avoid zoom and pan issues
|
|
745 popoto.graph.force.drag()
|
|
746 .on("dragstart", function (d) {
|
|
747 d3.event.sourceEvent.stopPropagation();
|
|
748 })
|
|
749 .on("dragend", function (d) {
|
|
750 d3.event.sourceEvent.stopPropagation();
|
|
751 });
|
|
752 };
|
|
753
|
|
754 /**
|
|
755 * Add a listener to the specified event.
|
|
756 *
|
|
757 * @param event name of the event to add the listener.
|
|
758 * @param listener the listener to add.
|
|
759 */
|
|
760 popoto.graph.on = function (event, listener) {
|
|
761 if (event === popoto.graph.Events.NODE_ROOT_ADD) {
|
|
762 popoto.graph.rootNodeAddListeners.push(listener);
|
|
763 }
|
|
764 if (event === popoto.graph.Events.NODE_EXPAND_RELATIONSHIP) {
|
|
765 popoto.graph.nodeExpandRelationsipListeners.push(listener);
|
|
766 }
|
|
767 };
|
|
768
|
|
769 /**
|
|
770 * Adds graph root nodes using the label set as parameter.
|
|
771 * All the other nodes should have been removed first to avoid inconsistent data.
|
|
772 *
|
|
773 * @param label label of the node to add as root.
|
|
774 */
|
|
775 popoto.graph.addRootNode = function (label) {
|
|
776 if (popoto.graph.force.nodes().length > 0) {
|
|
777 popoto.logger.debug("popoto.graph.addRootNode is called but the graph is not empty.");
|
|
778 }
|
|
779
|
|
780 popoto.graph.force.nodes().push({
|
|
781 "id": "0",
|
|
782 "type": popoto.graph.node.NodeTypes.ROOT,
|
|
783 // x and y coordinates are set to the center of the SVG area.
|
|
784 // These coordinate will never change at runtime except if the window is resized.
|
|
785 "x": popoto.graph.getSVGWidth() / 2,
|
|
786 "y": popoto.graph.getSVGHeight() / 2,
|
|
787 "label": label,
|
|
788 // The node is fixed to always remain in the center of the svg area.
|
|
789 // This property should not be changed at runtime to avoid issues with the zoom and pan.
|
|
790 "fixed": true,
|
|
791 // Label used internally to identify the node.
|
|
792 // This label is used for example as cypher query identifier.
|
|
793 "internalLabel": popoto.graph.node.generateInternalLabel(label)
|
|
794 });
|
|
795
|
|
796 // Notify listeners
|
|
797 popoto.graph.rootNodeAddListeners.forEach(function (listener) {
|
|
798 listener(popoto.graph.getRootNode());
|
|
799 });
|
|
800 };
|
|
801
|
|
802 /**
|
|
803 * Get the graph root node.
|
|
804 * @returns {*}
|
|
805 */
|
|
806 popoto.graph.getRootNode = function () {
|
|
807 return popoto.graph.force.nodes()[0];
|
|
808 };
|
|
809
|
|
810 /**
|
|
811 * Function to call on D3.js force layout tick event.
|
|
812 * This function will update the position of all links and nodes elements in the graph with the force layout computed coordinate.
|
|
813 */
|
|
814 popoto.graph.tick = function () {
|
|
815 popoto.graph.svg.selectAll("#" + popoto.graph.link.gID + " > g")
|
|
816 .selectAll("path")
|
|
817 .attr("d", function (d) {
|
|
818 var parentAngle = popoto.graph.computeParentAngle(d.target);
|
|
819 var targetX = d.target.x + (popoto.graph.link.RADIUS * Math.cos(parentAngle)),
|
|
820 targetY = d.target.y - (popoto.graph.link.RADIUS * Math.sin(parentAngle));
|
|
821
|
|
822 var sourceX = d.source.x - (popoto.graph.link.RADIUS * Math.cos(parentAngle)),
|
|
823 sourceY = d.source.y + (popoto.graph.link.RADIUS * Math.sin(parentAngle));
|
|
824
|
|
825 if (d.source.x <= d.target.x) {
|
|
826 return "M" + sourceX + " " + sourceY + "L" + targetX + " " + targetY;
|
|
827 } else {
|
|
828 return "M" + targetX + " " + targetY + "L" + sourceX + " " + sourceY;
|
|
829 }
|
|
830 });
|
|
831
|
|
832 popoto.graph.svg.selectAll("#" + popoto.graph.node.gID + " > g")
|
|
833 .attr("transform", function (d) {
|
|
834 return "translate(" + (d.x) + "," + (d.y) + ")";
|
|
835 });
|
|
836 };
|
|
837
|
|
838 // LINKS -----------------------------------------------------------------------------------------------------------
|
|
839 popoto.graph.link = {};
|
|
840
|
|
841 /**
|
|
842 * Defines the radius around the node to start link drawing.
|
|
843 * If set to 0 links will start from the middle of the node.
|
|
844 */
|
|
845 popoto.graph.link.RADIUS = 25;
|
|
846
|
|
847 // ID of the g element in SVG graph containing all the link elements.
|
|
848 popoto.graph.link.gID = "popoto-glinks";
|
|
849
|
|
850 /**
|
|
851 * Defines the different type of link.
|
|
852 * RELATION is a relation link between two nodes.
|
|
853 * VALUE is a link between a generic node and a value.
|
|
854 */
|
|
855 popoto.graph.link.LinkTypes = Object.freeze({RELATION: 0, VALUE: 1});
|
|
856
|
|
857 /**
|
|
858 * Function to call to update the links after modification in the model.
|
|
859 * This function will update the graph with all removed, modified or added links using d3.js mechanisms.
|
|
860 */
|
|
861 popoto.graph.link.updateLinks = function () {
|
|
862 popoto.graph.link.svgLinkElements = popoto.graph.svg.select("#" + popoto.graph.link.gID).selectAll("g");
|
|
863 popoto.graph.link.updateData();
|
|
864 popoto.graph.link.removeElements();
|
|
865 popoto.graph.link.addNewElements();
|
|
866 popoto.graph.link.updateElements();
|
|
867 };
|
|
868
|
|
869 /**
|
|
870 * Update the links element with data coming from popoto.graph.force.links().
|
|
871 */
|
|
872 popoto.graph.link.updateData = function () {
|
|
873 popoto.graph.link.svgLinkElements = popoto.graph.link.svgLinkElements.data(popoto.graph.force.links(), function (d) {
|
|
874 return d.id;
|
|
875 });
|
|
876 };
|
|
877
|
|
878 /**
|
|
879 * Clean links elements removed from the list.
|
|
880 */
|
|
881 popoto.graph.link.removeElements = function () {
|
|
882 popoto.graph.link.svgLinkElements.exit().remove();
|
|
883 };
|
|
884
|
|
885 /**
|
|
886 * Create new elements.
|
|
887 */
|
|
888 popoto.graph.link.addNewElements = function () {
|
|
889
|
|
890 var newLinkElements = popoto.graph.link.svgLinkElements.enter().append("g")
|
|
891 .attr("class", "ppt-glink")
|
|
892 .on("mouseover", popoto.graph.link.mouseOverLink)
|
|
893 .on("mouseout", popoto.graph.link.mouseOutLink);
|
|
894
|
|
895 newLinkElements.append("path");
|
|
896
|
|
897 newLinkElements.append("text")
|
|
898 .attr("text-anchor", "middle")
|
|
899 .attr("dy", "-4")
|
|
900 .append("textPath")
|
|
901 .attr("class", "ppt-textPath")
|
|
902 .attr("startOffset", "50%");
|
|
903
|
|
904 };
|
|
905
|
|
906 /**
|
|
907 * Update all the elements (new + modified)
|
|
908 */
|
|
909 popoto.graph.link.updateElements = function () {
|
|
910 popoto.graph.link.svgLinkElements
|
|
911 .attr("id", function (d) {
|
|
912 return "ppt-glink_" + d.id;
|
|
913 });
|
|
914
|
|
915 popoto.graph.link.svgLinkElements.selectAll("path")
|
|
916 .attr("id", function (d) {
|
|
917 return "ppt-path_" + d.id
|
|
918 })
|
|
919 .attr("class", function (d) {
|
|
920 if (d.type === popoto.graph.link.LinkTypes.VALUE) {
|
|
921 return "ppt-link-value";
|
|
922 } else {
|
|
923 if (d.target.count == 0) {
|
|
924 return "ppt-link-relation disabled";
|
|
925 } else {
|
|
926 if (d.target.value !== undefined) {
|
|
927 return "ppt-link-relation value";
|
|
928 } else {
|
|
929 return "ppt-link-relation";
|
|
930 }
|
|
931 }
|
|
932 }
|
|
933 });
|
|
934
|
|
935 // Due to a bug on webkit browsers (as of 30/01/2014) textPath cannot be selected
|
|
936 // To workaround this issue the selection is done with its associated css class
|
|
937 popoto.graph.link.svgLinkElements.selectAll("text")
|
|
938 .attr("id", function (d) {
|
|
939 return "ppt-text_" + d.id
|
|
940 })
|
|
941 .attr("class", function (d) {
|
|
942 if (d.type === popoto.graph.link.LinkTypes.VALUE) {
|
|
943 return "ppt-link-text-value";
|
|
944 } else {
|
|
945 if (d.target.count == 0) {
|
|
946 return "ppt-link-text-relation disabled";
|
|
947 } else {
|
|
948 if (d.target.value !== undefined) {
|
|
949 return "ppt-link-text-relation value";
|
|
950 } else {
|
|
951 return "ppt-link-text-relation";
|
|
952 }
|
|
953 }
|
|
954 }
|
|
955 })
|
|
956 .selectAll(".ppt-textPath")
|
|
957 .attr("id", function (d) {
|
|
958 return "ppt-textpath_" + d.id
|
|
959 })
|
|
960 .attr("xlink:href", function (d) {
|
|
961 return "#ppt-path_" + d.id
|
|
962 })
|
|
963 .text(function (d) {
|
|
964 return popoto.provider.getLinkTextValue(d);
|
|
965 });
|
|
966 };
|
|
967
|
|
968 /**
|
|
969 * Function called when mouse is over the link.
|
|
970 * This function is used to change the CSS class on hover of the link and query viewer element.
|
|
971 *
|
|
972 * TODO try to introduce event instead of directly access query spans here. This could be used in future extensions.
|
|
973 */
|
|
974 popoto.graph.link.mouseOverLink = function () {
|
|
975 d3.select(this).select("path").classed("ppt-link-hover", true);
|
|
976 d3.select(this).select("text").classed("ppt-link-hover", true);
|
|
977
|
|
978 if (popoto.queryviewer.isActive) {
|
|
979 var hoveredLink = d3.select(this).data()[0];
|
|
980
|
|
981 popoto.queryviewer.queryConstraintSpanElements.filter(function (d) {
|
|
982 return d.ref === hoveredLink;
|
|
983 }).classed("hover", true);
|
|
984 popoto.queryviewer.querySpanElements.filter(function (d) {
|
|
985 return d.ref === hoveredLink;
|
|
986 }).classed("hover", true);
|
|
987 }
|
|
988 };
|
|
989
|
|
990 /**
|
|
991 * Function called when mouse goes out of the link.
|
|
992 * This function is used to reinitialize the CSS class of the link and query viewer element.
|
|
993 */
|
|
994 popoto.graph.link.mouseOutLink = function () {
|
|
995 d3.select(this).select("path").classed("ppt-link-hover", false);
|
|
996 d3.select(this).select("text").classed("ppt-link-hover", false);
|
|
997
|
|
998 if (popoto.queryviewer.isActive) {
|
|
999 var hoveredLink = d3.select(this).data()[0];
|
|
1000
|
|
1001 popoto.queryviewer.queryConstraintSpanElements.filter(function (d) {
|
|
1002 return d.ref === hoveredLink;
|
|
1003 }).classed("hover", false);
|
|
1004 popoto.queryviewer.querySpanElements.filter(function (d) {
|
|
1005 return d.ref === hoveredLink;
|
|
1006 }).classed("hover", false);
|
|
1007 }
|
|
1008 };
|
|
1009
|
|
1010 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
1011 // NODES -----------------------------------------------------------------------------------------------------------
|
|
1012
|
|
1013 popoto.graph.node = {};
|
|
1014
|
|
1015 // ID of the g element in SVG graph containing all the link elements.
|
|
1016 popoto.graph.node.gID = "popoto-gnodes";
|
|
1017
|
|
1018 // Node ellipse size used by default for text nodes.
|
|
1019 popoto.graph.node.ELLIPSE_RX = 50;
|
|
1020 popoto.graph.node.ELLIPSE_RY = 25;
|
|
1021 popoto.graph.node.TEXT_Y = 8;
|
|
1022 popoto.graph.node.BACK_CIRCLE_R = 70;
|
|
1023 // Define the max number of character displayed in ellipses.
|
|
1024 popoto.graph.node.NODE_MAX_CHARS = 11;
|
|
1025
|
|
1026 // Number of nodes displayed per page during value selection.
|
|
1027 popoto.graph.node.PAGE_SIZE = 10;
|
|
1028
|
|
1029 // Count box default size
|
|
1030 popoto.graph.node.CountBox = {x: 16, y: 33, w: 52, h: 19};
|
|
1031
|
|
1032 // Store choose node state to avoid multiple node expand at the same time
|
|
1033 popoto.graph.node.chooseWaiting = false;
|
|
1034
|
|
1035 /**
|
|
1036 * Defines the list of possible nodes.
|
|
1037 * ROOT: Node used as graph root. It is the target of the query. Only one node of this type should be available in graph.
|
|
1038 * CHOOSE: Nodes defining a generic node label. From these node is is possible to select a value or explore relations.
|
|
1039 * VALUE: Unique node containing a value constraint. Usually replace CHOOSE nodes once a value as been selected.
|
|
1040 * GROUP: Empty node used to group relations. No value can be selected but relations can be explored. These nodes doesn't have count.
|
|
1041 */
|
|
1042 popoto.graph.node.NodeTypes = Object.freeze({ROOT: 0, CHOOSE: 1, VALUE: 2, GROUP: 3});
|
|
1043
|
|
1044 // Variable used to generate unique id for each new nodes.
|
|
1045 popoto.graph.node.idgen = 0;
|
|
1046
|
|
1047 // Used to generate unique internal labels used for example as identifier in Cypher query.
|
|
1048 popoto.graph.node.internalLabels = {};
|
|
1049
|
|
1050 /**
|
|
1051 * Create a normalized identifier from a node label.
|
|
1052 * Multiple calls with the same node label will generate different unique identifier.
|
|
1053 *
|
|
1054 * @param nodeLabel
|
|
1055 * @returns {string}
|
|
1056 */
|
|
1057 popoto.graph.node.generateInternalLabel = function (nodeLabel) {
|
|
1058 var label = nodeLabel.toLowerCase().replace(/ /g, '');
|
|
1059
|
|
1060 if (label in popoto.graph.node.internalLabels) {
|
|
1061 popoto.graph.node.internalLabels[label] = popoto.graph.node.internalLabels[label] + 1;
|
|
1062 } else {
|
|
1063 popoto.graph.node.internalLabels[label] = 0;
|
|
1064 return label;
|
|
1065 }
|
|
1066
|
|
1067 return label + popoto.graph.node.internalLabels[label];
|
|
1068 };
|
|
1069
|
|
1070 /**
|
|
1071 * Update Nodes SVG elements using D3.js update mechanisms.
|
|
1072 */
|
|
1073 popoto.graph.node.updateNodes = function () {
|
|
1074 if (!popoto.graph.node.svgNodeElements) {
|
|
1075 popoto.graph.node.svgNodeElements = popoto.graph.svg.select("#" + popoto.graph.node.gID).selectAll("g");
|
|
1076 }
|
|
1077 popoto.graph.node.updateData();
|
|
1078 popoto.graph.node.removeElements();
|
|
1079 popoto.graph.node.addNewElements();
|
|
1080 popoto.graph.node.updateElements();
|
|
1081 };
|
|
1082
|
|
1083 /**
|
|
1084 * Update node data with changes done in popoto.graph.force.nodes() model.
|
|
1085 */
|
|
1086 popoto.graph.node.updateData = function () {
|
|
1087 popoto.graph.node.svgNodeElements = popoto.graph.node.svgNodeElements.data(popoto.graph.force.nodes(), function (d) {
|
|
1088 return d.id;
|
|
1089 });
|
|
1090
|
|
1091 if (popoto.graph.hasGraphChanged) {
|
|
1092 popoto.graph.node.updateCount();
|
|
1093 popoto.graph.hasGraphChanged = false;
|
|
1094 }
|
|
1095 };
|
|
1096
|
|
1097 /**
|
|
1098 * Update nodes count by executing a query for every nodes with the new graph structure.
|
|
1099 */
|
|
1100 popoto.graph.node.updateCount = function () {
|
|
1101
|
|
1102 var statements = [];
|
|
1103
|
|
1104 var counterNodes = popoto.graph.force.nodes()
|
|
1105 .filter(function (d) {
|
|
1106 return d.type !== popoto.graph.node.NodeTypes.VALUE && d.type !== popoto.graph.node.NodeTypes.GROUP;
|
|
1107 });
|
|
1108
|
|
1109 counterNodes.forEach(function (node) {
|
|
1110 var query = popoto.query.generateNodeCountCypherQuery(node);
|
|
1111 statements.push(
|
|
1112 {
|
|
1113 "statement": query
|
|
1114 }
|
|
1115 );
|
|
1116 });
|
|
1117
|
|
1118 popoto.logger.info("Count nodes ==> ");
|
|
1119 popoto.rest.post(
|
|
1120 {
|
|
1121 "statements": statements
|
|
1122 })
|
|
1123 .done(function (returnedData) {
|
|
1124
|
|
1125 if (returnedData.errors && returnedData.errors.length > 0) {
|
|
1126 popoto.logger.error("Cypher query error:" + JSON.stringify(returnedData.errors));
|
|
1127 }
|
|
1128
|
|
1129 if (returnedData.results && returnedData.results.length > 0) {
|
|
1130 for (var i = 0; i < counterNodes.length; i++) {
|
|
1131 counterNodes[i].count = returnedData.results[i].data[0].row[0];
|
|
1132 }
|
|
1133 } else {
|
|
1134 counterNodes.forEach(function (node) {
|
|
1135 node.count = 0;
|
|
1136 });
|
|
1137 }
|
|
1138 popoto.graph.node.updateElements();
|
|
1139 popoto.graph.link.updateElements();
|
|
1140 })
|
|
1141 .fail(function (xhr, textStatus, errorThrown) {
|
|
1142 popoto.logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + popoto.rest.CYPHER_URL + "\" defined in \"popoto.rest.CYPHER_URL\" property: " + errorThrown);
|
|
1143 counterNodes.forEach(function (node) {
|
|
1144 node.count = 0;
|
|
1145 });
|
|
1146 popoto.graph.node.updateElements();
|
|
1147 popoto.graph.link.updateElements();
|
|
1148 });
|
|
1149 };
|
|
1150
|
|
1151 /**
|
|
1152 * Remove old elements.
|
|
1153 * Should be called after updateData.
|
|
1154 */
|
|
1155 popoto.graph.node.removeElements = function () {
|
|
1156 var toRemove = popoto.graph.node.svgNodeElements.exit();
|
|
1157
|
|
1158 // Nodes without parent are simply removed.
|
|
1159 toRemove.filter(function (d) {
|
|
1160 return !d.parent;
|
|
1161 }).remove();
|
|
1162
|
|
1163 // Nodes with a parent are removed with an animation (nodes are collapsed to their parents before being removed)
|
|
1164 toRemove.filter(function (d) {
|
|
1165 return d.parent;
|
|
1166 }).transition().duration(300).attr("transform", function (d) {
|
|
1167 return "translate(" + d.parent.x + "," + d.parent.y + ")";
|
|
1168 }).remove();
|
|
1169 };
|
|
1170
|
|
1171 /**
|
|
1172 * Add all new elements.
|
|
1173 * Only the skeleton of new nodes are added custom data will be added during the element update phase.
|
|
1174 * Should be called after updateData and before updateElements.
|
|
1175 */
|
|
1176 popoto.graph.node.addNewElements = function () {
|
|
1177 var gNewNodeElements = popoto.graph.node.svgNodeElements.enter()
|
|
1178 .append("g")
|
|
1179 .on("click", popoto.graph.node.nodeClick)
|
|
1180 .on("mouseover", popoto.graph.node.mouseOverNode)
|
|
1181 .on("mouseout", popoto.graph.node.mouseOutNode);
|
|
1182
|
|
1183 // Add right click on all nodes except value
|
|
1184 gNewNodeElements.filter(function (d) {
|
|
1185 return d.type !== popoto.graph.node.NodeTypes.VALUE;
|
|
1186 }).on("contextmenu", popoto.graph.node.clearSelection);
|
|
1187
|
|
1188 // Disable right click context menu on value nodes
|
|
1189 gNewNodeElements.filter(function (d) {
|
|
1190 return d.type === popoto.graph.node.NodeTypes.VALUE;
|
|
1191 }).on("contextmenu", function () {
|
|
1192 // Disable context menu on
|
|
1193 d3.event.preventDefault();
|
|
1194 });
|
|
1195
|
|
1196 // Most browser will generate a tooltip if a title is specified for the SVG element
|
|
1197 // TODO Introduce an SVG tooltip instead?
|
|
1198 gNewNodeElements.append("title").attr("class", "ppt-svg-title");
|
|
1199
|
|
1200 // Nodes are composed of 3 layouts and skeleton are created here.
|
|
1201 popoto.graph.node.addBackgroundElements(gNewNodeElements);
|
|
1202 popoto.graph.node.addMiddlegroundElements(gNewNodeElements);
|
|
1203 popoto.graph.node.addForegroundElements(gNewNodeElements);
|
|
1204 };
|
|
1205
|
|
1206 /**
|
|
1207 * Create the background for a new node element.
|
|
1208 * The background of a node is defined by a circle not visible by default (fill-opacity set to 0) but can be used to highlight a node with animation on this attribute.
|
|
1209 * This circle also define the node zone that can receive events like mouse clicks.
|
|
1210 *
|
|
1211 * @param gNewNodeElements
|
|
1212 */
|
|
1213 popoto.graph.node.addBackgroundElements = function (gNewNodeElements) {
|
|
1214 var background = gNewNodeElements
|
|
1215 .append("g")
|
|
1216 .attr("class", "ppt-g-node-background");
|
|
1217
|
|
1218 background.append("circle")
|
|
1219 .attr("class", function (d) {
|
|
1220 var cssClass = "ppt-node-background-circle";
|
|
1221 if (d.value !== undefined) {
|
|
1222 cssClass = cssClass + " selected-value";
|
|
1223 } else if (d.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
1224 cssClass = cssClass + " root";
|
|
1225 } else if (d.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
1226 cssClass = cssClass + " choose";
|
|
1227 } else if (d.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
1228 cssClass = cssClass + " value";
|
|
1229 } else if (d.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
1230 cssClass = cssClass + " group";
|
|
1231 }
|
|
1232
|
|
1233 return cssClass;
|
|
1234 })
|
|
1235 .style("fill-opacity", 0)
|
|
1236 .attr("r", popoto.graph.node.BACK_CIRCLE_R);
|
|
1237 };
|
|
1238
|
|
1239 /**
|
|
1240 * Create the node main elements.
|
|
1241 *
|
|
1242 * @param gNewNodeElements
|
|
1243 */
|
|
1244 popoto.graph.node.addMiddlegroundElements = function (gNewNodeElements) {
|
|
1245 var middle = gNewNodeElements
|
|
1246 .append("g")
|
|
1247 .attr("class", "ppt-g-node-middleground");
|
|
1248 };
|
|
1249
|
|
1250 /**
|
|
1251 * Create the node foreground elements.
|
|
1252 * It contains node additional elements, count or tools like navigation arrows.
|
|
1253 *
|
|
1254 * @param gNewNodeElements
|
|
1255 */
|
|
1256 popoto.graph.node.addForegroundElements = function (gNewNodeElements) {
|
|
1257 var foreground = gNewNodeElements
|
|
1258 .append("g")
|
|
1259 .attr("class", "ppt-g-node-foreground");
|
|
1260
|
|
1261 // plus sign
|
|
1262 var gRelationship = foreground.filter(function (d) {
|
|
1263 return d.type !== popoto.graph.node.NodeTypes.VALUE;
|
|
1264 }).append("g").attr("class", "ppt-rel-plus-icon");
|
|
1265
|
|
1266 gRelationship.append("title")
|
|
1267 .text("Add relationship");
|
|
1268
|
|
1269 gRelationship
|
|
1270 .append("circle")
|
|
1271 .attr("class", "ppt-rel-plus-background")
|
|
1272 .attr("cx", "32")
|
|
1273 .attr("cy", "-43")
|
|
1274 .attr("r", "16");
|
|
1275
|
|
1276 gRelationship
|
|
1277 .append("path")
|
|
1278 .attr("class", "ppt-rel-plus-path")
|
|
1279 .attr("d", "M 40,-45 35,-45 35,-50 30,-50 30,-45 25,-45 25,-40 30,-40 30,-35 35,-35 35,-40 40,-40 z");
|
|
1280
|
|
1281 gRelationship
|
|
1282 .on("mouseover", function () {
|
|
1283 d3.select(this).select(".ppt-rel-plus-background").transition().style("fill-opacity", 0.5);
|
|
1284 })
|
|
1285 .on("mouseout", function () {
|
|
1286 d3.select(this).select(".ppt-rel-plus-background").transition().style("fill-opacity", 0);
|
|
1287 })
|
|
1288 .on("click", function () {
|
|
1289 d3.event.stopPropagation(); // To avoid click event on svg element in background
|
|
1290 popoto.graph.node.expandRelationship.call(this);
|
|
1291 });
|
|
1292
|
|
1293 // Minus sign
|
|
1294 var gMinusRelationship = foreground.filter(function (d) {
|
|
1295 return d.type !== popoto.graph.node.NodeTypes.VALUE;
|
|
1296 }).append("g").attr("class", "ppt-rel-minus-icon");
|
|
1297
|
|
1298 gMinusRelationship.append("title")
|
|
1299 .text("Remove relationship");
|
|
1300
|
|
1301 gMinusRelationship
|
|
1302 .append("circle")
|
|
1303 .attr("class", "ppt-rel-minus-background")
|
|
1304 .attr("cx", "32")
|
|
1305 .attr("cy", "-43")
|
|
1306 .attr("r", "16");
|
|
1307
|
|
1308 gMinusRelationship
|
|
1309 .append("path")
|
|
1310 .attr("class", "ppt-rel-minus-path")
|
|
1311 .attr("d", "M 40,-45 25,-45 25,-40 40,-40 z");
|
|
1312
|
|
1313 gMinusRelationship
|
|
1314 .on("mouseover", function () {
|
|
1315 d3.select(this).select(".ppt-rel-minus-background").transition().style("fill-opacity", 0.5);
|
|
1316 })
|
|
1317 .on("mouseout", function () {
|
|
1318 d3.select(this).select(".ppt-rel-minus-background").transition().style("fill-opacity", 0);
|
|
1319 })
|
|
1320 .on("click", function () {
|
|
1321 d3.event.stopPropagation(); // To avoid click event on svg element in background
|
|
1322 popoto.graph.node.collapseRelationship.call(this);
|
|
1323 });
|
|
1324
|
|
1325 // Arrows icons added only for root and choose nodes
|
|
1326 var gArrow = foreground.filter(function (d) {
|
|
1327 return d.type === popoto.graph.node.NodeTypes.ROOT || d.type === popoto.graph.node.NodeTypes.CHOOSE;
|
|
1328 })
|
|
1329 .append("g")
|
|
1330 .attr("class", "ppt-node-foreground-g-arrows");
|
|
1331
|
|
1332 var glArrow = gArrow.append("g");
|
|
1333 //glArrow.append("polygon")
|
|
1334 //.attr("points", "-53,-23 -33,-33 -33,-13");
|
|
1335 glArrow.append("circle")
|
|
1336 .attr("class", "ppt-larrow")
|
|
1337 .attr("cx", "-43")
|
|
1338 .attr("cy", "-23")
|
|
1339 .attr("r", "17");
|
|
1340
|
|
1341 glArrow.append("path")
|
|
1342 .attr("class", "ppt-arrow")
|
|
1343 .attr("d", "m -44.905361,-23 6.742,-6.742 c 0.81,-0.809 0.81,-2.135 0,-2.944 l -0.737,-0.737 c -0.81,-0.811 -2.135,-0.811 -2.945,0 l -8.835,8.835 c -0.435,0.434 -0.628,1.017 -0.597,1.589 -0.031,0.571 0.162,1.154 0.597,1.588 l 8.835,8.834 c 0.81,0.811 2.135,0.811 2.945,0 l 0.737,-0.737 c 0.81,-0.808 0.81,-2.134 0,-2.943 l -6.742,-6.743 z");
|
|
1344
|
|
1345 glArrow.on("click", function (clickedNode) {
|
|
1346 d3.event.stopPropagation(); // To avoid click event on svg element in background
|
|
1347
|
|
1348 // On left arrow click page number is decreased and node expanded to display the new page
|
|
1349 if (clickedNode.page > 1) {
|
|
1350 clickedNode.page--;
|
|
1351 popoto.graph.node.collapseNode(clickedNode);
|
|
1352 popoto.graph.node.expandNode(clickedNode);
|
|
1353 }
|
|
1354 });
|
|
1355
|
|
1356 var grArrow = gArrow.append("g");
|
|
1357 //grArrow.append("polygon")
|
|
1358 //.attr("points", "53,-23 33,-33 33,-13");
|
|
1359
|
|
1360 grArrow.append("circle")
|
|
1361 .attr("class", "ppt-rarrow")
|
|
1362 .attr("cx", "43")
|
|
1363 .attr("cy", "-23")
|
|
1364 .attr("r", "17");
|
|
1365
|
|
1366 grArrow.append("path")
|
|
1367 .attr("class", "ppt-arrow")
|
|
1368 .attr("d", "m 51.027875,-24.5875 -8.835,-8.835 c -0.811,-0.811 -2.137,-0.811 -2.945,0 l -0.738,0.737 c -0.81,0.81 -0.81,2.136 0,2.944 l 6.742,6.742 -6.742,6.742 c -0.81,0.81 -0.81,2.136 0,2.943 l 0.737,0.737 c 0.81,0.811 2.136,0.811 2.945,0 l 8.835,-8.836 c 0.435,-0.434 0.628,-1.017 0.597,-1.588 0.032,-0.569 -0.161,-1.152 -0.596,-1.586 z");
|
|
1369
|
|
1370 grArrow.on("click", function (clickedNode) {
|
|
1371 d3.event.stopPropagation(); // To avoid click event on svg element in background
|
|
1372
|
|
1373 if (clickedNode.page * popoto.graph.node.PAGE_SIZE < clickedNode.count) {
|
|
1374 clickedNode.page++;
|
|
1375 popoto.graph.node.collapseNode(clickedNode);
|
|
1376 popoto.graph.node.expandNode(clickedNode);
|
|
1377 }
|
|
1378 });
|
|
1379
|
|
1380 // Count box
|
|
1381 var countForeground = foreground.filter(function (d) {
|
|
1382 return d.type !== popoto.graph.node.NodeTypes.GROUP;
|
|
1383 });
|
|
1384
|
|
1385 countForeground
|
|
1386 .append("rect")
|
|
1387 .attr("x", popoto.graph.node.CountBox.x)
|
|
1388 .attr("y", popoto.graph.node.CountBox.y)
|
|
1389 .attr("width", popoto.graph.node.CountBox.w)
|
|
1390 .attr("height", popoto.graph.node.CountBox.h)
|
|
1391 .attr("class", "ppt-count-box");
|
|
1392
|
|
1393 countForeground
|
|
1394 .append("text")
|
|
1395 .attr("x", 42)
|
|
1396 .attr("y", 48)
|
|
1397 .attr("text-anchor", "middle")
|
|
1398 .attr("class", "ppt-count-text");
|
|
1399 };
|
|
1400
|
|
1401 /**
|
|
1402 * Updates all elements.
|
|
1403 */
|
|
1404 popoto.graph.node.updateElements = function () {
|
|
1405 popoto.graph.node.svgNodeElements.attr("id", function (d) {
|
|
1406 return "popoto-gnode_" + d.id;
|
|
1407 });
|
|
1408
|
|
1409 popoto.graph.node.svgNodeElements
|
|
1410 .selectAll(".ppt-svg-title")
|
|
1411 .text(function (d) {
|
|
1412 return popoto.provider.getTextValue(d);
|
|
1413 });
|
|
1414
|
|
1415 popoto.graph.node.svgNodeElements.filter(function (n) {
|
|
1416 return n.type !== popoto.graph.node.NodeTypes.ROOT
|
|
1417 }).call(popoto.graph.force.drag);
|
|
1418
|
|
1419 popoto.graph.node.updateBackgroundElements();
|
|
1420 popoto.graph.node.updateMiddlegroundElements();
|
|
1421 popoto.graph.node.updateForegroundElements();
|
|
1422 };
|
|
1423
|
|
1424 popoto.graph.node.updateBackgroundElements = function () {
|
|
1425 popoto.graph.node.svgNodeElements.selectAll(".ppt-g-node-background")
|
|
1426 .selectAll(".ppt-node-background-circle")
|
|
1427 .attr("class", function (d) {
|
|
1428 var cssClass = "ppt-node-background-circle";
|
|
1429
|
|
1430 if (d.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
1431 cssClass = cssClass + " value";
|
|
1432 } else if (d.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
1433 cssClass = cssClass + " group";
|
|
1434 } else {
|
|
1435 if (d.value !== undefined) {
|
|
1436 if (d.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
1437 cssClass = cssClass + " selected-root-value";
|
|
1438 } else if (d.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
1439 cssClass = cssClass + " selected-value";
|
|
1440 }
|
|
1441 } else {
|
|
1442 if (d.count == 0) {
|
|
1443 cssClass = cssClass + " disabled";
|
|
1444 } else {
|
|
1445 if (d.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
1446 cssClass = cssClass + " root";
|
|
1447 } else if (d.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
1448 cssClass = cssClass + " choose";
|
|
1449 }
|
|
1450 }
|
|
1451 }
|
|
1452 }
|
|
1453
|
|
1454 return cssClass;
|
|
1455 })
|
|
1456 .attr("r", popoto.graph.node.BACK_CIRCLE_R);
|
|
1457 };
|
|
1458
|
|
1459 /**
|
|
1460 * Update the middle layer of nodes.
|
|
1461 * TODO refactor node generation to allow future extensions (for example add plugin with new node types...)
|
|
1462 */
|
|
1463 popoto.graph.node.updateMiddlegroundElements = function () {
|
|
1464
|
|
1465 var middleG = popoto.graph.node.svgNodeElements.selectAll(".ppt-g-node-middleground");
|
|
1466
|
|
1467 // Clear all content in case node type has changed
|
|
1468 middleG.selectAll("*").remove();
|
|
1469
|
|
1470 //-------------------------------
|
|
1471 // Update IMAGE nodes
|
|
1472 var imageMiddle = middleG.filter(function (d) {
|
|
1473 return popoto.provider.getNodeDisplayType(d) === popoto.provider.NodeDisplayTypes.IMAGE;
|
|
1474 }).append("image").attr("class", "ppt-node-image");
|
|
1475
|
|
1476 imageMiddle
|
|
1477 .attr("width", function (d) {
|
|
1478 return popoto.provider.getImageWidth(d);
|
|
1479 })
|
|
1480 .attr("height", function (d) {
|
|
1481 return popoto.provider.getImageHeight(d);
|
|
1482 })
|
|
1483 // Center the image on node
|
|
1484 .attr("transform", function (d) {
|
|
1485 return "translate(" + (-popoto.provider.getImageWidth(d) / 2) + "," + (-popoto.provider.getImageHeight(d) / 2) + ")";
|
|
1486 })
|
|
1487 .attr("xlink:href", function (d) {
|
|
1488 return popoto.provider.getImagePath(d);
|
|
1489 });
|
|
1490
|
|
1491 //-------------------------
|
|
1492 // Update TEXT nodes
|
|
1493 var ellipseMiddle = middleG.filter(function (d) {
|
|
1494 return popoto.provider.getNodeDisplayType(d) === popoto.provider.NodeDisplayTypes.TEXT;
|
|
1495 }).append("ellipse").attr("rx", popoto.graph.node.ELLIPSE_RX).attr("ry", popoto.graph.node.ELLIPSE_RY);
|
|
1496
|
|
1497 // Set class according to node type
|
|
1498 ellipseMiddle
|
|
1499 .attr("rx", popoto.graph.node.ELLIPSE_RX)
|
|
1500 .attr("ry", popoto.graph.node.ELLIPSE_RY)
|
|
1501 .attr("class", function (d) {
|
|
1502 if (d.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
1503 if (d.value) {
|
|
1504 return "ppt-node-ellipse selected-root-value"
|
|
1505 } else {
|
|
1506 if (d.count == 0) {
|
|
1507 return "ppt-node-ellipse root disabled";
|
|
1508 } else {
|
|
1509 return "ppt-node-ellipse root";
|
|
1510 }
|
|
1511 }
|
|
1512 } else if (d.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
1513 if (d.value) {
|
|
1514 return "ppt-node-ellipse selected-value"
|
|
1515 } else {
|
|
1516 if (d.count == 0) {
|
|
1517 return "ppt-node-ellipse choose disabled";
|
|
1518 } else {
|
|
1519 return "ppt-node-ellipse choose";
|
|
1520 }
|
|
1521 }
|
|
1522 } else if (d.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
1523 return "ppt-node-ellipse value";
|
|
1524 } else if (d.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
1525 return "ppt-node-ellipse group";
|
|
1526 }
|
|
1527 });
|
|
1528
|
|
1529 //-------------------------
|
|
1530 // Update SVG nodes
|
|
1531 var svgMiddle = middleG.filter(function (d) {
|
|
1532 return popoto.provider.getNodeDisplayType(d) === popoto.provider.NodeDisplayTypes.SVG;
|
|
1533 }).append("g")
|
|
1534 // Add D3.js nested data with all paths required to render the svg element.
|
|
1535 .selectAll("path").data(function (d) {
|
|
1536 return popoto.provider.getSVGPaths(d);
|
|
1537 });
|
|
1538
|
|
1539 // Update nested data elements
|
|
1540 svgMiddle.exit().remove();
|
|
1541
|
|
1542 svgMiddle.enter().append("path");
|
|
1543
|
|
1544 middleG
|
|
1545 .selectAll("path")
|
|
1546 .attr("d", function (d) {
|
|
1547 return d.d;
|
|
1548 })
|
|
1549 .attr("class", function (d) {
|
|
1550 return d["class"];
|
|
1551 });
|
|
1552
|
|
1553 // Update text
|
|
1554 var textMiddle = middleG.filter(function (d) {
|
|
1555 return popoto.provider.isTextDisplayed(d);
|
|
1556 }).append('text')
|
|
1557 .attr('x', 0)
|
|
1558 .attr('y', popoto.graph.node.TEXT_Y)
|
|
1559 .attr('text-anchor', 'middle');
|
|
1560 textMiddle
|
|
1561 .attr('y', popoto.graph.node.TEXT_Y)
|
|
1562 .attr("class", function (d) {
|
|
1563 switch (d.type) {
|
|
1564 case popoto.graph.node.NodeTypes.CHOOSE:
|
|
1565 if (d.value === undefined) {
|
|
1566 if (d.count == 0) {
|
|
1567 return "ppt-node-text-choose disabled";
|
|
1568 } else {
|
|
1569 return "ppt-node-text-choose";
|
|
1570 }
|
|
1571 } else {
|
|
1572 return "ppt-node-text-choose selected-value";
|
|
1573 }
|
|
1574 case popoto.graph.node.NodeTypes.GROUP:
|
|
1575 return "ppt-node-text-group";
|
|
1576 case popoto.graph.node.NodeTypes.ROOT:
|
|
1577 if (d.value === undefined) {
|
|
1578 if (d.count == 0) {
|
|
1579 return "ppt-node-text-root disabled";
|
|
1580 } else {
|
|
1581 return "ppt-node-text-root";
|
|
1582 }
|
|
1583 } else {
|
|
1584 return "ppt-node-text-root selected-value";
|
|
1585 }
|
|
1586 case popoto.graph.node.NodeTypes.VALUE:
|
|
1587 return "ppt-node-text-value";
|
|
1588 }
|
|
1589 })
|
|
1590 .text(function (d) {
|
|
1591 if (popoto.provider.isTextDisplayed(d)) {
|
|
1592 return popoto.provider.getTextValue(d);
|
|
1593 } else {
|
|
1594 return "";
|
|
1595 }
|
|
1596 });
|
|
1597 };
|
|
1598
|
|
1599 /**
|
|
1600 * Updates the foreground elements
|
|
1601 */
|
|
1602 popoto.graph.node.updateForegroundElements = function () {
|
|
1603
|
|
1604 // Updates browse arrows status
|
|
1605 var gArrows = popoto.graph.node.svgNodeElements.selectAll(".ppt-g-node-foreground")
|
|
1606 .selectAll(".ppt-node-foreground-g-arrows");
|
|
1607 gArrows.classed("active", function (d) {
|
|
1608 return d.valueExpanded && d.data && d.data.length > popoto.graph.node.PAGE_SIZE;
|
|
1609 });
|
|
1610
|
|
1611 gArrows.selectAll(".ppt-larrow").classed("enabled", function (d) {
|
|
1612 return d.page > 1;
|
|
1613 });
|
|
1614
|
|
1615 gArrows.selectAll(".ppt-rarrow").classed("enabled", function (d) {
|
|
1616 if (d.data) {
|
|
1617 var count = d.data.length;
|
|
1618 return d.page * popoto.graph.node.PAGE_SIZE < count;
|
|
1619 } else {
|
|
1620 return false;
|
|
1621 }
|
|
1622 });
|
|
1623
|
|
1624 // Update count box class depending on node type
|
|
1625 var gForegrounds = popoto.graph.node.svgNodeElements.selectAll(".ppt-g-node-foreground");
|
|
1626
|
|
1627 gForegrounds.selectAll(".ppt-count-box").filter(function (d) {
|
|
1628 return d.type !== popoto.graph.node.NodeTypes.CHOOSE;
|
|
1629 }).classed("root", true);
|
|
1630
|
|
1631 gForegrounds.selectAll(".ppt-count-box").filter(function (d) {
|
|
1632 return d.type === popoto.graph.node.NodeTypes.CHOOSE;
|
|
1633 }).classed("value", true);
|
|
1634
|
|
1635 gForegrounds.selectAll(".ppt-count-box").classed("disabled", function (d) {
|
|
1636 return d.count == 0;
|
|
1637 });
|
|
1638
|
|
1639 gForegrounds.selectAll(".ppt-count-text")
|
|
1640 .text(function (d) {
|
|
1641 if (d.count != null) {
|
|
1642 return d.count;
|
|
1643 } else {
|
|
1644 return "...";
|
|
1645 }
|
|
1646 })
|
|
1647 .classed("disabled", function (d) {
|
|
1648 return d.count == 0;
|
|
1649 });
|
|
1650
|
|
1651 // Hide/Show plus icon (set disabled CSS class) if node already has been expanded.
|
|
1652 gForegrounds.selectAll(".ppt-rel-plus-icon")
|
|
1653 .classed("disabled", function (d) {
|
|
1654 return d.linkExpanded || d.count == 0 || d.linkCount == 0;
|
|
1655 });
|
|
1656
|
|
1657 gForegrounds.selectAll(".ppt-rel-minus-icon")
|
|
1658 .classed("disabled", function (d) {
|
|
1659 return (!d.linkExpanded) || d.count == 0 || d.linkCount == 0;
|
|
1660 });
|
|
1661
|
|
1662 };
|
|
1663
|
|
1664 /**
|
|
1665 * Handle the mouse over event on nodes.
|
|
1666 */
|
|
1667 popoto.graph.node.mouseOverNode = function () {
|
|
1668 d3.event.preventDefault();
|
|
1669
|
|
1670 // TODO don't work on IE (nodes unstable) find another way to move node in foreground on mouse over?
|
|
1671 // d3.select(this).moveToFront();
|
|
1672
|
|
1673 d3.select(this).select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0.5);
|
|
1674
|
|
1675 if (popoto.queryviewer.isActive) {
|
|
1676 // Get the hovered node data
|
|
1677 var hoveredNode = d3.select(this).data()[0];
|
|
1678
|
|
1679 // Hover the node in query
|
|
1680 popoto.queryviewer.queryConstraintSpanElements.filter(function (d) {
|
|
1681 return d.ref === hoveredNode;
|
|
1682 }).classed("hover", true);
|
|
1683 popoto.queryviewer.querySpanElements.filter(function (d) {
|
|
1684 return d.ref === hoveredNode;
|
|
1685 }).classed("hover", true);
|
|
1686 }
|
|
1687 };
|
|
1688
|
|
1689 /**
|
|
1690 * Handle mouse out event on nodes.
|
|
1691 */
|
|
1692 popoto.graph.node.mouseOutNode = function () {
|
|
1693 d3.event.preventDefault();
|
|
1694
|
|
1695 d3.select(this).select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0);
|
|
1696
|
|
1697 if (popoto.queryviewer.isActive) {
|
|
1698 // Get the hovered node data
|
|
1699 var hoveredNode = d3.select(this).data()[0];
|
|
1700
|
|
1701 // Remove hover class on node.
|
|
1702 popoto.queryviewer.queryConstraintSpanElements.filter(function (d) {
|
|
1703 return d.ref === hoveredNode;
|
|
1704 }).classed("hover", false);
|
|
1705 popoto.queryviewer.querySpanElements.filter(function (d) {
|
|
1706 return d.ref === hoveredNode;
|
|
1707 }).classed("hover", false);
|
|
1708 }
|
|
1709 };
|
|
1710
|
|
1711 /**
|
|
1712 * Handle the click event on nodes.
|
|
1713 */
|
|
1714 popoto.graph.node.nodeClick = function () {
|
|
1715 var clickedNode = d3.select(this).data()[0]; // Clicked node data
|
|
1716 popoto.logger.debug("nodeClick (" + clickedNode.label + ")");
|
|
1717
|
|
1718 if (clickedNode.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
1719 popoto.graph.node.valueNodeClick(clickedNode);
|
|
1720 } else if (clickedNode.type === popoto.graph.node.NodeTypes.CHOOSE || clickedNode.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
1721 if (clickedNode.valueExpanded) {
|
|
1722 popoto.graph.node.collapseNode(clickedNode);
|
|
1723 } else {
|
|
1724 popoto.graph.node.chooseNodeClick(clickedNode);
|
|
1725 }
|
|
1726 }
|
|
1727 };
|
|
1728
|
|
1729 /**
|
|
1730 * Remove all the value node directly linked to clicked node.
|
|
1731 *
|
|
1732 * @param clickedNode
|
|
1733 */
|
|
1734 popoto.graph.node.collapseNode = function (clickedNode) {
|
|
1735 if (clickedNode.valueExpanded) { // node is collapsed only if it has been expanded first
|
|
1736 popoto.logger.debug("collapseNode (" + clickedNode.label + ")");
|
|
1737
|
|
1738 var linksToRemove = popoto.graph.force.links().filter(function (l) {
|
|
1739 return l.source === clickedNode && l.type === popoto.graph.link.LinkTypes.VALUE;
|
|
1740 });
|
|
1741
|
|
1742 // Remove children nodes from model
|
|
1743 linksToRemove.forEach(function (l) {
|
|
1744 popoto.graph.force.nodes().splice(popoto.graph.force.nodes().indexOf(l.target), 1);
|
|
1745 });
|
|
1746
|
|
1747 // Remove links from model
|
|
1748 for (var i = popoto.graph.force.links().length - 1; i >= 0; i--) {
|
|
1749 if (linksToRemove.indexOf(popoto.graph.force.links()[i]) >= 0) {
|
|
1750 popoto.graph.force.links().splice(i, 1);
|
|
1751 }
|
|
1752 }
|
|
1753
|
|
1754 // Node has been fixed when expanded so we unfix it back here.
|
|
1755 if (clickedNode.type !== popoto.graph.node.NodeTypes.ROOT) {
|
|
1756 clickedNode.fixed = false;
|
|
1757 }
|
|
1758
|
|
1759 // Parent node too if not root
|
|
1760 if (clickedNode.parent && clickedNode.parent.type !== popoto.graph.node.NodeTypes.ROOT) {
|
|
1761 clickedNode.parent.fixed = false;
|
|
1762 }
|
|
1763
|
|
1764 clickedNode.valueExpanded = false;
|
|
1765 popoto.update();
|
|
1766
|
|
1767 } else {
|
|
1768 popoto.logger.debug("collapseNode called on an unexpanded node");
|
|
1769 }
|
|
1770 };
|
|
1771
|
|
1772 /**
|
|
1773 * Function called on a value node click.
|
|
1774 * In this case the value is added in the parent node and all the value nodes are collapsed.
|
|
1775 *
|
|
1776 * @param clickedNode
|
|
1777 */
|
|
1778 popoto.graph.node.valueNodeClick = function (clickedNode) {
|
|
1779 popoto.logger.debug("valueNodeClick (" + clickedNode.label + ")");
|
|
1780 clickedNode.parent.value = clickedNode;
|
|
1781 popoto.result.hasChanged = true;
|
|
1782 popoto.graph.hasGraphChanged = true;
|
|
1783
|
|
1784 popoto.graph.node.collapseNode(clickedNode.parent);
|
|
1785 };
|
|
1786
|
|
1787 /**
|
|
1788 * Function called on choose node click.
|
|
1789 * In this case a query is executed to get all the possible value
|
|
1790 * @param clickedNode
|
|
1791 * TODO optimize with cached data?
|
|
1792 */
|
|
1793 popoto.graph.node.chooseNodeClick = function (clickedNode) {
|
|
1794 popoto.logger.debug("chooseNodeClick (" + clickedNode.label + ") with waiting state set to " + popoto.graph.node.chooseWaiting);
|
|
1795 if (!popoto.graph.node.chooseWaiting && !clickedNode.immutable) {
|
|
1796
|
|
1797 // Collapse all expanded nodes first
|
|
1798 popoto.graph.force.nodes().forEach(function (n) {
|
|
1799 if ((n.type == popoto.graph.node.NodeTypes.ROOT || n.type == popoto.graph.node.NodeTypes.CHOOSE) && n.valueExpanded) {
|
|
1800 popoto.graph.node.collapseNode(n);
|
|
1801 }
|
|
1802 });
|
|
1803
|
|
1804 // Set waiting state to true to avoid multiple call on slow query execution
|
|
1805 popoto.graph.node.chooseWaiting = true;
|
|
1806
|
|
1807 popoto.logger.info("Values (" + clickedNode.label + ") ==> ");
|
|
1808 popoto.rest.post(
|
|
1809 {
|
|
1810 "statements": [
|
|
1811 {
|
|
1812 "statement": popoto.query.generateValueQuery(clickedNode)
|
|
1813 }]
|
|
1814 })
|
|
1815 .done(function (data) {
|
|
1816 clickedNode.id = (++popoto.graph.node.idgen);
|
|
1817 clickedNode.data = popoto.graph.node.parseResultData(data);
|
|
1818 clickedNode.page = 1;
|
|
1819 popoto.graph.node.expandNode(clickedNode);
|
|
1820 popoto.graph.node.chooseWaiting = false;
|
|
1821 })
|
|
1822 .fail(function (xhr, textStatus, errorThrown) {
|
|
1823 popoto.graph.node.chooseWaiting = false;
|
|
1824 popoto.logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + popoto.rest.CYPHER_URL + "\" defined in \"popoto.rest.CYPHER_URL\" property: " + errorThrown);
|
|
1825 });
|
|
1826 }
|
|
1827 };
|
|
1828
|
|
1829 /**
|
|
1830 * Parse query execution result and generate an array of object.
|
|
1831 * These objects contains of a list of properties made of result attributes with their value.
|
|
1832 *
|
|
1833 * @param data query execution raw data
|
|
1834 * @returns {Array} array of structured object with result attributes.
|
|
1835 */
|
|
1836 popoto.graph.node.parseResultData = function (data) {
|
|
1837 var results = [];
|
|
1838
|
|
1839 for (var x = 0; x < data.results[0].data.length; x++) {
|
|
1840 var obj = {};
|
|
1841
|
|
1842 for (var i = 0; i < data.results[0].columns.length; i++) {
|
|
1843 obj[data.results[0].columns[i]] = data.results[0].data[x].row[i];
|
|
1844 }
|
|
1845
|
|
1846 results.push(obj);
|
|
1847 }
|
|
1848
|
|
1849 return results;
|
|
1850 };
|
|
1851
|
|
1852 /**
|
|
1853 * Compute the angle in radian between the node and its parent.
|
|
1854 * TODO: clean or add comments to explain the code...
|
|
1855 *
|
|
1856 * @param node node to compute angle.
|
|
1857 * @returns {number} angle in radian.
|
|
1858 */
|
|
1859 popoto.graph.computeParentAngle = function (node) {
|
|
1860 var angleRadian = 0;
|
|
1861 var r = 100;
|
|
1862 if (node.parent) {
|
|
1863 var xp = node.parent.x;
|
|
1864 var yp = node.parent.y;
|
|
1865 var x0 = node.x;
|
|
1866 var y0 = node.y;
|
|
1867 var dist = Math.sqrt(Math.pow(xp - x0, 2) + Math.pow(yp - y0, 2));
|
|
1868
|
|
1869 var k = r / (dist - r);
|
|
1870 var xc = (x0 + (k * xp)) / (1 + k);
|
|
1871
|
|
1872 var val = (xc - x0) / r;
|
|
1873 if (val < -1) {
|
|
1874 val = -1;
|
|
1875 }
|
|
1876 if (val > 1) {
|
|
1877 val = 1;
|
|
1878 }
|
|
1879
|
|
1880 angleRadian = Math.acos(val);
|
|
1881
|
|
1882 if (yp > y0) {
|
|
1883 angleRadian = 2 * Math.PI - angleRadian;
|
|
1884 }
|
|
1885 }
|
|
1886 return angleRadian;
|
|
1887 };
|
|
1888
|
|
1889 /**
|
|
1890 * Function called to expand a node containing values.
|
|
1891 * This function will create the value nodes with the clicked node internal data.
|
|
1892 * Only nodes corresponding to the current page index will be generated.
|
|
1893 *
|
|
1894 * @param clickedNode
|
|
1895 */
|
|
1896 popoto.graph.node.expandNode = function (clickedNode) {
|
|
1897
|
|
1898 // Get subset of node corresponding to the current node page and page size
|
|
1899 var lIndex = clickedNode.page * popoto.graph.node.PAGE_SIZE;
|
|
1900 var sIndex = lIndex - popoto.graph.node.PAGE_SIZE;
|
|
1901
|
|
1902 var dataToAdd = clickedNode.data.slice(sIndex, lIndex);
|
|
1903 var parentAngle = popoto.graph.computeParentAngle(clickedNode);
|
|
1904
|
|
1905 // Then each node are created and dispatched around the clicked node using computed coordinates.
|
|
1906 var i = 1;
|
|
1907 dataToAdd.forEach(function (d) {
|
|
1908 var angleDeg;
|
|
1909 if (clickedNode.parent) {
|
|
1910 angleDeg = (((360 / (dataToAdd.length + 1)) * i));
|
|
1911 } else {
|
|
1912 angleDeg = (((360 / (dataToAdd.length)) * i));
|
|
1913 }
|
|
1914
|
|
1915 var nx = clickedNode.x + (100 * Math.cos((angleDeg * (Math.PI / 180)) - parentAngle)),
|
|
1916 ny = clickedNode.y + (100 * Math.sin((angleDeg * (Math.PI / 180)) - parentAngle));
|
|
1917
|
|
1918 var node = {
|
|
1919 "id": (++popoto.graph.node.idgen),
|
|
1920 "parent": clickedNode,
|
|
1921 "attributes": d,
|
|
1922 "type": popoto.graph.node.NodeTypes.VALUE,
|
|
1923 "label": clickedNode.label,
|
|
1924 "count": d.count,
|
|
1925 "x": nx,
|
|
1926 "y": ny,
|
|
1927 "internalID": d[popoto.query.NEO4J_INTERNAL_ID.queryInternalName]
|
|
1928 };
|
|
1929
|
|
1930 popoto.graph.force.nodes().push(node);
|
|
1931
|
|
1932 popoto.graph.force.links().push(
|
|
1933 {
|
|
1934 id: "l" + (++popoto.graph.node.idgen),
|
|
1935 source: clickedNode,
|
|
1936 target: node,
|
|
1937 type: popoto.graph.link.LinkTypes.VALUE
|
|
1938 }
|
|
1939 );
|
|
1940
|
|
1941 i++;
|
|
1942 });
|
|
1943
|
|
1944 // Pin clicked node and its parent to avoid the graph to move for selection, only new value nodes will blossom around the clicked node.
|
|
1945 clickedNode.fixed = true;
|
|
1946 if (clickedNode.parent && clickedNode.parent.type !== popoto.graph.node.NodeTypes.ROOT) {
|
|
1947 clickedNode.parent.fixed = true;
|
|
1948 }
|
|
1949 // Change node state
|
|
1950 clickedNode.valueExpanded = true;
|
|
1951 popoto.update();
|
|
1952 };
|
|
1953
|
|
1954 /**
|
|
1955 * Function called on a right click on a node.
|
|
1956 *
|
|
1957 * In this case all expanded nodes in the graph will first be closed then if no relation have been added yet a Cypher query is executed to get all the related nodes.
|
|
1958 * A first coordinate pre-computation is done to dispatch the new node correctly around the parent node and the nodes are added with a link in the model.
|
|
1959 *
|
|
1960 * If no relation are found or relation were already added the right click event is used to remove the node current selection.
|
|
1961 *
|
|
1962 */
|
|
1963 popoto.graph.node.expandRelationship = function () {
|
|
1964 // Prevent default right click event opening menu.
|
|
1965 d3.event.preventDefault();
|
|
1966
|
|
1967 // Notify listeners
|
|
1968 popoto.graph.nodeExpandRelationsipListeners.forEach(function (listener) {
|
|
1969 listener(this);
|
|
1970 });
|
|
1971
|
|
1972 // Get clicked node.
|
|
1973 var clickedNode = d3.select(this).data()[0];
|
|
1974
|
|
1975 if (!clickedNode.linkExpanded && !popoto.graph.node.linkWaiting && !clickedNode.valueExpanded) {
|
|
1976 popoto.graph.node.linkWaiting = true;
|
|
1977
|
|
1978 popoto.logger.info("Relations (" + clickedNode.label + ") ==> ");
|
|
1979 popoto.rest.post(
|
|
1980 {
|
|
1981 "statements": [
|
|
1982 {
|
|
1983 "statement": popoto.query.generateLinkQuery(clickedNode)
|
|
1984 }]
|
|
1985 })
|
|
1986 .done(function (data) {
|
|
1987 var parsedData = popoto.graph.node.parseResultData(data);
|
|
1988
|
|
1989 parsedData = parsedData.filter(function (d) {
|
|
1990 return popoto.query.filterRelation(d);
|
|
1991 });
|
|
1992
|
|
1993 if (parsedData.length <= 0) {
|
|
1994 // Set linkExpanded to true to avoid a new query call on next right click
|
|
1995 clickedNode.linkExpanded = true;
|
|
1996 clickedNode.linkCount = 0;
|
|
1997 popoto.graph.hasGraphChanged = true;
|
|
1998 popoto.update();
|
|
1999 } else {
|
|
2000 var parentAngle = popoto.graph.computeParentAngle(clickedNode);
|
|
2001
|
|
2002 var i = 1;
|
|
2003 parsedData.forEach(function (d) {
|
|
2004 var angleDeg;
|
|
2005 if (parentAngle) {
|
|
2006 angleDeg = (((360 / (parsedData.length + 1)) * i));
|
|
2007 } else {
|
|
2008 angleDeg = (((360 / (parsedData.length)) * i));
|
|
2009 }
|
|
2010
|
|
2011 var nx = clickedNode.x + (100 * Math.cos((angleDeg * (Math.PI / 180)) - parentAngle)),
|
|
2012 ny = clickedNode.y + (100 * Math.sin((angleDeg * (Math.PI / 180)) - parentAngle));
|
|
2013
|
|
2014 var isGroupNode = popoto.provider.getIsGroup(d);
|
|
2015 var node = {
|
|
2016 "id": "" + (++popoto.graph.node.idgen),
|
|
2017 "parent": clickedNode,
|
|
2018 "type": (isGroupNode) ? popoto.graph.node.NodeTypes.GROUP : popoto.graph.node.NodeTypes.CHOOSE,
|
|
2019 "label": d.label,
|
|
2020 "fixed": false,
|
|
2021 "internalLabel": popoto.graph.node.generateInternalLabel(d.label),
|
|
2022 "x": nx,
|
|
2023 "y": ny
|
|
2024 };
|
|
2025
|
|
2026 popoto.graph.force.nodes().push(node);
|
|
2027
|
|
2028 popoto.graph.force.links().push(
|
|
2029 {
|
|
2030 id: "l" + (++popoto.graph.node.idgen),
|
|
2031 source: clickedNode,
|
|
2032 target: node,
|
|
2033 type: popoto.graph.link.LinkTypes.RELATION,
|
|
2034 label: d.relationship
|
|
2035 }
|
|
2036 );
|
|
2037
|
|
2038 i++;
|
|
2039 });
|
|
2040
|
|
2041 popoto.graph.hasGraphChanged = true;
|
|
2042 clickedNode.linkExpanded = true;
|
|
2043 clickedNode.linkCount = parsedData.length;
|
|
2044 popoto.update();
|
|
2045 }
|
|
2046 popoto.graph.node.linkWaiting = false;
|
|
2047 })
|
|
2048 .fail(function (xhr, textStatus, errorThrown) {
|
|
2049 popoto.logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + popoto.rest.CYPHER_URL + "\" defined in \"popoto.rest.CYPHER_URL\" property: " + errorThrown);
|
|
2050 popoto.graph.node.linkWaiting = false;
|
|
2051 });
|
|
2052 }
|
|
2053 };
|
|
2054
|
|
2055 /**
|
|
2056 * Remove all relationships from context node (including children).
|
|
2057 */
|
|
2058 popoto.graph.node.collapseRelationship = function () {
|
|
2059 d3.event.preventDefault();
|
|
2060
|
|
2061 // Get clicked node.
|
|
2062 var clickedNode = d3.select(this).data()[0];
|
|
2063
|
|
2064 if (clickedNode.linkExpanded && clickedNode.linkCount > 0 && !popoto.graph.node.linkWaiting && !clickedNode.valueExpanded) {
|
|
2065
|
|
2066 // Collapse all expanded choose nodes first to avoid having invalid displayed value node if collapsed relation contains a value.
|
|
2067 popoto.graph.force.nodes().forEach(function (n) {
|
|
2068 if ((n.type === popoto.graph.node.NodeTypes.CHOOSE || n.type === popoto.graph.node.NodeTypes.ROOT) && n.valueExpanded) {
|
|
2069 popoto.graph.node.collapseNode(n);
|
|
2070 }
|
|
2071 });
|
|
2072
|
|
2073 var linksToRemove = popoto.graph.force.links().filter(function (l) {
|
|
2074 return l.source === clickedNode && l.type === popoto.graph.link.LinkTypes.RELATION;
|
|
2075 });
|
|
2076
|
|
2077 // Remove children nodes from model
|
|
2078 linksToRemove.forEach(function (l) {
|
|
2079 popoto.graph.node.removeNode(l.target);
|
|
2080 });
|
|
2081
|
|
2082 // Remove links from model
|
|
2083 for (var i = popoto.graph.force.links().length - 1; i >= 0; i--) {
|
|
2084 if (linksToRemove.indexOf(popoto.graph.force.links()[i]) >= 0) {
|
|
2085 popoto.graph.force.links().splice(i, 1);
|
|
2086 }
|
|
2087 }
|
|
2088
|
|
2089 clickedNode.linkExpanded = false;
|
|
2090 popoto.result.hasChanged = true;
|
|
2091 popoto.graph.hasGraphChanged = true;
|
|
2092 popoto.update();
|
|
2093 }
|
|
2094 };
|
|
2095
|
|
2096 /**
|
|
2097 * Remove a node and its relationships (recursively) from the graph.
|
|
2098 *
|
|
2099 * @param node the node to remove.
|
|
2100 */
|
|
2101 popoto.graph.node.removeNode = function (node) {
|
|
2102
|
|
2103 var linksToRemove = popoto.graph.force.links().filter(function (l) {
|
|
2104 return l.source === node;
|
|
2105 });
|
|
2106
|
|
2107 // Remove children nodes from model
|
|
2108 linksToRemove.forEach(function (l) {
|
|
2109 popoto.graph.node.removeNode(l.target);
|
|
2110 });
|
|
2111
|
|
2112 // Remove links from model
|
|
2113 for (var i = popoto.graph.force.links().length - 1; i >= 0; i--) {
|
|
2114 if (linksToRemove.indexOf(popoto.graph.force.links()[i]) >= 0) {
|
|
2115 popoto.graph.force.links().splice(i, 1);
|
|
2116 }
|
|
2117 }
|
|
2118
|
|
2119 popoto.graph.force.nodes().splice(popoto.graph.force.nodes().indexOf(node), 1);
|
|
2120
|
|
2121 };
|
|
2122
|
|
2123 /**
|
|
2124 * Function to add on node event to clear the selection.
|
|
2125 * Call to this function on a node will remove the selected value and triger a graph update.
|
|
2126 */
|
|
2127 popoto.graph.node.clearSelection = function () {
|
|
2128 // Prevent default event like right click opening menu.
|
|
2129 d3.event.preventDefault();
|
|
2130
|
|
2131 // Get clicked node.
|
|
2132 var clickedNode = d3.select(this).data()[0];
|
|
2133
|
|
2134 // Collapse all expanded choose nodes first
|
|
2135 popoto.graph.force.nodes().forEach(function (n) {
|
|
2136 if ((n.type === popoto.graph.node.NodeTypes.CHOOSE || n.type === popoto.graph.node.NodeTypes.ROOT) && n.valueExpanded) {
|
|
2137 popoto.graph.node.collapseNode(n);
|
|
2138 }
|
|
2139 });
|
|
2140
|
|
2141 if (clickedNode.value != null && !clickedNode.immutable) {
|
|
2142 // Remove selected value of choose node
|
|
2143 delete clickedNode.value;
|
|
2144
|
|
2145 popoto.result.hasChanged = true;
|
|
2146 popoto.graph.hasGraphChanged = true;
|
|
2147 popoto.update();
|
|
2148 }
|
|
2149 };
|
|
2150
|
|
2151 // QUERY VIEWER -----------------------------------------------------------------------------------------------------
|
|
2152 popoto.queryviewer = {};
|
|
2153 popoto.queryviewer.containerId = "popoto-query";
|
|
2154 popoto.queryviewer.QUERY_STARTER = "I'm looking for";
|
|
2155 popoto.queryviewer.CHOOSE_LABEL = "choose";
|
|
2156
|
|
2157 /**
|
|
2158 * Create the query viewer area.
|
|
2159 *
|
|
2160 */
|
|
2161 popoto.queryviewer.createQueryArea = function () {
|
|
2162 var id = "#" + popoto.queryviewer.containerId;
|
|
2163
|
|
2164 popoto.queryviewer.queryConstraintSpanElements = d3.select(id).append("p").attr("class", "ppt-query-constraint-elements").selectAll(".queryConstraintSpan");
|
|
2165 popoto.queryviewer.querySpanElements = d3.select(id).append("p").attr("class", "ppt-query-elements").selectAll(".querySpan");
|
|
2166 };
|
|
2167
|
|
2168 /**
|
|
2169 * Update all the elements displayed on the query viewer based on current graph.
|
|
2170 */
|
|
2171 popoto.queryviewer.updateQuery = function () {
|
|
2172
|
|
2173 // Remove all query span elements
|
|
2174 popoto.queryviewer.queryConstraintSpanElements = popoto.queryviewer.queryConstraintSpanElements.data([]);
|
|
2175 popoto.queryviewer.querySpanElements = popoto.queryviewer.querySpanElements.data([]);
|
|
2176
|
|
2177 popoto.queryviewer.queryConstraintSpanElements.exit().remove();
|
|
2178 popoto.queryviewer.querySpanElements.exit().remove();
|
|
2179
|
|
2180 // Update data
|
|
2181 popoto.queryviewer.queryConstraintSpanElements = popoto.queryviewer.queryConstraintSpanElements.data(popoto.queryviewer.generateConstraintData(popoto.graph.force.links(), popoto.graph.force.nodes()));
|
|
2182 popoto.queryviewer.querySpanElements = popoto.queryviewer.querySpanElements.data(popoto.queryviewer.generateData(popoto.graph.force.links(), popoto.graph.force.nodes()));
|
|
2183
|
|
2184 // Remove old span (not needed as all have been cleaned before)
|
|
2185 // popoto.queryviewer.querySpanElements.exit().remove();
|
|
2186
|
|
2187 // Add new span
|
|
2188 popoto.queryviewer.queryConstraintSpanElements.enter().append("span")
|
|
2189 .on("contextmenu", popoto.queryviewer.rightClickSpan)
|
|
2190 .on("click", popoto.queryviewer.clickSpan)
|
|
2191 .on("mouseover", popoto.queryviewer.mouseOverSpan)
|
|
2192 .on("mouseout", popoto.queryviewer.mouseOutSpan);
|
|
2193
|
|
2194 popoto.queryviewer.querySpanElements.enter().append("span")
|
|
2195 .on("contextmenu", popoto.queryviewer.rightClickSpan)
|
|
2196 .on("click", popoto.queryviewer.clickSpan)
|
|
2197 .on("mouseover", popoto.queryviewer.mouseOverSpan)
|
|
2198 .on("mouseout", popoto.queryviewer.mouseOutSpan);
|
|
2199
|
|
2200 // Update all span
|
|
2201 popoto.queryviewer.queryConstraintSpanElements
|
|
2202 .attr("id", function (d) {
|
|
2203 return d.id
|
|
2204 })
|
|
2205 .attr("class", function (d) {
|
|
2206 if (d.isLink) {
|
|
2207 return "ppt-span-link";
|
|
2208 } else {
|
|
2209 if (d.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
2210 return "ppt-span-root";
|
|
2211 } else if (d.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
2212 if (d.ref.value) {
|
|
2213 return "ppt-span-value";
|
|
2214 } else {
|
|
2215 return "ppt-span-choose";
|
|
2216 }
|
|
2217 } else if (d.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
2218 return "ppt-span-value";
|
|
2219 } else if (d.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
2220 return "ppt-span-group";
|
|
2221 } else {
|
|
2222 return "ppt-span";
|
|
2223 }
|
|
2224 }
|
|
2225 })
|
|
2226 .text(function (d) {
|
|
2227 return d.term + " ";
|
|
2228 });
|
|
2229
|
|
2230 popoto.queryviewer.querySpanElements
|
|
2231 .attr("id", function (d) {
|
|
2232 return d.id
|
|
2233 })
|
|
2234 .attr("class", function (d) {
|
|
2235 if (d.isLink) {
|
|
2236 return "ppt-span-link";
|
|
2237 } else {
|
|
2238 if (d.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
2239 return "ppt-span-root";
|
|
2240 } else if (d.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
2241 if (d.ref.value) {
|
|
2242 return "ppt-span-value";
|
|
2243 } else {
|
|
2244 return "ppt-span-choose";
|
|
2245 }
|
|
2246 } else if (d.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
2247 return "ppt-span-value";
|
|
2248 } else if (d.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
2249 return "ppt-span-group";
|
|
2250 } else {
|
|
2251 return "ppt-span";
|
|
2252 }
|
|
2253 }
|
|
2254 })
|
|
2255 .text(function (d) {
|
|
2256 return d.term + " ";
|
|
2257 });
|
|
2258 };
|
|
2259
|
|
2260 popoto.queryviewer.generateConstraintData = function (links, nodes) {
|
|
2261 var elmts = [], id = 0;
|
|
2262
|
|
2263 // Add
|
|
2264 elmts.push(
|
|
2265 {id: id++, term: popoto.queryviewer.QUERY_STARTER}
|
|
2266 );
|
|
2267
|
|
2268 // Add the root node as query term
|
|
2269 if (nodes.length > 0) {
|
|
2270 elmts.push(
|
|
2271 {id: id++, type: nodes[0].type, term: popoto.provider.getSemanticValue(nodes[0]), ref: nodes[0]}
|
|
2272 );
|
|
2273 }
|
|
2274
|
|
2275 // Add a span for each link and its target node
|
|
2276 links.forEach(function (l) {
|
|
2277
|
|
2278 var sourceNode = l.source;
|
|
2279 var targetNode = l.target;
|
|
2280 if (l.type === popoto.graph.link.LinkTypes.RELATION && targetNode.type !== popoto.graph.node.NodeTypes.GROUP && targetNode.value) {
|
|
2281 if (sourceNode.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
2282 elmts.push(
|
|
2283 {id: id++, type: sourceNode.type, term: popoto.provider.getSemanticValue(sourceNode), ref: sourceNode}
|
|
2284 );
|
|
2285 }
|
|
2286
|
|
2287 elmts.push({id: id++, isLink: true, term: popoto.provider.getLinkSemanticValue(l), ref: l});
|
|
2288
|
|
2289 if (targetNode.type !== popoto.graph.node.NodeTypes.GROUP) {
|
|
2290 if (targetNode.value) {
|
|
2291 elmts.push(
|
|
2292 {id: id++, type: targetNode.type, term: popoto.provider.getSemanticValue(targetNode), ref: targetNode}
|
|
2293 );
|
|
2294 } else {
|
|
2295 elmts.push(
|
|
2296 {id: id++, type: targetNode.type, term: "<" + popoto.queryviewer.CHOOSE_LABEL + " " + popoto.provider.getSemanticValue(targetNode) + ">", ref: targetNode}
|
|
2297 );
|
|
2298 }
|
|
2299 }
|
|
2300 }
|
|
2301 });
|
|
2302
|
|
2303 return elmts;
|
|
2304 };
|
|
2305
|
|
2306 // TODO add option nodes in generated query when no value is available
|
|
2307 popoto.queryviewer.generateData = function (links, nodes) {
|
|
2308 var elmts = [], options = [], id = 0;
|
|
2309
|
|
2310 // Add a span for each link and its target node
|
|
2311 links.forEach(function (l) {
|
|
2312
|
|
2313 var sourceNode = l.source;
|
|
2314 var targetNode = l.target;
|
|
2315
|
|
2316 if (targetNode.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
2317 options.push(
|
|
2318 {id: id++, type: targetNode.type, term: popoto.provider.getSemanticValue(targetNode), ref: targetNode}
|
|
2319 );
|
|
2320 }
|
|
2321
|
|
2322 if (l.type === popoto.graph.link.LinkTypes.RELATION && targetNode.type !== popoto.graph.node.NodeTypes.GROUP && !targetNode.value) {
|
|
2323 if (sourceNode.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
2324 elmts.push(
|
|
2325 {id: id++, type: sourceNode.type, term: popoto.provider.getSemanticValue(sourceNode), ref: sourceNode}
|
|
2326 );
|
|
2327 }
|
|
2328
|
|
2329 elmts.push({id: id++, isLink: true, term: popoto.provider.getLinkSemanticValue(l), ref: l});
|
|
2330
|
|
2331 if (targetNode.type !== popoto.graph.node.NodeTypes.GROUP) {
|
|
2332 if (targetNode.value) {
|
|
2333 elmts.push(
|
|
2334 {id: id++, type: targetNode.type, term: popoto.provider.getSemanticValue(targetNode), ref: targetNode}
|
|
2335 );
|
|
2336 } else {
|
|
2337 elmts.push(
|
|
2338 {id: id++, type: targetNode.type, term: "<" + popoto.queryviewer.CHOOSE_LABEL + " " + popoto.provider.getSemanticValue(targetNode) + ">", ref: targetNode}
|
|
2339 );
|
|
2340 }
|
|
2341 }
|
|
2342 }
|
|
2343 });
|
|
2344
|
|
2345 return elmts.concat(options);
|
|
2346 };
|
|
2347
|
|
2348 /**
|
|
2349 *
|
|
2350 */
|
|
2351 popoto.queryviewer.mouseOverSpan = function () {
|
|
2352 d3.select(this).classed("hover", function (d) {
|
|
2353 return d.ref;
|
|
2354 });
|
|
2355
|
|
2356 var hoveredSpan = d3.select(this).data()[0];
|
|
2357
|
|
2358 if (hoveredSpan.ref) {
|
|
2359 var linkElmt = popoto.graph.svg.selectAll("#" + popoto.graph.link.gID + " > g").filter(function (d) {
|
|
2360 return d === hoveredSpan.ref;
|
|
2361 });
|
|
2362 linkElmt.select("path").classed("ppt-link-hover", true);
|
|
2363 linkElmt.select("text").classed("ppt-link-hover", true);
|
|
2364
|
|
2365 var nodeElmt = popoto.graph.svg.selectAll("#" + popoto.graph.node.gID + " > g").filter(function (d) {
|
|
2366 return d === hoveredSpan.ref;
|
|
2367 });
|
|
2368
|
|
2369 nodeElmt.select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0.5);
|
|
2370 }
|
|
2371 };
|
|
2372
|
|
2373 popoto.queryviewer.rightClickSpan = function () {
|
|
2374 var hoveredSpan = d3.select(this).data()[0];
|
|
2375
|
|
2376 if (!hoveredSpan.isLink && hoveredSpan.ref) {
|
|
2377 var nodeElmt = popoto.graph.svg.selectAll("#" + popoto.graph.node.gID + " > g").filter(function (d) {
|
|
2378 return d === hoveredSpan.ref;
|
|
2379 });
|
|
2380
|
|
2381 nodeElmt.on("contextmenu").call(nodeElmt.node(), hoveredSpan.ref);
|
|
2382 }
|
|
2383 };
|
|
2384
|
|
2385 popoto.queryviewer.clickSpan = function () {
|
|
2386 var hoveredSpan = d3.select(this).data()[0];
|
|
2387
|
|
2388 if (!hoveredSpan.isLink && hoveredSpan.ref) {
|
|
2389 var nodeElmt = popoto.graph.svg.selectAll("#" + popoto.graph.node.gID + " > g").filter(function (d) {
|
|
2390 return d === hoveredSpan.ref;
|
|
2391 });
|
|
2392
|
|
2393 nodeElmt.on("click").call(nodeElmt.node(), hoveredSpan.ref);
|
|
2394 }
|
|
2395 };
|
|
2396
|
|
2397 /**
|
|
2398 *
|
|
2399 */
|
|
2400 popoto.queryviewer.mouseOutSpan = function () {
|
|
2401 d3.select(this).classed("hover", false);
|
|
2402
|
|
2403 var hoveredSpan = d3.select(this).data()[0];
|
|
2404
|
|
2405 if (hoveredSpan.ref) {
|
|
2406 var linkElmt = popoto.graph.svg.selectAll("#" + popoto.graph.link.gID + " > g").filter(function (d) {
|
|
2407 return d === hoveredSpan.ref;
|
|
2408 });
|
|
2409 linkElmt.select("path").classed("ppt-link-hover", false);
|
|
2410 linkElmt.select("text").classed("ppt-link-hover", false);
|
|
2411
|
|
2412 var nodeElmt = popoto.graph.svg.selectAll("#" + popoto.graph.node.gID + " > g").filter(function (d) {
|
|
2413 return d === hoveredSpan.ref;
|
|
2414 });
|
|
2415 nodeElmt.select(".ppt-g-node-background").selectAll("circle").transition().style("fill-opacity", 0);
|
|
2416 }
|
|
2417 };
|
|
2418
|
|
2419 // CYPHER VIEWER -----------------------------------------------------------------------------------------------------
|
|
2420
|
|
2421 // TODO not available yet
|
|
2422 popoto.cypherviewer = {};
|
|
2423 popoto.cypherviewer.containerId = "popoto-cypher";
|
|
2424
|
|
2425 // QUERY ------------------------------------------------------------------------------------------------------------
|
|
2426 popoto.query = {};
|
|
2427 /**
|
|
2428 * Define the number of results displayed in result list.
|
|
2429 */
|
|
2430 popoto.query.RESULTS_PAGE_SIZE = 100;
|
|
2431 popoto.query.VALUE_QUERY_LIMIT = 1000;
|
|
2432 popoto.query.USE_PARENT_RELATION = false;
|
|
2433 popoto.query.USE_RELATION_DIRECTION = true;
|
|
2434
|
|
2435 /**
|
|
2436 * Immutable constant object to identify Neo4j internal ID
|
|
2437 */
|
|
2438 popoto.query.NEO4J_INTERNAL_ID = Object.freeze({queryInternalName: "NEO4JID"});
|
|
2439
|
|
2440 /**
|
|
2441 * Function used to filter returned relations
|
|
2442 * return false if the result should be filtered out.
|
|
2443 *
|
|
2444 * @param d relation returned object
|
|
2445 * @returns {boolean}
|
|
2446 */
|
|
2447 popoto.query.filterRelation = function (d) {
|
|
2448 return true;
|
|
2449 };
|
|
2450
|
|
2451 /**
|
|
2452 * Generate the query to count nodes of a label.
|
|
2453 * If the label is defined as distinct in configuration the query will count only distinct values on constraint attribute.
|
|
2454 */
|
|
2455 popoto.query.generateTaxonomyCountQuery = function (label) {
|
|
2456 var constraintAttr = popoto.provider.getConstraintAttribute(label);
|
|
2457
|
|
2458 var whereElements = [];
|
|
2459
|
|
2460 var predefinedConstraints = popoto.provider.getPredefinedConstraints(label);
|
|
2461 predefinedConstraints.forEach(function (predefinedConstraint) {
|
|
2462 whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), "n"));
|
|
2463 });
|
|
2464
|
|
2465 if (constraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2466 return "MATCH (n:`" + label + "`)" + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN count(DISTINCT ID(n)) as count"
|
|
2467 } else {
|
|
2468 return "MATCH (n:`" + label + "`)" + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN count(DISTINCT n." + constraintAttr + ") as count"
|
|
2469 }
|
|
2470 };
|
|
2471
|
|
2472 /**
|
|
2473 * Generate Cypher query match and where elements from root node, selected node and a set of the graph links.
|
|
2474 *
|
|
2475 * @param rootNode root node in the graph.
|
|
2476 * @param selectedNode graph target node.
|
|
2477 * @param links list of links subset of the graph.
|
|
2478 * @returns {{matchElements: Array, whereElements: Array}} list of match and where elements.
|
|
2479 * @param isConstraintNeeded
|
|
2480 */
|
|
2481 popoto.query.generateQueryElements = function (rootNode, selectedNode, links, isConstraintNeeded) {
|
|
2482 var matchElements = [];
|
|
2483 var whereElements = [];
|
|
2484 var rel = popoto.query.USE_RELATION_DIRECTION ? "->" : "-";
|
|
2485
|
|
2486 var rootPredefinedConstraints = popoto.provider.getPredefinedConstraints(rootNode.label);
|
|
2487
|
|
2488 rootPredefinedConstraints.forEach(function (predefinedConstraint) {
|
|
2489 whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), rootNode.internalLabel));
|
|
2490 });
|
|
2491
|
|
2492 // Generate root node match element
|
|
2493 if (rootNode.value && (isConstraintNeeded || rootNode.immutable)) {
|
|
2494 var rootConstraintAttr = popoto.provider.getConstraintAttribute(rootNode.label);
|
|
2495 if (rootConstraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2496 matchElements.push("(" + rootNode.internalLabel + ":`" + rootNode.label + "`)");
|
|
2497 whereElements.push("ID(" + rootNode.internalLabel + ") = " + rootNode.value.internalID);
|
|
2498 } else {
|
|
2499 var constraintValue = rootNode.value.attributes[rootConstraintAttr];
|
|
2500
|
|
2501 if (typeof constraintValue === "boolean" || typeof constraintValue === "number") {
|
|
2502 matchElements.push("(" + rootNode.internalLabel + ":`" + rootNode.label + "`{`" + rootConstraintAttr + "`:" + constraintValue + "})");
|
|
2503 } else {
|
|
2504 matchElements.push("(" + rootNode.internalLabel + ":`" + rootNode.label + "`{`" + rootConstraintAttr + "`:\"" + constraintValue + "\"})");
|
|
2505 }
|
|
2506 }
|
|
2507 } else {
|
|
2508 matchElements.push("(" + rootNode.internalLabel + ":`" + rootNode.label + "`)");
|
|
2509 }
|
|
2510
|
|
2511 // Generate match elements for each links
|
|
2512 links.forEach(function (l) {
|
|
2513 var sourceNode = l.source;
|
|
2514 var targetNode = l.target;
|
|
2515
|
|
2516 var predefinedConstraints = popoto.provider.getPredefinedConstraints(targetNode.label);
|
|
2517
|
|
2518 predefinedConstraints.forEach(function (predefinedConstraint) {
|
|
2519 whereElements.push(predefinedConstraint.replace(new RegExp("\\$identifier", 'g'), targetNode.internalLabel));
|
|
2520 });
|
|
2521
|
|
2522 if (targetNode.value && targetNode !== selectedNode && (isConstraintNeeded || rootNode.immutable)) {
|
|
2523 var constraintAttr = popoto.provider.getConstraintAttribute(targetNode.label);
|
|
2524 var constraintValue = targetNode.value.attributes[constraintAttr];
|
|
2525 if (constraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2526 matchElements.push("(" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)-[:`" + l.label + "`]" + rel + "(" + targetNode.internalLabel + ":`" + targetNode.label + "`)");
|
|
2527 whereElements.push("ID(" + targetNode.internalLabel + ") = " + targetNode.value.internalID);
|
|
2528 } else {
|
|
2529 if (typeof constraintValue === "boolean" || typeof constraintValue === "number") {
|
|
2530 matchElements.push("(" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)-[:`" + l.label + "`]" + rel + "(" + targetNode.internalLabel + ":`" + targetNode.label + "`{`" + constraintAttr + "`:" + constraintValue + "})");
|
|
2531 } else {
|
|
2532 matchElements.push("(" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)-[:`" + l.label + "`]" + rel + "(" + targetNode.internalLabel + ":`" + targetNode.label + "`{`" + constraintAttr + "`:\"" + constraintValue + "\"})");
|
|
2533 }
|
|
2534 }
|
|
2535 } else {
|
|
2536 matchElements.push("(" + sourceNode.internalLabel + ":`" + sourceNode.label + "`)-[:`" + l.label + "`]" + rel + "(" + targetNode.internalLabel + ":`" + targetNode.label + "`)");
|
|
2537 }
|
|
2538 });
|
|
2539
|
|
2540 return {"matchElements": matchElements, "whereElements": whereElements};
|
|
2541 };
|
|
2542
|
|
2543 /**
|
|
2544 * Filter links to get only paths from root to leaf containing a value or being the selectedNode.
|
|
2545 * All other paths in the graph containing no value are ignored.
|
|
2546 *
|
|
2547 * @param rootNode root node of the graph.
|
|
2548 * @param targetNode node in the graph target of the query.
|
|
2549 * @param initialLinks list of links repreasenting the graph to filter.
|
|
2550 * @returns {Array} list of relevant links.
|
|
2551 */
|
|
2552 popoto.query.getRelevantLinks = function (rootNode, targetNode, initialLinks) {
|
|
2553
|
|
2554 var links = initialLinks.slice();
|
|
2555 var filteredLinks = [];
|
|
2556 var finalLinks = [];
|
|
2557
|
|
2558 // Filter all links to keep only those containing a value or being the selected node.
|
|
2559 links.forEach(function (l) {
|
|
2560 if (l.target.value || l.target === targetNode) {
|
|
2561 filteredLinks.push(l);
|
|
2562 }
|
|
2563 });
|
|
2564
|
|
2565 // All the filtered links are removed from initial links list.
|
|
2566 filteredLinks.forEach(function (l) {
|
|
2567 links.splice(links.indexOf(l), 1);
|
|
2568 });
|
|
2569
|
|
2570 // Then all the intermediate links up to the root node are added to get only the relevant links.
|
|
2571 filteredLinks.forEach(function (fl) {
|
|
2572 var sourceNode = fl.source;
|
|
2573 var search = true;
|
|
2574
|
|
2575 while (search) {
|
|
2576 var intermediateLink = null;
|
|
2577 links.forEach(function (l) {
|
|
2578 if (l.target === sourceNode) {
|
|
2579 intermediateLink = l;
|
|
2580 }
|
|
2581 });
|
|
2582
|
|
2583 if (intermediateLink === null) { // no intermediate links needed
|
|
2584 search = false
|
|
2585 } else {
|
|
2586 if (intermediateLink.source === rootNode) {
|
|
2587 finalLinks.push(intermediateLink);
|
|
2588 links.splice(links.indexOf(intermediateLink), 1);
|
|
2589 search = false;
|
|
2590 } else {
|
|
2591 finalLinks.push(intermediateLink);
|
|
2592 links.splice(links.indexOf(intermediateLink), 1);
|
|
2593 sourceNode = intermediateLink.source;
|
|
2594 }
|
|
2595 }
|
|
2596 }
|
|
2597 });
|
|
2598
|
|
2599 return filteredLinks.concat(finalLinks);
|
|
2600 };
|
|
2601
|
|
2602 /**
|
|
2603 * Get the list of link defining the complete path from node to root.
|
|
2604 * All other links are ignored.
|
|
2605 *
|
|
2606 * @param node The node where to start in the graph.
|
|
2607 * @param links
|
|
2608 */
|
|
2609 popoto.query.getLinksToRoot = function (node, links) {
|
|
2610 var pathLinks = [];
|
|
2611 var targetNode = node;
|
|
2612
|
|
2613 while (targetNode !== popoto.graph.getRootNode()) {
|
|
2614 var nodeLink;
|
|
2615
|
|
2616 for (var i = 0; i < links.length; i++) {
|
|
2617 var link = links[i];
|
|
2618 if (link.target === targetNode) {
|
|
2619 nodeLink = link;
|
|
2620 break;
|
|
2621 }
|
|
2622 }
|
|
2623
|
|
2624 if (nodeLink) {
|
|
2625 pathLinks.push(nodeLink);
|
|
2626 targetNode = nodeLink.source;
|
|
2627 }
|
|
2628 }
|
|
2629
|
|
2630 return pathLinks;
|
|
2631 };
|
|
2632
|
|
2633 /**
|
|
2634 * Generate a Cypher query to retrieve all the relation available for a given node.
|
|
2635 *
|
|
2636 * @param targetNode
|
|
2637 * @returns {string}
|
|
2638 */
|
|
2639 popoto.query.generateLinkQuery = function (targetNode) {
|
|
2640
|
|
2641 var linksToRoot = popoto.query.getLinksToRoot(targetNode, popoto.graph.force.links());
|
|
2642 var queryElements = popoto.query.generateQueryElements(popoto.graph.getRootNode(), targetNode, linksToRoot, false);
|
|
2643 var matchElements = queryElements.matchElements,
|
|
2644 returnElements = [],
|
|
2645 whereElements = queryElements.whereElements,
|
|
2646 endElements = [];
|
|
2647 var rel = popoto.query.USE_RELATION_DIRECTION ? "->" : "-";
|
|
2648
|
|
2649 matchElements.push("(" + targetNode.internalLabel + ":`" + targetNode.label + "`)-[r]" + rel + "(x)");
|
|
2650 returnElements.push("type(r) AS relationship");
|
|
2651 if (popoto.query.USE_PARENT_RELATION) {
|
|
2652 returnElements.push("head(labels(x)) AS label");
|
|
2653 } else {
|
|
2654 returnElements.push("last(labels(x)) AS label");
|
|
2655 }
|
|
2656 returnElements.push("count(r) AS count");
|
|
2657 endElements.push("ORDER BY count(r) DESC");
|
|
2658
|
|
2659 return "MATCH " + matchElements.join(", ") + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN " + returnElements.join(", ") + " " + endElements.join(" ");
|
|
2660 };
|
|
2661
|
|
2662 /**
|
|
2663 * Generate a Cypher query
|
|
2664 * @returns {string}
|
|
2665 */
|
|
2666 popoto.query.generateResultCypherQuery = function () {
|
|
2667
|
|
2668 var rootNode = popoto.graph.getRootNode();
|
|
2669 var queryElements = popoto.query.generateQueryElements(rootNode, rootNode, popoto.query.getRelevantLinks(rootNode, rootNode, popoto.graph.force.links()), true);
|
|
2670 var matchElements = queryElements.matchElements,
|
|
2671 returnElements = [],
|
|
2672 whereElements = queryElements.whereElements,
|
|
2673 endElements = [];
|
|
2674
|
|
2675 // Sort results by specified attribute
|
|
2676 var resultOrderByAttribute = popoto.provider.getResultOrderByAttribute(rootNode.label);
|
|
2677 if (resultOrderByAttribute) {
|
|
2678 var order = popoto.provider.isResultOrderAscending(rootNode.label) ? "ASC" : "DESC";
|
|
2679 endElements.push("ORDER BY " + resultOrderByAttribute + " " + order);
|
|
2680 }
|
|
2681
|
|
2682 endElements.push("LIMIT " + popoto.query.RESULTS_PAGE_SIZE);
|
|
2683
|
|
2684 var resultAttributes = popoto.provider.getReturnAttributes(rootNode.label);
|
|
2685 var constraintAttribute = popoto.provider.getConstraintAttribute(rootNode.label);
|
|
2686
|
|
2687 for (var i = 0; i < resultAttributes.length; i++) {
|
|
2688 var attribute = resultAttributes[i];
|
|
2689 if (attribute === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2690 if (attribute == constraintAttribute) {
|
|
2691 returnElements.push("ID(" + rootNode.internalLabel + ") AS " + popoto.query.NEO4J_INTERNAL_ID.queryInternalName);
|
|
2692 } else {
|
|
2693 returnElements.push("COLLECT(DISTINCT ID(" + rootNode.internalLabel + ")) AS " + popoto.query.NEO4J_INTERNAL_ID.queryInternalName);
|
|
2694 }
|
|
2695 } else {
|
|
2696 if (attribute == constraintAttribute) {
|
|
2697 returnElements.push(rootNode.internalLabel + "." + attribute + " AS " + attribute);
|
|
2698 } else {
|
|
2699 returnElements.push("COLLECT(DISTINCT " + rootNode.internalLabel + "." + attribute + ") AS " + attribute);
|
|
2700 }
|
|
2701 }
|
|
2702 }
|
|
2703
|
|
2704 return "MATCH " + matchElements.join(", ") + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN DISTINCT " + returnElements.join(", ") + " " + endElements.join(" ");
|
|
2705 };
|
|
2706
|
|
2707 popoto.query.generateResultCypherQueryCount = function () {
|
|
2708
|
|
2709 var rootNode = popoto.graph.getRootNode();
|
|
2710 var queryElements = popoto.query.generateQueryElements(rootNode, rootNode, popoto.query.getRelevantLinks(rootNode, rootNode, popoto.graph.force.links()), true);
|
|
2711 var constraintAttribute = popoto.provider.getConstraintAttribute(rootNode.label);
|
|
2712 var matchElements = queryElements.matchElements,
|
|
2713 returnElements = [],
|
|
2714 whereElements = queryElements.whereElements,
|
|
2715 endElements = [];
|
|
2716
|
|
2717 if (constraintAttribute === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2718 returnElements.push("count(DISTINCT ID(" + rootNode.internalLabel + ")) AS count");
|
|
2719 } else {
|
|
2720 returnElements.push("count(DISTINCT " + rootNode.internalLabel + "." + constraintAttribute + ") AS count");
|
|
2721 }
|
|
2722
|
|
2723 return "MATCH " + matchElements.join(", ") + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN " + returnElements.join(", ") + (endElements.length > 0 ? " " + endElements.join(" ") : "");
|
|
2724 };
|
|
2725
|
|
2726 /**
|
|
2727 * Generate the query to update node counts.
|
|
2728 *
|
|
2729 * @param countedNode the counted node
|
|
2730 * @returns {string} the node count cypher query;
|
|
2731 */
|
|
2732 popoto.query.generateNodeCountCypherQuery = function (countedNode) {
|
|
2733
|
|
2734 var queryElements = popoto.query.generateQueryElements(popoto.graph.getRootNode(), countedNode, popoto.query.getRelevantLinks(popoto.graph.getRootNode(), countedNode, popoto.graph.force.links()), true);
|
|
2735 var matchElements = queryElements.matchElements,
|
|
2736 whereElements = queryElements.whereElements,
|
|
2737 returnElements = [];
|
|
2738
|
|
2739 var countAttr = popoto.provider.getConstraintAttribute(countedNode.label);
|
|
2740
|
|
2741 if (countAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2742 returnElements.push("count(DISTINCT ID(" + countedNode.internalLabel + ")) as count");
|
|
2743 } else {
|
|
2744 returnElements.push("count(DISTINCT " + countedNode.internalLabel + "." + countAttr + ") as count");
|
|
2745 }
|
|
2746
|
|
2747 return "MATCH " + matchElements.join(", ") + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN " + returnElements.join(", ");
|
|
2748 };
|
|
2749
|
|
2750 /**
|
|
2751 * Generate a Cypher query from the graph model to get all the possible values for the targetNode element.
|
|
2752 *
|
|
2753 * @param targetNode node in the graph to get the values.
|
|
2754 * @returns {string} the query to execute to get all the values of targetNode corresponding to the graph.
|
|
2755 */
|
|
2756 popoto.query.generateValueQuery = function (targetNode) {
|
|
2757
|
|
2758 var rootNode = popoto.graph.getRootNode();
|
|
2759 var queryElements = popoto.query.generateQueryElements(rootNode, targetNode, popoto.query.getRelevantLinks(rootNode, targetNode, popoto.graph.force.links()), true);
|
|
2760 var matchElements = queryElements.matchElements,
|
|
2761 endElements = [],
|
|
2762 whereElements = queryElements.whereElements,
|
|
2763 returnElements = [];
|
|
2764
|
|
2765 // Sort results by specified attribute
|
|
2766 var valueOrderByAttribute = popoto.provider.getValueOrderByAttribute(targetNode.label);
|
|
2767 if (valueOrderByAttribute) {
|
|
2768 var order = popoto.provider.isValueOrderAscending(targetNode.label) ? "ASC" : "DESC";
|
|
2769 endElements.push("ORDER BY " + valueOrderByAttribute + " " + order);
|
|
2770 }
|
|
2771
|
|
2772 endElements.push("LIMIT " + popoto.query.VALUE_QUERY_LIMIT);
|
|
2773
|
|
2774 var resultAttributes = popoto.provider.getReturnAttributes(targetNode.label);
|
|
2775 var constraintAttribute = popoto.provider.getConstraintAttribute(targetNode.label);
|
|
2776
|
|
2777 for (var i = 0; i < resultAttributes.length; i++) {
|
|
2778 if (resultAttributes[i] === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2779 if (resultAttributes[i] == constraintAttribute) {
|
|
2780 returnElements.push("ID(" + targetNode.internalLabel + ") AS " + popoto.query.NEO4J_INTERNAL_ID.queryInternalName);
|
|
2781 } else {
|
|
2782 returnElements.push("COLLECT (DISTINCT ID(" + targetNode.internalLabel + ")) AS " + popoto.query.NEO4J_INTERNAL_ID.queryInternalName);
|
|
2783 }
|
|
2784 } else {
|
|
2785 if (resultAttributes[i] == constraintAttribute) {
|
|
2786 returnElements.push(targetNode.internalLabel + "." + resultAttributes[i] + " AS " + resultAttributes[i]);
|
|
2787 } else {
|
|
2788 returnElements.push("COLLECT(DISTINCT " + targetNode.internalLabel + "." + resultAttributes[i] + ") AS " + resultAttributes[i]);
|
|
2789 }
|
|
2790 }
|
|
2791 }
|
|
2792
|
|
2793 // Add count return attribute on root node
|
|
2794 var rootConstraintAttr = popoto.provider.getConstraintAttribute(rootNode.label);
|
|
2795
|
|
2796 if (rootConstraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
2797 returnElements.push("count(DISTINCT ID(" + rootNode.internalLabel + ")) AS count");
|
|
2798 } else {
|
|
2799 returnElements.push("count(DISTINCT " + rootNode.internalLabel + "." + rootConstraintAttr + ") AS count");
|
|
2800 }
|
|
2801
|
|
2802 return "MATCH " + matchElements.join(", ") + ((whereElements.length > 0) ? " WHERE " + whereElements.join(" AND ") : "") + " RETURN DISTINCT " + returnElements.join(", ") + " " + endElements.join(" ");
|
|
2803 };
|
|
2804
|
|
2805 ///////////////////////////////////////////////////////////////////
|
|
2806 // Results
|
|
2807
|
|
2808 popoto.result = {};
|
|
2809 popoto.result.containerId = "popoto-results";
|
|
2810 popoto.result.hasChanged = true;
|
|
2811 popoto.result.resultCountListeners = [];
|
|
2812 popoto.result.resultListeners = [];
|
|
2813
|
|
2814 /**
|
|
2815 * Register a listener to the result count event.
|
|
2816 * This listener will be called on evry result change with total result count.
|
|
2817 */
|
|
2818 popoto.result.onTotalResultCount = function (listener) {
|
|
2819 popoto.result.resultCountListeners.push(listener);
|
|
2820 };
|
|
2821
|
|
2822 popoto.result.onResultReceived = function (listener) {
|
|
2823 popoto.result.resultListeners.push(listener);
|
|
2824 };
|
|
2825
|
|
2826 /**
|
|
2827 * Parse REST returned data and generate a list of result objects.
|
|
2828 *
|
|
2829 * @param data
|
|
2830 * @returns {Array}
|
|
2831 */
|
|
2832 popoto.result.parseResultData = function (data) {
|
|
2833
|
|
2834 var results = [];
|
|
2835 if (data.results && data.results.length > 0) {
|
|
2836 for (var x = 0; x < data.results[0].data.length; x++) {
|
|
2837
|
|
2838 var obj = {
|
|
2839 "resultIndex": x,
|
|
2840 "label": popoto.graph.getRootNode().label,
|
|
2841 "attributes": {}
|
|
2842 };
|
|
2843
|
|
2844 for (var i = 0; i < data.results[0].columns.length; i++) {
|
|
2845 // Some results can be an array as collect is used in query
|
|
2846 // So all values are converted to string
|
|
2847 obj.attributes[data.results[0].columns[i]] = "" + data.results[0].data[x].row[i];
|
|
2848 }
|
|
2849
|
|
2850 results.push(obj);
|
|
2851 }
|
|
2852 }
|
|
2853
|
|
2854 return results;
|
|
2855 };
|
|
2856
|
|
2857 popoto.result.updateResults = function () {
|
|
2858 if (popoto.result.hasChanged) {
|
|
2859 var query = popoto.query.generateResultCypherQuery();
|
|
2860
|
|
2861 // FIXME temporary cypher query update here. To be replaced by real interactive cypher viewer.
|
|
2862 if (popoto.cypherviewer.isActive) {
|
|
2863 d3.select("#" + popoto.cypherviewer.containerId)
|
|
2864 // In this temporary version only the match part of the query is displayed to avoid huge query with lot of return attributes.
|
|
2865 .text(query.split("RETURN")[0] + " RETURN " + popoto.graph.getRootNode().internalLabel);
|
|
2866 }
|
|
2867
|
|
2868 popoto.logger.info("Results ==> ");
|
|
2869 popoto.rest.post(
|
|
2870 {
|
|
2871 "statements": [
|
|
2872 {
|
|
2873 "statement": query
|
|
2874 }]
|
|
2875 })
|
|
2876 .done(function (data) {
|
|
2877
|
|
2878 if (data.errors && data.errors.length > 0) {
|
|
2879 popoto.logger.error("Cypher query error:" + JSON.stringify(data.errors));
|
|
2880 }
|
|
2881
|
|
2882 // Parse data
|
|
2883 var resultObjects = popoto.result.parseResultData(data);
|
|
2884
|
|
2885 // Notify listeners
|
|
2886 popoto.result.resultListeners.forEach(function (listener) {
|
|
2887 listener(resultObjects);
|
|
2888 });
|
|
2889
|
|
2890 // Update displayed results only if needed ()
|
|
2891 if (popoto.result.isActive) {
|
|
2892 // Clear all results
|
|
2893 var results = d3.select("#" + popoto.result.containerId).selectAll(".ppt-result").data([]);
|
|
2894 results.exit().remove();
|
|
2895
|
|
2896 // Update data
|
|
2897 results = d3.select("#" + popoto.result.containerId).selectAll(".ppt-result").data(resultObjects, function (d) {
|
|
2898 return d.resultIndex;
|
|
2899 });
|
|
2900
|
|
2901 // Add new elements
|
|
2902 var pElmt = results.enter()
|
|
2903 .append("p")
|
|
2904 .attr("class", "ppt-result")
|
|
2905 .attr("id", function (d) {
|
|
2906 return "popoto-result-" + d.resultIndex;
|
|
2907 });
|
|
2908
|
|
2909 // Generate results with providers
|
|
2910 pElmt.each(function (d) {
|
|
2911 popoto.provider.getDisplayResultFunction(d.label)(d3.select(this));
|
|
2912 });
|
|
2913 }
|
|
2914
|
|
2915 popoto.result.hasChanged = false;
|
|
2916 })
|
|
2917 .fail(function (xhr, textStatus, errorThrown) {
|
|
2918 popoto.logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + popoto.rest.CYPHER_URL + "\" defined in \"popoto.rest.CYPHER_URL\" property: " + errorThrown);
|
|
2919
|
|
2920 // Notify listeners
|
|
2921 popoto.result.resultListeners.forEach(function (listener) {
|
|
2922 listener([]);
|
|
2923 });
|
|
2924
|
|
2925 });
|
|
2926
|
|
2927 // Execute query to get total result count
|
|
2928 // But only if needed, if listeners have been added
|
|
2929 if (popoto.result.resultCountListeners.length > 0) {
|
|
2930 popoto.logger.info("Results count ==> ");
|
|
2931 popoto.rest.post(
|
|
2932 {
|
|
2933 "statements": [
|
|
2934 {
|
|
2935 "statement": popoto.query.generateResultCypherQueryCount()
|
|
2936 }]
|
|
2937 })
|
|
2938 .done(function (data) {
|
|
2939
|
|
2940 if (data.errors && data.errors.length > 0) {
|
|
2941 popoto.logger.error("Cypher query error:" + JSON.stringify(data.errors));
|
|
2942 }
|
|
2943
|
|
2944 var count = 0;
|
|
2945
|
|
2946 if (data.results && data.results.length > 0) {
|
|
2947 count = data.results[0].data[0].row[0];
|
|
2948 }
|
|
2949
|
|
2950 popoto.result.resultCountListeners.forEach(function (listener) {
|
|
2951 listener(count);
|
|
2952 });
|
|
2953
|
|
2954 })
|
|
2955 .fail(function (xhr, textStatus, errorThrown) {
|
|
2956 popoto.logger.error(textStatus + ": error while accessing Neo4j server on URL:\"" + popoto.rest.CYPHER_URL + "\" defined in \"popoto.rest.CYPHER_URL\" property: " + errorThrown);
|
|
2957
|
|
2958 popoto.result.resultCountListeners.forEach(function (listener) {
|
|
2959 listener(0);
|
|
2960 });
|
|
2961 });
|
|
2962 }
|
|
2963 }
|
|
2964 };
|
|
2965
|
|
2966 // NODE LABEL PROVIDERS -----------------------------------------------------------------------------------------------------
|
|
2967
|
|
2968 popoto.provider = {};
|
|
2969 popoto.provider.linkProvider = {};
|
|
2970 popoto.provider.taxonomyProvider = {};
|
|
2971 popoto.provider.nodeProviders = {};
|
|
2972
|
|
2973 /**
|
|
2974 * Get the text representation of a link.
|
|
2975 *
|
|
2976 * @param link the link to get the text representation.
|
|
2977 * @returns {string} the text representation of the link.
|
|
2978 */
|
|
2979 popoto.provider.getLinkTextValue = function (link) {
|
|
2980 if (popoto.provider.linkProvider.hasOwnProperty("getLinkTextValue")) {
|
|
2981 return popoto.provider.linkProvider.getLinkTextValue(link);
|
|
2982 } else {
|
|
2983 if (popoto.provider.DEFAULT_LINK_PROVIDER.hasOwnProperty("getLinkTextValue")) {
|
|
2984 return popoto.provider.DEFAULT_LINK_PROVIDER.getLinkTextValue(link);
|
|
2985 } else {
|
|
2986 popoto.logger.error("No provider defined for getLinkTextValue");
|
|
2987 }
|
|
2988 }
|
|
2989 };
|
|
2990
|
|
2991 /**
|
|
2992 * Get the semantic text representation of a link.
|
|
2993 *
|
|
2994 * @param link the link to get the semantic text representation.
|
|
2995 * @returns {string} the semantic text representation of the link.
|
|
2996 */
|
|
2997 popoto.provider.getLinkSemanticValue = function (link) {
|
|
2998 if (popoto.provider.linkProvider.hasOwnProperty("getLinkSemanticValue")) {
|
|
2999 return popoto.provider.linkProvider.getLinkSemanticValue(link);
|
|
3000 } else {
|
|
3001 if (popoto.provider.DEFAULT_LINK_PROVIDER.hasOwnProperty("getLinkSemanticValue")) {
|
|
3002 return popoto.provider.DEFAULT_LINK_PROVIDER.getLinkSemanticValue(link);
|
|
3003 } else {
|
|
3004 popoto.logger.error("No provider defined for getLinkSemanticValue");
|
|
3005 }
|
|
3006 }
|
|
3007 };
|
|
3008
|
|
3009 /**
|
|
3010 * Label provider used by default if none have been defined for a label.
|
|
3011 * This provider can be changed if needed to customize default behavior.
|
|
3012 * If some properties are not found in user customized providers, default values will be extracted from this provider.
|
|
3013 */
|
|
3014 popoto.provider.DEFAULT_LINK_PROVIDER = Object.freeze(
|
|
3015 {
|
|
3016 /**
|
|
3017 * Function used to return the text representation of a link.
|
|
3018 *
|
|
3019 * The default behavior is to return the internal relation name as text for relation links.
|
|
3020 * And return the target node text value for links between a node and its expanded values but only if text is not displayed on value node.
|
|
3021 *
|
|
3022 * @param link the link to represent as text.
|
|
3023 * @returns {string} the text representation of the link.
|
|
3024 */
|
|
3025 "getLinkTextValue": function (link) {
|
|
3026 if (link.type === popoto.graph.link.LinkTypes.VALUE) {
|
|
3027 // Links between node and list of values.
|
|
3028
|
|
3029 if (popoto.provider.isTextDisplayed(link.target)) {
|
|
3030 // Don't display text on link if text is displayed on target node.
|
|
3031 return "";
|
|
3032 } else {
|
|
3033 // No text is displayed on target node then the text is displayed on link.
|
|
3034 return popoto.provider.getTextValue(link.target);
|
|
3035 }
|
|
3036
|
|
3037 } else {
|
|
3038
|
|
3039 // Link
|
|
3040 return link.label
|
|
3041 }
|
|
3042 },
|
|
3043
|
|
3044 /**
|
|
3045 * Function used to return a descriptive text representation of a link.
|
|
3046 * This representation should be more complete than getLinkTextValue and can contain semantic data.
|
|
3047 * This function is used for example to generate the label in the query viewer.
|
|
3048 *
|
|
3049 * The default behavior is to return the getLinkTextValue.
|
|
3050 *
|
|
3051 * @param link the link to represent as text.
|
|
3052 * @returns {string} the text semantic representation of the link.
|
|
3053 */
|
|
3054 "getLinkSemanticValue": function (link) {
|
|
3055 return popoto.provider.getLinkTextValue(link);
|
|
3056 }
|
|
3057 });
|
|
3058 popoto.provider.linkProvider = popoto.provider.DEFAULT_LINK_PROVIDER;
|
|
3059
|
|
3060 /**
|
|
3061 * Get the text representation of a taxonomy.
|
|
3062 *
|
|
3063 * @param label the label used for the taxonomy.
|
|
3064 * @returns {string} the text representation of the taxonomy.
|
|
3065 */
|
|
3066 popoto.provider.getTaxonomyTextValue = function (label) {
|
|
3067 if (popoto.provider.taxonomyProvider.hasOwnProperty("getTextValue")) {
|
|
3068 return popoto.provider.taxonomyProvider.getTextValue(label);
|
|
3069 } else {
|
|
3070 if (popoto.provider.DEFAULT_TAXONOMY_PROVIDER.hasOwnProperty("getTextValue")) {
|
|
3071 return popoto.provider.DEFAULT_TAXONOMY_PROVIDER.getTextValue(label);
|
|
3072 } else {
|
|
3073 popoto.logger.error("No provider defined for taxonomy getTextValue");
|
|
3074 }
|
|
3075 }
|
|
3076 };
|
|
3077
|
|
3078 /**
|
|
3079 * Label provider used by default if none have been defined for a label.
|
|
3080 * This provider can be changed if needed to customize default behavior.
|
|
3081 * If some properties are not found in user customized providers, default values will be extracted from this provider.
|
|
3082 */
|
|
3083 popoto.provider.DEFAULT_TAXONOMY_PROVIDER = Object.freeze(
|
|
3084 {
|
|
3085 /**
|
|
3086 * Function used to return the text representation of a taxonomy.
|
|
3087 *
|
|
3088 * The default behavior is to return the label without changes.
|
|
3089 *
|
|
3090 * @param label the label used to represent the taxonomy.
|
|
3091 * @returns {string} the text representation of the taxonomy.
|
|
3092 */
|
|
3093 "getTextValue": function (label) {
|
|
3094 return label;
|
|
3095 }
|
|
3096 });
|
|
3097 popoto.provider.taxonomyProvider = popoto.provider.DEFAULT_TAXONOMY_PROVIDER;
|
|
3098
|
|
3099 /**
|
|
3100 * Define the different type of rendering of a node for a given label.
|
|
3101 * TEXT: default rendering type, the node will be displayed with an ellipse and a text in it.
|
|
3102 * IMAGE: the node is displayed as an image using the image tag in the svg graph.
|
|
3103 * In this case an image path is required.
|
|
3104 * SVG: the node is displayed using a list of svg path, each path can contain its own color.
|
|
3105 */
|
|
3106 popoto.provider.NodeDisplayTypes = Object.freeze({TEXT: 0, IMAGE: 1, SVG: 2});
|
|
3107
|
|
3108 /**
|
|
3109 * Get the label provider for the given label.
|
|
3110 * If no provider is defined for the label:
|
|
3111 * First search in parent provider.
|
|
3112 * Then if not found will create one from default provider.
|
|
3113 *
|
|
3114 * @param label to retrieve the corresponding label provider.
|
|
3115 * @returns {object} corresponding label provider.
|
|
3116 */
|
|
3117 popoto.provider.getProvider = function (label) {
|
|
3118 if (label === undefined) {
|
|
3119 popoto.logger.error("Node label is undefined, no label provider can be found.");
|
|
3120 } else {
|
|
3121 if (popoto.provider.nodeProviders.hasOwnProperty(label)) {
|
|
3122 return popoto.provider.nodeProviders[label];
|
|
3123 } else {
|
|
3124 popoto.logger.debug("No direct provider found for label " + label);
|
|
3125
|
|
3126 // Search in all children list definitions to find the parent provider.
|
|
3127 for (var p in popoto.provider.nodeProviders) {
|
|
3128 if (popoto.provider.nodeProviders.hasOwnProperty(p)) {
|
|
3129 var provider = popoto.provider.nodeProviders[p];
|
|
3130 if (provider.hasOwnProperty("children")) {
|
|
3131 if (provider["children"].indexOf(label) > -1) {
|
|
3132 popoto.logger.debug("No provider is defined for label (" + label + "), parent (" + p + ") will be used");
|
|
3133 // A provider containing the required label in its children definition has been found it will be cloned.
|
|
3134
|
|
3135 var newProvider = {"parent": p};
|
|
3136 for (var pr in provider) {
|
|
3137 if (provider.hasOwnProperty(pr) && pr != "children" && pr != "parent") {
|
|
3138 newProvider[pr] = provider[pr];
|
|
3139 }
|
|
3140 }
|
|
3141
|
|
3142 popoto.provider.nodeProviders[label] = newProvider;
|
|
3143 return popoto.provider.nodeProviders[label];
|
|
3144 }
|
|
3145 }
|
|
3146 }
|
|
3147 }
|
|
3148
|
|
3149 popoto.logger.debug("No label provider defined for label (" + label + ") default one will be created from popoto.provider.DEFAULT_PROVIDER");
|
|
3150
|
|
3151 popoto.provider.nodeProviders[label] = {};
|
|
3152 // Clone default provider properties in new provider.
|
|
3153 for (var prop in popoto.provider.DEFAULT_PROVIDER) {
|
|
3154 if (popoto.provider.DEFAULT_PROVIDER.hasOwnProperty(prop)) {
|
|
3155 popoto.provider.nodeProviders[label][prop] = popoto.provider.DEFAULT_PROVIDER[prop];
|
|
3156 }
|
|
3157 }
|
|
3158 return popoto.provider.nodeProviders[label];
|
|
3159 }
|
|
3160 }
|
|
3161 };
|
|
3162
|
|
3163 /**
|
|
3164 * Get the property or function defined in node label provider.
|
|
3165 * If the property is not found search is done in parents.
|
|
3166 * If not found in parent, property defined in popoto.provider.DEFAULT_PROVIDER is returned.
|
|
3167 * If not found in default provider, defaultValue is set and returned.
|
|
3168 *
|
|
3169 * @param label node label to get the property in its provider.
|
|
3170 * @param name name of the property to retrieve.
|
|
3171 * @returns {*} node property defined in its label provider.
|
|
3172 */
|
|
3173 popoto.provider.getProperty = function (label, name) {
|
|
3174 var provider = popoto.provider.getProvider(label);
|
|
3175
|
|
3176 if (!provider.hasOwnProperty(name)) {
|
|
3177 var providerIterator = provider;
|
|
3178
|
|
3179 // Check parents
|
|
3180 var isPropertyFound = false;
|
|
3181 while (providerIterator.hasOwnProperty("parent") && !isPropertyFound) {
|
|
3182 providerIterator = popoto.provider.getProvider(providerIterator.parent);
|
|
3183 if (providerIterator.hasOwnProperty(name)) {
|
|
3184
|
|
3185 // Set attribute in child to optimize next call.
|
|
3186 provider[name] = providerIterator[name];
|
|
3187 isPropertyFound = true;
|
|
3188 }
|
|
3189 }
|
|
3190
|
|
3191 if (!isPropertyFound) {
|
|
3192 popoto.logger.debug("No \"" + name + "\" property found for node label provider (" + label + "), default value will be used");
|
|
3193 if (popoto.provider.DEFAULT_PROVIDER.hasOwnProperty(name)) {
|
|
3194 provider[name] = popoto.provider.DEFAULT_PROVIDER[name];
|
|
3195 } else {
|
|
3196 popoto.logger.error("No default value for \"" + name + "\" property found for label provider (" + label + ")");
|
|
3197 }
|
|
3198 }
|
|
3199 }
|
|
3200 return provider[name];
|
|
3201 };
|
|
3202
|
|
3203 /**
|
|
3204 * Return the "isSearchable" property for the node label provider.
|
|
3205 * Is Searchable defined whether the label can be used as graph query builder root.
|
|
3206 * If true the label can be displayed in the taxonomy filter.
|
|
3207 *
|
|
3208 * @param label
|
|
3209 * @returns {*}
|
|
3210 */
|
|
3211 popoto.provider.getIsSearchable = function (label) {
|
|
3212 return popoto.provider.getProperty(label, "isSearchable");
|
|
3213 };
|
|
3214
|
|
3215 /**
|
|
3216 * Return the list of attributes defined in node label provider.
|
|
3217 * Parents return attributes are also returned.
|
|
3218 *
|
|
3219 * @param label used to retrieve parent attributes.
|
|
3220 * @returns {Array} list of return attributes for a node.
|
|
3221 */
|
|
3222 popoto.provider.getReturnAttributes = function (label) {
|
|
3223 var provider = popoto.provider.getProvider(label);
|
|
3224 var attributes = {}; // Object is used as a Set to merge possible duplicate in parents
|
|
3225
|
|
3226 if (provider.hasOwnProperty("returnAttributes")) {
|
|
3227 for (var i = 0; i < provider.returnAttributes.length; i++) {
|
|
3228 if (provider.returnAttributes[i] === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3229 attributes[popoto.query.NEO4J_INTERNAL_ID.queryInternalName] = true;
|
|
3230 } else {
|
|
3231 attributes[provider.returnAttributes[i]] = true;
|
|
3232 }
|
|
3233 }
|
|
3234 }
|
|
3235
|
|
3236 // Add parent attributes
|
|
3237 while (provider.hasOwnProperty("parent")) {
|
|
3238 provider = popoto.provider.getProvider(provider.parent);
|
|
3239 if (provider.hasOwnProperty("returnAttributes")) {
|
|
3240 for (var j = 0; j < provider.returnAttributes.length; j++) {
|
|
3241 if (provider.returnAttributes[j] === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3242 attributes[popoto.query.NEO4J_INTERNAL_ID.queryInternalName] = true;
|
|
3243 } else {
|
|
3244 attributes[provider.returnAttributes[j]] = true;
|
|
3245 }
|
|
3246 }
|
|
3247 }
|
|
3248 }
|
|
3249
|
|
3250 // Add default provider attributes if any but not internal id as this id is added only if none has been found.
|
|
3251 if (popoto.provider.DEFAULT_PROVIDER.hasOwnProperty("returnAttributes")) {
|
|
3252 for (var k = 0; k < popoto.provider.DEFAULT_PROVIDER.returnAttributes.length; k++) {
|
|
3253 if (popoto.provider.DEFAULT_PROVIDER.returnAttributes[k] !== popoto.query.NEO4J_INTERNAL_ID) {
|
|
3254 attributes[popoto.provider.DEFAULT_PROVIDER.returnAttributes[k]] = true;
|
|
3255 }
|
|
3256 }
|
|
3257 }
|
|
3258
|
|
3259 // Add constraint attribute in the list
|
|
3260 var constraintAttribute = popoto.provider.getConstraintAttribute(label);
|
|
3261 if (constraintAttribute === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3262 attributes[popoto.query.NEO4J_INTERNAL_ID.queryInternalName] = true;
|
|
3263 } else {
|
|
3264 attributes[constraintAttribute] = true;
|
|
3265 }
|
|
3266
|
|
3267
|
|
3268 // Add all in array
|
|
3269 var attrList = [];
|
|
3270 for (var attr in attributes) {
|
|
3271 if (attributes.hasOwnProperty(attr)) {
|
|
3272 if (attr == popoto.query.NEO4J_INTERNAL_ID.queryInternalName) {
|
|
3273 attrList.push(popoto.query.NEO4J_INTERNAL_ID);
|
|
3274 } else {
|
|
3275 attrList.push(attr);
|
|
3276 }
|
|
3277 }
|
|
3278 }
|
|
3279
|
|
3280 // If no attributes have been found internal ID is used
|
|
3281 if (attrList.length <= 0) {
|
|
3282 attrList.push(popoto.query.NEO4J_INTERNAL_ID);
|
|
3283 }
|
|
3284 return attrList;
|
|
3285 };
|
|
3286
|
|
3287 /**
|
|
3288 * Return the attribute to use as constraint attribute for a node defined in its label provider.
|
|
3289 *
|
|
3290 * @param label
|
|
3291 * @returns {*}
|
|
3292 */
|
|
3293 popoto.provider.getConstraintAttribute = function (label) {
|
|
3294 return popoto.provider.getProperty(label, "constraintAttribute");
|
|
3295 };
|
|
3296
|
|
3297 /**
|
|
3298 * Return a list of predefined constraint defined in the node label configuration.
|
|
3299 *
|
|
3300 * @param label
|
|
3301 * @returns {*}
|
|
3302 */
|
|
3303 popoto.provider.getPredefinedConstraints = function (label) {
|
|
3304 return popoto.provider.getProperty(label, "getPredefinedConstraints")();
|
|
3305 };
|
|
3306
|
|
3307
|
|
3308 popoto.provider.getValueOrderByAttribute = function (label) {
|
|
3309 return popoto.provider.getProperty(label, "valueOrderByAttribute");
|
|
3310 };
|
|
3311
|
|
3312 popoto.provider.isValueOrderAscending = function (label) {
|
|
3313 return popoto.provider.getProperty(label, "isValueOrderAscending");
|
|
3314 };
|
|
3315
|
|
3316 popoto.provider.getResultOrderByAttribute = function (label) {
|
|
3317 return popoto.provider.getProperty(label, "resultOrderByAttribute");
|
|
3318 };
|
|
3319
|
|
3320 popoto.provider.isResultOrderAscending = function (label) {
|
|
3321 return popoto.provider.getProperty(label, "isResultOrderAscending");
|
|
3322 };
|
|
3323
|
|
3324 /**
|
|
3325 * Return the value of the getTextValue function defined in the label provider corresponding to the parameter node.
|
|
3326 * If no "getTextValue" function is defined in the provider, search is done in parents.
|
|
3327 * If none is found in parent default provider method is used.
|
|
3328 *
|
|
3329 * @param node
|
|
3330 */
|
|
3331 popoto.provider.getTextValue = function (node) {
|
|
3332 return popoto.provider.getProperty(node.label, "getTextValue")(node);
|
|
3333 };
|
|
3334
|
|
3335
|
|
3336 /**
|
|
3337 * Return the value of the getTextValue function defined in the label provider corresponding to the parameter node.
|
|
3338 * If no "getTextValue" function is defined in the provider, search is done in parents.
|
|
3339 * If none is found in parent default provider method is used.
|
|
3340 *
|
|
3341 * @param node
|
|
3342 */
|
|
3343 popoto.provider.getTextValue = function (node) {
|
|
3344 return popoto.provider.getProperty(node.label, "getTextValue")(node);
|
|
3345 };
|
|
3346
|
|
3347 /**
|
|
3348 * Return the value of the getSemanticValue function defined in the label provider corresponding to the parameter node.
|
|
3349 * The semantic value is a more detailed description of the node used for example in the query viewer.
|
|
3350 * If no "getTextValue" function is defined in the provider, search is done in parents.
|
|
3351 * If none is found in parent default provider method is used.
|
|
3352 *
|
|
3353 * @param node
|
|
3354 * @returns {*}
|
|
3355 */
|
|
3356 popoto.provider.getSemanticValue = function (node) {
|
|
3357 return popoto.provider.getProperty(node.label, "getSemanticValue")(node);
|
|
3358 };
|
|
3359
|
|
3360 /**
|
|
3361 * Return a list of SVG paths objects, each defined by a "d" property containing the path and "f" property for the color.
|
|
3362 *
|
|
3363 * @param node
|
|
3364 * @returns {*}
|
|
3365 */
|
|
3366 popoto.provider.getSVGPaths = function (node) {
|
|
3367 return popoto.provider.getProperty(node.label, "getSVGPaths")(node);
|
|
3368 };
|
|
3369
|
|
3370 /**
|
|
3371 * Check in label provider if text must be displayed with images nodes.
|
|
3372 * @param node
|
|
3373 * @returns {*}
|
|
3374 */
|
|
3375 popoto.provider.isTextDisplayed = function (node) {
|
|
3376 return popoto.provider.getProperty(node.label, "getIsTextDisplayed")(node);
|
|
3377 };
|
|
3378
|
|
3379 /**
|
|
3380 * Return the getIsGroup property.
|
|
3381 *
|
|
3382 * @param node
|
|
3383 * @returns {*}
|
|
3384 */
|
|
3385 popoto.provider.getIsGroup = function (node) {
|
|
3386 return popoto.provider.getProperty(node.label, "getIsGroup")(node);
|
|
3387 };
|
|
3388
|
|
3389 /**
|
|
3390 * Return the node display type.
|
|
3391 * can be TEXT, IMAGE, SVG or GROUP.
|
|
3392 *
|
|
3393 * @param node
|
|
3394 * @returns {*}
|
|
3395 */
|
|
3396 popoto.provider.getNodeDisplayType = function (node) {
|
|
3397 return popoto.provider.getProperty(node.label, "getDisplayType")(node);
|
|
3398 };
|
|
3399
|
|
3400 /**
|
|
3401 * Return the file path of the image defined in the provider.
|
|
3402 *
|
|
3403 * @param node the node to get the image path.
|
|
3404 * @returns {string} the path of the node image.
|
|
3405 */
|
|
3406 popoto.provider.getImagePath = function (node) {
|
|
3407 return popoto.provider.getProperty(node.label, "getImagePath")(node);
|
|
3408 };
|
|
3409
|
|
3410 /**
|
|
3411 * Return the width size of the node image.
|
|
3412 *
|
|
3413 * @param node the node to get the image width.
|
|
3414 * @returns {int} the image width.
|
|
3415 */
|
|
3416 popoto.provider.getImageWidth = function (node) {
|
|
3417 return popoto.provider.getProperty(node.label, "getImageWidth")(node);
|
|
3418 };
|
|
3419
|
|
3420 /**
|
|
3421 * Return the height size of the node image.
|
|
3422 *
|
|
3423 * @param node the node to get the image height.
|
|
3424 * @returns {int} the image height.
|
|
3425 */
|
|
3426 popoto.provider.getImageHeight = function (node) {
|
|
3427 return popoto.provider.getProperty(node.label, "getImageHeight")(node);
|
|
3428 };
|
|
3429
|
|
3430 /**
|
|
3431 * Return the displayResults function defined in label parameter's provider.
|
|
3432 *
|
|
3433 * @param label
|
|
3434 * @returns {*}
|
|
3435 */
|
|
3436 popoto.provider.getDisplayResultFunction = function (label) {
|
|
3437 return popoto.provider.getProperty(label, "displayResults");
|
|
3438 };
|
|
3439
|
|
3440 /**
|
|
3441 * Label provider used by default if none have been defined for a label.
|
|
3442 * This provider can be changed if needed to customize default behavior.
|
|
3443 * If some properties are not found in user customized providers, default values will be extracted from this provider.
|
|
3444 */
|
|
3445 popoto.provider.DEFAULT_PROVIDER = Object.freeze(
|
|
3446 {
|
|
3447 /**********************************************************************
|
|
3448 * Label specific parameters:
|
|
3449 *
|
|
3450 * These attributes are specific to a node label and will be used for every node having this label.
|
|
3451 **********************************************************************/
|
|
3452
|
|
3453 /**
|
|
3454 * Defines whether this label can be used as root element of the graph query builder.
|
|
3455 * This property is also used to determine whether the label can be displayed in the taxonomy filter.
|
|
3456 *
|
|
3457 * The default value is true.
|
|
3458 */
|
|
3459 "isSearchable": true,
|
|
3460
|
|
3461 /**
|
|
3462 * Defines the list of attribute to return for node of this label.
|
|
3463 * All the attributes listed here will be added in generated cypher queries and available in result list and in node provider's functions.
|
|
3464 *
|
|
3465 * The default value contains only the Neo4j internal id.
|
|
3466 * This id is used by default because it is a convenient way to identify a node when nothing is known about its attributes.
|
|
3467 * But you should really consider using your application attributes instead, it is a bad practice to rely on this attribute.
|
|
3468 */
|
|
3469 "returnAttributes": [popoto.query.NEO4J_INTERNAL_ID],
|
|
3470
|
|
3471 /**
|
|
3472 * Defines the attribute used to order the value displayed on node.
|
|
3473 *
|
|
3474 * Default value is "count" attribute.
|
|
3475 */
|
|
3476 "valueOrderByAttribute": "count",
|
|
3477
|
|
3478 /**
|
|
3479 * Defines whether the value query order by is ascending, if false order by will be descending.
|
|
3480 *
|
|
3481 * Default value is false (descending)
|
|
3482 */
|
|
3483 "isValueOrderAscending": false,
|
|
3484
|
|
3485 /**
|
|
3486 * Defines the attribute used to order the results.
|
|
3487 *
|
|
3488 * Default value is "null" to disable order by.
|
|
3489 */
|
|
3490 "resultOrderByAttribute": null,
|
|
3491
|
|
3492 /**
|
|
3493 * Defines whether the result query order by is ascending, if false order by will be descending.
|
|
3494 *
|
|
3495 * Default value is true (ascending)
|
|
3496 */
|
|
3497 "isResultOrderAscending": true,
|
|
3498
|
|
3499 /**
|
|
3500 * Defines the attribute of the node to use in query constraint for nodes of this label.
|
|
3501 * This attribute is used in the generated cypher query to build the constraints with selected values.
|
|
3502 *
|
|
3503 * The default value is the Neo4j internal id.
|
|
3504 * This id is used by default because it is a convenient way to identify a node when nothing is known about its attributes.
|
|
3505 * But you should really consider using your application attributes instead, it is a bad practice to rely on this attribute.
|
|
3506 */
|
|
3507 "constraintAttribute": popoto.query.NEO4J_INTERNAL_ID,
|
|
3508
|
|
3509 /**
|
|
3510 * Return the list of predefined constraints to add for the given label.
|
|
3511 * These constraints will be added in every generated Cypher query.
|
|
3512 *
|
|
3513 * For example if the returned list contain ["$identifier.born > 1976"] for "Person" nodes everywhere in popoto.js the generated Cypher query will add the constraint
|
|
3514 * "WHERE person.born > 1976"
|
|
3515 *
|
|
3516 * @returns {Array}
|
|
3517 */
|
|
3518 "getPredefinedConstraints": function () {
|
|
3519 return [];
|
|
3520 },
|
|
3521
|
|
3522 /**********************************************************************
|
|
3523 * Node specific parameters:
|
|
3524 *
|
|
3525 * These attributes are specific to nodes (in graph or query viewer) for a given label.
|
|
3526 * But they can be customized for nodes of the same label.
|
|
3527 * The parameters are defined by a function that will be called with the node as parameter.
|
|
3528 * In this function the node internal attributes can be used to customize the value to return.
|
|
3529 **********************************************************************/
|
|
3530
|
|
3531 /**
|
|
3532 * Function returning the display type of a node.
|
|
3533 * This type defines how the node will be drawn in the graph.
|
|
3534 *
|
|
3535 * The result must be one of the following values:
|
|
3536 *
|
|
3537 * popoto.provider.NodeDisplayTypes.IMAGE
|
|
3538 * In this case the node will be drawn as an image and "getImagePath" function is required to return the node image path.
|
|
3539 *
|
|
3540 * popoto.provider.NodeDisplayTypes.SVG
|
|
3541 * In this case the node will be drawn as SVG paths and "getSVGPaths"
|
|
3542 *
|
|
3543 * popoto.provider.NodeDisplayTypes.TEXT
|
|
3544 * In this case the node will be drawn as a simple ellipse.
|
|
3545 *
|
|
3546 * Default value is TEXT.
|
|
3547 *
|
|
3548 * @param node the node to extract its type.
|
|
3549 * @returns {number} one value from popoto.provider.NodeDisplayTypes
|
|
3550 */
|
|
3551 "getDisplayType": function (node) {
|
|
3552 return popoto.provider.NodeDisplayTypes.TEXT;
|
|
3553 },
|
|
3554
|
|
3555 /**
|
|
3556 * Function defining whether the node is a group node.
|
|
3557 * In this case no count are displayed and no value can be selected on the node.
|
|
3558 *
|
|
3559 * Default value is false.
|
|
3560 */
|
|
3561 "getIsGroup": function (node) {
|
|
3562 return false;
|
|
3563 },
|
|
3564
|
|
3565 /**
|
|
3566 * Function defining whether the node text representation must be displayed on graph.
|
|
3567 * If true the value returned for getTextValue on node will be displayed on graph.
|
|
3568 *
|
|
3569 * This text will be added in addition to the getDisplayType representation.
|
|
3570 * It can be displayed on all type of node display, images, SVG or text.
|
|
3571 *
|
|
3572 * Default value is true
|
|
3573 *
|
|
3574 * @param node the node to display on graph.
|
|
3575 * @returns {boolean} true if text must be displayed on graph for the node.
|
|
3576 */
|
|
3577 "getIsTextDisplayed": function (node) {
|
|
3578 return true;
|
|
3579 },
|
|
3580
|
|
3581 /**
|
|
3582 * Function used to return the text representation of a node.
|
|
3583 *
|
|
3584 * The default behavior is to return the label of the node
|
|
3585 * or the value of constraint attribute of the node if it contains value.
|
|
3586 *
|
|
3587 * The returned value is truncated using popoto.graph.node.NODE_MAX_CHARS property.
|
|
3588 *
|
|
3589 * @param node the node to represent as text.
|
|
3590 * @returns {string} the text representation of the node.
|
|
3591 */
|
|
3592 "getTextValue": function (node) {
|
|
3593 var text;
|
|
3594 var constraintAttr = popoto.provider.getProperty(node.label, "constraintAttribute");
|
|
3595 if (node.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
3596 if (constraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3597 text = "" + node.internalID;
|
|
3598 } else {
|
|
3599 text = "" + node.attributes[constraintAttr];
|
|
3600 }
|
|
3601 } else {
|
|
3602 if (node.value === undefined) {
|
|
3603 text = node.label;
|
|
3604 } else {
|
|
3605 if (constraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3606 text = "" + node.value.internalID;
|
|
3607 } else {
|
|
3608 text = "" + node.value.attributes[constraintAttr];
|
|
3609 }
|
|
3610 }
|
|
3611 }
|
|
3612 // Text is truncated to fill the ellipse
|
|
3613 return text.substring(0, popoto.graph.node.NODE_MAX_CHARS);
|
|
3614 },
|
|
3615
|
|
3616 /**
|
|
3617 * Function used to return a descriptive text representation of a link.
|
|
3618 * This representation should be more complete than getTextValue and can contain semantic data.
|
|
3619 * This function is used for example to generate the label in the query viewer.
|
|
3620 *
|
|
3621 * The default behavior is to return the getTextValue not truncated.
|
|
3622 *
|
|
3623 * @param node the node to represent as text.
|
|
3624 * @returns {string} the text semantic representation of the node.
|
|
3625 */
|
|
3626 "getSemanticValue": function (node) {
|
|
3627 var text;
|
|
3628 var constraintAttr = popoto.provider.getProperty(node.label, "constraintAttribute");
|
|
3629 if (node.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
3630 if (constraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3631 text = "" + node.internalID;
|
|
3632 } else {
|
|
3633 text = "" + node.attributes[constraintAttr];
|
|
3634 }
|
|
3635 } else {
|
|
3636 if (node.value === undefined) {
|
|
3637 text = node.label;
|
|
3638 } else {
|
|
3639 if (constraintAttr === popoto.query.NEO4J_INTERNAL_ID) {
|
|
3640 text = "" + node.value.internalID;
|
|
3641 } else {
|
|
3642 text = "" + node.value.attributes[constraintAttr];
|
|
3643 }
|
|
3644 }
|
|
3645 }
|
|
3646 return text;
|
|
3647 },
|
|
3648
|
|
3649 /**
|
|
3650 * Function returning the image file path to use for a node.
|
|
3651 * This function is only used for popoto.provider.NodeDisplayTypes.IMAGE type nodes.
|
|
3652 *
|
|
3653 * @param node
|
|
3654 * @returns {string}
|
|
3655 */
|
|
3656 "getImagePath": function (node) {
|
|
3657 if (node.type === popoto.graph.node.NodeTypes.VALUE) {
|
|
3658 return "css/image/node-yellow.png";
|
|
3659 } else {
|
|
3660 if (node.value === undefined) {
|
|
3661 if (node.type === popoto.graph.node.NodeTypes.ROOT) {
|
|
3662 return "css/image/node-blue.png";
|
|
3663 }
|
|
3664 if (node.type === popoto.graph.node.NodeTypes.CHOOSE) {
|
|
3665 return "css/image/node-green.png";
|
|
3666 }
|
|
3667 if (node.type === popoto.graph.node.NodeTypes.GROUP) {
|
|
3668 return "css/image/node-black.png";
|
|
3669 }
|
|
3670 } else {
|
|
3671 return "css/image/node-orange.png";
|
|
3672 }
|
|
3673 }
|
|
3674 },
|
|
3675
|
|
3676 /**
|
|
3677 * Function returning the image width of the node.
|
|
3678 * This function is only used for popoto.provider.NodeDisplayTypes.IMAGE type nodes.
|
|
3679 *
|
|
3680 * @param node
|
|
3681 * @returns {number}
|
|
3682 */
|
|
3683 "getImageWidth": function (node) {
|
|
3684 return 125;
|
|
3685 },
|
|
3686
|
|
3687 /**
|
|
3688 * Function returning the image height of the node.
|
|
3689 * This function is only used for popoto.provider.NodeDisplayTypes.IMAGE type nodes.
|
|
3690 *
|
|
3691 * @param node
|
|
3692 * @returns {number}
|
|
3693 */
|
|
3694 "getImageHeight": function (node) {
|
|
3695 return 125;
|
|
3696 },
|
|
3697
|
|
3698 /**********************************************************************
|
|
3699 * Results specific parameters:
|
|
3700 *
|
|
3701 * These attributes are specific to result display.
|
|
3702 **********************************************************************/
|
|
3703
|
|
3704 /**
|
|
3705 * Generate the result entry content using d3.js mechanisms.
|
|
3706 *
|
|
3707 * The parameter of the function is the <p> selected with d3.js
|
|
3708 *
|
|
3709 * The default behavior is to generate a <table> containing all the return attributes in a <th> and their value in a <td>.
|
|
3710 *
|
|
3711 * @param pElmt the <p> element generated in the result list.
|
|
3712 */
|
|
3713 "displayResults": function (pElmt) {
|
|
3714 var result = pElmt.data()[0];
|
|
3715
|
|
3716 var returnAttributes = popoto.provider.getReturnAttributes(result.label);
|
|
3717
|
|
3718 var table = pElmt.append("table").attr("class", "ppt-result-table");
|
|
3719
|
|
3720 returnAttributes.forEach(function (attribute) {
|
|
3721 var tr = table.append("tr");
|
|
3722 tr.append("th").text(function () {
|
|
3723 return attribute + ":";
|
|
3724 });
|
|
3725 if (result.attributes[attribute] !== undefined) {
|
|
3726 tr.append("td").text(function (result) {
|
|
3727 return result.attributes[attribute];
|
|
3728 });
|
|
3729 }
|
|
3730 });
|
|
3731 }
|
|
3732
|
|
3733 });
|
|
3734
|
|
3735 return popoto;
|
|
3736 }(); |